Using existing contracts#

Existing contracts#

Although it is possible to use Client to interact with contracts, it requires translating python values into Cairo values. Contract offers that and some other utilities.

Let’s say we have a contract with this interface:

abi = [
    {
        "inputs": [
            {"name": "sender", "type": "felt"},
            {"name": "recipient", "type": "felt"},
            {"name": "amount", "type": "felt"},
        ],
        "name": "transferFrom",
        "outputs": [{"name": "success", "type": "felt"}],
        "type": "function",
    },
    {
        "inputs": [{"name": "account", "type": "felt"}],
        "name": "balanceOf",
        "outputs": [{"name": "balance", "type": "felt"}],
        "stateMutability": "view",
        "type": "function",
    },
]

This is how we can interact with it:

from starknet_py.contract import Contract

address = "0x00178130dd6286a9a0e031e4c73b2bd04ffa92804264a25c1c08c1612559f458"

# When ABI is known statically just use the Contract constructor
contract = Contract(address=address, abi=abi, provider=account)
# or if it is not known
# Contract.from_address makes additional request to fetch the ABI
contract = await Contract.from_address(provider=account, address=address)

sender = "321"
recipient = "123"

# Using only positional arguments
invocation = await contract.functions["transferFrom"].invoke_v1(
    sender, recipient, 10000, max_fee=int(1e16)
)

# Using only keyword arguments
invocation = await contract.functions["transferFrom"].invoke_v1(
    sender=sender, recipient=recipient, amount=10000, max_fee=int(1e16)
)

# Mixing positional with keyword arguments
invocation = await contract.functions["transferFrom"].invoke_v1(
    sender, recipient, amount=10000, max_fee=int(1e16)
)

# Creating a prepared function call with arguments
# It is also possible to use `prepare_invoke_v3`
transfer = contract.functions["transferFrom"].prepare_invoke_v1(
    sender, recipient, amount=10000, max_fee=int(1e16)
)
invocation = await transfer.invoke()

# Wait for tx
await invocation.wait_for_acceptance()

(balance,) = await contract.functions["balanceOf"].call(recipient)

# You can also use key access, call returns TupleDataclass which behaves similar to NamedTuple
result = await contract.functions["balanceOf"].call(recipient)
balance = result.balance

Raw contract calls#

If you do not have ABI statically, but you know the interface of the contract on Starknet, you can make a raw call:

from starknet_py.hash.selector import get_selector_from_name
from starknet_py.net.client_models import Call

eth_token_address = 0x049D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7

# Create a call to function "balanceOf" at address `eth_token_address`
call = Call(
    to_addr=eth_token_address,
    selector=get_selector_from_name("balanceOf"),
    calldata=[account.address],
)
# Pass the created call to Client.call_contract
account_balance = await account.client.call_contract(call)

# Note that a Contract instance cannot be used here, since it needs ABI to generate the functions

Fees#

Starknet.py requires you to specify amount of Wei (for V1 transaction) or Fri (for V3 transaction) you are willing to pay when executing either invoke_v1() or invoke_v3() transactions. Alternatively, you can estimate fee automatically, as described in the Automatic fee estimation section below.

await contract.functions["put"].invoke_v1(k, v, max_fee=5000)

The max_fee argument can be also defined in prepare_invoke_v1(). Subsequently, the invoke() method on a prepared call can be used either with max_fee omitted or with its value overridden. The same behavior applies to prepare_invoke_v3() and l1_resource_bounds.

prepared_call = contract.function["put"].prepare_invoke_v1(k, v, max_fee=5000)
await prepared_call.invoke()
# or max_fee can be overridden
await prepared_call.invoke(max_fee=10000)

Warning

For V1 transactions if max_fee is not specified at any step it will default to None, and will raise an exception when invoking a transaction, unless auto_estimate is specified and is set to True. The same applies to l1_resource_bounds and V3 transactions.

Please note you will need to have enough Wei (for V1 transaction) or Fri (for V3 transaction) in your Starknet account otherwise transaction will be rejected.

Fee estimation#

You can estimate required amount of fee that will need to be paid for transaction using PreparedFunctionInvoke.estimate_fee()

await contract.functions["put"].prepare_invoke_v1(k, v).estimate_fee()

Automatic fee estimation#

For testing purposes it is possible to enable automatic fee estimation when making a transaction. Starknet.py will then call estimate_fee() internally and use the returned value, multiplied by 1.5 to mitigate fluctuations in price, as a max_fee for V1 transactions. For V3 transactions, max_amount will be multiplied by 1.1, and max_price_per_unit by 1.5.

await contract.functions["put"].invoke_v1(k, v, auto_estimate=True)

Warning

It is strongly discouraged to use automatic fee estimation in production code as it may lead to unexpectedly high fee.

Note

For V1 transactions it is possible to configure the value by which the estimated fee is multiplied, by changing ESTIMATED_FEE_MULTIPLIER in Account. The same applies to ESTIMATED_AMOUNT_MULTIPLIER and ESTIMATED_UNIT_PRICE_MULTIPLIER for V3 transactions.

Account and Client interoperability#

Contract methods have been designed to be compatible with Account and Client.

PreparedFunctionInvokeV1 and PreparedFunctionInvokeV3 returned by ContractFunction.prepare_invoke_v1() and ContractFunction.prepare_invoke_v3() respectively can be used in Account methods to create Invoke transactions.

from starknet_py.net.client_models import Call

# Prepare a call through Contract
call = contract.functions["put"].prepare_invoke_v1(key=20, value=30)
assert issubclass(type(call), Call)

# Crate an Invoke transaction from call
invoke_transaction = await account.sign_invoke_v1(call, max_fee=max_fee)

Similarly, PreparedFunctionCall returned by ContractFunction.prepare_call() can be used in Client.call_contract()

from starknet_py.net.client_models import Call

# Prepare a call through Contract
call = contract.functions["get"].prepare_invoke_v1(key=1234)
assert issubclass(type(call), Call)

# Use call directly through Client
result = await client.call_contract(call)