Skip to content

Commit

Permalink
Merge pull request #334 from michaelhly/versioned-transaction-rpc
Browse files Browse the repository at this point in the history
Versioned transaction rpc support
  • Loading branch information
kevinheavey authored Jan 12, 2023
2 parents c44a66b + a759938 commit fd6604b
Show file tree
Hide file tree
Showing 16 changed files with 309 additions and 91 deletions.
5 changes: 1 addition & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@
"120"
],
"editor.formatOnSave": true,
"python.languageServer": "Jedi",
"autoDocstring.docstringFormat": "sphinx",
"python.pythonPath": ".venv/bin/python",
"restructuredtext.confPath": "${workspaceFolder}/docs",
"python.languageServer": "Pylance",
"ruff.args": [
"--config=pyproject.toml"
]
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

## [0.29.0] - Unreleased

## Added

- Add VersionedTransaction support to `send_transaction` and `simulate_transaction` methods [(#334)](https://github.com/michaelhly/solana-py/pull/334):

## Changed

- Remove redundant classes [(#329)](https://github.com/michaelhly/solana-py/pull/329):
- Remove `PublicKey`, in favour of `solders.pubkey.Pubkey`.
- Remove `AccountMeta` in favour of `solders.instruction.AccountMeta`.
- Remove `TransactionInstruction` in favour of `solders.instruction.Instruction`.
- Use latest solders [(#332)](https://github.com/michaelhly/solana-py/pull/332)
- Use latest solders [(#334)](https://github.com/michaelhly/solana-py/pull/334)
- Use new `solders.rpc.requests.SendRawTransasction` in `send_raw_transaction` methods

## [0.28.1] - 2022-12-29
Expand Down
215 changes: 153 additions & 62 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ typing-extensions = ">=4.2.0"
cachetools = "^4.2.2"
types-cachetools = "^4.2.4"
websockets = "^10.3"
solders = "^0.13.0"
solders = "^0.14.0"

[tool.poetry.dev-dependencies]
black = "^22.3"
Expand Down Expand Up @@ -94,3 +94,6 @@ convention = "google"

[tool.black]
line-length = 120

[tool.pyright]
reportMissingModuleSource = false
21 changes: 16 additions & 5 deletions src/solana/rpc/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
ValidatorExitResp,
)
from solders.signature import Signature
from solders.transaction import VersionedTransaction

from solana.blockhash import Blockhash, BlockhashCache
from solana.keypair import Keypair
Expand Down Expand Up @@ -993,19 +994,20 @@ def send_raw_transaction(self, txn: bytes, opts: Optional[types.TxOpts] = None)

def send_transaction(
self,
txn: Transaction,
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.
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 solana.keypair import Keypair
Expand All @@ -1023,6 +1025,15 @@ def send_transaction(
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)
return self.send_raw_transaction(bytes(txn), opts=versioned_tx_opts)
last_valid_block_height = None
if recent_blockhash is None:
if self.blockhash_cache:
Expand Down Expand Up @@ -1058,14 +1069,14 @@ def send_transaction(

def simulate_transaction(
self,
txn: Transaction,
txn: Union[Transaction, VersionedTransaction],
sig_verify: bool = False,
commitment: Optional[Commitment] = None,
) -> SimulateTransactionResp:
"""Simulate sending a transaction.
Args:
txn: A Transaction object, a transaction in wire format, or a transaction as base-64 encoded string
txn: A transaction object.
The transaction must have a valid blockhash, but is not required to be signed.
sig_verify: If true the transaction signatures will be verified (default: false).
commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed".
Expand Down
19 changes: 15 additions & 4 deletions src/solana/rpc/async_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
ValidatorExitResp,
)
from solders.signature import Signature
from solders.transaction import VersionedTransaction

from solana.blockhash import Blockhash, BlockhashCache
from solana.keypair import Keypair
Expand Down Expand Up @@ -1008,19 +1009,20 @@ async def send_raw_transaction(self, txn: bytes, opts: Optional[types.TxOpts] =

async def send_transaction(
self,
txn: Transaction,
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.
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 solana.keypair import Keypair
Expand All @@ -1036,6 +1038,15 @@ async def send_transaction(
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)
return await self.send_raw_transaction(bytes(txn), opts=versioned_tx_opts)
last_valid_block_height = None
if recent_blockhash is None:
if self.blockhash_cache:
Expand Down Expand Up @@ -1069,7 +1080,7 @@ async def send_transaction(

async def simulate_transaction(
self,
txn: Transaction,
txn: Union[Transaction, VersionedTransaction],
sig_verify: bool = False,
commitment: Optional[Commitment] = None,
) -> SimulateTransactionResp:
Expand Down
28 changes: 22 additions & 6 deletions src/solana/rpc/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pylint: disable=too-many-arguments
"""Helper code for api.py and async_api.py."""
from typing import List, Optional, Sequence, Tuple, Union, cast
from typing import List, Optional, Sequence, Tuple, Union, cast, overload

try:
from typing import Literal # type: ignore
Expand Down Expand Up @@ -73,11 +73,13 @@
MinimumLedgerSlot,
RequestAirdrop,
SendRawTransaction,
SimulateTransaction,
SimulateLegacyTransaction,
SimulateVersionedTransaction,
ValidatorExit,
)
from solders.rpc.responses import GetLatestBlockhashResp, SendTransactionResp
from solders.signature import Signature
from solders.transaction import VersionedTransaction
from solders.transaction_status import UiTransactionEncoding

from solana.blockhash import Blockhash, BlockhashCache
Expand Down Expand Up @@ -475,14 +477,28 @@ def _send_raw_transaction_post_send_args(
) -> Tuple[SendTransactionResp, Commitment, Optional[int]]:
return resp, opts.preflight_commitment, opts.last_valid_block_height

@overload
def _simulate_transaction_body(
self, txn: Transaction, sig_verify: bool, commitment: Optional[Commitment]
) -> SimulateTransaction:
if txn.recent_blockhash is None:
raise ValueError("transaction must have a valid blockhash")
) -> SimulateLegacyTransaction:
...

@overload
def _simulate_transaction_body(
self, txn: VersionedTransaction, sig_verify: bool, commitment: Optional[Commitment]
) -> SimulateVersionedTransaction:
...

def _simulate_transaction_body(
self, txn: Union[Transaction, VersionedTransaction], sig_verify: bool, commitment: Optional[Commitment]
) -> Union[SimulateLegacyTransaction, SimulateVersionedTransaction]:
commitment_to_use = _COMMITMENT_TO_SOLDERS[commitment or self._commitment]
config = RpcSimulateTransactionConfig(sig_verify=sig_verify, commitment=commitment_to_use)
return SimulateTransaction(txn.to_solders(), config)
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 SimulateVersionedTransaction(txn, config)

@staticmethod
def _post_send(resp: SendTransactionResp) -> SendTransactionResp:
Expand Down
15 changes: 15 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from solana.rpc.commitment import Processed
from solders.pubkey import Pubkey

from tests.utils import AIRDROP_AMOUNT, assert_valid_response


class Clients(NamedTuple):
"""Container for http clients."""
Expand Down Expand Up @@ -191,3 +193,16 @@ def check() -> bool:
docker_services.wait_until_responsive(timeout=15, pause=1, check=check)
yield http_client
event_loop.run_until_complete(http_client.close())


@pytest.mark.integration
@pytest.fixture(scope="function")
def random_funded_keypair(test_http_client: Client) -> Keypair:
"""A new keypair with some lamports."""
kp = Keypair()
resp = test_http_client.request_airdrop(kp.public_key, AIRDROP_AMOUNT)
assert_valid_response(resp)
test_http_client.confirm_transaction(resp.value)
balance = test_http_client.get_balance(kp.public_key)
assert balance.value == AIRDROP_AMOUNT
return kp
41 changes: 39 additions & 2 deletions tests/integration/test_async_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
from solana.rpc.core import RPCException, TransactionExpiredBlockheightExceededError
from solana.rpc.types import DataSliceOpts, TxOpts
from solana.transaction import Transaction
from solders.message import MessageV0
from solders.pubkey import Pubkey
from solders.rpc.errors import SendTransactionPreflightFailureMessage
from solders.rpc.requests import GetBlockHeight, GetFirstAvailableBlock
from solders.rpc.responses import GetBlockHeightResp, GetFirstAvailableBlockResp, Resp
from solders.transaction import VersionedTransaction
from spl.token.constants import WRAPPED_SOL_MINT

from .utils import AIRDROP_AMOUNT, assert_valid_response
from ..utils import AIRDROP_AMOUNT, assert_valid_response


@pytest.mark.integration
Expand Down Expand Up @@ -87,7 +89,9 @@ async def test_request_air_drop_cached_blockhash(


@pytest.mark.integration
async def test_send_transaction_and_get_balance(async_stubbed_sender, async_stubbed_receiver, test_http_client_async):
async def test_send_transaction_and_get_balance(
async_stubbed_sender: Keypair, async_stubbed_receiver: Pubkey, test_http_client_async: AsyncClient
):
"""Test sending a transaction to localnet."""
# Create transfer tx to transfer lamports from stubbed sender to async_stubbed_receiver
transfer_tx = Transaction().add(
Expand All @@ -110,6 +114,39 @@ async def test_send_transaction_and_get_balance(async_stubbed_sender, async_stub
assert resp.value == 10000001000


@pytest.mark.integration
async def test_send_versioned_transaction_and_get_balance(
random_funded_keypair: Keypair, test_http_client_async: AsyncClient
):
"""Test sending a transaction to localnet."""
receiver = Keypair()
amount = 1_000_000
transfer_ix = sp.transfer(
sp.TransferParams(from_pubkey=random_funded_keypair.public_key, to_pubkey=receiver.public_key, lamports=amount)
)
recent_blockhash = (await test_http_client_async.get_latest_blockhash()).value.blockhash
msg = MessageV0.try_compile(
payer=random_funded_keypair.public_key,
instructions=[transfer_ix],
address_lookup_table_accounts=[],
recent_blockhash=recent_blockhash,
)
transfer_tx = VersionedTransaction(msg, [random_funded_keypair.to_solders()])
sim_resp = await test_http_client_async.simulate_transaction(transfer_tx)
assert_valid_response(sim_resp)
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)
# Check balances
resp = await test_http_client_async.get_balance(random_funded_keypair.public_key)
assert_valid_response(resp)
assert resp.value == AIRDROP_AMOUNT - amount - 5000
resp = await test_http_client_async.get_balance(receiver.public_key)
assert_valid_response(resp)
assert resp.value == amount


@pytest.mark.integration
async def test_send_bad_transaction(stubbed_receiver: Pubkey, test_http_client_async: AsyncClient):
"""Test sending a transaction that errors."""
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_async_token_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from spl.token.async_client import AsyncToken
from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID

from .utils import AIRDROP_AMOUNT, OPTS, assert_valid_response
from ..utils import AIRDROP_AMOUNT, OPTS, assert_valid_response


@pytest.mark.integration
Expand Down
35 changes: 34 additions & 1 deletion tests/integration/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
from solana.rpc.core import RPCException, TransactionExpiredBlockheightExceededError
from solana.rpc.types import DataSliceOpts, TxOpts
from solana.transaction import Transaction
from solders.message import MessageV0
from solders.pubkey import Pubkey
from solders.rpc.errors import SendTransactionPreflightFailureMessage
from solders.rpc.requests import GetBlockHeight, GetFirstAvailableBlock
from solders.rpc.responses import GetBlockHeightResp, GetFirstAvailableBlockResp, Resp
from solders.transaction import VersionedTransaction
from spl.token.constants import WRAPPED_SOL_MINT

from .utils import AIRDROP_AMOUNT, assert_valid_response
from ..utils import AIRDROP_AMOUNT, assert_valid_response


@pytest.mark.integration
Expand Down Expand Up @@ -83,6 +85,8 @@ def test_send_transaction_and_get_balance(stubbed_sender, stubbed_receiver, test
transfer_tx = Transaction().add(
sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=1000))
)
sim_resp = test_http_client.simulate_transaction(transfer_tx)
assert_valid_response(sim_resp)
resp = test_http_client.send_transaction(transfer_tx, stubbed_sender)
assert_valid_response(resp)
# Confirm transaction
Expand All @@ -96,6 +100,35 @@ def test_send_transaction_and_get_balance(stubbed_sender, stubbed_receiver, test
assert bal_resp2.value == 10000001000


@pytest.mark.integration
def test_send_versioned_transaction_and_get_balance(random_funded_keypair: Keypair, test_http_client: Client):
"""Test sending a transaction to localnet."""
receiver = Keypair()
amount = 1_000_000
transfer_ix = sp.transfer(
sp.TransferParams(from_pubkey=random_funded_keypair.public_key, to_pubkey=receiver.public_key, lamports=amount)
)
recent_blockhash = test_http_client.get_latest_blockhash().value.blockhash
msg = MessageV0.try_compile(
payer=random_funded_keypair.public_key,
instructions=[transfer_ix],
address_lookup_table_accounts=[],
recent_blockhash=recent_blockhash,
)
transfer_tx = VersionedTransaction(msg, [random_funded_keypair.to_solders()])
resp = test_http_client.send_transaction(transfer_tx)
assert_valid_response(resp)
# Confirm transaction
test_http_client.confirm_transaction(resp.value)
# Check balances
resp = test_http_client.get_balance(random_funded_keypair.public_key)
assert_valid_response(resp)
assert resp.value == AIRDROP_AMOUNT - amount - 5000
resp = test_http_client.get_balance(receiver.public_key)
assert_valid_response(resp)
assert resp.value == amount


@pytest.mark.integration
def test_send_bad_transaction(stubbed_receiver: Pubkey, test_http_client: Client):
"""Test sending a transaction that errors."""
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_memo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from spl.memo.constants import MEMO_PROGRAM_ID
from spl.memo.instructions import MemoParams, create_memo

from .utils import AIRDROP_AMOUNT, assert_valid_response
from ..utils import AIRDROP_AMOUNT, assert_valid_response


@pytest.mark.integration
Expand Down
Loading

0 comments on commit fd6604b

Please sign in to comment.