diff --git a/docs/Account.md b/docs/Account.md index 31ba964f5..5a79df4cd 100644 --- a/docs/Account.md +++ b/docs/Account.md @@ -11,7 +11,8 @@ A more detailed writeup on the topic can be found on [Perama's blogpost](https:/ * [Quickstart](#quickstart) * [Standard Interface](#standard-interface) * [Keys, signatures and signers](#keys-signatures-and-signers) - * [Signer utility](#signer-utility) + * [Signer](#signer) + * [TestSigner utility](#TestSigner-utility) * [Call and MultiCall format](#call-and-multicall-format) * [API Specification](#api-specification) * [`get_public_key`](#get_public_key) @@ -33,9 +34,9 @@ The general workflow is: In Python, this would look as follows: -```cairo +```python from starkware.starknet.testing.starknet import Starknet -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) starknet = await Starknet.empty() # 1. Deploy Account @@ -90,23 +91,50 @@ While the interface is agnostic of signature validation schemes, this implementa Note that although the current implementation works only with StarkKeys, support for Ethereum's ECDSA algorithm will be added in the future. -### Signer utility +### Signer + +The signer is responsible for creating a transaction signature with the user's private key for a given transaction. This implementation utilizes [Nile's Signer](https://github.com/OpenZeppelin/nile/blob/main/src/nile/signer.py) class to create transaction signatures through the `Signer` method `sign_transaction`. + +`sign_transaction` expects the following parameters per transaction: + +* `sender` the contract address invoking the tx +* `calls` a list containing a sublist of each call to be sent. Each sublist must consist of: + 1. `to` the address of the target contract of the message + 2. `selector` the function to be called on the target contract + 3. `calldata` the parameters for the given `selector` +* `nonce` an unique identifier of this message to prevent transaction replays. Current implementation requires nonces to be incremental +* `max_fee` the maximum fee a user will pay + +Which returns: + +* `calls` a list of calls to be bundled in the transaction +* `calldata` a list of arguments for each call +* `sig_r` the transaction signature +* `sig_s` the transaction signature + +While the `Signer` class performs much of the work for a transaction to be sent, it neither manages nonces nor invokes the actual transaction on the Account contract. To simplify Account management, most of this is abstracted away with `TestSigner`. + +### TestSigner utility + +The `TestSigner` class in [utils.py](../tests/utils.py) is used to perform transactions on a given Account, crafting the transaction and managing nonces. + +The flow of a transaction starts with checking the nonce and converting the `to` contract address of each call to hexadecimal format. The hexadecimal conversion is necessary because Nile's `Signer` converts the address to a base-16 integer (which requires a string argument). Note that directly converting `to` to a string will ultimately result in an integer exceeding Cairo's `FIELD_PRIME`. + +The values included in the transaction are passed to the `sign_transaction` method of Nile's `Signer` which creates and returns a signature. Finally, the `TestSigner` instance invokes the account contract's `__execute__` with the transaction data. -The `Signer()` class in [utils.py](../tests/utils.py) is used to perform transactions on a given Account, crafting the tx and managing nonces. +Users only need to interact with the following exposed methods to perform a transaction: -It exposes three functions: +* `send_transaction(account, to, selector_name, calldata, nonce=None, max_fee=0)` returns a future of a signed transaction, ready to be sent. -* `def sign(message_hash)` receives a hash and returns a signed message of it -* `def send_transaction(account, to, selector_name, calldata, nonce=None, max_fee=0)` returns a future of a signed transaction, ready to be sent. -* `def send_transactions(account, calls, nonce=None, max_fee=0)` returns a future of batched signed transactions, ready to be sent. +* `send_transactions(account, calls, nonce=None, max_fee=0)` returns a future of batched signed transactions, ready to be sent. -To use Signer, pass a private key when instantiating the class: +To use `TestSigner`, pass a private key when instantiating the class: ```python -from utils import Signer +from utils import TestSigner PRIVATE_KEY = 123456789987654321 -signer = Signer(PRIVATE_KEY) +signer = TestSigner(PRIVATE_KEY) ``` Then send single transactions with the `send_transaction` method. @@ -198,7 +226,7 @@ Where: await signer.send_transaction(account, account.contract_address, 'set_public_key', [NEW_KEY]) ``` -Note that Signer's `send_transaction` and `send_transactions` call `__execute__` under the hood. +Note that `TestSigner`'s `send_transaction` and `send_transactions` call `__execute__` under the hood. Or if you want to update the Account's L1 address on the `AccountRegistry` contract, you would diff --git a/docs/ERC20.md b/docs/ERC20.md index 1001a4abc..49d91df53 100644 --- a/docs/ERC20.md +++ b/docs/ERC20.md @@ -114,7 +114,7 @@ erc20 = await starknet.deploy( As most StarkNet contracts, it expects to be called by another contract and it identifies it through `get_caller_address` (analogous to Solidity's `this.address`). This is why we need an Account contract to interact with it. For example: ```python -signer = Signer(PRIVATE_KEY) +signer = TestSigner(PRIVATE_KEY) amount = uint(100) account = await starknet.deploy( diff --git a/docs/ERC721.md b/docs/ERC721.md index 4ddb02cf6..48fd873cc 100644 --- a/docs/ERC721.md +++ b/docs/ERC721.md @@ -153,7 +153,7 @@ erc721 = await starknet.deploy( To mint a non-fungible token, send a transaction like this: ```python -signer = Signer(PRIVATE_KEY) +signer = TestSigner(PRIVATE_KEY) tokenId = uint(1) await signer.send_transaction( diff --git a/docs/Utilities.md b/docs/Utilities.md index de121b8fe..e2f47ec59 100644 --- a/docs/Utilities.md +++ b/docs/Utilities.md @@ -225,6 +225,6 @@ def foo_factory(contract_defs, foo_init): return cached_foo # return cached contracts ``` -## Signer +## TestSigner -`Signer` is used to perform transactions on a given Account, crafting the tx and managing nonces. See the [Account documentation](../docs/Account.md#signer-utility) for in-depth information. +`TestSigner` is used to perform transactions with an instance of [Nile's Signer](https://github.com/OpenZeppelin/nile/blob/main/src/nile/signer.py) on a given Account, crafting the transaction and managing nonces. The `Signer` instance manages signatures and is leveraged by `TestSigner` to operate with the Account contract's `__execute__` method. See [TestSigner utility](../docs/Account.md#activatedsigner-utility) for more information. diff --git a/tests/access/test_Ownable.py b/tests/access/test_Ownable.py index 493b607d2..7df905646 100644 --- a/tests/access/test_Ownable.py +++ b/tests/access/test_Ownable.py @@ -1,9 +1,9 @@ import pytest from starkware.starknet.testing.starknet import Starknet -from utils import Signer, contract_path +from utils import TestSigner, contract_path -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) @pytest.fixture(scope='module') diff --git a/tests/account/test_Account.py b/tests/account/test_Account.py index 511efb477..d480ed0d3 100644 --- a/tests/account/test_Account.py +++ b/tests/account/test_Account.py @@ -2,11 +2,11 @@ from starkware.starknet.testing.starknet import Starknet from starkware.starkware_utils.error_handling import StarkException from starkware.starknet.definitions.error_codes import StarknetErrorCode -from utils import Signer, assert_revert, contract_path +from utils import TestSigner, assert_revert, contract_path -signer = Signer(123456789987654321) -other = Signer(987654321123456789) +signer = TestSigner(123456789987654321) +other = TestSigner(987654321123456789) IACCOUNT_ID = 0xf10dbd44 TRUE = 1 diff --git a/tests/account/test_AddressRegistry.py b/tests/account/test_AddressRegistry.py index 80b6060d4..de3c2bafb 100644 --- a/tests/account/test_AddressRegistry.py +++ b/tests/account/test_AddressRegistry.py @@ -1,9 +1,9 @@ import pytest from starkware.starknet.testing.starknet import Starknet -from utils import Signer, contract_path +from utils import TestSigner, contract_path -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) L1_ADDRESS = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 ANOTHER_ADDRESS = 0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f diff --git a/tests/token/erc20/test_ERC20.py b/tests/token/erc20/test_ERC20.py index 8043c5d9d..c5ee802f7 100644 --- a/tests/token/erc20/test_ERC20.py +++ b/tests/token/erc20/test_ERC20.py @@ -1,11 +1,12 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, to_uint, add_uint, sub_uint, str_to_felt, MAX_UINT256, ZERO_ADDRESS, INVALID_UINT256, - TRUE, get_contract_def, cached_contract, assert_revert, assert_event_emitted, contract_path + TestSigner, to_uint, add_uint, sub_uint, str_to_felt, MAX_UINT256, + ZERO_ADDRESS, INVALID_UINT256, TRUE, get_contract_def, cached_contract, + assert_revert, assert_event_emitted, contract_path ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) # testing vars RECIPIENT = 123 diff --git a/tests/token/erc20/test_ERC20_Burnable_mock.py b/tests/token/erc20/test_ERC20_Burnable_mock.py index 0a2a0865d..d742b6cdf 100644 --- a/tests/token/erc20/test_ERC20_Burnable_mock.py +++ b/tests/token/erc20/test_ERC20_Burnable_mock.py @@ -1,12 +1,11 @@ import pytest -import asyncio from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, to_uint, add_uint, sub_uint, str_to_felt, ZERO_ADDRESS, INVALID_UINT256, - get_contract_def, cached_contract, assert_revert, assert_event_emitted, contract_path + TestSigner, to_uint, add_uint, sub_uint, str_to_felt, ZERO_ADDRESS, INVALID_UINT256, + get_contract_def, cached_contract, assert_revert, assert_event_emitted, ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) # testing vars INIT_SUPPLY = to_uint(1000) @@ -17,9 +16,6 @@ DECIMALS = 18 -signer = Signer(123456789987654321) - - @pytest.fixture(scope='module') def contract_defs(): account_def = get_contract_def('openzeppelin/account/Account.cairo') diff --git a/tests/token/erc20/test_ERC20_Mintable.py b/tests/token/erc20/test_ERC20_Mintable.py index 354c7941d..3bb3df932 100644 --- a/tests/token/erc20/test_ERC20_Mintable.py +++ b/tests/token/erc20/test_ERC20_Mintable.py @@ -1,11 +1,12 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, to_uint, add_uint, sub_uint, str_to_felt, MAX_UINT256, ZERO_ADDRESS, INVALID_UINT256, - get_contract_def, cached_contract, assert_revert, assert_event_emitted + TestSigner, to_uint, add_uint, sub_uint, str_to_felt, + MAX_UINT256, ZERO_ADDRESS, INVALID_UINT256, get_contract_def, + cached_contract, assert_revert, assert_event_emitted ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) # testing vars RECIPIENT = 123 diff --git a/tests/token/erc20/test_ERC20_Pausable.py b/tests/token/erc20/test_ERC20_Pausable.py index 6f69f8844..818bf295a 100644 --- a/tests/token/erc20/test_ERC20_Pausable.py +++ b/tests/token/erc20/test_ERC20_Pausable.py @@ -1,11 +1,11 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, TRUE, FALSE, to_uint, str_to_felt, assert_revert, get_contract_def, - cached_contract + TestSigner, TRUE, FALSE, to_uint, str_to_felt, assert_revert, + get_contract_def, cached_contract ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) # testing vars INIT_SUPPLY = to_uint(1000) diff --git a/tests/token/erc20/test_ERC20_Upgradeable.py b/tests/token/erc20/test_ERC20_Upgradeable.py index a95196ab5..97202f195 100644 --- a/tests/token/erc20/test_ERC20_Upgradeable.py +++ b/tests/token/erc20/test_ERC20_Upgradeable.py @@ -1,12 +1,12 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, to_uint, sub_uint, str_to_felt, assert_revert, + TestSigner, to_uint, sub_uint, str_to_felt, assert_revert, get_contract_def, cached_contract ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) USER = 999 INIT_SUPPLY = to_uint(1000) diff --git a/tests/token/erc721/test_ERC721_Mintable_Burnable.py b/tests/token/erc721/test_ERC721_Mintable_Burnable.py index 093aa5e20..67e262419 100644 --- a/tests/token/erc721/test_ERC721_Mintable_Burnable.py +++ b/tests/token/erc721/test_ERC721_Mintable_Burnable.py @@ -1,12 +1,12 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, str_to_felt, ZERO_ADDRESS, TRUE, FALSE, assert_revert, INVALID_UINT256, + TestSigner, str_to_felt, ZERO_ADDRESS, TRUE, FALSE, assert_revert, INVALID_UINT256, assert_event_emitted, get_contract_def, cached_contract, to_uint, sub_uint, add_uint ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) NONEXISTENT_TOKEN = to_uint(999) # random token IDs diff --git a/tests/token/erc721/test_ERC721_Mintable_Pausable.py b/tests/token/erc721/test_ERC721_Mintable_Pausable.py index ce8ede11c..009737768 100644 --- a/tests/token/erc721/test_ERC721_Mintable_Pausable.py +++ b/tests/token/erc721/test_ERC721_Mintable_Pausable.py @@ -1,11 +1,12 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, str_to_felt, TRUE, FALSE, get_contract_def, cached_contract, assert_revert, to_uint + TestSigner, str_to_felt, TRUE, FALSE, get_contract_def, cached_contract, + assert_revert, to_uint ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) # random token IDs TOKENS = [to_uint(5042), to_uint(793)] diff --git a/tests/token/erc721/test_ERC721_SafeMintable_mock.py b/tests/token/erc721/test_ERC721_SafeMintable_mock.py index e8cd97dbe..647957255 100644 --- a/tests/token/erc721/test_ERC721_SafeMintable_mock.py +++ b/tests/token/erc721/test_ERC721_SafeMintable_mock.py @@ -1,12 +1,12 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, str_to_felt, ZERO_ADDRESS, INVALID_UINT256, assert_revert, + TestSigner, str_to_felt, ZERO_ADDRESS, INVALID_UINT256, assert_revert, assert_event_emitted, get_contract_def, cached_contract, to_uint ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) # random token id TOKEN = to_uint(5042) diff --git a/tests/token/erc721_enumerable/test_ERC721_Enumerable_Mintable_Burnable.py b/tests/token/erc721_enumerable/test_ERC721_Enumerable_Mintable_Burnable.py index 614350559..0648c4de7 100644 --- a/tests/token/erc721_enumerable/test_ERC721_Enumerable_Mintable_Burnable.py +++ b/tests/token/erc721_enumerable/test_ERC721_Enumerable_Mintable_Burnable.py @@ -1,12 +1,12 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, str_to_felt, MAX_UINT256, get_contract_def, cached_contract, + TestSigner, str_to_felt, MAX_UINT256, get_contract_def, cached_contract, TRUE, assert_revert, to_uint, sub_uint, add_uint ) -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) # random token IDs TOKENS = [ diff --git a/tests/upgrades/test_Proxy.py b/tests/upgrades/test_Proxy.py index 2adb89317..737015a2e 100644 --- a/tests/upgrades/test_Proxy.py +++ b/tests/upgrades/test_Proxy.py @@ -1,14 +1,14 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, assert_revert, get_contract_def, cached_contract + TestSigner, assert_revert, get_contract_def, cached_contract ) # random value VALUE = 123 -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) @pytest.fixture(scope='module') diff --git a/tests/upgrades/test_upgrades.py b/tests/upgrades/test_upgrades.py index 4d478b2dd..46ae2490f 100644 --- a/tests/upgrades/test_upgrades.py +++ b/tests/upgrades/test_upgrades.py @@ -1,7 +1,7 @@ import pytest from starkware.starknet.testing.starknet import Starknet from utils import ( - Signer, assert_revert, assert_event_emitted, get_contract_def, cached_contract + TestSigner, assert_revert, assert_event_emitted, get_contract_def, cached_contract ) @@ -9,7 +9,7 @@ VALUE_1 = 123 VALUE_2 = 987 -signer = Signer(123456789987654321) +signer = TestSigner(123456789987654321) @pytest.fixture(scope='module') diff --git a/tests/utils.py b/tests/utils.py index 6801630bb..24d9b47d8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,15 +2,12 @@ from pathlib import Path import math -from starkware.crypto.signature.signature import private_to_stark_key, sign from starkware.starknet.public.abi import get_selector_from_name from starkware.starknet.compiler.compile import compile_starknet_files from starkware.starkware_utils.error_handling import StarkException from starkware.starknet.testing.starknet import StarknetContract -from starkware.starknet.definitions.general_config import StarknetChainId from starkware.starknet.business_logic.execution.objects import Event -from starkware.starknet.core.os.transaction_hash.transaction_hash import calculate_transaction_hash_common, TransactionHashPrefix - +from nile.signer import Signer MAX_UINT256 = (2**128 - 1, 2**128 - 1) @@ -128,7 +125,7 @@ def cached_contract(state, definition, deployed): return contract -class Signer(): +class TestSigner(): """ Utility for sending signed transactions to an Account on Starknet. @@ -139,27 +136,30 @@ class Signer(): Examples --------- - Constructing a Signer object + Constructing a TestSigner object - >>> signer = Signer(1234) + >>> signer = TestSigner(1234) Sending a transaction - >>> await signer.send_transaction(account, - account.contract_address, - 'set_public_key', - [other.public_key] - ) + >>> await signer.send_transaction( + account, contract_address, 'contract_method', [arg_1] + ) - """ + Sending multiple transactions + >>> await signer.send_transaction( + account, [ + (contract_address, 'contract_method', [arg_1]), + (contract_address, 'another_method', [arg_1, arg_2]) + ] + ) + + """ def __init__(self, private_key): - self.private_key = private_key - self.public_key = private_to_stark_key(private_key) - - def sign(self, message_hash): - return sign(msg_hash=message_hash, priv_key=self.private_key) - + self.signer = Signer(private_key) + self.public_key = self.signer.public_key + async def send_transaction(self, account, to, selector_name, calldata, nonce=None, max_fee=0): return await self.send_transactions(account, [(to, selector_name, calldata)], nonce, max_fee) @@ -168,40 +168,11 @@ async def send_transactions(self, account, calls, nonce=None, max_fee=0): execution_info = await account.get_nonce().call() nonce, = execution_info.result - (call_array, calldata) = from_call_to_call_array(calls) - - message_hash = get_transaction_hash(account.contract_address, call_array, calldata, nonce, max_fee) - sig_r, sig_s = self.sign(message_hash) + build_calls = [] + for call in calls: + build_call = list(call) + build_call[0] = hex(build_call[0]) + build_calls.append(build_call) + (call_array, calldata, sig_r, sig_s) = self.signer.sign_transaction(hex(account.contract_address), build_calls, nonce, max_fee) return await account.__execute__(call_array, calldata, nonce).invoke(signature=[sig_r, sig_s]) - - -def from_call_to_call_array(calls): - call_array = [] - calldata = [] - for i, call in enumerate(calls): - assert len(call) == 3, "Invalid call parameters" - entry = (call[0], get_selector_from_name( - call[1]), len(calldata), len(call[2])) - call_array.append(entry) - calldata.extend(call[2]) - return (call_array, calldata) - -def get_transaction_hash(account, call_array, calldata, nonce, max_fee): - execute_calldata = [ - len(call_array), - *[x for t in call_array for x in t], - len(calldata), - *calldata, - nonce] - - return calculate_transaction_hash_common( - TransactionHashPrefix.INVOKE, - TRANSACTION_VERSION, - account, - get_selector_from_name('__execute__'), - execute_calldata, - max_fee, - StarknetChainId.TESTNET.value, - [] - ) diff --git a/tox.ini b/tox.ini index 0856f1693..54abc6376 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ passenv = PYTHONPATH deps = cairo-lang==0.8.1 + cairo-nile==0.6.1 pytest-xdist # See https://github.com/starkware-libs/cairo-lang/issues/52 marshmallow-dataclass==8.5.3