Interacting with a contract¶
This guide describes how you can leverage aepp-sdk interact with a deployed aeternity smart contract.
See also
Sophia: An Æternity Blockchain Language
The Sophia is a language in the ML family. It is strongly typed and has restricted mutable state. Check out more about sophia here.
Prerequisites¶
- An account with some initial AE
- An address/contract_id of a deployed contract
- An aeternity node
- A sophia aehttp compiler
Assumptions
We’re going to assume that you have working knowledge of the SDK and know how to create an Account instance, a NodeClient, and a CompilerClient.
Sample Sophia Contract¶
Below is the sample sophia contract that we’ll use in this guide.
@compiler >= 4
contract CryptoHamster =
record state = {
index : int,
map_hamsters : map(string, hamster),
testvalue: int}
record hamster = {
id : int,
name : string,
dna : int}
stateful entrypoint init() =
{ index = 1,
map_hamsters = {},
testvalue = 42}
public entrypoint read_test_value() : int =
state.testvalue
public entrypoint return_caller() : address =
Call.caller
public entrypoint cause_error() : unit =
require(2 == 1, "require failed")
public stateful entrypoint add_test_value(one: int, two: int) : int =
put(state{testvalue = one + two})
one + two
public entrypoint locally_add_two(one: int, two: int) : int =
one + two
public stateful entrypoint statefully_add_two(one: int, two: int) =
put(state{testvalue = one + two})
stateful entrypoint create_hamster(hamster_name: string) =
require(!name_exists(hamster_name), "Name is already taken")
let dna : int = generate_random_dna(hamster_name)
create_hamster_by_name_dna(hamster_name, dna)
entrypoint name_exists(name: string) : bool =
Map.member(name, state.map_hamsters)
entrypoint get_hamster_dna(name: string, test: option(int)) : int =
require(name_exists(name), "There is no hamster with that name!")
let needed_hamster : hamster = state.map_hamsters[name]
needed_hamster.dna
private stateful function create_hamster_by_name_dna(name: string, dna: int) =
let new_hamster : hamster = {
id = state.index,
name = name,
dna = dna}
put(state{map_hamsters[name] = new_hamster})
put(state{index = (state.index + 1)})
private function generate_random_dna(name: string) : int =
get_block_hash_bytes_as_int() - Chain.timestamp + state.index
private function get_block_hash_bytes_as_int() : int =
switch(Chain.block_hash(Chain.block_height - 1))
None => abort("blockhash not found")
Some(bytes) => Bytes.to_int(bytes)
entrypoint test(name: string) : hash =
String.sha3(name)
Importing required classes and methods¶
We need to import the following classes to use contracts.
from aeternity.node import NodeClient, Config
from aeternity.compiler import CompilerClient
from aeternity.contract_native import ContractNative
from aeternity.signing import Account
Initializing NodeClient and Compiler¶
Below are the steps required to initialize the NodeClient and Compiler. As you can see below, during the initialization of NodeClient we’re also providing the internal_url.
internal_url provides the debug endpoint to dry_run a contract method which can also be used to do static calls on deployed contracts and this is what exactly we’re going to use this for.
You can also not provide the internal_url but then you’ll have to disable the use of dry-run endpoint. We’ll see how to do that when we initialize our contract.
NODE_URL = os.environ.get('TEST_URL', 'http://127.0.0.1:3013')
NODE_INTERNAL_URL = os.environ.get('TEST_DEBUG_URL', 'http://127.0.0.1:3113')
COMPILER_URL = os.environ.get('TEST_COMPILER__URL', 'https://compiler.aepps.com')
node_cli = NodeClient(Config(
external_url=NODE_URL,
internal_url=NODE_INTERNAL_URL,
blocking_mode=True,
))
compiler = CompilerClient(compiler_url=COMPILER_URL)
Generate an Account¶
You’ll need an account (using the Account class) for stateful contract calls.
# genrate ALICE account (and transfer AE to alice account)
alice = Account.generate()
Read the Contract from file and initialize¶
You can read the contract from the stored .aes file and use it to initialize the contract instance. If you have not provided the internal_endpoint or simple do not want to use the dry-run functionality you can disable it by passing use-dry-run=False to the ContractNative constructor.
Warning
If you DO NOT provide the internal_url during NodeClient initialization and also DID NOT disable the dry-run then the contract method calls for un-stateful methods WILL FAIL.
CONTRACT_FILE = os.path.join(os.path.dirname(__file__), "testdata/CryptoHamster.aes")
# read contract file
with open(CONTRACT_FILE, 'r') as file:
crypto_hamster_contract = file.read()
"""
Initialize the contract instance
Note: To disable use of dry-run endpoint add the parameter
use_dry_run=False
"""
crypto_hamster = ContractNative(client=node_cli,
compiler=compiler,
account=alice,
source=crypto_hamster_contract)
Now pass the address of the deployed contract
Warning
If the contract is not found at the provided address and for the given network, the method will fail
# CONTRACT_ID is the address of the deployed contract
crypto_hamster.at(CONTRACT_ID)
Call the contract methods¶
All the methods inside the contract are also available (with same signature) to use from the contract instance.
Note
All the methods that are NOT stateful, by default are processed using the dry-run endpoint to save gas. And therefore, a transaction hash will also not be provided for them. This functionality can be either diabled for the contract instance or per method by using use_dry_run argument.
# call the contract method (stateful)
tx_info, tx_result = crypto_hamster.add_test_value(1, 2)
print(f"Transaction Hash: {tx_info.tx_hash}")
print(f"Transaction Result/Return Data: {tx_result}")
And in a similar way a not stateful call can be invoked
# call contract method (not stateful)
tx_info, tx_result = crypto_hamster.get_hamster_dna("SuperCryptoHamster", None)
print(f"Transaction Result/Return Data: {tx_result}")