From 42af4a2e018951f1c41ca208a010ef6c9d08ba68 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 13:15:19 +0400 Subject: [PATCH 01/23] delete pointless test --- tests/unit/test_confirmed_block.py | 57 ------------------------------ 1 file changed, 57 deletions(-) delete mode 100644 tests/unit/test_confirmed_block.py diff --git a/tests/unit/test_confirmed_block.py b/tests/unit/test_confirmed_block.py deleted file mode 100644 index f0e49de8..00000000 --- a/tests/unit/test_confirmed_block.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Test get confirmed block.""" - -from solders.keypair import Keypair -from solders.signature import Signature -from solders.system_program import TransferParams, transfer - -import solana.transaction as txlib - - -def test_verify_confirmed_block(stubbed_blockhash): - """Test verifying signature in a confirmed block.""" - kp0, kp1, kp2, kp3 = (Keypair() for _ in range(4)) - # Create a couple signed transaction - txn1 = txlib.Transaction(recent_blockhash=stubbed_blockhash).add( - transfer(TransferParams(from_pubkey=kp0.pubkey(), to_pubkey=kp1.pubkey(), lamports=123)) - ) - txn1.sign(kp0) - txn2 = txlib.Transaction(recent_blockhash=stubbed_blockhash).add( - transfer(TransferParams(from_pubkey=kp2.pubkey(), to_pubkey=kp3.pubkey(), lamports=456)) - ) - txn2.sign(kp2) - # Build confirmed_block with dummy data for blockhases and balances - confirmed_block = { - "blockhash": stubbed_blockhash, - "previousBlockhash": stubbed_blockhash, - "transactions": [ - { - "transaction": txn1, - "meta": { - "fee": 0, - "preBalances": [100000, 100000, 1, 1, 1], - "postBalances": [99877, 100123, 1, 1, 1], - "status": {"Ok": None}, - "err": None, - }, - }, - { - "transaction": txn2, - "meta": { - "fee": 0, - "preBalances": [100000, 100000, 1, 1, 1], - "postBalances": [99544, 100456, 1, 1, 1], - "status": {"Ok": None}, - "err": None, - }, - }, - ], - "rewards": [], - } - # Verify signatures in confirmed_block - assert all(tx_with_meta["transaction"].verify_signatures() for tx_with_meta in confirmed_block["transactions"]) - # Test block with bogus signature - bogus_signature = Signature.default() - bogus_txn1 = txlib.Transaction.populate(txn1.compile_message(), [bogus_signature]) - bad_confirmed_block = confirmed_block - bad_confirmed_block["transactions"][0]["transaction"] = bogus_txn1 - assert not all(tx_with_meta["transaction"].verify_signatures() for tx_with_meta in confirmed_block["transactions"]) From 00da75e0198268d7272a9270980feb0531e846ad Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 13:26:30 +0400 Subject: [PATCH 02/23] remove transaction module --- src/solana/rpc/api.py | 19 +- src/solana/rpc/async_api.py | 60 +-- src/solana/rpc/core.py | 2 +- src/solana/transaction.py | 347 -------------- src/spl/token/core.py | 2 +- tests/integration/test_async_http_client.py | 2 +- tests/integration/test_http_client.py | 2 +- tests/integration/test_memo.py | 2 +- tests/integration/test_websockets.py | 2 +- tests/unit/test_transaction.py | 491 -------------------- tests/unit/test_vote_program.py | 30 +- 11 files changed, 45 insertions(+), 914 deletions(-) delete mode 100644 src/solana/transaction.py delete mode 100644 tests/unit/test_transaction.py diff --git a/src/solana/rpc/api.py b/src/solana/rpc/api.py index 5a130d0b..8137a257 100644 --- a/src/solana/rpc/api.py +++ b/src/solana/rpc/api.py @@ -64,7 +64,7 @@ from solders.transaction import VersionedTransaction from solana.rpc import types -from solana.transaction import Transaction +from solders.transaction import Transaction from .commitment import Commitment, Finalized from .core import ( @@ -407,13 +407,13 @@ def get_fee_for_message( Example: >>> from solders.keypair import Keypair >>> from solders.system_program import TransferParams, transfer - >>> from solana.transaction import Transaction + >>> from solders.message import Message >>> leading_zeros = [0] * 31 >>> sender, receiver = Keypair.from_seed(leading_zeros + [1]), Keypair.from_seed(leading_zeros + [2]) - >>> txn = Transaction().add(transfer(TransferParams( - ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))) + >>> msg = Message([transfer(TransferParams( + ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))]) >>> solana_client = Client("http://localhost:8899") - >>> solana_client.get_fee_for_message(txn.compile_message()).value # doctest: +SKIP + >>> solana_client.get_fee_for_message(msg).value # doctest: +SKIP 5000 """ body = self._get_fee_for_message_body(message, commitment) @@ -1018,11 +1018,11 @@ def send_transaction( >>> from solders.pubkey import Pubkey >>> from solana.rpc.api import Client >>> from solders.system_program import TransferParams, transfer - >>> from solana.transaction import Transaction + >>> from solders.message import Message >>> leading_zeros = [0] * 31 >>> sender, receiver = Keypair.from_seed(leading_zeros + [1]), Keypair.from_seed(leading_zeros + [2]) - >>> txn = Transaction().add(transfer(TransferParams( - ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))) + >>> msg = Message([transfer(TransferParams( + ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))]) >>> solana_client = Client("http://localhost:8899") >>> solana_client.send_transaction(txn, sender).value # doctest: +SKIP Signature( @@ -1074,6 +1074,7 @@ def simulate_transaction( commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". Example: + >>> from solders.transaction import Transaction >>> solana_client = Client("http://localhost:8899") >>> full_signed_tx_hex = ( ... '01b3795ccfaac3eee838bb05c3b8284122c18acedcd645c914fe8e178c3b62640d8616d061cc818b26cab8ecf3855ecc' @@ -1082,7 +1083,7 @@ def simulate_transaction( ... '000000000000000000000000000000000000000000839618f701ba7e9ba27ae59825dd6d6bb66d14f6d5d0eae215161d7' ... '1851a106901020200010c0200000040420f0000000000' ... ) - >>> tx = Transaction.deserialize(bytes.fromhex(full_signed_tx_hex)) + >>> tx = Transaction.from_bytes(bytes.fromhex(full_signed_tx_hex)) >>> solana_client.simulate_transaction(tx).value.logs # doctest: +SKIP ['BPF program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success'] """ diff --git a/src/solana/rpc/async_api.py b/src/solana/rpc/async_api.py index 62ad9c49..cdaa415b 100644 --- a/src/solana/rpc/async_api.py +++ b/src/solana/rpc/async_api.py @@ -62,7 +62,7 @@ from solders.transaction import VersionedTransaction from solana.rpc import types -from solana.transaction import Transaction +from solders.transaction import Transaction from .commitment import Commitment, Finalized from .core import ( @@ -419,11 +419,11 @@ async def get_fee_for_message( Example: >>> from solders.keypair import Keypair >>> from solders.system_program import TransferParams, transfer - >>> from solana.transaction import Transaction + >>> from solders.message import Message >>> leading_zeros = [0] * 31 >>> sender, receiver = Keypair.from_seed(leading_zeros + [1]), Keypair.from_seed(leading_zeros + [2]) - >>> txn = Transaction().add(transfer(TransferParams( - ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))) + >>> msg = Message([transfer(TransferParams( + ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))]) >>> solana_client = AsyncClient("http://localhost:8899") >>> (await solana_client.get_fee_for_message(txn.compile_message())).value # doctest: +SKIP 5000 @@ -1012,62 +1012,28 @@ async def send_raw_transaction(self, txn: bytes, opts: Optional[types.TxOpts] = async def send_transaction( self, txn: Union[VersionedTransaction, Transaction], - *signers: Keypair, opts: Optional[types.TxOpts] = None, - recent_blockhash: Optional[Blockhash] = None, ) -> SendTransactionResp: """Send a transaction. Args: txn: transaction object. - signers: Signers to sign the transaction. Only supported for legacy Transaction. opts: (optional) Transaction options. - recent_blockhash: (optional) Pass a valid recent blockhash here if you want to - skip fetching the recent blockhash or relying on the cache. - Only supported for legacy Transaction. Example: >>> from solders.keypair import Keypair >>> from solders.system_program import TransferParams, transfer - >>> from solana.transaction import Transaction + >>> from solders.message import Message + >>> from solders.transaction import Transaction >>> leading_zeros = [0] * 31 >>> sender, receiver = Keypair.from_seed(leading_zeros + [1]), Keypair.from_seed(leading_zeros + [2]) - >>> txn = Transaction().add(transfer(TransferParams( - ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))) - >>> solana_client = AsyncClient("http://localhost:8899") - >>> (await solana_client.send_transaction(txn, sender)).value # doctest: +SKIP - Signature( - 1111111111111111111111111111111111111111111111111111111111111111, - ) + >>> ixns = [transfer(TransferParams( + ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))] + >>> msg = Message(ixns, sender.pubkey()) + >>> client = AsyncClient("http://localhost:8899") + >>> (await client.send_transaction(Transaction([sender], msg, (await client.get_latest_blockhash())))).value # doctest: +SKIP """ - if isinstance(txn, VersionedTransaction): - if signers: - msg = "*signers args are not used when sending VersionedTransaction." - raise ValueError(msg) - if recent_blockhash is not None: - msg = "recent_blockhash arg is not used when sending VersionedTransaction." - raise ValueError(msg) - versioned_tx_opts = types.TxOpts(preflight_commitment=self._commitment) if opts is None else opts - return await self.send_raw_transaction(bytes(txn), opts=versioned_tx_opts) - last_valid_block_height = None - if recent_blockhash is None: - blockhash_resp = await self.get_latest_blockhash(Finalized) - recent_blockhash = self.parse_recent_blockhash(blockhash_resp) - last_valid_block_height = blockhash_resp.value.last_valid_block_height - - txn.recent_blockhash = recent_blockhash - - txn.sign(*signers) - opts_to_use = ( - types.TxOpts( - preflight_commitment=self._commitment, - last_valid_block_height=last_valid_block_height, - ) - if opts is None - else opts - ) - txn_resp = await self.send_raw_transaction(txn.serialize(), opts=opts_to_use) - return txn_resp + return await self.send_raw_transaction(bytes(txn), opts=opts) async def simulate_transaction( self, @@ -1092,7 +1058,7 @@ async def simulate_transaction( ... '000000000000000000000000000000000000000000839618f701ba7e9ba27ae59825dd6d6bb66d14f6d5d0eae215161d7' ... '1851a106901020200010c0200000040420f0000000000' ... ) - >>> tx = Transaction.deserialize(bytes.fromhex(full_signed_tx_hex)) + >>> tx = Transaction.from_bytes(bytes.fromhex(full_signed_tx_hex)) >>> (await solana_client.simulate_transaction(tx)).value.logs # doctest: +SKIP ['BPF program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success'] """ diff --git a/src/solana/rpc/core.py b/src/solana/rpc/core.py index 1f42d795..d2990e03 100644 --- a/src/solana/rpc/core.py +++ b/src/solana/rpc/core.py @@ -81,7 +81,7 @@ from solders.transaction_status import UiTransactionEncoding from solana.rpc import types -from solana.transaction import Transaction +from solders.transaction import Transaction from .commitment import Commitment, Confirmed, Finalized, Processed diff --git a/src/solana/transaction.py b/src/solana/transaction.py deleted file mode 100644 index b7525262..00000000 --- a/src/solana/transaction.py +++ /dev/null @@ -1,347 +0,0 @@ -"""Library to package an atomic sequence of instructions to a transaction.""" -from __future__ import annotations - -from typing import Any, List, NamedTuple, Optional, Sequence, Tuple, Union - -from solders.hash import Hash as Blockhash -from solders.instruction import AccountMeta, Instruction -from solders.keypair import Keypair -from solders.message import Message -from solders.message import Message as SoldersMessage -from solders.presigner import Presigner -from solders.pubkey import Pubkey -from solders.signature import Signature -from solders.transaction import Transaction as SoldersTx -from solders.transaction import TransactionError - -PACKET_DATA_SIZE = 1280 - 40 - 8 -"""Constant for maximum over-the-wire size of a Transaction.""" - - -class NonceInformation(NamedTuple): - """NonceInformation to be used to build a Transaction.""" - - nonce: Blockhash - """The current Nonce blockhash.""" - nonce_instruction: Instruction - """AdvanceNonceAccount Instruction.""" - - -def _build_solders_tx( - recent_blockhash: Optional[Blockhash] = None, - nonce_info: Optional[NonceInformation] = None, - fee_payer: Optional[Pubkey] = None, - instructions: Optional[Sequence[Instruction]] = None, -) -> SoldersTx: - core_instructions = [] if instructions is None else instructions - underlying_instructions = ( - core_instructions if nonce_info is None else [nonce_info.nonce_instruction, *core_instructions] - ) - underlying_blockhash: Optional[Blockhash] - if nonce_info is not None: - underlying_blockhash = nonce_info.nonce - elif recent_blockhash is not None: - underlying_blockhash = recent_blockhash - else: - underlying_blockhash = None - underlying_fee_payer = None if fee_payer is None else fee_payer - underlying_blockhash = Blockhash.default() if underlying_blockhash is None else underlying_blockhash - msg = SoldersMessage.new_with_blockhash(underlying_instructions, underlying_fee_payer, underlying_blockhash) - return SoldersTx.new_unsigned(msg) - - -def _decompile_instructions(msg: SoldersMessage) -> List[Instruction]: - account_keys = msg.account_keys - decompiled_instructions: List[Instruction] = [] - for compiled_ix in msg.instructions: - program_id = account_keys[compiled_ix.program_id_index] - account_metas = [ - AccountMeta( - account_keys[idx], - is_signer=msg.is_signer(idx), - is_writable=msg.is_writable(idx), - ) - for idx in compiled_ix.accounts - ] - decompiled_instructions.append(Instruction(program_id, compiled_ix.data, account_metas)) - return decompiled_instructions - - -class Transaction: - """Transaction class to represent an atomic transaction. - - Args: - recent_blockhash: A recent transaction id. - nonce_info: Nonce information. - If populated, transaction will use a durable Nonce hash instead of a `recent_blockhash`. - fee_payer: The transaction fee payer. - instructions: The instructions to be executed in this transaction. - """ - - # Default (empty) signature - __DEFAULT_SIG = bytes(64) - - def __init__( - self, - recent_blockhash: Optional[Blockhash] = None, - nonce_info: Optional[NonceInformation] = None, - fee_payer: Optional[Pubkey] = None, - instructions: Optional[Sequence[Instruction]] = None, - ) -> None: - """Init transaction object.""" - self._solders = _build_solders_tx( - recent_blockhash=recent_blockhash, - nonce_info=nonce_info, - fee_payer=fee_payer, - instructions=instructions, - ) - - @classmethod - def from_solders(cls, txn: SoldersTx) -> Transaction: - """Convert from a `solders` transaction. - - Args: - txn: The `solders` transaction. - - Returns: - The `solana-py` transaction. - """ - new_tx = cls() - new_tx._solders = txn - return new_tx - - def to_solders(self) -> SoldersTx: - """Convert to a `solders` transaction. - - Returns: - The `solders` transaction. - """ - return self._solders - - def __eq__(self, other: Any) -> bool: - """Equality defintion for Transactions.""" - if not isinstance(other, Transaction): - return False - return self.to_solders() == other.to_solders() - - @property - def recent_blockhash(self) -> Optional[Blockhash]: - """Optional[Blockhash]: The blockhash assigned to this transaction.""" - return self._solders.message.recent_blockhash - - @recent_blockhash.setter - def recent_blockhash(self, blockhash: Optional[Blockhash]) -> None: # noqa: D102 - self._solders = _build_solders_tx( - recent_blockhash=blockhash, - nonce_info=None, - fee_payer=self.fee_payer, - instructions=self.instructions, - ) - - @property - def fee_payer(self) -> Optional[Pubkey]: - """Optional[Pubkey]: The transaction fee payer.""" - account_keys = self._solders.message.account_keys - return account_keys[0] if account_keys else None - - @fee_payer.setter - def fee_payer(self, payer: Optional[Pubkey]) -> None: # noqa: D102 - self._solders = _build_solders_tx( - recent_blockhash=self.recent_blockhash, - nonce_info=None, - fee_payer=payer, - instructions=self.instructions, - ) - - @property - def instructions(self) -> Tuple[Instruction, ...]: - """Tuple[Instruction]: The instructions contained in this transaction.""" - msg = self._solders.message - return tuple(_decompile_instructions(msg)) - - @instructions.setter - def instructions(self, ixns: Sequence[Instruction]) -> None: # noqa: D102 - self._solders = _build_solders_tx( - recent_blockhash=self.recent_blockhash, - nonce_info=None, - fee_payer=self.fee_payer, - instructions=ixns, - ) - - @property - def signatures(self) -> Tuple[Signature, ...]: - """Tuple[Signature]: Signatures for the transaction.""" - return tuple(self._solders.signatures) - - def signature(self) -> Signature: - """The first (payer) Transaction signature. - - Returns: - The payer signature. - """ - return self._solders.signatures[0] - - def add(self, *args: Union[Transaction, Instruction]) -> Transaction: - """Add one or more instructions to this Transaction. - - Args: - *args: The instructions to add to this Transaction. - If a `Transaction` is passsed, the instructions will be extracted from it. - - Returns: - The transaction with the added instructions. - """ - for arg in args: - if isinstance(arg, Transaction): - self.instructions = self.instructions + arg.instructions - elif isinstance(arg, Instruction): - self.instructions = (*self.instructions, arg) - else: - raise ValueError("invalid instruction:", arg) - - return self - - def compile_message(self) -> Message: # pylint: disable=too-many-locals - """Compile transaction data. - - Returns: - The compiled message. - """ - return self._solders.message - - def serialize_message(self) -> bytes: - """Get raw transaction data that need to be covered by signatures. - - Returns: - The serialized message. - """ - return bytes(self.compile_message()) - - def sign_partial(self, *partial_signers: Keypair) -> None: - """Partially sign a Transaction with the specified keypairs. - - All the caveats from the `sign` method apply to `sign_partial` - """ - self._solders.partial_sign(partial_signers, self._solders.message.recent_blockhash) - - def sign(self, *signers: Keypair) -> None: - """Sign the Transaction with the specified accounts. - - Multiple signatures may be applied to a Transaction. The first signature - is considered "primary" and is used when testing for Transaction confirmation. - - Transaction fields should not be modified after the first call to `sign`, - as doing so may invalidate the signature and cause the Transaction to be - rejected. - - The Transaction must be assigned a valid `recent_blockhash` before invoking this method. - """ - self._solders.sign(signers, self._solders.message.recent_blockhash) - - def add_signature(self, pubkey: Pubkey, signature: Signature) -> None: - """Add an externally created signature to a transaction. - - Args: - pubkey: The public key that created the signature. - signature: The signature to add. - """ - presigner = Presigner(pubkey, signature) - self._solders.partial_sign([presigner], self._solders.message.recent_blockhash) - - def verify_signatures(self) -> bool: - """Verify signatures of a complete, signed Transaction. - - Returns: - a bool indicating if the signatures are correct or not. - """ - try: - self._solders.verify() - except TransactionError: - return False - return True - - def serialize(self, verify_signatures: bool = True) -> bytes: - """Serialize the Transaction in the wire format. - - The Transaction must have a valid `signature` before invoking this method. - verify_signatures can be added if the signature does not require to be verified. - - Args: - verify_signatures: a bool indicating to verify the signature or not. Defaults to True - - Example: - >>> from solders.keypair import Keypair - >>> from solders.pubkey import Pubkey - >>> from solders.hash import Hash - >>> from solders.system_program import transfer, TransferParams - >>> leading_zeros = [0] * 31 - >>> seed = bytes(leading_zeros + [1]) - >>> sender, receiver = Keypair.from_seed(seed), Pubkey(leading_zeros + [2]) - >>> transfer_tx = Transaction().add(transfer(TransferParams(from_pubkey=sender.pubkey(), to_pubkey=receiver, lamports=1000))) - >>> transfer_tx.recent_blockhash = Hash(leading_zeros + [3]) - >>> transfer_tx.sign(sender) - >>> transfer_tx.serialize().hex() - '019d53be8af3a7c30f86c1092d2c3ea61d270c0cfa275a23ba504674c8fbbb724827b23b42dc8e08019e23120f1b6f40f9799355ce54185b4415be37ca2cee6e0e010001034cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba2900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000301020200010c02000000e803000000000000' - - Returns: - The serialized transaction. - """ # noqa: E501 pylint: disable=line-too-long - if self.signatures == [Signature.default() for sig in self.signatures]: - raise AttributeError("transaction has not been signed") - - if verify_signatures and not self.verify_signatures(): - raise AttributeError("transaction has not been signed correctly") - - return bytes(self._solders) - - @classmethod - def deserialize(cls, raw_transaction: bytes) -> Transaction: - """Parse a wire transaction into a Transaction object. - - Example: - >>> raw_transaction = bytes.fromhex( - ... '019d53be8af3a7c30f86c1092d2c3ea61d270c0cfa2' - ... '75a23ba504674c8fbbb724827b23b42dc8e08019e23' - ... '120f1b6f40f9799355ce54185b4415be37ca2cee6e0' - ... 'e010001034cb5abf6ad79fbf5abbccafcc269d85cd2' - ... '651ed4b885b5869f241aedf0a5ba290000000000000' - ... '0000000000000000000000000000000000000000000' - ... '0000000200000000000000000000000000000000000' - ... '0000000000000000000000000000000000000000000' - ... '0000000000000000000000000000000000000000000' - ... '000000301020200010c02000000e803000000000000' - ... ) - >>> type(Transaction.deserialize(raw_transaction)) - - - Returns: - The deserialized transaction. - """ - return cls.from_solders(SoldersTx.from_bytes(raw_transaction)) - - @classmethod - def populate(cls, message: Message, signatures: List[Signature]) -> Transaction: - """Populate Transaction object from message and signatures. - - Example: - >>> raw_message = bytes.fromhex( - ... '0200030500000000000000000000000000000000000000000000' - ... '0000000000000000000100000000000000000000000000000000' - ... '0000000000000000000000000000000200000000000000000000' - ... '0000000000000000000000000000000000000000000300000000' - ... '0000000000000000000000000000000000000000000000000000' - ... '0004000000000000000000000000000000000000000000000000' - ... '0000000000000005c49ae77603782054f17a9decea43b444eba0' - ... 'edb12c6f1d31c6e0e4a84bf052eb010403010203050909090909' - ... ) - >>> from solders.message import Message - >>> from solders.signature import Signature - >>> msg = Message.from_bytes(raw_message) - >>> signatures = [Signature(bytes([1] * Signature.LENGTH)), Signature(bytes([2] * Signature.LENGTH))] - >>> type(Transaction.populate(msg, signatures)) - - - Returns: - The populated transaction. - """ - return cls.from_solders(SoldersTx.populate(message, signatures)) diff --git a/src/spl/token/core.py b/src/spl/token/core.py index 1ff1b030..0b1eb9b6 100644 --- a/src/spl/token/core.py +++ b/src/spl/token/core.py @@ -14,7 +14,7 @@ from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Commitment from solana.rpc.types import TokenAccountOpts, TxOpts -from solana.transaction import Transaction +from solders.transaction import Transaction from spl.token._layouts import ACCOUNT_LAYOUT, MINT_LAYOUT, MULTISIG_LAYOUT # type: ignore from spl.token.constants import WRAPPED_SOL_MINT diff --git a/tests/integration/test_async_http_client.py b/tests/integration/test_async_http_client.py index e77d6ec1..bbd54097 100644 --- a/tests/integration/test_async_http_client.py +++ b/tests/integration/test_async_http_client.py @@ -17,7 +17,7 @@ from solana.rpc.commitment import Confirmed, Finalized, Processed from solana.rpc.core import RPCException, TransactionExpiredBlockheightExceededError from solana.rpc.types import DataSliceOpts, TxOpts -from solana.transaction import Transaction +from solders.transaction import Transaction from ..utils import AIRDROP_AMOUNT, assert_valid_response diff --git a/tests/integration/test_http_client.py b/tests/integration/test_http_client.py index 57af1224..de60cb45 100644 --- a/tests/integration/test_http_client.py +++ b/tests/integration/test_http_client.py @@ -16,7 +16,7 @@ from solana.rpc.commitment import Confirmed, Finalized, Processed from solana.rpc.core import RPCException, TransactionExpiredBlockheightExceededError from solana.rpc.types import DataSliceOpts, TxOpts -from solana.transaction import Transaction +from solders.transaction import Transaction from spl.token.constants import WRAPPED_SOL_MINT from ..utils import AIRDROP_AMOUNT, assert_valid_response diff --git a/tests/integration/test_memo.py b/tests/integration/test_memo.py index 1c7b3446..07bda4f4 100644 --- a/tests/integration/test_memo.py +++ b/tests/integration/test_memo.py @@ -7,7 +7,7 @@ from solana.rpc.api import Client from solana.rpc.commitment import Finalized -from solana.transaction import Transaction +from solders.transaction import Transaction from ..utils import AIRDROP_AMOUNT, assert_valid_response diff --git a/tests/integration/test_websockets.py b/tests/integration/test_websockets.py index b88ddc8f..fdd7daf9 100644 --- a/tests/integration/test_websockets.py +++ b/tests/integration/test_websockets.py @@ -27,7 +27,7 @@ from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Finalized from solana.rpc.websocket_api import SolanaWsClientProtocol, connect -from solana.transaction import Transaction +from solders.transaction import Transaction from ..utils import AIRDROP_AMOUNT diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py deleted file mode 100644 index 17bb82fa..00000000 --- a/tests/unit/test_transaction.py +++ /dev/null @@ -1,491 +0,0 @@ -"""Unit tests for solana.transaction.""" -from base64 import b64decode, b64encode - -import pytest -import solders.system_program as sp -from solders.hash import Hash as Blockhash -from solders.instruction import AccountMeta, CompiledInstruction -from solders.keypair import Keypair -from solders.message import Message -from solders.message import Message as SoldersMessage -from solders.pubkey import Pubkey -from solders.signature import Signature -from solders.transaction import Transaction as SoldersTx - -import solana.transaction as txlib - - -def example_tx(stubbed_blockhash, kp0: Keypair, kp1: Keypair, kp2: Keypair) -> txlib.Transaction: - """Example tx for testing.""" - ixn = txlib.Instruction( - program_id=Pubkey.default(), - data=bytes([0, 0, 0, 0]), - accounts=[ - AccountMeta(kp0.pubkey(), True, True), - AccountMeta(kp1.pubkey(), True, True), - AccountMeta(kp2.pubkey(), True, True), - ], - ) - return txlib.Transaction(fee_payer=kp0.pubkey(), instructions=[ixn], recent_blockhash=stubbed_blockhash) - - -def test_to_solders(stubbed_blockhash: Blockhash) -> None: - """Test converting a Transaction to solders.""" - kp1, kp2 = Keypair(), Keypair() - transfer = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) - solders_transfer = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) - assert transfer.data == solders_transfer.data - txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer) - solders_msg = SoldersMessage.new_with_blockhash([solders_transfer], None, stubbed_blockhash) - solders_txn = SoldersTx.new_unsigned(solders_msg) - assert txn.to_solders() == solders_txn - assert txlib.Transaction.from_solders(solders_txn) == txn - - -def test_sign_partial(stubbed_blockhash): - """Test partially sigining a transaction.""" - keypair0 = Keypair() - keypair1 = Keypair() - keypair2 = Keypair() - ixn = txlib.Instruction( - program_id=Pubkey.default(), - data=bytes([0, 0, 0, 0]), - accounts=[ - AccountMeta(keypair0.pubkey(), True, True), - AccountMeta(keypair1.pubkey(), True, True), - AccountMeta(keypair2.pubkey(), True, True), - ], - ) - txn = txlib.Transaction(fee_payer=keypair0.pubkey(), instructions=[ixn], recent_blockhash=stubbed_blockhash) - assert txn.to_solders().message.header.num_required_signatures == 3 - txn.sign_partial(keypair0, keypair2) - assert not txn.to_solders().is_signed() - txn.sign_partial(keypair1) - assert txn.to_solders().is_signed() - expected_tx = txlib.Transaction(fee_payer=keypair0.pubkey(), instructions=[ixn], recent_blockhash=stubbed_blockhash) - expected_tx.sign(keypair0, keypair1, keypair2) - assert txn == expected_tx - - -def test_recent_blockhash_setter(stubbed_blockhash): - """Test the recent_blockhash setter property works.""" - kp0, kp1, kp2 = Keypair(), Keypair(), Keypair() - tx0 = example_tx(stubbed_blockhash, kp0, kp1, kp2) - tx1 = example_tx(stubbed_blockhash, kp0, kp1, kp2) - tx1.recent_blockhash = tx0.recent_blockhash - assert tx0 == tx1 - - -def test_transfer_signatures(stubbed_blockhash): - """Test signing transfer transactions.""" - kp1, kp2 = Keypair(), Keypair() - transfer1 = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) - transfer2 = sp.transfer(sp.TransferParams(from_pubkey=kp2.pubkey(), to_pubkey=kp1.pubkey(), lamports=123)) - txn = txlib.Transaction(recent_blockhash=stubbed_blockhash) - txn.add(transfer1, transfer2) - txn.sign(kp1, kp2) - - expected = txlib.Transaction.populate(txn.compile_message(), txn.signatures) - assert txn == expected - - -def test_dedup_signatures(stubbed_blockhash): - """Test signature deduplication.""" - kp1, kp2 = Keypair(), Keypair() - transfer1 = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) - transfer2 = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) - txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer1, transfer2) - txn.sign(kp1) - - -def test_wire_format_and_desrialize(stubbed_blockhash, stubbed_receiver, stubbed_sender): - """Test serialize/derialize transaction to/from wire format.""" - transfer = sp.transfer( - sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=49) - ) - expected_txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer) - expected_txn.sign(stubbed_sender) - wire_txn = b64decode( - b"AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/kxj+nQ0BAAEDE5j2" - b"LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=" - ) - txn = txlib.Transaction.deserialize(wire_txn) - assert txn == expected_txn - assert wire_txn == expected_txn.serialize() - - -def test_populate(): - """Test populating transaction with a message and two signatures.""" - account_keys = [Pubkey([0] * 31 + [i + 1]) for i in range(5)] - msg = Message.new_with_compiled_instructions( - num_required_signatures=2, - num_readonly_signed_accounts=0, - num_readonly_unsigned_accounts=3, - account_keys=account_keys, - instructions=[CompiledInstruction(accounts=bytes([1, 2, 3]), data=bytes([9] * 5), program_id_index=4)], - recent_blockhash=Blockhash.default(), - ) - signatures = [Signature(bytes([1] * Signature.LENGTH)), Signature(bytes([2] * Signature.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 test_serialize_unsigned_transaction(stubbed_blockhash, stubbed_receiver, stubbed_sender): - """Test to serialize an unsigned transaction.""" - transfer = sp.transfer( - sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=49) - ) - txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer) - assert txn.signatures == (Signature.default(),) - # Empty signature array fails - with pytest.raises(AttributeError): - txn.serialize() - assert txn.signatures == (Signature.default(),) - - # Set fee payer - txn.fee_payer = stubbed_sender.pubkey() - # Serialize message - assert b64encode(txn.serialize_message()) == ( - b"AQABAxOY9ixtGkV8UbpqS189vS9p/KkyFiGNyJl+QWvRfZPK/UOfzLZnJ/KJxcbeO8So/l3V13dwvI/xXD7u3LFK8/wAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMSa53YDeCBU8Xqd7OpDtETroO2xLG8dMcbg5KhL8FLrAQICAAEMAgAAADEAAAAAAAAA" - ) - assert len(txn.instructions) == 1 - # Signature array populated with null signatures fails - with pytest.raises(AttributeError): - txn.serialize() - assert txn.signatures == (Signature.default(),) - # Properly signed transaction succeeds - txn.sign(stubbed_sender) - assert len(txn.instructions) == 1 - expected_serialization = b64decode( - b"AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/kxj+nQ0BAAEDE5j2" - b"LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=" - ) - assert txn.serialize() == expected_serialization - assert len(txn.signatures) == 1 - assert txn.signatures != (Signature.default(),) - - -def test_serialize_unsigned_transaction_without_verifying_signatures( - stubbed_blockhash, stubbed_receiver, stubbed_sender -): - """Test to serialize an unsigned transaction without verifying the signatures.""" - transfer = sp.transfer( - sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=49) - ) - txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer) - assert txn.signatures == (Signature.default(),) - - # empty signatures should not fail - txn.serialize(verify_signatures=False) - assert txn.signatures == (Signature.default(),) - - # Set fee payer - txn.fee_payer = stubbed_sender.pubkey() - # Serialize message - assert b64encode(txn.serialize_message()) == ( - b"AQABAxOY9ixtGkV8UbpqS189vS9p/KkyFiGNyJl+QWvRfZPK/UOfzLZnJ/KJxcbeO8So/l3V13dwvI/xXD7u3LFK8/wAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMSa53YDeCBU8Xqd7OpDtETroO2xLG8dMcbg5KhL8FLrAQICAAEMAgAAADEAAAAAAAAA" - ) - assert len(txn.instructions) == 1 - # Signature array populated with null signatures should not fail - txn.serialize(verify_signatures=False) - assert txn.signatures == (Signature.default(),) - - -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.pubkey() for x in [signer_one, signer_two, signer_three]], key=str) - sorted_signers_excluding_fee_payer = [x for x in sorted_signers if str(x) != str(fee_payer.pubkey())] - sorted_receivers = sorted([x.pubkey() for x in [receiver_one, receiver_two, receiver_three]], key=str) - - txn = txlib.Transaction(recent_blockhash=stubbed_blockhash) - txn.fee_payer = fee_payer.pubkey() - - # Add three transfer transactions - txn.add( - sp.transfer( - sp.TransferParams( - from_pubkey=signer_one.pubkey(), - to_pubkey=receiver_one.pubkey(), - lamports=2_000_000, - ) - ) - ) - txn.add( - sp.transfer( - sp.TransferParams( - from_pubkey=signer_two.pubkey(), - to_pubkey=receiver_two.pubkey(), - lamports=2_000_000, - ) - ) - ) - txn.add( - sp.transfer( - sp.TransferParams( - from_pubkey=signer_three.pubkey(), - to_pubkey=receiver_three.pubkey(), - 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(bytes(tx_msg)) == js_msg_b64_check - - # Transaction should organize AccountMetas by pubkey - assert tx_msg.account_keys[0] == fee_payer.pubkey() - 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 ec8c1a48..9271bb5c 100644 --- a/tests/unit/test_vote_program.py +++ b/tests/unit/test_vote_program.py @@ -3,9 +3,10 @@ from solders.hash import Hash from solders.keypair import Keypair +from solders.message import Message from solders.pubkey import Pubkey -import solana.transaction as txlib +import solders.transaction as txlib import solana.vote_program as vp @@ -80,19 +81,20 @@ def test_withdraw_from_vote_account(): ) vote_account_pubkey = Pubkey.from_string("CWqJy1JpmBcx7awpeANfrPk6AsQKkmego8ujjaYPGFEk") receiver_account_pubkey = Pubkey.from_string("A1V5gsis39WY42djdTKUFsgE5oamk4nrtg16WnKTuzZK") - - txn = txlib.Transaction(fee_payer=withdrawer_keypair.pubkey()) - txn.recent_blockhash = Hash.from_string("Add1tV7kJgNHhTtx3Dgs6dhC7kyXrGJQZ2tJGW15tLDH") - - txn.add( - vp.withdraw_from_vote_account( - vp.WithdrawFromVoteAccountParams( - vote_account_from_pubkey=vote_account_pubkey, - to_pubkey=receiver_account_pubkey, - withdrawer=withdrawer_keypair.pubkey(), - lamports=2_000_000_000, + recent_blockhash = Hash.from_string("Add1tV7kJgNHhTtx3Dgs6dhC7kyXrGJQZ2tJGW15tLDH") + msg = Message.new_with_blockhash( + [ + vp.withdraw_from_vote_account( + vp.WithdrawFromVoteAccountParams( + vote_account_from_pubkey=vote_account_pubkey, + to_pubkey=receiver_account_pubkey, + withdrawer=withdrawer_keypair.pubkey(), + lamports=2_000_000_000, + ) ) - ) + ], + withdrawer_keypair.pubkey(), + recent_blockhash, ) # solana withdraw-from-vote-account --dump-transaction-message \ @@ -108,7 +110,7 @@ def test_withdraw_from_vote_account(): b"AQABBDqF5SfUR/5I9i2gnIHHEr01j2JItmpFHSaRd74NaZ1whdju9KDr87dR4CFbvp8kmkq1rSYitXg2nDzw1kcQsBarFQYO0flqHdOoQpaNxOZ8eSlkLWHns0kvxLHtDo6WbQdhSB01dHS7fE12JOvTvbPYNV5z0RBD/A2jU4AAAAAAjxrQaMS7FjmaR++mvFr3XE6XbzMUTMJUIpITrUWBzGwBAwMCAQAMAwAAAACUNXcAAAAA" # noqa: E501 pylint: disable=line-too-long ) - serialized_message = txn.serialize_message() + serialized_message = bytes(msg) assert serialized_message == js_wire_msg # XXX: Cli message serialization do not sort on account metas producing discrepency From 9404f6e7697b14c76e71eb36dcf78dc5a2c0a357 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 14:54:25 +0400 Subject: [PATCH 03/23] small docstring fix --- src/solana/rpc/async_api.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/solana/rpc/async_api.py b/src/solana/rpc/async_api.py index cdaa415b..e2355ffc 100644 --- a/src/solana/rpc/async_api.py +++ b/src/solana/rpc/async_api.py @@ -3,8 +3,6 @@ from time import time from typing import Dict, List, Optional, Sequence, Union -from solders.hash import Hash as Blockhash -from solders.keypair import Keypair from solders.message import VersionedMessage from solders.pubkey import Pubkey from solders.rpc.responses import ( @@ -64,7 +62,7 @@ from solana.rpc import types from solders.transaction import Transaction -from .commitment import Commitment, Finalized +from .commitment import Commitment from .core import ( _COMMITMENT_TO_SOLDERS, TransactionExpiredBlockheightExceededError, @@ -1031,8 +1029,8 @@ async def send_transaction( ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))] >>> msg = Message(ixns, sender.pubkey()) >>> client = AsyncClient("http://localhost:8899") - >>> (await client.send_transaction(Transaction([sender], msg, (await client.get_latest_blockhash())))).value # doctest: +SKIP - """ + >>> (await client.send_transaction(Transaction([sender], msg, (await client.get_latest_blockhash()).value.blockhash))) # doctest: +SKIP + """ # noqa: E501 return await self.send_raw_transaction(bytes(txn), opts=opts) async def simulate_transaction( From 1441a2b168cc27954f5fc7eea07dfc575a208587 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 14:58:47 +0400 Subject: [PATCH 04/23] update sync send_transaction method --- src/solana/rpc/api.py | 55 +++++++------------------------------------ 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/src/solana/rpc/api.py b/src/solana/rpc/api.py index 8137a257..67f49cea 100644 --- a/src/solana/rpc/api.py +++ b/src/solana/rpc/api.py @@ -4,8 +4,6 @@ from time import sleep, time from typing import Dict, List, Optional, Sequence, Union -from solders.hash import Hash as Blockhash -from solders.keypair import Keypair from solders.message import VersionedMessage from solders.pubkey import Pubkey from solders.rpc.responses import ( @@ -66,7 +64,7 @@ from solana.rpc import types from solders.transaction import Transaction -from .commitment import Commitment, Finalized +from .commitment import Commitment from .core import ( _COMMITMENT_TO_SOLDERS, RPCException, @@ -999,19 +997,13 @@ def send_raw_transaction(self, txn: bytes, opts: Optional[types.TxOpts] = None) def send_transaction( self, txn: Union[VersionedTransaction, Transaction], - *signers: Keypair, opts: Optional[types.TxOpts] = None, - recent_blockhash: Optional[Blockhash] = None, ) -> SendTransactionResp: """Send a transaction. Args: txn: transaction object. - signers: Signers to sign the transaction. Only supported for legacy Transaction. opts: (optional) Transaction options. - recent_blockhash: (optional) Pass a valid recent blockhash here if you want to - skip fetching the recent blockhash or relying on the cache. - Only supported for legacy Transaction. Example: >>> from solders.keypair import Keypair @@ -1021,43 +1013,14 @@ def send_transaction( >>> from solders.message import Message >>> leading_zeros = [0] * 31 >>> sender, receiver = Keypair.from_seed(leading_zeros + [1]), Keypair.from_seed(leading_zeros + [2]) - >>> msg = Message([transfer(TransferParams( - ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))]) - >>> solana_client = Client("http://localhost:8899") - >>> solana_client.send_transaction(txn, sender).value # doctest: +SKIP - Signature( - 1111111111111111111111111111111111111111111111111111111111111111, - ) - """ - if isinstance(txn, VersionedTransaction): - if signers: - msg = "*signers args are not used when sending VersionedTransaction." - raise ValueError(msg) - if recent_blockhash is not None: - msg = "recent_blockhash arg is not used when sending VersionedTransaction." - raise ValueError(msg) - versioned_tx_opts = types.TxOpts(preflight_commitment=self._commitment) if opts is None else opts - return self.send_raw_transaction(bytes(txn), opts=versioned_tx_opts) - last_valid_block_height = None - if recent_blockhash is None: - blockhash_resp = self.get_latest_blockhash(Finalized) - recent_blockhash = self.parse_recent_blockhash(blockhash_resp) - last_valid_block_height = blockhash_resp.value.last_valid_block_height - - txn.recent_blockhash = recent_blockhash - - txn.sign(*signers) - opts_to_use = ( - types.TxOpts( - preflight_commitment=self._commitment, - last_valid_block_height=last_valid_block_height, - ) - if opts is None - else opts - ) - - txn_resp = self.send_raw_transaction(txn.serialize(), opts=opts_to_use) - return txn_resp + >>> ixns = [transfer(TransferParams( + ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))] + >>> msg = Message(ixns, sender.pubkey()) + >>> client = Client("http://localhost:8899") + >>> client.send_transaction(Transaction([sender], msg, client.get_latest_blockhash()).value.blockhash) # doctest: +SKIP + """ # noqa: E501 + tx_opts = types.TxOpts(preflight_commitment=self._commitment) if opts is None else opts + return self.send_raw_transaction(bytes(txn), opts=tx_opts) def simulate_transaction( self, From a791b4c5c3c1dd93d60e6d2e623f63d993e881d5 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 15:00:15 +0400 Subject: [PATCH 05/23] update _simulate_transaction_body --- src/solana/rpc/core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/solana/rpc/core.py b/src/solana/rpc/core.py index d2990e03..c84d810b 100644 --- a/src/solana/rpc/core.py +++ b/src/solana/rpc/core.py @@ -499,9 +499,7 @@ def _simulate_transaction_body( commitment_to_use = _COMMITMENT_TO_SOLDERS[commitment or self._commitment] config = RpcSimulateTransactionConfig(sig_verify=sig_verify, commitment=commitment_to_use) if isinstance(txn, Transaction): - if txn.recent_blockhash is None: - raise ValueError("transaction must have a valid blockhash") - return SimulateLegacyTransaction(txn.to_solders(), config) + return SimulateLegacyTransaction(txn, config) return SimulateVersionedTransaction(txn, config) @staticmethod From aefa01c77e8f25b378470d5a17c79bed26940988 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 15:03:30 +0400 Subject: [PATCH 06/23] update token client to use solders transaction --- src/spl/token/async_client.py | 149 ++++++++++++++----- src/spl/token/client.py | 143 +++++++++++++----- src/spl/token/core.py | 263 ++++++++++++++++++---------------- 3 files changed, 355 insertions(+), 200 deletions(-) diff --git a/src/spl/token/async_client.py b/src/spl/token/async_client.py index 640d67a9..23e12be7 100644 --- a/src/spl/token/async_client.py +++ b/src/spl/token/async_client.py @@ -206,7 +206,10 @@ async def create_mint( # Allocate memory for the account balance_needed = await AsyncToken.get_min_balance_rent_for_exempt_for_mint(conn) # Construct transaction - token, txn, payer, mint_account, opts = _TokenCore._create_mint_args( + recent_blockhash_to_use = ( + (await conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + token, txn, opts = _TokenCore._create_mint_args( conn, payer, mint_authority, @@ -217,9 +220,10 @@ async def create_mint( balance_needed, cls, conn.commitment, + recent_blockhash_to_use, ) # Send the two instructions - await conn.send_transaction(txn, payer, mint_account, opts=opts, recent_blockhash=recent_blockhash) + await conn.send_transaction(txn, opts=opts) return cast(AsyncToken, token) async def create_account( @@ -244,11 +248,14 @@ async def create_account( or until the transaction is confirmed. """ balance_needed = await AsyncToken.get_min_balance_rent_for_exempt_for_account(self._conn) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) new_account_pk, txn, payer, new_account, opts = self._create_account_args( - owner, skip_confirmation, balance_needed, self._conn.commitment + owner, skip_confirmation, balance_needed, self._conn.commitment, recent_blockhash_to_use ) # Send the two instructions - await self._conn.send_transaction(txn, payer, new_account, opts=opts, recent_blockhash=recent_blockhash) + await self._conn.send_transaction(txn, opts=opts) return new_account_pk async def create_associated_token_account( @@ -271,10 +278,13 @@ async def create_associated_token_account( or until the transaction is confirmed. """ # Construct transaction + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) public_key, txn, payer, opts = self._create_associated_token_account_args( - owner, skip_confirmation, self._conn.commitment + owner, skip_confirmation, self._conn.commitment, recent_blockhash_to_use ) - await self._conn.send_transaction(txn, payer, opts=opts, recent_blockhash=recent_blockhash) + await self._conn.send_transaction(txn, opts=opts) return public_key @staticmethod @@ -306,6 +316,9 @@ async def create_wrapped_native_account( """ # Allocate memory for the account balance_needed = await AsyncToken.get_min_balance_rent_for_exempt_for_account(conn) + recent_blockhash_to_use = ( + (await conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) (new_account_public_key, txn, payer, new_account, opts,) = _TokenCore._create_wrapped_native_account_args( program_id, owner, @@ -314,8 +327,9 @@ async def create_wrapped_native_account( skip_confirmation, balance_needed, conn.commitment, + recent_blockhash_to_use, ) - await conn.send_transaction(txn, payer, new_account, opts=opts, recent_blockhash=recent_blockhash) + await conn.send_transaction(txn, opts=opts) return new_account_public_key async def create_multisig( @@ -337,9 +351,12 @@ async def create_multisig( Public key of the new multisig account. """ balance_needed = await AsyncToken.get_min_balance_rent_for_exempt_for_multisig(self._conn) - txn, payer, multisig = self._create_multisig_args(m, multi_signers, balance_needed) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, payer, multisig = self._create_multisig_args(m, multi_signers, balance_needed, recent_blockhash_to_use) opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - await self._conn.send_transaction(txn, payer, multisig, opts=opts_to_use, recent_blockhash=recent_blockhash) + await self._conn.send_transaction(txn, opts=opts_to_use) return multisig.pubkey() async def get_mint_info(self) -> MintInfo: @@ -374,8 +391,13 @@ async def transfer( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._transfer_args(source, dest, owner, amount, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._transfer_args( + source, dest, owner, amount, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return await self._conn.send_transaction(txn, opts=opts) async def approve( self, @@ -399,8 +421,13 @@ async def approve( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, payer, signers, opts = self._approve_args(source, delegate, owner, amount, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, payer, signers, opts = self._approve_args( + source, delegate, owner, amount, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return await self._conn.send_transaction(txn, opts=opts) async def revoke( self, @@ -420,8 +447,13 @@ async def revoke( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, payer, signers, opts = self._revoke_args(account, owner, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, payer, signers, opts = self._revoke_args( + account, owner, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return await self._conn.send_transaction(txn, opts=opts) async def set_authority( self, @@ -445,6 +477,9 @@ async def set_authority( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) txn, payer, signers, opts = self._set_authority_args( account, current_authority, @@ -452,8 +487,9 @@ async def set_authority( new_authority, multi_signers, opts_to_use, + recent_blockhash_to_use, ) - return await self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) + return await self._conn.send_transaction(txn, opts=opts) async def mint_to( self, @@ -478,8 +514,13 @@ async def mint_to( or until the transaction is confirmed. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._mint_to_args(dest, mint_authority, amount, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._mint_to_args( + dest, mint_authority, amount, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return await self._conn.send_transaction(txn, opts=opts) async def burn( self, @@ -501,8 +542,11 @@ async def burn( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._burn_args(account, owner, amount, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._burn_args(account, owner, amount, multi_signers, opts_to_use, recent_blockhash_to_use) + return await self._conn.send_transaction(txn, opts=opts) async def close_account( self, @@ -524,8 +568,13 @@ async def close_account( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._close_account_args(account, dest, authority, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._close_account_args( + account, dest, authority, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return await self._conn.send_transaction(txn, opts=opts) async def freeze_account( self, @@ -545,8 +594,11 @@ async def freeze_account( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._freeze_account_args(account, authority, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._freeze_account_args(account, authority, multi_signers, opts_to_use, recent_blockhash_to_use) + return await self._conn.send_transaction(txn, opts=opts) async def thaw_account( self, @@ -566,8 +618,11 @@ async def thaw_account( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._thaw_account_args(account, authority, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._thaw_account_args(account, authority, multi_signers, opts_to_use, recent_blockhash_to_use) + return await self._conn.send_transaction(txn, opts=opts) async def transfer_checked( self, @@ -593,10 +648,13 @@ async def transfer_checked( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._transfer_checked_args( - source, dest, owner, amount, decimals, multi_signers, opts_to_use + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash ) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + txn, opts = self._transfer_checked_args( + source, dest, owner, amount, decimals, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return await self._conn.send_transaction(txn, opts=opts) async def approve_checked( self, @@ -624,10 +682,13 @@ async def approve_checked( recent_blockhash (optional): A prefetched blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, payer, signers, opts = self._approve_checked_args( - source, delegate, owner, amount, decimals, multi_signers, opts_to_use + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._approve_checked_args( + source, delegate, owner, amount, decimals, multi_signers, opts_to_use, recent_blockhash_to_use ) - return await self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) + return await self._conn.send_transaction(txn, opts=opts) async def mint_to_checked( self, @@ -651,10 +712,13 @@ async def mint_to_checked( recent_blockhash (optional): A prefetched blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._mint_to_checked_args( - dest, mint_authority, amount, decimals, multi_signers, opts_to_use + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash ) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + txn, opts = self._mint_to_checked_args( + dest, mint_authority, amount, decimals, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return await self._conn.send_transaction(txn, opts=opts) async def burn_checked( self, @@ -678,5 +742,16 @@ async def burn_checked( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._burn_checked_args(account, owner, amount, decimals, multi_signers, opts_to_use) - return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._burn_checked_args( + account, + owner, + amount, + decimals, + multi_signers, + opts_to_use, + recent_blockhash_to_use, + ) + return await self._conn.send_transaction(txn, opts=opts) diff --git a/src/spl/token/client.py b/src/spl/token/client.py index e44f5309..7bcaa6e0 100644 --- a/src/spl/token/client.py +++ b/src/spl/token/client.py @@ -205,7 +205,10 @@ def create_mint( # Allocate memory for the account balance_needed = Token.get_min_balance_rent_for_exempt_for_mint(conn) # Construct transaction - token, txn, payer, mint_account, opts = _TokenCore._create_mint_args( + recent_blockhash_to_use = ( + conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + token, txn, opts = _TokenCore._create_mint_args( conn, payer, mint_authority, @@ -216,9 +219,10 @@ def create_mint( balance_needed, cls, conn.commitment, + recent_blockhash_to_use, ) # Send the two instructions - conn.send_transaction(txn, payer, mint_account, opts=opts, recent_blockhash=recent_blockhash) + conn.send_transaction(txn, opts=opts) return cast(Token, token) def create_account( @@ -243,11 +247,14 @@ def create_account( or until the transaction is confirmed. """ balance_needed = Token.get_min_balance_rent_for_exempt_for_account(self._conn) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) new_account_pk, txn, payer, new_account, opts = self._create_account_args( - owner, skip_confirmation, balance_needed, self._conn.commitment + owner, skip_confirmation, balance_needed, self._conn.commitment, recent_blockhash_to_use ) # Send the two instructions - self._conn.send_transaction(txn, payer, new_account, opts=opts, recent_blockhash=recent_blockhash) + self._conn.send_transaction(txn, opts=opts) return new_account_pk def create_associated_token_account( @@ -270,10 +277,13 @@ def create_associated_token_account( or until the transaction is confirmed. """ # Construct transaction + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) public_key, txn, payer, opts = self._create_associated_token_account_args( - owner, skip_confirmation, self._conn.commitment + owner, skip_confirmation, self._conn.commitment, recent_blockhash_to_use ) - self._conn.send_transaction(txn, payer, opts=opts, recent_blockhash=recent_blockhash) + self._conn.send_transaction(txn, opts=opts) return public_key @staticmethod @@ -305,6 +315,9 @@ def create_wrapped_native_account( """ # Allocate memory for the account balance_needed = Token.get_min_balance_rent_for_exempt_for_account(conn) + recent_blockhash_to_use = ( + conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) (new_account_public_key, txn, payer, new_account, opts,) = _TokenCore._create_wrapped_native_account_args( program_id, owner, @@ -313,8 +326,9 @@ def create_wrapped_native_account( skip_confirmation, balance_needed, conn.commitment, + recent_blockhash_to_use, ) - conn.send_transaction(txn, payer, new_account, opts=opts, recent_blockhash=recent_blockhash) + conn.send_transaction(txn, opts=opts) return new_account_public_key def create_multisig( @@ -336,9 +350,12 @@ def create_multisig( Public key of the new multisig account. """ balance_needed = Token.get_min_balance_rent_for_exempt_for_multisig(self._conn) - txn, payer, multisig = self._create_multisig_args(m, multi_signers, balance_needed) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, payer, multisig = self._create_multisig_args(m, multi_signers, balance_needed, recent_blockhash_to_use) opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - self._conn.send_transaction(txn, payer, multisig, opts=opts_to_use, recent_blockhash=recent_blockhash) + self._conn.send_transaction(txn, opts=opts_to_use) return multisig.pubkey() def get_mint_info(self) -> MintInfo: @@ -373,8 +390,13 @@ def transfer( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._transfer_args(source, dest, owner, amount, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._transfer_args( + source, dest, owner, amount, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return self._conn.send_transaction(txn, opts=opts) def approve( self, @@ -398,8 +420,13 @@ def approve( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, payer, signers, opts = self._approve_args(source, delegate, owner, amount, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, payer, signers, opts = self._approve_args( + source, delegate, owner, amount, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return self._conn.send_transaction(txn, opts=opts) def revoke( self, @@ -419,8 +446,13 @@ def revoke( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, payer, signers, opts = self._revoke_args(account, owner, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, payer, signers, opts = self._revoke_args( + account, owner, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return self._conn.send_transaction(txn, opts=opts) def set_authority( self, @@ -444,6 +476,9 @@ def set_authority( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) txn, payer, signers, opts = self._set_authority_args( account, current_authority, @@ -451,8 +486,9 @@ def set_authority( new_authority, multi_signers, opts_to_use, + recent_blockhash_to_use, ) - return self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) + return self._conn.send_transaction(txn, opts=opts) def mint_to( self, @@ -477,8 +513,13 @@ def mint_to( or until the transaction is confirmed. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._mint_to_args(dest, mint_authority, amount, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._mint_to_args( + dest, mint_authority, amount, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return self._conn.send_transaction(txn, opts=opts) def burn( self, @@ -500,8 +541,11 @@ def burn( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._burn_args(account, owner, amount, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._burn_args(account, owner, amount, multi_signers, opts_to_use, recent_blockhash_to_use) + return self._conn.send_transaction(txn, opts=opts) def close_account( self, @@ -523,8 +567,13 @@ def close_account( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._close_account_args(account, dest, authority, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._close_account_args( + account, dest, authority, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return self._conn.send_transaction(txn, opts=opts) def freeze_account( self, @@ -544,8 +593,11 @@ def freeze_account( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._freeze_account_args(account, authority, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._freeze_account_args(account, authority, multi_signers, opts_to_use, recent_blockhash_to_use) + return self._conn.send_transaction(txn, opts=opts) def thaw_account( self, @@ -565,8 +617,11 @@ def thaw_account( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._thaw_account_args(account, authority, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._thaw_account_args(account, authority, multi_signers, opts_to_use, recent_blockhash_to_use) + return self._conn.send_transaction(txn, opts=opts) def transfer_checked( self, @@ -592,10 +647,13 @@ def transfer_checked( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._transfer_checked_args( - source, dest, owner, amount, decimals, multi_signers, opts_to_use + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._transfer_checked_args( + source, dest, owner, amount, decimals, multi_signers, opts_to_use, recent_blockhash_to_use ) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + return self._conn.send_transaction(txn, opts=opts) def approve_checked( self, @@ -623,10 +681,13 @@ def approve_checked( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, payer, signers, opts = self._approve_checked_args( - source, delegate, owner, amount, decimals, multi_signers, opts_to_use + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._approve_checked_args( + source, delegate, owner, amount, decimals, multi_signers, opts_to_use, recent_blockhash_to_use ) - return self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) + return self._conn.send_transaction(txn, opts=opts) def mint_to_checked( self, @@ -650,10 +711,13 @@ def mint_to_checked( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._mint_to_checked_args( - dest, mint_authority, amount, decimals, multi_signers, opts_to_use + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash ) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + txn, opts = self._mint_to_checked_args( + dest, mint_authority, amount, decimals, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return self._conn.send_transaction(txn, opts=opts) def burn_checked( self, @@ -677,5 +741,10 @@ def burn_checked( recent_blockhash: (optional) a prefetched Blockhash for the transaction. """ opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts - txn, signers, opts = self._burn_checked_args(account, owner, amount, decimals, multi_signers, opts_to_use) - return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) + recent_blockhash_to_use = ( + self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash + ) + txn, opts = self._burn_checked_args( + account, owner, amount, decimals, multi_signers, opts_to_use, recent_blockhash_to_use + ) + return self._conn.send_transaction(txn, opts=opts) diff --git a/src/spl/token/core.py b/src/spl/token/core.py index 0b1eb9b6..794e1336 100644 --- a/src/spl/token/core.py +++ b/src/spl/token/core.py @@ -14,6 +14,8 @@ from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Commitment from solana.rpc.types import TokenAccountOpts, TxOpts +from solders.hash import Hash as Blockhash +from solders.message import Message from solders.transaction import Transaction from spl.token._layouts import ACCOUNT_LAYOUT, MINT_LAYOUT, MULTISIG_LAYOUT # type: ignore from spl.token.constants import WRAPPED_SOL_MINT @@ -113,12 +115,12 @@ def _create_mint_args( balance_needed: int, cls: Union[Type[Token], Type[AsyncToken]], commitment: Commitment, - ) -> Tuple[Union[Token, AsyncToken], Transaction, Keypair, Keypair, TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Union[Token, AsyncToken], Transaction, TxOpts]: mint_keypair = Keypair() token = cls(conn, mint_keypair.pubkey(), program_id, payer) # type: ignore # Construct transaction - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( + ixs = [ sp.create_account( sp.CreateAccountParams( from_pubkey=payer.pubkey(), @@ -127,9 +129,7 @@ def _create_mint_args( space=MINT_LAYOUT.sizeof(), owner=program_id, ) - ) - ) - txn.add( + ), spl_token.initialize_mint( spl_token.InitializeMintParams( program_id=program_id, @@ -138,13 +138,13 @@ def _create_mint_args( mint_authority=mint_authority, freeze_authority=freeze_authority, ) - ) - ) + ), + ] + msg = Message.new_with_blockhash(ixs, payer.pubkey(), recent_blockhash) + txn = Transaction([payer, mint_keypair], msg, recent_blockhash) return ( token, txn, - payer, - mint_keypair, TxOpts(skip_confirmation=skip_confirmation, preflight_commitment=commitment), ) @@ -154,13 +154,13 @@ def _create_account_args( skip_confirmation: bool, balance_needed: int, commitment: Commitment, + recent_blockhash: Blockhash, ) -> Tuple[Pubkey, Transaction, Keypair, Keypair, TxOpts]: new_keypair = Keypair() # Allocate memory for the account # Construct transaction - txn = Transaction(fee_payer=self.payer.pubkey()) - txn.add( + ixs = [ sp.create_account( sp.CreateAccountParams( from_pubkey=self.payer.pubkey(), @@ -169,9 +169,7 @@ def _create_account_args( space=ACCOUNT_LAYOUT.sizeof(), owner=self.program_id, ) - ) - ) - txn.add( + ), spl_token.initialize_account( spl_token.InitializeAccountParams( account=new_keypair.pubkey(), @@ -179,8 +177,10 @@ def _create_account_args( owner=owner, program_id=self.program_id, ) - ) - ) + ), + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) return ( new_keypair.pubkey(), txn, @@ -190,18 +190,15 @@ def _create_account_args( ) def _create_associated_token_account_args( - self, - owner: Pubkey, - skip_confirmation: bool, - commitment: Commitment, + self, owner: Pubkey, skip_confirmation: bool, commitment: Commitment, recent_blockhash: Blockhash ) -> Tuple[Pubkey, Transaction, Keypair, TxOpts]: # Construct transaction - txn = Transaction(fee_payer=self.payer.pubkey()) - create_txn = spl_token.create_associated_token_account(payer=self.payer.pubkey(), owner=owner, mint=self.pubkey) - txn.add(create_txn) + ix = spl_token.create_associated_token_account(payer=self.payer.pubkey(), owner=owner, mint=self.pubkey) + msg = Message.new_with_blockhash([ix], self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) return ( - create_txn.accounts[1].pubkey, + ix.accounts[1].pubkey, txn, self.payer, TxOpts(skip_confirmation=skip_confirmation, preflight_commitment=commitment), @@ -216,12 +213,12 @@ def _create_wrapped_native_account_args( skip_confirmation: bool, balance_needed: int, commitment: Commitment, + recent_blockhash: Blockhash, ) -> Tuple[Pubkey, Transaction, Keypair, Keypair, TxOpts]: new_keypair = Keypair() # Allocate memory for the account # Construct transaction - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( + ixs = [ sp.create_account( sp.CreateAccountParams( from_pubkey=payer.pubkey(), @@ -230,20 +227,14 @@ def _create_wrapped_native_account_args( space=ACCOUNT_LAYOUT.sizeof(), owner=program_id, ) - ) - ) - - txn.add( + ), sp.transfer( sp.TransferParams( from_pubkey=payer.pubkey(), to_pubkey=new_keypair.pubkey(), lamports=amount, ) - ) - ) - - txn.add( + ), spl_token.initialize_account( spl_token.InitializeAccountParams( account=new_keypair.pubkey(), @@ -251,8 +242,10 @@ def _create_wrapped_native_account_args( owner=owner, program_id=program_id, ) - ) - ) + ), + ] + msg = Message.new_with_blockhash(ixs, payer.pubkey(), recent_blockhash) + txn = Transaction([payer], msg, recent_blockhash) return ( new_keypair.pubkey(), @@ -270,15 +263,15 @@ def _transfer_args( amount: int, multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(owner, Keypair): owner_pubkey = owner.pubkey() signers = [owner] else: owner_pubkey = owner signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.transfer( spl_token.TransferParams( program_id=self.program_id, @@ -289,8 +282,10 @@ def _transfer_args( signers=[signer.pubkey() for signer in signers], ) ) - ) - return txn, signers, opts + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) + return txn, opts def _set_authority_args( self, @@ -300,6 +295,7 @@ def _set_authority_args( new_authority: Optional[Pubkey], multi_signers: Optional[List[Keypair]], opts: TxOpts, + recent_blockhash: Blockhash, ) -> Tuple[Transaction, Keypair, List[Keypair], TxOpts]: if isinstance(current_authority, Keypair): current_authority_pubkey = current_authority.pubkey() @@ -307,8 +303,7 @@ def _set_authority_args( else: current_authority_pubkey = current_authority signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.set_authority( spl_token.SetAuthorityParams( program_id=self.program_id, @@ -319,7 +314,9 @@ def _set_authority_args( new_authority=new_authority, ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) return txn, self.payer, signers, opts @@ -330,15 +327,15 @@ def _mint_to_args( amount: int, multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(mint_authority, Keypair): owner_pubkey = mint_authority.pubkey() signers = [mint_authority] else: owner_pubkey = mint_authority signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.mint_to( spl_token.MintToParams( program_id=self.program_id, @@ -349,8 +346,10 @@ def _mint_to_args( signers=[signer.pubkey() for signer in signers], ) ) - ) - return txn, signers, opts + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) + return txn, opts def _create_mint_info(self, info: GetAccountInfoResp) -> MintInfo: value = info.value @@ -436,6 +435,7 @@ def _approve_args( amount: int, multi_signers: Optional[List[Keypair]], opts: TxOpts, + recent_blockhash: Blockhash, ) -> Tuple[Transaction, Keypair, List[Keypair], TxOpts]: if isinstance(owner, Keypair): owner_pubkey = owner.pubkey() @@ -443,8 +443,7 @@ def _approve_args( else: owner_pubkey = owner signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.approve( spl_token.ApproveParams( program_id=self.program_id, @@ -455,7 +454,9 @@ def _approve_args( signers=[signer.pubkey() for signer in signers], ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) return txn, self.payer, signers, opts def _revoke_args( @@ -464,6 +465,7 @@ def _revoke_args( owner: Union[Keypair, Pubkey], multi_signers: Optional[List[Keypair]], opts: TxOpts, + recent_blockhash: Blockhash, ) -> Tuple[Transaction, Keypair, List[Keypair], TxOpts]: if isinstance(owner, Keypair): owner_pubkey = owner.pubkey() @@ -471,8 +473,7 @@ def _revoke_args( else: owner_pubkey = owner signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.revoke( spl_token.RevokeParams( program_id=self.program_id, @@ -481,7 +482,9 @@ def _revoke_args( signers=[signer.pubkey() for signer in signers], ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) return txn, self.payer, signers, opts def _freeze_account_args( @@ -490,7 +493,8 @@ def _freeze_account_args( authority: Union[Pubkey, Keypair], multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(authority, Keypair): authority_pubkey = authority.pubkey() base_signers = [authority] @@ -498,7 +502,7 @@ def _freeze_account_args( authority_pubkey = authority base_signers = multi_signers if multi_signers else [] fee_payer_keypair = self.payer - txn = Transaction(fee_payer=fee_payer_keypair.pubkey()).add( + ixs = [ spl_token.freeze_account( spl_token.FreezeAccountParams( program_id=self.program_id, @@ -508,9 +512,11 @@ def _freeze_account_args( multi_signers=[signer.pubkey() for signer in base_signers], ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) signers = list(set(base_signers) | {fee_payer_keypair}) - return txn, signers, opts + txn = Transaction(signers, msg, recent_blockhash) + return txn, opts def _thaw_account_args( self, @@ -518,7 +524,8 @@ def _thaw_account_args( authority: Union[Pubkey, Keypair], multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(authority, Keypair): authority_pubkey = authority.pubkey() base_signers = [authority] @@ -526,7 +533,7 @@ def _thaw_account_args( authority_pubkey = authority base_signers = multi_signers if multi_signers else [] fee_payer_keypair = self.payer - txn = Transaction(fee_payer=fee_payer_keypair.pubkey()).add( + ixs = [ spl_token.thaw_account( spl_token.ThawAccountParams( program_id=self.program_id, @@ -536,9 +543,11 @@ def _thaw_account_args( multi_signers=[signer.pubkey() for signer in base_signers], ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) signers = list(set(base_signers) | {fee_payer_keypair}) - return txn, signers, opts + txn = Transaction(signers, msg, recent_blockhash) + return txn, opts def _close_account_args( self, @@ -547,15 +556,15 @@ def _close_account_args( authority: Union[Pubkey, Keypair], multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(authority, Keypair): authority_pubkey = authority.pubkey() signers = [authority] else: authority_pubkey = authority signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.close_account( spl_token.CloseAccountParams( program_id=self.program_id, @@ -565,8 +574,10 @@ def _close_account_args( signers=[signer.pubkey() for signer in signers], ) ) - ) - return txn, signers, opts + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) + return txn, opts def _burn_args( self, @@ -575,15 +586,15 @@ def _burn_args( amount: int, multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(owner, Keypair): owner_pubkey = owner.pubkey() signers = [owner] else: owner_pubkey = owner signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.burn( spl_token.BurnParams( program_id=self.program_id, @@ -594,19 +605,16 @@ def _burn_args( signers=[signer.pubkey() for signer in signers], ) ) - ) - return txn, signers, opts + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) + return txn, opts def _create_multisig_args( - self, - m: int, - signers: List[Pubkey], - balance_needed: int, + self, m: int, signers: List[Pubkey], balance_needed: int, recent_blockhash: Blockhash ) -> Tuple[Transaction, Keypair, Keypair]: multisig_keypair = Keypair() - - txn = Transaction(fee_payer=self.payer.pubkey()) - txn.add( + ixs = [ sp.create_account( sp.CreateAccountParams( from_pubkey=self.payer.pubkey(), @@ -615,9 +623,7 @@ def _create_multisig_args( space=MULTISIG_LAYOUT.sizeof(), owner=self.program_id, ) - ) - ) - txn.add( + ), spl_token.initialize_multisig( spl_token.InitializeMultisigParams( program_id=self.program_id, @@ -625,9 +631,10 @@ def _create_multisig_args( m=m, signers=signers, ) - ) - ) - + ), + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) return txn, self.payer, multisig_keypair def _transfer_checked_args( @@ -639,15 +646,15 @@ def _transfer_checked_args( decimals: int, multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(owner, Keypair): owner_pubkey = owner.pubkey() signers = [owner] else: owner_pubkey = owner signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.transfer_checked( spl_token.TransferCheckedParams( program_id=self.program_id, @@ -660,8 +667,10 @@ def _transfer_checked_args( signers=[signer.pubkey() for signer in signers], ) ) - ) - return txn, signers, opts + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) + return txn, opts def _mint_to_checked_args( self, @@ -671,15 +680,15 @@ def _mint_to_checked_args( decimals: int, multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(mint_authority, Keypair): owner_pubkey = mint_authority.pubkey() signers = [mint_authority] else: owner_pubkey = mint_authority signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( + ixs = [ spl_token.mint_to_checked( spl_token.MintToCheckedParams( program_id=self.program_id, @@ -691,8 +700,10 @@ def _mint_to_checked_args( signers=[signer.pubkey() for signer in signers], ) ) - ) - return txn, signers, opts + ] + msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) + return txn, opts def _burn_checked_args( self, @@ -702,28 +713,28 @@ def _burn_checked_args( decimals: int, multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(owner, Keypair): owner_pubkey = owner.pubkey() signers = [owner] else: owner_pubkey = owner signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( - spl_token.burn_checked( - spl_token.BurnCheckedParams( - program_id=self.program_id, - mint=self.pubkey, - account=account, - owner=owner_pubkey, - amount=amount, - decimals=decimals, - signers=[signer.pubkey() for signer in signers], - ) + ix = spl_token.burn_checked( + spl_token.BurnCheckedParams( + program_id=self.program_id, + mint=self.pubkey, + account=account, + owner=owner_pubkey, + amount=amount, + decimals=decimals, + signers=[signer.pubkey() for signer in signers], ) ) - return txn, signers, opts + msg = Message.new_with_blockhash([ix], self.payer.pubkey(), recent_blockhash) + txn = Transaction(signers, msg, recent_blockhash) + return txn, opts def _approve_checked_args( self, @@ -734,26 +745,26 @@ def _approve_checked_args( decimals: int, multi_signers: Optional[List[Keypair]], opts: TxOpts, - ) -> Tuple[Transaction, Keypair, List[Keypair], TxOpts]: + recent_blockhash: Blockhash, + ) -> Tuple[Transaction, TxOpts]: if isinstance(owner, Keypair): owner_pubkey = owner.pubkey() signers = [owner] else: owner_pubkey = owner signers = multi_signers if multi_signers else [] - - txn = Transaction(fee_payer=self.payer.pubkey()).add( - spl_token.approve_checked( - spl_token.ApproveCheckedParams( - program_id=self.program_id, - source=source, - mint=self.pubkey, - delegate=delegate, - owner=owner_pubkey, - amount=amount, - decimals=decimals, - signers=[signer.pubkey() for signer in signers], - ) + ix = spl_token.approve_checked( + spl_token.ApproveCheckedParams( + program_id=self.program_id, + source=source, + mint=self.pubkey, + delegate=delegate, + owner=owner_pubkey, + amount=amount, + decimals=decimals, + signers=[signer.pubkey() for signer in signers], ) ) - return txn, self.payer, signers, opts + msg = Message.new_with_blockhash([ix], self.payer.pubkey(), recent_blockhash) + txn = Transaction([self.payer], msg, recent_blockhash) + return txn, opts From 12d0a6e2e7bf2420d8cc1a3013db4516236db106 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 15:03:35 +0400 Subject: [PATCH 07/23] unused import --- tests/unit/test_vote_program.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/test_vote_program.py b/tests/unit/test_vote_program.py index 9271bb5c..f24a3ad3 100644 --- a/tests/unit/test_vote_program.py +++ b/tests/unit/test_vote_program.py @@ -5,8 +5,6 @@ from solders.keypair import Keypair from solders.message import Message from solders.pubkey import Pubkey - -import solders.transaction as txlib import solana.vote_program as vp From 5742d5ab8bac4fff175439cd91f706c66848de11 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 15:26:59 +0400 Subject: [PATCH 08/23] update integration tests to use solders transaction --- tests/integration/test_async_http_client.py | 51 +++++++++++++++------ tests/integration/test_http_client.py | 50 ++++++++++++++------ tests/integration/test_memo.py | 6 ++- tests/integration/test_websockets.py | 8 ++-- 4 files changed, 81 insertions(+), 34 deletions(-) diff --git a/tests/integration/test_async_http_client.py b/tests/integration/test_async_http_client.py index bbd54097..4c80634f 100644 --- a/tests/integration/test_async_http_client.py +++ b/tests/integration/test_async_http_client.py @@ -4,7 +4,7 @@ import pytest import solders.system_program as sp from solders.keypair import Keypair -from solders.message import MessageV0 +from solders.message import MessageV0, Message from solders.pubkey import Pubkey from solders.rpc.errors import SendTransactionPreflightFailureMessage from solders.rpc.requests import GetBlockHeight, GetFirstAvailableBlock @@ -68,13 +68,16 @@ async def test_send_transaction_and_get_balance( ): """Test sending a transaction to localnet.""" # Create transfer tx to transfer lamports from stubbed sender to async_stubbed_receiver - transfer_tx = Transaction().add( + ixs = [ sp.transfer( sp.TransferParams( from_pubkey=async_stubbed_sender.pubkey(), to_pubkey=async_stubbed_receiver, lamports=1000 ) ) - ) + ] + blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash + msg = Message.new_with_blockhash(ixs, async_stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([async_stubbed_sender], msg, blockhash) resp = await test_http_client_async.send_transaction(transfer_tx, async_stubbed_sender) assert_valid_response(resp) # Confirm transaction @@ -132,13 +135,16 @@ async def test_send_bad_transaction(stubbed_receiver: Pubkey, test_http_client_a balance = await test_http_client_async.get_balance(poor_account.pubkey()) assert balance.value == airdrop_amount # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction().add( + blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash + ixs = [ sp.transfer( sp.TransferParams( from_pubkey=poor_account.pubkey(), to_pubkey=stubbed_receiver, lamports=airdrop_amount + 1 ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, poor_account.pubkey(), blockhash) + transfer_tx = Transaction([poor_account], msg, blockhash) with pytest.raises(RPCException) as exc_info: await test_http_client_async.send_transaction(transfer_tx, poor_account) err = exc_info.value.args[0] @@ -152,7 +158,8 @@ async def test_send_transaction_prefetched_blockhash( ): """Test sending a transaction to localnet.""" # Create transfer tx to transfer lamports from stubbed sender to async_stubbed_receiver - transfer_tx = Transaction().add( + blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash + ixs = [ sp.transfer( sp.TransferParams( from_pubkey=async_stubbed_sender_prefetched_blockhash.pubkey(), @@ -160,7 +167,9 @@ async def test_send_transaction_prefetched_blockhash( lamports=1000, ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, async_stubbed_sender_prefetched_blockhash.pubkey(), blockhash) + transfer_tx = Transaction([async_stubbed_sender_prefetched_blockhash], msg, blockhash) resp = await test_http_client_async.send_transaction(transfer_tx, async_stubbed_sender_prefetched_blockhash) assert_valid_response(resp) # Confirm transaction @@ -185,13 +194,16 @@ async def test_send_raw_transaction_and_get_balance( recent_blockhash = resp.value.blockhash assert recent_blockhash is not None # Create transfer tx transfer lamports from stubbed sender to async_stubbed_receiver - transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash + ixs = [ sp.transfer( sp.TransferParams( from_pubkey=async_stubbed_sender.pubkey(), to_pubkey=async_stubbed_receiver, lamports=1000 ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, async_stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([async_stubbed_sender], msg, blockhash) # Sign transaction transfer_tx.sign(async_stubbed_sender) # Send raw transaction @@ -220,13 +232,16 @@ async def test_send_raw_transaction_and_get_balance_using_latest_blockheight( assert recent_blockhash is not None last_valid_block_height = resp.value.last_valid_block_height # Create transfer tx transfer lamports from stubbed sender to async_stubbed_receiver - transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash + ixs = [ sp.transfer( sp.TransferParams( from_pubkey=async_stubbed_sender.pubkey(), to_pubkey=async_stubbed_receiver, lamports=1000 ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, async_stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([async_stubbed_sender], msg, blockhash) # Sign transaction transfer_tx.sign(async_stubbed_sender) # Send raw transaction @@ -255,9 +270,12 @@ async def test_confirm_expired_transaction(stubbed_sender, stubbed_receiver, tes assert recent_blockhash is not None last_valid_block_height = resp.value.last_valid_block_height - 330 # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash + ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) - ) + ] + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([stubbed_sender], msg, blockhash) # Sign transaction transfer_tx.sign(stubbed_sender) # Send raw transaction @@ -282,9 +300,12 @@ async def test_get_fee_for_transaction_message(stubbed_sender, stubbed_receiver, recent_blockhash = resp.value.blockhash assert recent_blockhash is not None # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash + ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) - ) + ] + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([stubbed_sender], msg, blockhash) # Get fee for transaction message fee_resp = await test_http_client_async.get_fee_for_message(transfer_tx.compile_message()) assert_valid_response(fee_resp) diff --git a/tests/integration/test_http_client.py b/tests/integration/test_http_client.py index de60cb45..b6bdf299 100644 --- a/tests/integration/test_http_client.py +++ b/tests/integration/test_http_client.py @@ -4,7 +4,7 @@ import pytest import solders.system_program as sp from solders.keypair import Keypair -from solders.message import MessageV0 +from solders.message import MessageV0, Message from solders.pubkey import Pubkey from solders.rpc.errors import SendTransactionPreflightFailureMessage from solders.rpc.requests import GetBlockHeight, GetFirstAvailableBlock @@ -62,9 +62,12 @@ def test_request_air_drop_prefetched_blockhash( def test_send_transaction_and_get_balance(stubbed_sender, stubbed_receiver, test_http_client: Client): """Test sending a transaction to localnet.""" # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction().add( + blockhash = test_http_client.get_latest_blockhash().value.blockhash + ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) - ) + ] + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([stubbed_sender], msg, blockhash) sim_resp = test_http_client.simulate_transaction(transfer_tx) assert_valid_response(sim_resp) resp = test_http_client.send_transaction(transfer_tx, stubbed_sender) @@ -120,13 +123,16 @@ def test_send_bad_transaction(stubbed_receiver: Pubkey, test_http_client: Client balance = test_http_client.get_balance(poor_account.pubkey()) assert balance.value == airdrop_amount # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction().add( + blockhash = test_http_client.get_latest_blockhash().value.blockhash + ixs = [ sp.transfer( sp.TransferParams( from_pubkey=poor_account.pubkey(), to_pubkey=stubbed_receiver, lamports=airdrop_amount + 1 ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, poor_account.pubkey(), blockhash) + transfer_tx = Transaction([poor_account], msg, blockhash) with pytest.raises(RPCException) as exc_info: test_http_client.send_transaction(transfer_tx, poor_account) err = exc_info.value.args[0] @@ -140,7 +146,8 @@ def test_send_transaction_prefetched_blockhash( ): """Test sending a transaction to localnet.""" # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction().add( + blockhash = test_http_client.get_latest_blockhash().value.blockhash + ixs = [ sp.transfer( sp.TransferParams( from_pubkey=stubbed_sender_prefetched_blockhash.pubkey(), @@ -148,7 +155,9 @@ def test_send_transaction_prefetched_blockhash( lamports=1000, ) ) - ) + ] + msg = Message.new_with_blockhash(ixs, stubbed_sender_prefetched_blockhash.pubkey(), blockhash) + transfer_tx = Transaction([stubbed_sender_prefetched_blockhash], msg, blockhash) recent_blockhash = test_http_client.parse_recent_blockhash(test_http_client.get_latest_blockhash()) resp = test_http_client.send_transaction( transfer_tx, stubbed_sender_prefetched_blockhash, recent_blockhash=recent_blockhash @@ -174,9 +183,12 @@ def test_send_raw_transaction_and_get_balance(stubbed_sender, stubbed_receiver, recent_blockhash = resp.value.blockhash assert recent_blockhash is not None # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + blockhash = test_http_client.get_latest_blockhash().value.blockhash + ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) - ) + ] + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([stubbed_sender], msg, blockhash) # Sign transaction transfer_tx.sign(stubbed_sender) # Send raw transaction @@ -205,9 +217,12 @@ def test_send_raw_transaction_and_get_balance_using_latest_blockheight( assert recent_blockhash is not None last_valid_block_height = resp.value.last_valid_block_height # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + blockhash = test_http_client.get_latest_blockhash().value.blockhash + ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) - ) + ] + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([stubbed_sender], msg, blockhash) # Sign transaction transfer_tx.sign(stubbed_sender) # Send raw transaction @@ -236,9 +251,11 @@ def test_confirm_expired_transaction(stubbed_sender, stubbed_receiver, test_http assert recent_blockhash is not None last_valid_block_height = resp.value.last_valid_block_height - 330 # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) - ) + ] + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), recent_blockhash) + transfer_tx = Transaction([stubbed_sender], msg, recent_blockhash) # Sign transaction transfer_tx.sign(stubbed_sender) # Send raw transaction @@ -261,9 +278,12 @@ def test_get_fee_for_transaction(stubbed_sender, stubbed_receiver, test_http_cli recent_blockhash = resp.value.blockhash assert recent_blockhash is not None # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver - transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + blockhash = test_http_client.get_latest_blockhash().value.blockhash + ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) - ) + ] + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([stubbed_sender], msg, blockhash) # get fee for transaction fee_resp = test_http_client.get_fee_for_message(transfer_tx.compile_message()) assert_valid_response(fee_resp) diff --git a/tests/integration/test_memo.py b/tests/integration/test_memo.py index 07bda4f4..40f8c00e 100644 --- a/tests/integration/test_memo.py +++ b/tests/integration/test_memo.py @@ -1,6 +1,7 @@ """Tests for the Memo program.""" import pytest from solders.keypair import Keypair +from solders.message import Message from solders.transaction_status import ParsedInstruction from spl.memo.constants import MEMO_PROGRAM_ID from spl.memo.instructions import MemoParams, create_memo @@ -27,7 +28,10 @@ def test_send_memo_in_transaction(stubbed_sender: Keypair, test_http_client: Cli message=message, ) # Create transfer tx to add memo to transaction from stubbed sender - transfer_tx = Transaction().add(create_memo(memo_params)) + blockhash = test_http_client.get_latest_blockhash().value.blockhash + ixs = [create_memo(memo_params)] + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) + transfer_tx = Transaction([stubbed_sender], msg, blockhash) resp = test_http_client.send_transaction(transfer_tx, stubbed_sender) assert_valid_response(resp) txn_id = resp.value diff --git a/tests/integration/test_websockets.py b/tests/integration/test_websockets.py index fdd7daf9..4e9d076f 100644 --- a/tests/integration/test_websockets.py +++ b/tests/integration/test_websockets.py @@ -6,6 +6,7 @@ import pytest from solders import system_program as sp from solders.keypair import Keypair +from solders.message import Message from solders.pubkey import Pubkey from solders.rpc.config import RpcTransactionLogsFilter, RpcTransactionLogsFilterMentions from solders.rpc.requests import AccountSubscribe, AccountUnsubscribe, Body, LogsSubscribe, LogsUnsubscribe @@ -283,9 +284,10 @@ async def test_program_subscribe( ): """Test program subscription.""" program, owned = program_subscribed - instruction = sp.assign(sp.AssignParams(pubkey=owned.pubkey(), owner=program.pubkey())) - transaction = Transaction() - transaction.add(instruction) + ixs = [sp.assign(sp.AssignParams(pubkey=owned.pubkey(), owner=program.pubkey()))] + blockhash = (test_http_client_async.get_latest_blockhash()).value.blockhash + msg = Message.new_with_blockhash(ixs, owned.pubkey(), blockhash) + transaction = Transaction([owned], msg, blockhash) await test_http_client_async.send_transaction(transaction, owned) main_resp = await websocket.recv() msg = main_resp[0] From 3b57d5ab898c28b2549063cf8b974ee65e6f7214 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 15:48:06 +0400 Subject: [PATCH 09/23] fix some send_transaction calls --- tests/integration/test_async_http_client.py | 12 +++++------ tests/integration/test_http_client.py | 23 ++++++++------------- tests/integration/test_memo.py | 2 +- tests/integration/test_websockets.py | 2 +- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/tests/integration/test_async_http_client.py b/tests/integration/test_async_http_client.py index 4c80634f..355e6d4b 100644 --- a/tests/integration/test_async_http_client.py +++ b/tests/integration/test_async_http_client.py @@ -78,7 +78,7 @@ async def test_send_transaction_and_get_balance( blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash msg = Message.new_with_blockhash(ixs, async_stubbed_sender.pubkey(), blockhash) transfer_tx = Transaction([async_stubbed_sender], msg, blockhash) - resp = await test_http_client_async.send_transaction(transfer_tx, async_stubbed_sender) + resp = await test_http_client_async.send_transaction(transfer_tx) assert_valid_response(resp) # Confirm transaction await test_http_client_async.confirm_transaction(resp.value) @@ -146,7 +146,7 @@ async def test_send_bad_transaction(stubbed_receiver: Pubkey, test_http_client_a msg = Message.new_with_blockhash(ixs, poor_account.pubkey(), blockhash) transfer_tx = Transaction([poor_account], msg, blockhash) with pytest.raises(RPCException) as exc_info: - await test_http_client_async.send_transaction(transfer_tx, poor_account) + await test_http_client_async.send_transaction(transfer_tx) err = exc_info.value.args[0] assert isinstance(err, SendTransactionPreflightFailureMessage) assert err.data.logs @@ -170,7 +170,7 @@ async def test_send_transaction_prefetched_blockhash( ] msg = Message.new_with_blockhash(ixs, async_stubbed_sender_prefetched_blockhash.pubkey(), blockhash) transfer_tx = Transaction([async_stubbed_sender_prefetched_blockhash], msg, blockhash) - resp = await test_http_client_async.send_transaction(transfer_tx, async_stubbed_sender_prefetched_blockhash) + resp = await test_http_client_async.send_transaction(transfer_tx) assert_valid_response(resp) # Confirm transaction await test_http_client_async.confirm_transaction(resp.value) @@ -207,7 +207,7 @@ async def test_send_raw_transaction_and_get_balance( # Sign transaction transfer_tx.sign(async_stubbed_sender) # Send raw transaction - resp = await test_http_client_async.send_raw_transaction(transfer_tx.serialize()) + resp = await test_http_client_async.send_raw_transaction(bytes(transfer_tx)) assert_valid_response(resp) # Confirm transaction resp = await test_http_client_async.confirm_transaction(resp.value) @@ -246,7 +246,7 @@ async def test_send_raw_transaction_and_get_balance_using_latest_blockheight( transfer_tx.sign(async_stubbed_sender) # Send raw transaction resp = await test_http_client_async.send_raw_transaction( - transfer_tx.serialize(), + bytes(transfer_tx), opts=TxOpts(preflight_commitment=Processed, last_valid_block_height=last_valid_block_height), ) assert_valid_response(resp) @@ -280,7 +280,7 @@ async def test_confirm_expired_transaction(stubbed_sender, stubbed_receiver, tes transfer_tx.sign(stubbed_sender) # Send raw transaction resp = await test_http_client_async.send_raw_transaction( - transfer_tx.serialize(), opts=TxOpts(skip_confirmation=True, skip_preflight=True) + bytes(transfer_tx), opts=TxOpts(skip_confirmation=True, skip_preflight=True) ) assert_valid_response(resp) # Confirm transaction diff --git a/tests/integration/test_http_client.py b/tests/integration/test_http_client.py index b6bdf299..eabd38de 100644 --- a/tests/integration/test_http_client.py +++ b/tests/integration/test_http_client.py @@ -70,7 +70,7 @@ def test_send_transaction_and_get_balance(stubbed_sender, stubbed_receiver, test transfer_tx = Transaction([stubbed_sender], msg, blockhash) sim_resp = test_http_client.simulate_transaction(transfer_tx) assert_valid_response(sim_resp) - resp = test_http_client.send_transaction(transfer_tx, stubbed_sender) + resp = test_http_client.send_transaction(transfer_tx) assert_valid_response(resp) # Confirm transaction test_http_client.confirm_transaction(resp.value) @@ -134,7 +134,7 @@ def test_send_bad_transaction(stubbed_receiver: Pubkey, test_http_client: Client msg = Message.new_with_blockhash(ixs, poor_account.pubkey(), blockhash) transfer_tx = Transaction([poor_account], msg, blockhash) with pytest.raises(RPCException) as exc_info: - test_http_client.send_transaction(transfer_tx, poor_account) + test_http_client.send_transaction(transfer_tx) err = exc_info.value.args[0] assert isinstance(err, SendTransactionPreflightFailureMessage) assert err.data.logs @@ -146,7 +146,7 @@ def test_send_transaction_prefetched_blockhash( ): """Test sending a transaction to localnet.""" # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver - blockhash = test_http_client.get_latest_blockhash().value.blockhash + recent_blockhash = test_http_client.parse_recent_blockhash(test_http_client.get_latest_blockhash()) ixs = [ sp.transfer( sp.TransferParams( @@ -156,12 +156,9 @@ def test_send_transaction_prefetched_blockhash( ) ) ] - msg = Message.new_with_blockhash(ixs, stubbed_sender_prefetched_blockhash.pubkey(), blockhash) - transfer_tx = Transaction([stubbed_sender_prefetched_blockhash], msg, blockhash) - recent_blockhash = test_http_client.parse_recent_blockhash(test_http_client.get_latest_blockhash()) - resp = test_http_client.send_transaction( - transfer_tx, stubbed_sender_prefetched_blockhash, recent_blockhash=recent_blockhash - ) + msg = Message.new_with_blockhash(ixs, stubbed_sender_prefetched_blockhash.pubkey(), recent_blockhash) + transfer_tx = Transaction([stubbed_sender_prefetched_blockhash], msg, recent_blockhash) + resp = test_http_client.send_transaction(transfer_tx) assert_valid_response(resp) # Confirm transaction test_http_client.confirm_transaction(resp.value) @@ -192,7 +189,7 @@ def test_send_raw_transaction_and_get_balance(stubbed_sender, stubbed_receiver, # Sign transaction transfer_tx.sign(stubbed_sender) # Send raw transaction - tx_resp = test_http_client.send_raw_transaction(transfer_tx.serialize()) + tx_resp = test_http_client.send_raw_transaction(bytes(transfer_tx)) assert_valid_response(tx_resp) # Confirm transaction test_http_client.confirm_transaction(tx_resp.value) @@ -227,7 +224,7 @@ def test_send_raw_transaction_and_get_balance_using_latest_blockheight( transfer_tx.sign(stubbed_sender) # Send raw transaction resp = test_http_client.send_raw_transaction( - transfer_tx.serialize(), + bytes(transfer_tx), opts=TxOpts(preflight_commitment=Processed, last_valid_block_height=last_valid_block_height), ) assert_valid_response(resp) @@ -256,11 +253,9 @@ def test_confirm_expired_transaction(stubbed_sender, stubbed_receiver, test_http ] msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), recent_blockhash) transfer_tx = Transaction([stubbed_sender], msg, recent_blockhash) - # Sign transaction - transfer_tx.sign(stubbed_sender) # Send raw transaction tx_resp = test_http_client.send_raw_transaction( - transfer_tx.serialize(), opts=TxOpts(skip_confirmation=True, skip_preflight=True) + bytes(transfer_tx), opts=TxOpts(skip_confirmation=True, skip_preflight=True) ) assert_valid_response(tx_resp) # Confirm transaction diff --git a/tests/integration/test_memo.py b/tests/integration/test_memo.py index 40f8c00e..2eabbc77 100644 --- a/tests/integration/test_memo.py +++ b/tests/integration/test_memo.py @@ -32,7 +32,7 @@ def test_send_memo_in_transaction(stubbed_sender: Keypair, test_http_client: Cli ixs = [create_memo(memo_params)] msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) transfer_tx = Transaction([stubbed_sender], msg, blockhash) - resp = test_http_client.send_transaction(transfer_tx, stubbed_sender) + resp = test_http_client.send_transaction(transfer_tx) assert_valid_response(resp) txn_id = resp.value # Txn needs to be finalized in order to parse the logs. diff --git a/tests/integration/test_websockets.py b/tests/integration/test_websockets.py index 4e9d076f..367e08e7 100644 --- a/tests/integration/test_websockets.py +++ b/tests/integration/test_websockets.py @@ -288,7 +288,7 @@ async def test_program_subscribe( blockhash = (test_http_client_async.get_latest_blockhash()).value.blockhash msg = Message.new_with_blockhash(ixs, owned.pubkey(), blockhash) transaction = Transaction([owned], msg, blockhash) - await test_http_client_async.send_transaction(transaction, owned) + await test_http_client_async.send_transaction(transaction) main_resp = await websocket.recv() msg = main_resp[0] assert isinstance(msg, ProgramNotification) From 80764119b2eb7ec80c142aa7584ac9d255b9ac5b Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 15:55:01 +0400 Subject: [PATCH 10/23] remove references to compile_message() --- src/solana/rpc/async_api.py | 2 +- tests/integration/test_async_http_client.py | 7 +++---- tests/integration/test_http_client.py | 7 +++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/solana/rpc/async_api.py b/src/solana/rpc/async_api.py index e2355ffc..c4aa4bba 100644 --- a/src/solana/rpc/async_api.py +++ b/src/solana/rpc/async_api.py @@ -423,7 +423,7 @@ async def get_fee_for_message( >>> msg = Message([transfer(TransferParams( ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))]) >>> solana_client = AsyncClient("http://localhost:8899") - >>> (await solana_client.get_fee_for_message(txn.compile_message())).value # doctest: +SKIP + >>> (await solana_client.get_fee_for_message(msg)).value # doctest: +SKIP 5000 """ body = self._get_fee_for_message_body(message, commitment) diff --git a/tests/integration/test_async_http_client.py b/tests/integration/test_async_http_client.py index 355e6d4b..8e1c3ebc 100644 --- a/tests/integration/test_async_http_client.py +++ b/tests/integration/test_async_http_client.py @@ -3,6 +3,7 @@ import pytest import solders.system_program as sp +from solders.hash import Hash from solders.keypair import Keypair from solders.message import MessageV0, Message from solders.pubkey import Pubkey @@ -300,14 +301,12 @@ async def test_get_fee_for_transaction_message(stubbed_sender, stubbed_receiver, recent_blockhash = resp.value.blockhash assert recent_blockhash is not None # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver - blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) ] - msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) - transfer_tx = Transaction([stubbed_sender], msg, blockhash) + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), Hash.default()) # Get fee for transaction message - fee_resp = await test_http_client_async.get_fee_for_message(transfer_tx.compile_message()) + fee_resp = await test_http_client_async.get_fee_for_message(msg) assert_valid_response(fee_resp) assert fee_resp.value is not None diff --git a/tests/integration/test_http_client.py b/tests/integration/test_http_client.py index eabd38de..eb3aedb7 100644 --- a/tests/integration/test_http_client.py +++ b/tests/integration/test_http_client.py @@ -3,6 +3,7 @@ import pytest import solders.system_program as sp +from solders.hash import Hash from solders.keypair import Keypair from solders.message import MessageV0, Message from solders.pubkey import Pubkey @@ -273,14 +274,12 @@ def test_get_fee_for_transaction(stubbed_sender, stubbed_receiver, test_http_cli recent_blockhash = resp.value.blockhash assert recent_blockhash is not None # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver - blockhash = test_http_client.get_latest_blockhash().value.blockhash ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) ] - msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) - transfer_tx = Transaction([stubbed_sender], msg, blockhash) + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), Hash.default()) # get fee for transaction - fee_resp = test_http_client.get_fee_for_message(transfer_tx.compile_message()) + fee_resp = test_http_client.get_fee_for_message(msg) assert_valid_response(fee_resp) assert fee_resp.value is not None From 7b04bf7963e9776c5586f3e7716c2986ba5e9f64 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 15:55:40 +0400 Subject: [PATCH 11/23] missing await --- tests/integration/test_websockets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_websockets.py b/tests/integration/test_websockets.py index 367e08e7..db8bf7f0 100644 --- a/tests/integration/test_websockets.py +++ b/tests/integration/test_websockets.py @@ -285,7 +285,7 @@ async def test_program_subscribe( """Test program subscription.""" program, owned = program_subscribed ixs = [sp.assign(sp.AssignParams(pubkey=owned.pubkey(), owner=program.pubkey()))] - blockhash = (test_http_client_async.get_latest_blockhash()).value.blockhash + blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash msg = Message.new_with_blockhash(ixs, owned.pubkey(), blockhash) transaction = Transaction([owned], msg, blockhash) await test_http_client_async.send_transaction(transaction) From dee1b3a66e0e56bfa684de1418358d1547bacc95 Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 15:59:07 +0400 Subject: [PATCH 12/23] remove unnecessary .sign() calls --- tests/integration/test_async_http_client.py | 6 ------ tests/integration/test_http_client.py | 4 ---- 2 files changed, 10 deletions(-) diff --git a/tests/integration/test_async_http_client.py b/tests/integration/test_async_http_client.py index 8e1c3ebc..dba3ff5e 100644 --- a/tests/integration/test_async_http_client.py +++ b/tests/integration/test_async_http_client.py @@ -205,8 +205,6 @@ async def test_send_raw_transaction_and_get_balance( ] msg = Message.new_with_blockhash(ixs, async_stubbed_sender.pubkey(), blockhash) transfer_tx = Transaction([async_stubbed_sender], msg, blockhash) - # Sign transaction - transfer_tx.sign(async_stubbed_sender) # Send raw transaction resp = await test_http_client_async.send_raw_transaction(bytes(transfer_tx)) assert_valid_response(resp) @@ -243,8 +241,6 @@ async def test_send_raw_transaction_and_get_balance_using_latest_blockheight( ] msg = Message.new_with_blockhash(ixs, async_stubbed_sender.pubkey(), blockhash) transfer_tx = Transaction([async_stubbed_sender], msg, blockhash) - # Sign transaction - transfer_tx.sign(async_stubbed_sender) # Send raw transaction resp = await test_http_client_async.send_raw_transaction( bytes(transfer_tx), @@ -277,8 +273,6 @@ async def test_confirm_expired_transaction(stubbed_sender, stubbed_receiver, tes ] msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) transfer_tx = Transaction([stubbed_sender], msg, blockhash) - # Sign transaction - transfer_tx.sign(stubbed_sender) # Send raw transaction resp = await test_http_client_async.send_raw_transaction( bytes(transfer_tx), opts=TxOpts(skip_confirmation=True, skip_preflight=True) diff --git a/tests/integration/test_http_client.py b/tests/integration/test_http_client.py index eb3aedb7..8e978d8a 100644 --- a/tests/integration/test_http_client.py +++ b/tests/integration/test_http_client.py @@ -187,8 +187,6 @@ def test_send_raw_transaction_and_get_balance(stubbed_sender, stubbed_receiver, ] msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) transfer_tx = Transaction([stubbed_sender], msg, blockhash) - # Sign transaction - transfer_tx.sign(stubbed_sender) # Send raw transaction tx_resp = test_http_client.send_raw_transaction(bytes(transfer_tx)) assert_valid_response(tx_resp) @@ -221,8 +219,6 @@ def test_send_raw_transaction_and_get_balance_using_latest_blockheight( ] msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), blockhash) transfer_tx = Transaction([stubbed_sender], msg, blockhash) - # Sign transaction - transfer_tx.sign(stubbed_sender) # Send raw transaction resp = test_http_client.send_raw_transaction( bytes(transfer_tx), From a5ae4f024103ab37d4c9abc69d74ee922ad1bc3e Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 16:04:24 +0400 Subject: [PATCH 13/23] try use real blockhash in get_fee_for_message tests --- tests/integration/test_async_http_client.py | 3 +-- tests/integration/test_http_client.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_async_http_client.py b/tests/integration/test_async_http_client.py index dba3ff5e..6c04ac5a 100644 --- a/tests/integration/test_async_http_client.py +++ b/tests/integration/test_async_http_client.py @@ -3,7 +3,6 @@ import pytest import solders.system_program as sp -from solders.hash import Hash from solders.keypair import Keypair from solders.message import MessageV0, Message from solders.pubkey import Pubkey @@ -298,7 +297,7 @@ async def test_get_fee_for_transaction_message(stubbed_sender, stubbed_receiver, ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) ] - msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), Hash.default()) + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), recent_blockhash) # Get fee for transaction message fee_resp = await test_http_client_async.get_fee_for_message(msg) assert_valid_response(fee_resp) diff --git a/tests/integration/test_http_client.py b/tests/integration/test_http_client.py index 8e978d8a..a9df5603 100644 --- a/tests/integration/test_http_client.py +++ b/tests/integration/test_http_client.py @@ -3,7 +3,6 @@ import pytest import solders.system_program as sp -from solders.hash import Hash from solders.keypair import Keypair from solders.message import MessageV0, Message from solders.pubkey import Pubkey @@ -273,7 +272,7 @@ def test_get_fee_for_transaction(stubbed_sender, stubbed_receiver, test_http_cli ixs = [ sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=1000)) ] - msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), Hash.default()) + msg = Message.new_with_blockhash(ixs, stubbed_sender.pubkey(), recent_blockhash) # get fee for transaction fee_resp = test_http_client.get_fee_for_message(msg) assert_valid_response(fee_resp) From 1c5466121145c03ffa1b044da57d7d4f9bfebcab Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 16:06:32 +0400 Subject: [PATCH 14/23] fix _create_account_args --- src/spl/token/async_client.py | 2 +- src/spl/token/client.py | 2 +- src/spl/token/core.py | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/spl/token/async_client.py b/src/spl/token/async_client.py index 23e12be7..f88d063d 100644 --- a/src/spl/token/async_client.py +++ b/src/spl/token/async_client.py @@ -251,7 +251,7 @@ async def create_account( recent_blockhash_to_use = ( (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash ) - new_account_pk, txn, payer, new_account, opts = self._create_account_args( + new_account_pk, txn, opts = self._create_account_args( owner, skip_confirmation, balance_needed, self._conn.commitment, recent_blockhash_to_use ) # Send the two instructions diff --git a/src/spl/token/client.py b/src/spl/token/client.py index 7bcaa6e0..8a0f75e2 100644 --- a/src/spl/token/client.py +++ b/src/spl/token/client.py @@ -250,7 +250,7 @@ def create_account( recent_blockhash_to_use = ( self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash ) - new_account_pk, txn, payer, new_account, opts = self._create_account_args( + new_account_pk, txn, opts = self._create_account_args( owner, skip_confirmation, balance_needed, self._conn.commitment, recent_blockhash_to_use ) # Send the two instructions diff --git a/src/spl/token/core.py b/src/spl/token/core.py index 794e1336..860416c7 100644 --- a/src/spl/token/core.py +++ b/src/spl/token/core.py @@ -155,7 +155,7 @@ def _create_account_args( balance_needed: int, commitment: Commitment, recent_blockhash: Blockhash, - ) -> Tuple[Pubkey, Transaction, Keypair, Keypair, TxOpts]: + ) -> Tuple[Pubkey, Transaction, TxOpts]: new_keypair = Keypair() # Allocate memory for the account @@ -180,12 +180,10 @@ def _create_account_args( ), ] msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) - txn = Transaction([self.payer], msg, recent_blockhash) + txn = Transaction([self.payer, new_keypair], msg, recent_blockhash) return ( new_keypair.pubkey(), txn, - self.payer, - new_keypair, TxOpts(skip_confirmation=skip_confirmation, preflight_commitment=commitment), ) From 37b4b759c9b6f1cabc94bc0209d432e22a35c2ed Mon Sep 17 00:00:00 2001 From: kevinheavey Date: Fri, 5 Jul 2024 16:11:07 +0400 Subject: [PATCH 15/23] fix _create_multisig_args --- src/spl/token/async_client.py | 2 +- src/spl/token/client.py | 2 +- src/spl/token/core.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spl/token/async_client.py b/src/spl/token/async_client.py index f88d063d..1ec09626 100644 --- a/src/spl/token/async_client.py +++ b/src/spl/token/async_client.py @@ -354,7 +354,7 @@ async def create_multisig( recent_blockhash_to_use = ( (await self._conn.get_latest_blockhash()).value.blockhash if recent_blockhash is None else recent_blockhash ) - txn, payer, multisig = self._create_multisig_args(m, multi_signers, balance_needed, recent_blockhash_to_use) + txn, multisig = self._create_multisig_args(m, multi_signers, balance_needed, recent_blockhash_to_use) opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts await self._conn.send_transaction(txn, opts=opts_to_use) return multisig.pubkey() diff --git a/src/spl/token/client.py b/src/spl/token/client.py index 8a0f75e2..6b372587 100644 --- a/src/spl/token/client.py +++ b/src/spl/token/client.py @@ -353,7 +353,7 @@ def create_multisig( recent_blockhash_to_use = ( self._conn.get_latest_blockhash().value.blockhash if recent_blockhash is None else recent_blockhash ) - txn, payer, multisig = self._create_multisig_args(m, multi_signers, balance_needed, recent_blockhash_to_use) + txn, multisig = self._create_multisig_args(m, multi_signers, balance_needed, recent_blockhash_to_use) opts_to_use = TxOpts(preflight_commitment=self._conn.commitment) if opts is None else opts self._conn.send_transaction(txn, opts=opts_to_use) return multisig.pubkey() diff --git a/src/spl/token/core.py b/src/spl/token/core.py index 860416c7..a4ebd81c 100644 --- a/src/spl/token/core.py +++ b/src/spl/token/core.py @@ -610,7 +610,7 @@ def _burn_args( def _create_multisig_args( self, m: int, signers: List[Pubkey], balance_needed: int, recent_blockhash: Blockhash - ) -> Tuple[Transaction, Keypair, Keypair]: + ) -> Tuple[Transaction, Keypair]: multisig_keypair = Keypair() ixs = [ sp.create_account( @@ -632,8 +632,8 @@ def _create_multisig_args( ), ] msg = Message.new_with_blockhash(ixs, self.payer.pubkey(), recent_blockhash) - txn = Transaction([self.payer], msg, recent_blockhash) - return txn, self.payer, multisig_keypair + txn = Transaction([self.payer, multisig_keypair], msg, recent_blockhash) + return txn, multisig_keypair def _transfer_checked_args( self, From e4b3a649ccdddc79d4be52008584776715a83043 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sat, 12 Oct 2024 12:19:33 -0400 Subject: [PATCH 16/23] Deprecate transaction module instead of removal --- src/solana/transaction.py | 350 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 src/solana/transaction.py diff --git a/src/solana/transaction.py b/src/solana/transaction.py new file mode 100644 index 00000000..100efa10 --- /dev/null +++ b/src/solana/transaction.py @@ -0,0 +1,350 @@ +"""Library to package an atomic sequence of instructions to a transaction.""" +from __future__ import annotations + +from typing import Any, List, NamedTuple, Optional, Sequence, Tuple, Union +from warnings import warn + +from solders.hash import Hash as Blockhash +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.message import Message +from solders.message import Message as SoldersMessage +from solders.presigner import Presigner +from solders.pubkey import Pubkey +from solders.signature import Signature +from solders.transaction import Transaction as SoldersTx +from solders.transaction import TransactionError + +PACKET_DATA_SIZE = 1280 - 40 - 8 +"""Constant for maximum over-the-wire size of a Transaction.""" + + +class NonceInformation(NamedTuple): + """NonceInformation to be used to build a Transaction.""" + + nonce: Blockhash + """The current Nonce blockhash.""" + nonce_instruction: Instruction + """AdvanceNonceAccount Instruction.""" + + +def _build_solders_tx( + recent_blockhash: Optional[Blockhash] = None, + nonce_info: Optional[NonceInformation] = None, + fee_payer: Optional[Pubkey] = None, + instructions: Optional[Sequence[Instruction]] = None, +) -> SoldersTx: + core_instructions = [] if instructions is None else instructions + underlying_instructions = ( + core_instructions if nonce_info is None else [nonce_info.nonce_instruction, *core_instructions] + ) + underlying_blockhash: Optional[Blockhash] + if nonce_info is not None: + underlying_blockhash = nonce_info.nonce + elif recent_blockhash is not None: + underlying_blockhash = recent_blockhash + else: + underlying_blockhash = None + underlying_fee_payer = None if fee_payer is None else fee_payer + underlying_blockhash = Blockhash.default() if underlying_blockhash is None else underlying_blockhash + msg = SoldersMessage.new_with_blockhash(underlying_instructions, underlying_fee_payer, underlying_blockhash) + return SoldersTx.new_unsigned(msg) + + +def _decompile_instructions(msg: SoldersMessage) -> List[Instruction]: + account_keys = msg.account_keys + decompiled_instructions: List[Instruction] = [] + for compiled_ix in msg.instructions: + program_id = account_keys[compiled_ix.program_id_index] + account_metas = [ + AccountMeta( + account_keys[idx], + is_signer=msg.is_signer(idx), + is_writable=msg.is_writable(idx), + ) + for idx in compiled_ix.accounts + ] + decompiled_instructions.append(Instruction(program_id, compiled_ix.data, account_metas)) + return decompiled_instructions + + +class Transaction: + """Transaction class to represent an atomic transaction. + + Args: + recent_blockhash: A recent transaction id. + nonce_info: Nonce information. + If populated, transaction will use a durable Nonce hash instead of a `recent_blockhash`. + fee_payer: The transaction fee payer. + instructions: The instructions to be executed in this transaction. + """ + + def __init__( + self, + recent_blockhash: Optional[Blockhash] = None, + nonce_info: Optional[NonceInformation] = None, + fee_payer: Optional[Pubkey] = None, + instructions: Optional[Sequence[Instruction]] = None, + ) -> None: + """Init transaction object.""" + warn( + "Transaction is deprecated and will be removed in a later release. Please use the Transaction modules from solders.transaction.", + DeprecationWarning, + ) + + self._solders = _build_solders_tx( + recent_blockhash=recent_blockhash, + nonce_info=nonce_info, + fee_payer=fee_payer, + instructions=instructions, + ) + + @classmethod + def from_solders(cls, txn: SoldersTx) -> Transaction: + """Convert from a `solders` transaction. + + Args: + txn: The `solders` transaction. + + Returns: + The `solana-py` transaction. + """ + new_tx = cls() + new_tx._solders = txn + return new_tx + + def to_solders(self) -> SoldersTx: + """Convert to a `solders` transaction. + + Returns: + The `solders` transaction. + """ + return self._solders + + def __eq__(self, other: Any) -> bool: + """Equality defintion for Transactions.""" + if not isinstance(other, Transaction): + return False + return self.to_solders() == other.to_solders() + + @property + def recent_blockhash(self) -> Optional[Blockhash]: + """Optional[Blockhash]: The blockhash assigned to this transaction.""" + return self._solders.message.recent_blockhash + + @recent_blockhash.setter + def recent_blockhash(self, blockhash: Optional[Blockhash]) -> None: # noqa: D102 + self._solders = _build_solders_tx( + recent_blockhash=blockhash, + nonce_info=None, + fee_payer=self.fee_payer, + instructions=self.instructions, + ) + + @property + def fee_payer(self) -> Optional[Pubkey]: + """Optional[Pubkey]: The transaction fee payer.""" + account_keys = self._solders.message.account_keys + return account_keys[0] if account_keys else None + + @fee_payer.setter + def fee_payer(self, payer: Optional[Pubkey]) -> None: # noqa: D102 + self._solders = _build_solders_tx( + recent_blockhash=self.recent_blockhash, + nonce_info=None, + fee_payer=payer, + instructions=self.instructions, + ) + + @property + def instructions(self) -> Tuple[Instruction, ...]: + """Tuple[Instruction]: The instructions contained in this transaction.""" + msg = self._solders.message + return tuple(_decompile_instructions(msg)) + + @instructions.setter + def instructions(self, ixns: Sequence[Instruction]) -> None: # noqa: D102 + self._solders = _build_solders_tx( + recent_blockhash=self.recent_blockhash, + nonce_info=None, + fee_payer=self.fee_payer, + instructions=ixns, + ) + + @property + def signatures(self) -> Tuple[Signature, ...]: + """Tuple[Signature]: Signatures for the transaction.""" + return tuple(self._solders.signatures) + + def signature(self) -> Signature: + """The first (payer) Transaction signature. + + Returns: + The payer signature. + """ + return self._solders.signatures[0] + + def add(self, *args: Union[Transaction, Instruction]) -> Transaction: + """Add one or more instructions to this Transaction. + + Args: + *args: The instructions to add to this Transaction. + If a `Transaction` is passsed, the instructions will be extracted from it. + + Returns: + The transaction with the added instructions. + """ + for arg in args: + if isinstance(arg, Transaction): + self.instructions = self.instructions + arg.instructions + elif isinstance(arg, Instruction): + self.instructions = (*self.instructions, arg) + else: + raise ValueError("invalid instruction:", arg) + + return self + + def compile_message(self) -> Message: # pylint: disable=too-many-locals + """Compile transaction data. + + Returns: + The compiled message. + """ + return self._solders.message + + def serialize_message(self) -> bytes: + """Get raw transaction data that need to be covered by signatures. + + Returns: + The serialized message. + """ + return bytes(self.compile_message()) + + def sign_partial(self, *partial_signers: Keypair) -> None: + """Partially sign a Transaction with the specified keypairs. + + All the caveats from the `sign` method apply to `sign_partial` + """ + self._solders.partial_sign(partial_signers, self._solders.message.recent_blockhash) + + def sign(self, *signers: Keypair) -> None: + """Sign the Transaction with the specified accounts. + + Multiple signatures may be applied to a Transaction. The first signature + is considered "primary" and is used when testing for Transaction confirmation. + + Transaction fields should not be modified after the first call to `sign`, + as doing so may invalidate the signature and cause the Transaction to be + rejected. + + The Transaction must be assigned a valid `recent_blockhash` before invoking this method. + """ + self._solders.sign(signers, self._solders.message.recent_blockhash) + + def add_signature(self, pubkey: Pubkey, signature: Signature) -> None: + """Add an externally created signature to a transaction. + + Args: + pubkey: The public key that created the signature. + signature: The signature to add. + """ + presigner = Presigner(pubkey, signature) + self._solders.partial_sign([presigner], self._solders.message.recent_blockhash) + + def verify_signatures(self) -> bool: + """Verify signatures of a complete, signed Transaction. + + Returns: + a bool indicating if the signatures are correct or not. + """ + try: + self._solders.verify() + except TransactionError: + return False + return True + + def serialize(self, verify_signatures: bool = True) -> bytes: + """Serialize the Transaction in the wire format. + + The Transaction must have a valid `signature` before invoking this method. + verify_signatures can be added if the signature does not require to be verified. + + Args: + verify_signatures: a bool indicating to verify the signature or not. Defaults to True + + Example: + >>> from solders.keypair import Keypair + >>> from solders.pubkey import Pubkey + >>> from solders.hash import Hash + >>> from solders.system_program import transfer, TransferParams + >>> leading_zeros = [0] * 31 + >>> seed = bytes(leading_zeros + [1]) + >>> sender, receiver = Keypair.from_seed(seed), Pubkey(leading_zeros + [2]) + >>> transfer_tx = Transaction().add(transfer(TransferParams(from_pubkey=sender.pubkey(), to_pubkey=receiver, lamports=1000))) + >>> transfer_tx.recent_blockhash = Hash(leading_zeros + [3]) + >>> transfer_tx.sign(sender) + >>> transfer_tx.serialize().hex() + '019d53be8af3a7c30f86c1092d2c3ea61d270c0cfa275a23ba504674c8fbbb724827b23b42dc8e08019e23120f1b6f40f9799355ce54185b4415be37ca2cee6e0e010001034cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba2900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000301020200010c02000000e803000000000000' + + Returns: + The serialized transaction. + """ # noqa: E501 pylint: disable=line-too-long + if self.signatures == [Signature.default() for sig in self.signatures]: + raise AttributeError("transaction has not been signed") + + if verify_signatures and not self.verify_signatures(): + raise AttributeError("transaction has not been signed correctly") + + return bytes(self._solders) + + @classmethod + def deserialize(cls, raw_transaction: bytes) -> Transaction: + """Parse a wire transaction into a Transaction object. + + Example: + >>> raw_transaction = bytes.fromhex( + ... '019d53be8af3a7c30f86c1092d2c3ea61d270c0cfa2' + ... '75a23ba504674c8fbbb724827b23b42dc8e08019e23' + ... '120f1b6f40f9799355ce54185b4415be37ca2cee6e0' + ... 'e010001034cb5abf6ad79fbf5abbccafcc269d85cd2' + ... '651ed4b885b5869f241aedf0a5ba290000000000000' + ... '0000000000000000000000000000000000000000000' + ... '0000000200000000000000000000000000000000000' + ... '0000000000000000000000000000000000000000000' + ... '0000000000000000000000000000000000000000000' + ... '000000301020200010c02000000e803000000000000' + ... ) + >>> type(Transaction.deserialize(raw_transaction)) + + + Returns: + The deserialized transaction. + """ + return cls.from_solders(SoldersTx.from_bytes(raw_transaction)) + + @classmethod + def populate(cls, message: Message, signatures: List[Signature]) -> Transaction: + """Populate Transaction object from message and signatures. + + Example: + >>> raw_message = bytes.fromhex( + ... '0200030500000000000000000000000000000000000000000000' + ... '0000000000000000000100000000000000000000000000000000' + ... '0000000000000000000000000000000200000000000000000000' + ... '0000000000000000000000000000000000000000000300000000' + ... '0000000000000000000000000000000000000000000000000000' + ... '0004000000000000000000000000000000000000000000000000' + ... '0000000000000005c49ae77603782054f17a9decea43b444eba0' + ... 'edb12c6f1d31c6e0e4a84bf052eb010403010203050909090909' + ... ) + >>> from solders.message import Message + >>> from solders.signature import Signature + >>> msg = Message.from_bytes(raw_message) + >>> signatures = [Signature(bytes([1] * Signature.LENGTH)), Signature(bytes([2] * Signature.LENGTH))] + >>> type(Transaction.populate(msg, signatures)) + + + Returns: + The populated transaction. + """ + return cls.from_solders(SoldersTx.populate(message, signatures)) From 482a34dca7b05ee8354ce6444f699da6307cb64b Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sat, 12 Oct 2024 12:21:21 -0400 Subject: [PATCH 17/23] Update depcrecation message --- src/solana/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solana/transaction.py b/src/solana/transaction.py index 100efa10..98f90b3d 100644 --- a/src/solana/transaction.py +++ b/src/solana/transaction.py @@ -88,7 +88,7 @@ def __init__( ) -> None: """Init transaction object.""" warn( - "Transaction is deprecated and will be removed in a later release. Please use the Transaction modules from solders.transaction.", + "Transaction is deprecated and will be removed in a later release. Please use the Transaction module from solders.transaction instead.", DeprecationWarning, ) From 91b4e0c8af61b6ecbe53f80505eb1e13e5585194 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sat, 12 Oct 2024 12:24:08 -0400 Subject: [PATCH 18/23] Lint --- src/solana/transaction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/solana/transaction.py b/src/solana/transaction.py index 98f90b3d..a4d11bb2 100644 --- a/src/solana/transaction.py +++ b/src/solana/transaction.py @@ -88,7 +88,8 @@ def __init__( ) -> None: """Init transaction object.""" warn( - "Transaction is deprecated and will be removed in a later release. Please use the Transaction module from solders.transaction instead.", + """Transaction is deprecated and will be removed in a later release. + Please use the Transaction module from solders.transaction instead.""", DeprecationWarning, ) From 48ba5469073f3a53661f2330ea2196bfb081ac97 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sat, 12 Oct 2024 12:38:15 -0400 Subject: [PATCH 19/23] Add back send_legacy_transaction rpc methods --- src/solana/rpc/api.py | 65 +++++++++++++++++++++++++++++++++++-- src/solana/rpc/async_api.py | 59 +++++++++++++++++++++++++++++++-- 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/solana/rpc/api.py b/src/solana/rpc/api.py index 76794066..83479a15 100644 --- a/src/solana/rpc/api.py +++ b/src/solana/rpc/api.py @@ -4,8 +4,11 @@ from time import sleep, time from typing import Dict, List, Optional, Sequence, Union +from warnings import warn +from solders.hash import Hash as Blockhash from solders.message import VersionedMessage +from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.rpc.responses import ( GetAccountInfoMaybeJsonParsedResp, @@ -60,12 +63,12 @@ ValidatorExitResp, ) from solders.signature import Signature -from solders.transaction import VersionedTransaction +from solders.transaction import Transaction, VersionedTransaction from solana.rpc import types -from solders.transaction import Transaction +from solana.transaction import Transaction as LegacyTransaction -from .commitment import Commitment +from .commitment import Commitment, Finalized from .core import ( _COMMITMENT_TO_SOLDERS, RPCException, @@ -995,6 +998,62 @@ def send_raw_transaction(self, txn: bytes, opts: Optional[types.TxOpts] = None) post_send_args = self._send_raw_transaction_post_send_args(resp, opts_to_use) return self.__post_send_with_confirm(*post_send_args) + def send_legacy_transaction( + self, + txn: LegacyTransaction, + *signers: Keypair, + opts: Optional[types.TxOpts] = None, + recent_blockhash: Optional[Blockhash] = None, + ) -> SendTransactionResp: + """Send a legacy transaction. + + Args: + txn: transaction object. + signers: Signers to sign the transaction. Only supported for legacy Transaction. + opts: (optional) Transaction options. + recent_blockhash: (optional) Pass a valid recent blockhash here if you want to + skip fetching the recent blockhash or relying on the cache. + Only supported for legacy Transaction. + + Example: + >>> from solders.keypair import Keypair + >>> from solders.pubkey import Pubkey + >>> from solana.rpc.api import Client + >>> from solders.system_program import TransferParams, transfer + >>> from solana.transaction import Transaction + >>> leading_zeros = [0] * 31 + >>> sender, receiver = Keypair.from_seed(leading_zeros + [1]), Keypair.from_seed(leading_zeros + [2]) + >>> txn = Transaction().add(transfer(TransferParams( + ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))) + >>> solana_client = Client("http://localhost:8899") + >>> solana_client.send_transaction(txn, sender).value # doctest: +SKIP + Signature( + 1111111111111111111111111111111111111111111111111111111111111111, + ) + """ + warn("send_transaction_legacy is deprecated. Use send_transaction instead.", DeprecationWarning) + + last_valid_block_height = None + if recent_blockhash is None: + blockhash_resp = self.get_latest_blockhash(Finalized) + recent_blockhash = self.parse_recent_blockhash(blockhash_resp) + last_valid_block_height = blockhash_resp.value.last_valid_block_height + + txn.recent_blockhash = recent_blockhash + + txn.sign(*signers) + opts_to_use = ( + types.TxOpts( + preflight_commitment=self._commitment, + last_valid_block_height=last_valid_block_height, + ) + if opts is None + else opts + ) + + txn_resp = self.send_raw_transaction(txn.serialize(), opts=opts_to_use) + return txn_resp + def send_transaction( self, txn: Union[VersionedTransaction, Transaction], diff --git a/src/solana/rpc/async_api.py b/src/solana/rpc/async_api.py index 5334a639..8489ca71 100644 --- a/src/solana/rpc/async_api.py +++ b/src/solana/rpc/async_api.py @@ -4,7 +4,9 @@ from time import time from typing import Dict, List, Optional, Sequence, Union +from solders.hash import Hash as Blockhash from solders.message import VersionedMessage +from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.rpc.responses import ( GetAccountInfoMaybeJsonParsedResp, @@ -58,12 +60,12 @@ ValidatorExitResp, ) from solders.signature import Signature -from solders.transaction import VersionedTransaction +from solders.transaction import Transaction, VersionedTransaction from solana.rpc import types -from solders.transaction import Transaction +from solana.transaction import Transaction as LegacyTransaction -from .commitment import Commitment +from .commitment import Commitment, Finalized from .core import ( _COMMITMENT_TO_SOLDERS, TransactionExpiredBlockheightExceededError, @@ -1008,6 +1010,57 @@ async def send_raw_transaction(self, txn: bytes, opts: Optional[types.TxOpts] = post_send_args = self._send_raw_transaction_post_send_args(resp, opts_to_use) return await self.__post_send_with_confirm(*post_send_args) + async def send_legacy_transaction( + self, + txn: LegacyTransaction, + *signers: Keypair, + opts: Optional[types.TxOpts] = None, + recent_blockhash: Optional[Blockhash] = None, + ) -> SendTransactionResp: + """Send a legacy transaction. + + Args: + txn: transaction object. + signers: Signers to sign the transaction. Only supported for legacy Transaction. + opts: (optional) Transaction options. + recent_blockhash: (optional) Pass a valid recent blockhash here if you want to + skip fetching the recent blockhash or relying on the cache. + Only supported for legacy Transaction. + + Example: + >>> from solders.keypair import Keypair + >>> from solders.system_program import TransferParams, transfer + >>> from solana.transaction import Transaction + >>> leading_zeros = [0] * 31 + >>> sender, receiver = Keypair.from_seed(leading_zeros + [1]), Keypair.from_seed(leading_zeros + [2]) + >>> txn = Transaction().add(transfer(TransferParams( + ... from_pubkey=sender.pubkey(), to_pubkey=receiver.pubkey(), lamports=1000))) + >>> solana_client = AsyncClient("http://localhost:8899") + >>> (await solana_client.send_transaction(txn, sender)).value # doctest: +SKIP + Signature( + 1111111111111111111111111111111111111111111111111111111111111111, + ) + """ + last_valid_block_height = None + if recent_blockhash is None: + blockhash_resp = await self.get_latest_blockhash(Finalized) + recent_blockhash = self.parse_recent_blockhash(blockhash_resp) + last_valid_block_height = blockhash_resp.value.last_valid_block_height + + txn.recent_blockhash = recent_blockhash + + txn.sign(*signers) + opts_to_use = ( + types.TxOpts( + preflight_commitment=self._commitment, + last_valid_block_height=last_valid_block_height, + ) + if opts is None + else opts + ) + txn_resp = await self.send_raw_transaction(txn.serialize(), opts=opts_to_use) + return txn_resp + async def send_transaction( self, txn: Union[VersionedTransaction, Transaction], From bc9155ff521b676e5684fa73394fca4799e9c961 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sat, 12 Oct 2024 12:39:00 -0400 Subject: [PATCH 20/23] Add deprecation warning --- src/solana/rpc/async_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/solana/rpc/async_api.py b/src/solana/rpc/async_api.py index 8489ca71..bc3a86aa 100644 --- a/src/solana/rpc/async_api.py +++ b/src/solana/rpc/async_api.py @@ -3,6 +3,7 @@ import asyncio from time import time from typing import Dict, List, Optional, Sequence, Union +from warnings import warn from solders.hash import Hash as Blockhash from solders.message import VersionedMessage @@ -1041,6 +1042,8 @@ async def send_legacy_transaction( 1111111111111111111111111111111111111111111111111111111111111111, ) """ + warn("send_transaction_legacy is deprecated. Use send_transaction instead.", DeprecationWarning) + last_valid_block_height = None if recent_blockhash is None: blockhash_resp = await self.get_latest_blockhash(Finalized) From 1d7ed088b52101f431fe051daa9c28c56b2216e7 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sat, 12 Oct 2024 12:40:26 -0400 Subject: [PATCH 21/23] Rename --- src/solana/rpc/api.py | 2 +- src/solana/rpc/async_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solana/rpc/api.py b/src/solana/rpc/api.py index 83479a15..d1a49a36 100644 --- a/src/solana/rpc/api.py +++ b/src/solana/rpc/api.py @@ -1031,7 +1031,7 @@ def send_legacy_transaction( 1111111111111111111111111111111111111111111111111111111111111111, ) """ - warn("send_transaction_legacy is deprecated. Use send_transaction instead.", DeprecationWarning) + warn("send_legacy_transaction is deprecated. Use send_transaction instead.", DeprecationWarning) last_valid_block_height = None if recent_blockhash is None: diff --git a/src/solana/rpc/async_api.py b/src/solana/rpc/async_api.py index bc3a86aa..0699a77b 100644 --- a/src/solana/rpc/async_api.py +++ b/src/solana/rpc/async_api.py @@ -1042,7 +1042,7 @@ async def send_legacy_transaction( 1111111111111111111111111111111111111111111111111111111111111111, ) """ - warn("send_transaction_legacy is deprecated. Use send_transaction instead.", DeprecationWarning) + warn("send_legacy_transaction is deprecated. Use send_transaction instead.", DeprecationWarning) last_valid_block_height = None if recent_blockhash is None: From 57996805dcd62881aa1580129963ad8eeaad4f41 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sat, 12 Oct 2024 12:42:14 -0400 Subject: [PATCH 22/23] Add back unit test for legacy transaction --- tests/unit/test_legacy_transaction.py | 491 ++++++++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 tests/unit/test_legacy_transaction.py diff --git a/tests/unit/test_legacy_transaction.py b/tests/unit/test_legacy_transaction.py new file mode 100644 index 00000000..17bb82fa --- /dev/null +++ b/tests/unit/test_legacy_transaction.py @@ -0,0 +1,491 @@ +"""Unit tests for solana.transaction.""" +from base64 import b64decode, b64encode + +import pytest +import solders.system_program as sp +from solders.hash import Hash as Blockhash +from solders.instruction import AccountMeta, CompiledInstruction +from solders.keypair import Keypair +from solders.message import Message +from solders.message import Message as SoldersMessage +from solders.pubkey import Pubkey +from solders.signature import Signature +from solders.transaction import Transaction as SoldersTx + +import solana.transaction as txlib + + +def example_tx(stubbed_blockhash, kp0: Keypair, kp1: Keypair, kp2: Keypair) -> txlib.Transaction: + """Example tx for testing.""" + ixn = txlib.Instruction( + program_id=Pubkey.default(), + data=bytes([0, 0, 0, 0]), + accounts=[ + AccountMeta(kp0.pubkey(), True, True), + AccountMeta(kp1.pubkey(), True, True), + AccountMeta(kp2.pubkey(), True, True), + ], + ) + return txlib.Transaction(fee_payer=kp0.pubkey(), instructions=[ixn], recent_blockhash=stubbed_blockhash) + + +def test_to_solders(stubbed_blockhash: Blockhash) -> None: + """Test converting a Transaction to solders.""" + kp1, kp2 = Keypair(), Keypair() + transfer = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) + solders_transfer = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) + assert transfer.data == solders_transfer.data + txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer) + solders_msg = SoldersMessage.new_with_blockhash([solders_transfer], None, stubbed_blockhash) + solders_txn = SoldersTx.new_unsigned(solders_msg) + assert txn.to_solders() == solders_txn + assert txlib.Transaction.from_solders(solders_txn) == txn + + +def test_sign_partial(stubbed_blockhash): + """Test partially sigining a transaction.""" + keypair0 = Keypair() + keypair1 = Keypair() + keypair2 = Keypair() + ixn = txlib.Instruction( + program_id=Pubkey.default(), + data=bytes([0, 0, 0, 0]), + accounts=[ + AccountMeta(keypair0.pubkey(), True, True), + AccountMeta(keypair1.pubkey(), True, True), + AccountMeta(keypair2.pubkey(), True, True), + ], + ) + txn = txlib.Transaction(fee_payer=keypair0.pubkey(), instructions=[ixn], recent_blockhash=stubbed_blockhash) + assert txn.to_solders().message.header.num_required_signatures == 3 + txn.sign_partial(keypair0, keypair2) + assert not txn.to_solders().is_signed() + txn.sign_partial(keypair1) + assert txn.to_solders().is_signed() + expected_tx = txlib.Transaction(fee_payer=keypair0.pubkey(), instructions=[ixn], recent_blockhash=stubbed_blockhash) + expected_tx.sign(keypair0, keypair1, keypair2) + assert txn == expected_tx + + +def test_recent_blockhash_setter(stubbed_blockhash): + """Test the recent_blockhash setter property works.""" + kp0, kp1, kp2 = Keypair(), Keypair(), Keypair() + tx0 = example_tx(stubbed_blockhash, kp0, kp1, kp2) + tx1 = example_tx(stubbed_blockhash, kp0, kp1, kp2) + tx1.recent_blockhash = tx0.recent_blockhash + assert tx0 == tx1 + + +def test_transfer_signatures(stubbed_blockhash): + """Test signing transfer transactions.""" + kp1, kp2 = Keypair(), Keypair() + transfer1 = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) + transfer2 = sp.transfer(sp.TransferParams(from_pubkey=kp2.pubkey(), to_pubkey=kp1.pubkey(), lamports=123)) + txn = txlib.Transaction(recent_blockhash=stubbed_blockhash) + txn.add(transfer1, transfer2) + txn.sign(kp1, kp2) + + expected = txlib.Transaction.populate(txn.compile_message(), txn.signatures) + assert txn == expected + + +def test_dedup_signatures(stubbed_blockhash): + """Test signature deduplication.""" + kp1, kp2 = Keypair(), Keypair() + transfer1 = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) + transfer2 = sp.transfer(sp.TransferParams(from_pubkey=kp1.pubkey(), to_pubkey=kp2.pubkey(), lamports=123)) + txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer1, transfer2) + txn.sign(kp1) + + +def test_wire_format_and_desrialize(stubbed_blockhash, stubbed_receiver, stubbed_sender): + """Test serialize/derialize transaction to/from wire format.""" + transfer = sp.transfer( + sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=49) + ) + expected_txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer) + expected_txn.sign(stubbed_sender) + wire_txn = b64decode( + b"AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/kxj+nQ0BAAEDE5j2" + b"LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=" + ) + txn = txlib.Transaction.deserialize(wire_txn) + assert txn == expected_txn + assert wire_txn == expected_txn.serialize() + + +def test_populate(): + """Test populating transaction with a message and two signatures.""" + account_keys = [Pubkey([0] * 31 + [i + 1]) for i in range(5)] + msg = Message.new_with_compiled_instructions( + num_required_signatures=2, + num_readonly_signed_accounts=0, + num_readonly_unsigned_accounts=3, + account_keys=account_keys, + instructions=[CompiledInstruction(accounts=bytes([1, 2, 3]), data=bytes([9] * 5), program_id_index=4)], + recent_blockhash=Blockhash.default(), + ) + signatures = [Signature(bytes([1] * Signature.LENGTH)), Signature(bytes([2] * Signature.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 test_serialize_unsigned_transaction(stubbed_blockhash, stubbed_receiver, stubbed_sender): + """Test to serialize an unsigned transaction.""" + transfer = sp.transfer( + sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=49) + ) + txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer) + assert txn.signatures == (Signature.default(),) + # Empty signature array fails + with pytest.raises(AttributeError): + txn.serialize() + assert txn.signatures == (Signature.default(),) + + # Set fee payer + txn.fee_payer = stubbed_sender.pubkey() + # Serialize message + assert b64encode(txn.serialize_message()) == ( + b"AQABAxOY9ixtGkV8UbpqS189vS9p/KkyFiGNyJl+QWvRfZPK/UOfzLZnJ/KJxcbeO8So/l3V13dwvI/xXD7u3LFK8/wAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMSa53YDeCBU8Xqd7OpDtETroO2xLG8dMcbg5KhL8FLrAQICAAEMAgAAADEAAAAAAAAA" + ) + assert len(txn.instructions) == 1 + # Signature array populated with null signatures fails + with pytest.raises(AttributeError): + txn.serialize() + assert txn.signatures == (Signature.default(),) + # Properly signed transaction succeeds + txn.sign(stubbed_sender) + assert len(txn.instructions) == 1 + expected_serialization = b64decode( + b"AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/kxj+nQ0BAAEDE5j2" + b"LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=" + ) + assert txn.serialize() == expected_serialization + assert len(txn.signatures) == 1 + assert txn.signatures != (Signature.default(),) + + +def test_serialize_unsigned_transaction_without_verifying_signatures( + stubbed_blockhash, stubbed_receiver, stubbed_sender +): + """Test to serialize an unsigned transaction without verifying the signatures.""" + transfer = sp.transfer( + sp.TransferParams(from_pubkey=stubbed_sender.pubkey(), to_pubkey=stubbed_receiver, lamports=49) + ) + txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer) + assert txn.signatures == (Signature.default(),) + + # empty signatures should not fail + txn.serialize(verify_signatures=False) + assert txn.signatures == (Signature.default(),) + + # Set fee payer + txn.fee_payer = stubbed_sender.pubkey() + # Serialize message + assert b64encode(txn.serialize_message()) == ( + b"AQABAxOY9ixtGkV8UbpqS189vS9p/KkyFiGNyJl+QWvRfZPK/UOfzLZnJ/KJxcbeO8So/l3V13dwvI/xXD7u3LFK8/wAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMSa53YDeCBU8Xqd7OpDtETroO2xLG8dMcbg5KhL8FLrAQICAAEMAgAAADEAAAAAAAAA" + ) + assert len(txn.instructions) == 1 + # Signature array populated with null signatures should not fail + txn.serialize(verify_signatures=False) + assert txn.signatures == (Signature.default(),) + + +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.pubkey() for x in [signer_one, signer_two, signer_three]], key=str) + sorted_signers_excluding_fee_payer = [x for x in sorted_signers if str(x) != str(fee_payer.pubkey())] + sorted_receivers = sorted([x.pubkey() for x in [receiver_one, receiver_two, receiver_three]], key=str) + + txn = txlib.Transaction(recent_blockhash=stubbed_blockhash) + txn.fee_payer = fee_payer.pubkey() + + # Add three transfer transactions + txn.add( + sp.transfer( + sp.TransferParams( + from_pubkey=signer_one.pubkey(), + to_pubkey=receiver_one.pubkey(), + lamports=2_000_000, + ) + ) + ) + txn.add( + sp.transfer( + sp.TransferParams( + from_pubkey=signer_two.pubkey(), + to_pubkey=receiver_two.pubkey(), + lamports=2_000_000, + ) + ) + ) + txn.add( + sp.transfer( + sp.TransferParams( + from_pubkey=signer_three.pubkey(), + to_pubkey=receiver_three.pubkey(), + 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(bytes(tx_msg)) == js_msg_b64_check + + # Transaction should organize AccountMetas by pubkey + assert tx_msg.account_keys[0] == fee_payer.pubkey() + 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] From 858b9d83ab7b2e17315e7e5e012e7aa09777a078 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sat, 12 Oct 2024 12:44:01 -0400 Subject: [PATCH 23/23] Less diff --- src/solana/rpc/api.py | 2 +- src/solana/rpc/async_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solana/rpc/api.py b/src/solana/rpc/api.py index d1a49a36..fe78a23c 100644 --- a/src/solana/rpc/api.py +++ b/src/solana/rpc/api.py @@ -7,8 +7,8 @@ from warnings import warn from solders.hash import Hash as Blockhash -from solders.message import VersionedMessage from solders.keypair import Keypair +from solders.message import VersionedMessage from solders.pubkey import Pubkey from solders.rpc.responses import ( GetAccountInfoMaybeJsonParsedResp, diff --git a/src/solana/rpc/async_api.py b/src/solana/rpc/async_api.py index 0699a77b..95683ed2 100644 --- a/src/solana/rpc/async_api.py +++ b/src/solana/rpc/async_api.py @@ -6,8 +6,8 @@ from warnings import warn from solders.hash import Hash as Blockhash -from solders.message import VersionedMessage from solders.keypair import Keypair +from solders.message import VersionedMessage from solders.pubkey import Pubkey from solders.rpc.responses import ( GetAccountInfoMaybeJsonParsedResp,