Skip to content

Commit

Permalink
feat: add Contract Client to make calling contracts easier. (#998)
Browse files Browse the repository at this point in the history
  • Loading branch information
overcat authored Nov 22, 2024
1 parent a65f2e0 commit 126da77
Show file tree
Hide file tree
Showing 23 changed files with 2,091 additions and 469 deletions.
48 changes: 45 additions & 3 deletions .github/workflows/continuous-integration-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,52 @@ jobs:
poetry show
- name: Test with pytest
run: poetry run pytest -v -rs tests --runslow --cov=./ --cov-report=xml
run: poetry run pytest -v -rs tests --runslow

integration:
runs-on: ubuntu-latest
services:
rpc:
image: stellar/quickstart:testing@sha256:5333ec87069efd7bb61f6654a801dc093bf0aad91f43a5ba84806d3efe4a6322
ports:
- 8000:8000
env:
ENABLE_LOGS: true
NETWORK: local
ENABLE_SOROBAN_RPC: true
PROTOCOL_VERSION: 22
options: >-
--health-cmd "curl --no-progress-meter --fail-with-body -X POST \"http://localhost:8000/soroban/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getNetwork\"}' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'"
--health-interval 10s
--health-timeout 5s
--health-retries 50
steps:
- uses: actions/checkout@v3

- name: Wake up httpbin server
run: |
curl https://httpbinx.fly.dev/ip
- name: Install poetry
run: pipx install poetry

- name: Setup Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.13

- name: Install dependencies
run: |
poetry install --extras 'aiohttp'
- name: Echo installed packages
run: |
poetry show
- name: Test with pytest
run: poetry run pytest -v -rs tests --runslow --integration --cov=./ --cov-report=xml

- name: Upload coverage to Codecov
if: matrix.platform.python-version == '3.13' && matrix.platform.os == 'ubuntu-latest'
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -87,7 +129,7 @@ jobs:
fail_ci_if_error: true

deploy:
needs: test
needs: [ test, integration ]
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'created'
permissions:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Release History

### Pending

#### Update
- feat: add `stellar_sdk.contract.ContractClient` and `stellar_sdk.contract.ContractClientAsync`, this greatly reduces the difficulty of calling contracts, and you can learn more through the documentation and [examples](https://github.com/StellarCN/py-stellar-base/blob/main/examples/soroban_invoke_contract_function.py). ([#998](https://github.com/StellarCN/py-stellar-base/pull/998))

### Version 12.0.0-beta3

Released on November 14, 2024
Expand Down
32 changes: 32 additions & 0 deletions docs/en/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,39 @@ Response
.. autoclass:: stellar_sdk.client.response.Response
:members:

Contract
^^^^^^^^

ContractClient
---------------

.. autoclass:: stellar_sdk.contract.ContractClient
:members:

AssembledTransaction
--------------------

.. autoclass:: stellar_sdk.contract.AssembledTransaction
:members:


ContractClientAsync
-------------------

.. autoclass:: stellar_sdk.contract.ContractClientAsync
:members:

AssembledTransactionAsync
-------------------------

.. autoclass:: stellar_sdk.contract.AssembledTransactionAsync
:members:

Exceptions
----------

.. automodule:: stellar_sdk.contract.exceptions
:members:

Exceptions
^^^^^^^^^^
Expand Down
100 changes: 21 additions & 79 deletions examples/soroban_auth_atomic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,9 @@
https://soroban.stellar.org/docs/learn/authorization
"""

import time

from stellar_sdk import (
InvokeHostFunction,
Keypair,
Network,
SorobanServer,
TransactionBuilder,
scval,
)
from stellar_sdk.auth import authorize_entry
from stellar_sdk.exceptions import PrepareTransactionException
from stellar_sdk.soroban_rpc import GetTransactionStatus, SendTransactionStatus
from stellar_sdk import Keypair, Network, SorobanServer, scval
from stellar_sdk.contract import AssembledTransaction, ContractClient
from stellar_sdk.contract.exceptions import AssembledTransactionError

rpc_server_url = "https://soroban-testnet.stellar.org:443"
soroban_server = SorobanServer(rpc_server_url)
Expand All @@ -29,11 +19,11 @@
"SBPTTA3D3QYQ6E2GSACAZDUFH2UILBNG3EBJCK3NNP7BE4O757KGZUGA"
) # GAERW3OYAVYMZMPMVKHSCDS4ORFPLT5Z3YXA4VM3BVYEA2W7CG3V6YYB
bob_kp = Keypair.from_secret(
"SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"
) # GBMLPRFCZDZJPKUPHUSHCKA737GOZL7ERZLGGMJ6YGHBFJZ6ZKMKCZTM
atomic_swap_contract_id = "CCOYWFXSCED6GIUYAYEDVZ7EA4MFDNBXTKSB2RWDSDYMR3EQFT6MUSU5"
native_token_contract_id = "CB64D3G7SM2RTH6JSGG34DDTFTQ5CFDKVDZJZSODMCX4NJ2HV2KN7OHT"
cat_token_contract_id = "CDOZ4ZTY2OSEHMTSBL3AFMIDF2EGP3AZTFB7YTCKXNSYZMRV6SROUFAY"
"SBJQCT3YSSVRHVGNMGDHJ35SZ635KXPJGGDEBHWWKCPZ7ZY46H2LM7KM"
) # GCN326AH3JIS3QVOLSGWEIYIZETJROTONKKKGLBIPMKK6LUEYXCASX2N
atomic_swap_contract_id = "CAFOTJC77LH7GQHSV3OB4OOSVLD5S77YGPBPJIUILGG45EKSCAAVJUC6"
native_token_contract_id = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
cat_token_contract_id = "CBQFF6FIGSR2LNHGZ4CU32WIUFEV7332MUEPMA4SHHHFYRJ2UALAEPMA"

source = soroban_server.load_account(submitter_kp.public_key)

Expand All @@ -48,67 +38,19 @@
scval.to_int128(950), # min_a_for_b
]

tx = (
TransactionBuilder(source, network_passphrase, base_fee=50000)
.add_time_bounds(0, 0)
.append_invoke_contract_function_op(
contract_id=atomic_swap_contract_id,
function_name="swap",
parameters=args,
)
.build()
assemble_tx: AssembledTransaction = ContractClient(
atomic_swap_contract_id, rpc_server_url, network_passphrase
).invoke(
"swap",
args,
submitter_kp.public_key,
submitter_kp,
)
assemble_tx.sign_auth_entries(alice_kp)
assemble_tx.sign_auth_entries(bob_kp)

try:
simulate_resp = soroban_server.simulate_transaction(tx)
# You need to check the error in the response,
# if the error is not None, you need to handle it.
op = tx.transaction.operations[0]
assert isinstance(op, InvokeHostFunction)
assert simulate_resp.results is not None
assert simulate_resp.results[0].auth is not None
op.auth = [
authorize_entry(
simulate_resp.results[0].auth[0],
alice_kp,
simulate_resp.latest_ledger + 20,
network_passphrase,
), # alice sign the entry
authorize_entry(
simulate_resp.results[0].auth[1],
bob_kp,
simulate_resp.latest_ledger + 20,
network_passphrase,
), # bob sign the entry
]
tx = soroban_server.prepare_transaction(tx, simulate_resp)
except PrepareTransactionException as e:
print(f"Got exception: {e.simulate_transaction_response}")
raise e

# tx.transaction.soroban_data.resources.instructions = stellar_xdr.Uint32(
# tx.transaction.soroban_data.resources.instructions.uint32 * 2
# )

tx.sign(submitter_kp)
print(f"Signed XDR:\n{tx.to_xdr()}")


send_transaction_data = soroban_server.send_transaction(tx)
print(f"sent transaction: {send_transaction_data}")
if send_transaction_data.status != SendTransactionStatus.PENDING:
raise Exception("send transaction failed")

while True:
print("waiting for transaction to be confirmed...")
get_transaction_data = soroban_server.get_transaction(send_transaction_data.hash)
if get_transaction_data.status != GetTransactionStatus.NOT_FOUND:
break
time.sleep(3)

print(f"transaction: {get_transaction_data}")

if get_transaction_data.status == GetTransactionStatus.SUCCESS:
print(f"Transaction successful: {get_transaction_data.result_xdr}")
else:
print(f"Transaction failed: {get_transaction_data.result_xdr}")
assemble_tx.sign_and_submit()
print("Atomic swap success")
except AssembledTransactionError as e:
print("Transaction failed, check the exception for more details.", e)
98 changes: 19 additions & 79 deletions examples/soroban_auth_with_stellar_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,97 +4,37 @@
See https://soroban.stellar.org/docs/learn/authorization#stellar-account
"""

import time

from stellar_sdk import (
InvokeHostFunction,
Keypair,
Network,
SorobanServer,
TransactionBuilder,
scval,
)
from stellar_sdk import xdr as stellar_xdr
from stellar_sdk.auth import authorize_entry
from stellar_sdk.exceptions import PrepareTransactionException
from stellar_sdk.soroban_rpc import GetTransactionStatus, SendTransactionStatus
from stellar_sdk import Keypair, Network, scval
from stellar_sdk.contract import ContractClient, exceptions

rpc_server_url = "https://soroban-testnet.stellar.org:443"
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
soroban_server = SorobanServer(rpc_server_url)

# https://github.com/stellar/soroban-examples/tree/v0.6.0/auth
contract_id = "CDGPP5TBQIVN4ADNH6PL4METZNJ35OX4DIXKAQ3ENWYLBAJZMHHZE3EV"
# https://github.com/stellar/soroban-examples/tree/main/auth
contract_id = "CAQRLQH5RBQHSXMVRQJEGK7GINE3QPZUPY6SYSOHGSZA4XGDIXT34XRY"
tx_submitter_kp = Keypair.from_secret(
"SAAPYAPTTRZMCUZFPG3G66V4ZMHTK4TWA6NS7U4F7Z3IMUD52EK4DDEV"
)
op_invoker_kp = Keypair.from_secret(
"SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"
"SCS26R6UG5M6JA3D3VA7KTBTBJRHIYIBRY4ZHIIEWPCJ3PO54RKQEKFD"
)

func_name = "increment"
function_name = "increment"
args = [scval.to_address(op_invoker_kp.public_key), scval.to_uint32(10)]

source = soroban_server.load_account(tx_submitter_kp.public_key)
tx = (
TransactionBuilder(source, network_passphrase, base_fee=50000)
.add_time_bounds(0, 0)
.append_invoke_contract_function_op(
contract_id=contract_id,
function_name=func_name,
parameters=args,
)
.build()
)

try:
simulate_resp = soroban_server.simulate_transaction(tx)
# You need to check the error in the response,
# if the error is not None, you need to handle it.
op = tx.transaction.operations[0]
assert isinstance(op, InvokeHostFunction)
assert simulate_resp.results is not None
assert simulate_resp.results[0].auth is not None
op.auth = [
authorize_entry(
simulate_resp.results[0].auth[0],
op_invoker_kp,
simulate_resp.latest_ledger + 20,
network_passphrase,
result = (
ContractClient(contract_id, rpc_server_url, network_passphrase)
.invoke(
function_name,
args,
source=tx_submitter_kp.public_key,
parse_result_xdr_fn=lambda x: scval.from_uint32(x),
)
]
tx = soroban_server.prepare_transaction(tx, simulate_resp)
except PrepareTransactionException as e:
print(f"Got exception: {e.simulate_transaction_response}")
raise e

# tx.transaction.soroban_data.resources.instructions = stellar_xdr.Uint32(
# tx.transaction.soroban_data.resources.instructions.uint32 * 2
# )

tx.sign(tx_submitter_kp)
print(f"Signed XDR:\n{tx.to_xdr()}")

send_transaction_data = soroban_server.send_transaction(tx)
print(f"sent transaction: {send_transaction_data}")
if send_transaction_data.status != SendTransactionStatus.PENDING:
raise Exception("send transaction failed")

while True:
print("waiting for transaction to be confirmed...")
get_transaction_data = soroban_server.get_transaction(send_transaction_data.hash)
if get_transaction_data.status != GetTransactionStatus.NOT_FOUND:
break
time.sleep(3)

print(f"transaction: {get_transaction_data}")

if get_transaction_data.status == GetTransactionStatus.SUCCESS:
assert get_transaction_data.result_meta_xdr is not None
transaction_meta = stellar_xdr.TransactionMeta.from_xdr(
get_transaction_data.result_meta_xdr
.sign_auth_entries(op_invoker_kp)
.sign_and_submit(tx_submitter_kp)
)
result = transaction_meta.v3.soroban_meta.return_value.u32 # type: ignore
print(f"Function result: {result}")
else:
print(f"Transaction failed: {get_transaction_data.result_xdr}")
print("Transaction success, result:", result)
except exceptions.AssembledTransactionError as e:
print("Transaction failed, check the exception for more details.")
print(e.assembled_transaction)
Loading

0 comments on commit 126da77

Please sign in to comment.