diff --git a/page/docs/guide/development.md b/page/docs/guide/development.md index 995de456b..6cdfb911f 100644 --- a/page/docs/guide/development.md +++ b/page/docs/guide/development.md @@ -1,5 +1,5 @@ --- -sidebar_position: 17 +sidebar_position: 18 --- # Development diff --git a/page/docs/guide/devnet-speed-up.md b/page/docs/guide/devnet-speed-up.md index 836cef7da..34effe943 100644 --- a/page/docs/guide/devnet-speed-up.md +++ b/page/docs/guide/devnet-speed-up.md @@ -1,5 +1,5 @@ --- -sidebar_position: 16 +sidebar_position: 17 --- # Devnet speed-up troubleshooting diff --git a/page/docs/guide/fork.md b/page/docs/guide/fork.md new file mode 100644 index 000000000..757023169 --- /dev/null +++ b/page/docs/guide/fork.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 16 +--- + +# Fork + +To interact with contracts deployed on mainnet or testnet, you can use the forking feature to copy the remote origin and experiment with it locally with no changes to the origin. + +``` +starknet-devnet --fork-network [--fork-block ] +``` + +The value of `--fork-network` can either be a network name (`alpha-goerli`, `alpha-goerli2`, or `alpha-mainnet`) or a URL (e.g. `https://alpha4.starknet.io`). + +The `--fork-block` parameter is optional and its value should be the block number from which the forking is done. If none is provided, defaults to the `"latest"` block at the time of Devnet's start-up. + +All calls will first try Devnet's state and then fall back to the forking block. + +If you are forking another Devnet instance, keep in mind that it doesn't support polling specific blocks, but will always fall back to the currently latest block. diff --git a/page/docs/guide/mint-token.md b/page/docs/guide/mint-token.md index ee5ad5f51..921c6f446 100644 --- a/page/docs/guide/mint-token.md +++ b/page/docs/guide/mint-token.md @@ -6,7 +6,7 @@ sidebar_position: 14 Other than using prefunded predeployed accounts, you can also add funds to an account that you deployed yourself. -The ERC20 contract used for minting ETH tokens and charging fees is at: `0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488` +The ERC20 contract used for minting ETH tokens and charging fees is at: `0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7` ## Query fee token address @@ -19,7 +19,7 @@ Response: ``` { "symbol":"ETH", - "address":"0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488", + "address":"0x...", } ``` diff --git a/page/docs/guide/run.md b/page/docs/guide/run.md index ecdc78647..57781f8b1 100644 --- a/page/docs/guide/run.md +++ b/page/docs/guide/run.md @@ -9,7 +9,7 @@ Installing the package adds the `starknet-devnet` command. ```text usage: starknet-devnet [-h] [-v] [--host HOST] [--port PORT] [--load-path LOAD_PATH] [--dump-path DUMP_PATH] [--dump-on DUMP_ON] [--lite-mode] [--accounts ACCOUNTS] [--initial-balance INITIAL_BALANCE] [--seed SEED] [--hide-predeployed-accounts] [--start-time START_TIME] [--gas-price GAS_PRICE] [--timeout TIMEOUT] - [--account-class ACCOUNT_CLASS] + [--account-class ACCOUNT_CLASS] [--fork-network FORK_NETWORK] [--fork-block FORK_BLOCK] Run a local instance of StarkNet Devnet @@ -23,7 +23,7 @@ optional arguments: --dump-path DUMP_PATH Specify the path to dump to --dump-on DUMP_ON Specify when to dump; can dump on: exit, transaction - --lite-mode Introduces speed-up by skipping block hash and deploy transaction hash calculation - applies sequential numbering instead (0x0, 0x1, 0x2, ...). + --lite-mode Introduces speed-up by skipping block hash calculation - applies sequential numbering instead (0x0, 0x1, 0x2, ...). --accounts ACCOUNTS Specify the number of accounts to be predeployed; defaults to 10 --initial-balance INITIAL_BALANCE, -e INITIAL_BALANCE Specify the initial balance of accounts to be predeployed; defaults to 1e+21 @@ -37,9 +37,11 @@ optional arguments: --timeout TIMEOUT, -t TIMEOUT Specify the server timeout in seconds; defaults to 60 --account-class ACCOUNT_CLASS - Specify the account implementation to be used for predeploying; - should be a path to the compiled JSON artifact; - defaults to OpenZeppelin v0.5.0 + Specify the account implementation to be used for predeploying; should be a path to the compiled JSON artifact; defaults to OpenZeppelin v0.5.0 + --fork-network FORK_NETWORK + Specify the network to fork: can be a URL (e.g. https://alpha-mainnet.starknet.io) or network name (valid names: alpha-goerli, alpha-goerli2, alpha-mainnet) + --fork-block FORK_BLOCK + Specify the block number where the --fork-network is forked; defaults to latest ``` You can run `starknet-devnet` in a separate shell, or you can run it in background with `starknet-devnet &`. diff --git a/pyproject.toml b/pyproject.toml index 5f0e3e62b..8666fc1db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,4 +63,5 @@ asyncio_mode="strict" filterwarnings=[ "ignore::DeprecationWarning:lark.*:", "ignore::DeprecationWarning:frozendict.*:", + "ignore::DeprecationWarning:eth_abi.codec.*:", ] diff --git a/scripts/mint.sh b/scripts/mint.sh new file mode 100755 index 000000000..9a6995d3d --- /dev/null +++ b/scripts/mint.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -eu + +address=$1 + +curl localhost:5050/mint \ + -H "Content-Type: application/json" \ + -d "{ \"address\": \"$address\", \"amount\": 1000000000000000000, \"lite\": false }" diff --git a/scripts/test_fork.sh b/scripts/test_fork.sh new file mode 100755 index 000000000..5a81719b8 --- /dev/null +++ b/scripts/test_fork.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +set -eu + +trap 'for killable in $(jobs -p); do kill $killable; done' EXIT + +HOST=localhost +PORT1=5049 +DEVNET1_URL="http://$HOST:$PORT1" +PORT2=5050 +DEVNET2_URL="http://$HOST:$PORT2" + + +poetry run starknet-devnet --host "$HOST" --port "$PORT1" --seed 42 --accounts 1 --hide-predeployed-accounts & +DEVNET1_PID=$! +curl --retry 20 --retry-delay 1 --retry-connrefused -s -o /dev/null "$DEVNET1_URL/is_alive" +echo "Started up devnet1; pid: $DEVNET1_PID" + +poetry run starknet-devnet --host "$HOST" --port "$PORT2" --fork-network "$DEVNET1_URL" --accounts 0 & +DEVNET2_PID=$! +curl --retry 20 --retry-delay 1 --retry-connrefused -s -o /dev/null "$DEVNET2_URL/is_alive" +echo "Started up devnet2; pid: $DEVNET2_PID" + +# # get public key of predeployed account +# for port in "$PORT1" "$PORT2"; do +# echo "Polling devnet at :$port" +# poetry run starknet get_storage_at \ +# --feeder_gateway_url "http://$HOST:$port" \ +# --contract_address 0x347be35996a21f6bf0623e75dbce52baba918ad5ae8d83b6f416045ab22961a \ +# --key 550557492744938365112574611882025123252567779123164597803728068558738016655 +# done + +source ~/venvs/cairo_venv-0.10.1-pre/bin/activate +DEPLOYMENT_URL="$DEVNET1_URL" +starknet deploy \ + --contract test/artifacts/contracts/cairo/contract.cairo/contract.json \ + --inputs 10 \ + --gateway_url "$DEPLOYMENT_URL" \ + --feeder_gateway_url "$DEPLOYMENT_URL" \ + --salt 0x99 \ + --no_wallet +echo "Deployed contract" +CONTRACT_ADDRESS=0x07c80f5573d4c636960b56b02a01514d487c6e6a2c6f9242490280c932a32f71 + +export STARKNET_WALLET="starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount" +ACCOUNT_DIR="." +rm -rf ./starknet_open_zeppelin_accounts.json* + +starknet new_account --network alpha-goerli --account_dir "$ACCOUNT_DIR" + +ACCOUNT_DEPLOYMENT_URL="$DEVNET1_URL" +starknet deploy_account \ + --gateway_url "$ACCOUNT_DEPLOYMENT_URL" \ + --feeder_gateway_url "$ACCOUNT_DEPLOYMENT_URL" \ + --network alpha-goerli \ + --account_dir "$ACCOUNT_DIR" \ + --max_fee 0 +echo "Deployed account" + +INVOKE_URL="$DEVNET1_URL" +INVOKE_HASH=$(starknet invoke \ + --abi test/artifacts/contracts/cairo/contract.cairo/contract_abi.json \ + --function increase_balance \ + --inputs 10 20 \ + --address "$CONTRACT_ADDRESS" \ + --gateway_url "$INVOKE_URL" \ + --feeder_gateway_url "$INVOKE_URL" \ + --max_fee 0 \ + --network_id alpha-goerli \ + --chain_id 0x534e5f474f45524c49 \ + --account_dir "$ACCOUNT_DIR" | sed -rn 's/^Transaction hash: (.*)$/\1/p' +) +echo "Invoked contract on $INVOKE_URL" + +echo "Transaction on $DEVNET1_URL" +starknet get_transaction --hash "$INVOKE_HASH" --feeder_gateway_url "$DEVNET1_URL" +echo "Transaction on $DEVNET2_URL" +starknet get_transaction --hash "$INVOKE_HASH" --feeder_gateway_url "$DEVNET2_URL" +echo "Block on $DEVNET2_URL" +starknet get_block --feeder_gateway "$DEVNET2_URL" + +for url in "$DEVNET1_URL" "$DEVNET2_URL"; do + starknet call \ + --abi test/artifacts/contracts/cairo/contract.cairo/contract_abi.json \ + --feeder_gateway_url "$url" \ + --address "$CONTRACT_ADDRESS" \ + --function get_balance +done diff --git a/starknet_devnet/account.py b/starknet_devnet/account.py index 339f7cebb..20e6d9b44 100644 --- a/starknet_devnet/account.py +++ b/starknet_devnet/account.py @@ -76,14 +76,3 @@ async def deploy(self) -> StarknetContract: await starknet.state.state.set_storage_at( fee_token_address, balance_address + 1, initial_balance_uint256.high ) - - contract = StarknetContract( - state=starknet.state, - abi=contract_class.abi, - contract_address=self.address, - deploy_call_info=None, - ) - - await self.starknet_wrapper.store_contract( - self.address, contract, contract_class - ) diff --git a/starknet_devnet/accounts.py b/starknet_devnet/accounts.py index e02ee9951..d8b6d0b94 100644 --- a/starknet_devnet/accounts.py +++ b/starknet_devnet/accounts.py @@ -9,6 +9,7 @@ from starkware.crypto.signature.signature import private_to_stark_key from .account import Account +from .util import warn class Accounts: @@ -77,7 +78,7 @@ def __print(self): print(f"Initial balance of each account: {self.__initial_balance} WEI") print("Seed to replicate this account sequence:", self.__seed) - print( + warn( "WARNING: Use these accounts and their keys ONLY for local testing. " "DO NOT use them on mainnet or other live networks because you will LOSE FUNDS.\n", file=sys.stderr, diff --git a/starknet_devnet/blocks.py b/starknet_devnet/blocks.py index cb0cdf919..88b519f72 100644 --- a/starknet_devnet/blocks.py +++ b/starknet_devnet/blocks.py @@ -12,6 +12,7 @@ BlockStatus, ) from starkware.starknet.services.api.feeder_gateway.response_objects import ( + BlockIdentifier, BlockStateUpdate, ) from starkware.starkware_utils.error_handling import StarkErrorCode @@ -33,22 +34,16 @@ def __init__(self, origin: Origin, lite=False) -> None: self.__state_updates: Dict[int, BlockStateUpdate] = {} self.__hash2num: Dict[str, int] = {} - def get_last_block(self) -> StarknetBlock: + async def get_last_block(self) -> StarknetBlock: """Returns the last block stored so far.""" number_of_blocks = self.get_number_of_blocks() - return self.get_by_number(number_of_blocks - 1) + return await self.get_by_number(number_of_blocks - 1) def get_number_of_blocks(self) -> int: """Returns the number of blocks stored so far.""" return len(self.__num2block) + self.origin.get_number_of_blocks() - def get_by_number(self, block_number: int) -> StarknetBlock: - """Returns the block whose block_number is provided""" - if block_number is None: - if self.__num2block: - return self.get_last_block() - return self.origin.get_block_by_number(block_number) - + def __assert_block_number_in_range(self, block_number: BlockIdentifier): if block_number < 0: message = ( f"Block number must be a non-negative integer; got: {block_number}." @@ -63,12 +58,20 @@ def get_by_number(self, block_number: int) -> StarknetBlock: code=StarknetErrorCode.BLOCK_NOT_FOUND, message=message ) + async def get_by_number(self, block_number: BlockIdentifier) -> StarknetBlock: + """Returns the block whose block_number is provided""" + if block_number is None: + if self.__num2block: + return await self.get_last_block() + return await self.origin.get_block_by_number(block_number) + + self.__assert_block_number_in_range(block_number) if block_number in self.__num2block: return self.__num2block[block_number] - return self.origin.get_block_by_number(block_number) + return await self.origin.get_block_by_number(block_number) - def get_by_hash(self, block_hash: str) -> StarknetBlock: + async def get_by_hash(self, block_hash: str) -> StarknetBlock: """ Returns the block with the given block hash. """ @@ -76,11 +79,13 @@ def get_by_hash(self, block_hash: str) -> StarknetBlock: if numeric_hash in self.__hash2num: block_number = self.__hash2num[int(block_hash, 16)] - return self.get_by_number(block_number) + return await self.get_by_number(block_number) - return self.origin.get_block_by_hash(block_hash) + return await self.origin.get_block_by_hash(block_hash) - def get_state_update(self, block_hash=None, block_number=None) -> BlockStateUpdate: + async def get_state_update( + self, block_hash=None, block_number=None + ) -> BlockStateUpdate: """ Returns state update for the provided block hash or block number. It will return the last state update if block is not provided. @@ -89,19 +94,20 @@ def get_state_update(self, block_hash=None, block_number=None) -> BlockStateUpda numeric_hash = int(block_hash, 16) if numeric_hash not in self.__hash2num: - return self.origin.get_state_update(block_hash=block_hash) + return await self.origin.get_state_update(block_hash=block_hash) block_number = self.__hash2num[numeric_hash] if block_number is not None: - if block_number not in self.__state_updates: - return self.origin.get_state_update(block_number=block_number) + self.__assert_block_number_in_range(block_number) + if block_number in self.__state_updates: + return self.__state_updates[block_number] - return self.__state_updates[block_number] + return await self.origin.get_state_update(block_number=block_number) return ( self.__state_updates.get(self.get_number_of_blocks() - 1) - or self.origin.get_state_update() + or await self.origin.get_state_update() ) async def generate( @@ -122,7 +128,7 @@ async def generate( if block_number == 0: parent_block_hash = 0 else: - last_block = self.get_last_block() + last_block = await self.get_last_block() parent_block_hash = last_block.block_hash if is_empty_block: diff --git a/starknet_devnet/blueprints/feeder_gateway.py b/starknet_devnet/blueprints/feeder_gateway.py index 4891149f6..e50014c6e 100644 --- a/starknet_devnet/blueprints/feeder_gateway.py +++ b/starknet_devnet/blueprints/feeder_gateway.py @@ -17,11 +17,13 @@ CallL1Handler, ) from starkware.starknet.services.api.feeder_gateway.response_objects import ( + StarknetBlock, TransactionSimulationInfo, ) from starkware.starkware_utils.error_handling import StarkErrorCode from werkzeug.datastructures import MultiDict + from starknet_devnet.state import state from starknet_devnet.util import StarknetDevnetException, custom_int, fixed_length_hex @@ -68,25 +70,25 @@ def _check_block_arguments(block_hash, block_number): ) -def _get_block_object(block_hash: str, block_number: int): +async def _get_block_object(block_hash: str, block_number: int): """Returns the block object""" _check_block_arguments(block_hash, block_number) if block_hash is not None: - block = state.starknet_wrapper.blocks.get_by_hash(block_hash) - else: - block = state.starknet_wrapper.blocks.get_by_number(block_number) + return await state.starknet_wrapper.blocks.get_by_hash(block_hash) - return block + return await state.starknet_wrapper.blocks.get_by_number(block_number) -def _get_block_transaction_traces(block): +async def _get_block_transaction_traces(block: StarknetBlock): traces = [] if block.transaction_receipts: for transaction in block.transaction_receipts: tx_hash = hex(transaction.transaction_hash) - trace = state.starknet_wrapper.transactions.get_transaction_trace(tx_hash) + trace = await state.starknet_wrapper.transactions.get_transaction_trace( + tx_hash + ) # expected trace is equal to response of get_transaction, but with the hash property trace_dict = trace.dump() @@ -121,32 +123,32 @@ async def call_contract(): @feeder_gateway.route("/get_block", methods=["GET"]) -def get_block(): +async def get_block(): """Endpoint for retrieving a block identified by its hash or number.""" block_hash = request.args.get("blockHash") block_number = request.args.get("blockNumber", type=custom_int) - block = _get_block_object(block_hash=block_hash, block_number=block_number) + block = await _get_block_object(block_hash=block_hash, block_number=block_number) return Response(block.dumps(), status=200, mimetype="application/json") @feeder_gateway.route("/get_block_traces", methods=["GET"]) -def get_block_traces(): +async def get_block_traces(): """Returns the traces of the transactions in the specified block.""" block_hash = request.args.get("blockHash") block_number = request.args.get("blockNumber", type=custom_int) - block = _get_block_object(block_hash=block_hash, block_number=block_number) - block_transaction_traces = _get_block_transaction_traces(block) + block = await _get_block_object(block_hash=block_hash, block_number=block_number) + block_transaction_traces = await _get_block_transaction_traces(block) return jsonify(block_transaction_traces.dump()) @feeder_gateway.route("/get_code", methods=["GET"]) -def get_code(): +async def get_code(): """ Returns the ABI and bytecode of the contract whose contractAddress is provided. """ @@ -154,12 +156,12 @@ def get_code(): _check_block_hash(request.args) contract_address = request.args.get("contractAddress", type=custom_int) - result_dict = state.starknet_wrapper.contracts.get_code(contract_address) - return jsonify(result_dict) + code_dict = await state.starknet_wrapper.get_code(contract_address) + return jsonify(code_dict) @feeder_gateway.route("/get_full_contract", methods=["GET"]) -def get_full_contract(): +async def get_full_contract(): """ Returns the contract class of the contract whose contractAddress is provided. """ @@ -167,29 +169,26 @@ def get_full_contract(): contract_address = request.args.get("contractAddress", type=custom_int) - contract_class = state.starknet_wrapper.contracts.get_full_contract( - contract_address - ) - - return jsonify(contract_class.dump()) + contract_class = await state.starknet_wrapper.get_class_by_address(contract_address) + return jsonify(contract_class.remove_debug_info().dump()) @feeder_gateway.route("/get_class_hash_at", methods=["GET"]) -def get_class_hash_at(): +async def get_class_hash_at(): """Get contract class hash by contract address""" contract_address = request.args.get("contractAddress", type=custom_int) - class_hash = state.starknet_wrapper.contracts.get_class_hash_at(contract_address) + class_hash = await state.starknet_wrapper.get_class_hash_at(contract_address) return jsonify(fixed_length_hex(class_hash)) @feeder_gateway.route("/get_class_by_hash", methods=["GET"]) -def get_class_by_hash(): +async def get_class_by_hash(): """Get contract class by class hash""" class_hash = request.args.get("classHash", type=custom_int) - contract_class = state.starknet_wrapper.contracts.get_class_by_hash(class_hash) - return jsonify(contract_class.dump()) + contract_class = await state.starknet_wrapper.get_class_by_hash(class_hash) + return jsonify(contract_class.remove_debug_info().dump()) @feeder_gateway.route("/get_storage_at", methods=["GET"]) @@ -205,26 +204,26 @@ async def get_storage_at(): @feeder_gateway.route("/get_transaction_status", methods=["GET"]) -def get_transaction_status(): +async def get_transaction_status(): """ Returns the status of the transaction identified by the transactionHash argument in the GET request. """ transaction_hash = request.args.get("transactionHash") - transaction_status = state.starknet_wrapper.transactions.get_transaction_status( + tx_status = await state.starknet_wrapper.transactions.get_transaction_status( transaction_hash ) - return jsonify(transaction_status) + return jsonify(tx_status) @feeder_gateway.route("/get_transaction", methods=["GET"]) -def get_transaction(): +async def get_transaction(): """ Returns the transaction identified by the transactionHash argument in the GET request. """ transaction_hash = request.args.get("transactionHash") - transaction_info = state.starknet_wrapper.transactions.get_transaction( + transaction_info = await state.starknet_wrapper.transactions.get_transaction( transaction_hash ) return Response( @@ -233,28 +232,28 @@ def get_transaction(): @feeder_gateway.route("/get_transaction_receipt", methods=["GET"]) -def get_transaction_receipt(): +async def get_transaction_receipt(): """ Returns the transaction receipt identified by the transactionHash argument in the GET request. """ transaction_hash = request.args.get("transactionHash") - transaction_receipt = state.starknet_wrapper.transactions.get_transaction_receipt( + tx_receipt = await state.starknet_wrapper.transactions.get_transaction_receipt( transaction_hash ) return Response( - response=transaction_receipt.dumps(), status=200, mimetype="application/json" + response=tx_receipt.dumps(), status=200, mimetype="application/json" ) @feeder_gateway.route("/get_transaction_trace", methods=["GET"]) -def get_transaction_trace(): +async def get_transaction_trace(): """ Returns the trace of the transaction identified by the transactionHash argument in the GET request. """ transaction_hash = request.args.get("transactionHash") - transaction_trace = state.starknet_wrapper.transactions.get_transaction_trace( + transaction_trace = await state.starknet_wrapper.transactions.get_transaction_trace( transaction_hash ) @@ -264,7 +263,7 @@ def get_transaction_trace(): @feeder_gateway.route("/get_state_update", methods=["GET"]) -def get_state_update(): +async def get_state_update(): """ Returns the status update from the block identified by the blockHash argument in the GET request. If no block hash was provided it will default to the last block. @@ -273,7 +272,7 @@ def get_state_update(): block_hash = request.args.get("blockHash") block_number = request.args.get("blockNumber", type=custom_int) - state_update = state.starknet_wrapper.blocks.get_state_update( + state_update = await state.starknet_wrapper.blocks.get_state_update( block_hash=block_hash, block_number=block_number ) diff --git a/starknet_devnet/blueprints/rpc/blocks.py b/starknet_devnet/blueprints/rpc/blocks.py index b4fb47d01..5d3f4dd70 100644 --- a/starknet_devnet/blueprints/rpc/blocks.py +++ b/starknet_devnet/blueprints/rpc/blocks.py @@ -12,7 +12,7 @@ async def get_block_with_tx_hashes(block_id: BlockId) -> dict: """ Get block information with transaction hashes given the block id """ - block = get_block_by_block_id(block_id) + block = await get_block_by_block_id(block_id) return await rpc_block(block=block) @@ -20,7 +20,7 @@ async def get_block_with_txs(block_id: BlockId) -> dict: """ Get block information with full transactions given the block id """ - block = get_block_by_block_id(block_id) + block = await get_block_by_block_id(block_id) return await rpc_block(block=block, tx_type="FULL_TXNS") @@ -44,7 +44,7 @@ async def block_hash_and_number() -> dict: raise RpcError(code=32, message="There are no blocks") last_block_number = number_of_blocks - 1 - last_block = state.starknet_wrapper.blocks.get_by_number(last_block_number) + last_block = await state.starknet_wrapper.blocks.get_by_number(last_block_number) result = { "block_hash": rpc_felt(last_block.block_hash), @@ -57,5 +57,5 @@ async def get_block_transaction_count(block_id: BlockId) -> int: """ Get the number of transactions in a block given a block id """ - block = get_block_by_block_id(block_id) + block = await get_block_by_block_id(block_id) return len(block.transactions) diff --git a/starknet_devnet/blueprints/rpc/call.py b/starknet_devnet/blueprints/rpc/call.py index b62523479..d0c208130 100644 --- a/starknet_devnet/blueprints/rpc/call.py +++ b/starknet_devnet/blueprints/rpc/call.py @@ -36,9 +36,9 @@ async def call(request: RpcFunctionCall, block_id: BlockId) -> List[Felt]: """ Call a starknet function without creating a StarkNet transaction """ - assert_block_id_is_latest_or_pending(block_id) + await assert_block_id_is_latest_or_pending(block_id) - if not state.starknet_wrapper.contracts.is_deployed( + if not await state.starknet_wrapper.is_deployed( int(request["contract_address"], 16) ): raise RpcError(code=20, message="Contract not found") diff --git a/starknet_devnet/blueprints/rpc/classes.py b/starknet_devnet/blueprints/rpc/classes.py index 388003282..3e073716b 100644 --- a/starknet_devnet/blueprints/rpc/classes.py +++ b/starknet_devnet/blueprints/rpc/classes.py @@ -2,6 +2,8 @@ RPC classes endpoints """ +from starkware.starkware_utils.error_handling import StarkException + from starknet_devnet.blueprints.rpc.utils import ( assert_block_id_is_latest_or_pending, rpc_felt, @@ -21,10 +23,10 @@ async def get_class(block_id: BlockId, class_hash: Felt) -> dict: """ Get the contract class definition in the given block associated with the given hash """ - assert_block_id_is_latest_or_pending(block_id) + await assert_block_id_is_latest_or_pending(block_id) try: - result = state.starknet_wrapper.contracts.get_class_by_hash( + result = await state.starknet_wrapper.get_class_by_hash( class_hash=int(class_hash, 16) ) except StarknetDevnetException as ex: @@ -37,13 +39,13 @@ async def get_class_hash_at(block_id: BlockId, contract_address: Address) -> Fel """ Get the contract class hash in the given block for the contract deployed at the given address """ - assert_block_id_is_latest_or_pending(block_id) + await assert_block_id_is_latest_or_pending(block_id) try: - result = state.starknet_wrapper.contracts.get_class_hash_at( - address=int(contract_address, 16) + result = await state.starknet_wrapper.get_class_hash_at( + int(contract_address, 16) ) - except StarknetDevnetException as ex: + except StarkException as ex: raise RpcError(code=28, message="Class hash not found") from ex return rpc_felt(result) @@ -53,16 +55,13 @@ async def get_class_at(block_id: BlockId, contract_address: Address) -> dict: """ Get the contract class definition in the given block at the given address """ - assert_block_id_is_latest_or_pending(block_id) + await assert_block_id_is_latest_or_pending(block_id) try: - class_hash = state.starknet_wrapper.contracts.get_class_hash_at( - address=int(contract_address, 16) + result = await state.starknet_wrapper.get_class_by_address( + int(contract_address, 16) ) - result = state.starknet_wrapper.contracts.get_class_by_hash( - class_hash=class_hash - ) - except StarknetDevnetException as ex: + except StarkException as ex: raise RpcError(code=20, message="Contract not found") from ex return rpc_contract_class(result) diff --git a/starknet_devnet/blueprints/rpc/misc.py b/starknet_devnet/blueprints/rpc/misc.py index df4a92dcc..b6b716c31 100644 --- a/starknet_devnet/blueprints/rpc/misc.py +++ b/starknet_devnet/blueprints/rpc/misc.py @@ -89,7 +89,7 @@ async def get_events( else int(to_block) + 1 ) for block_number in range(int(from_block), to_block): - block = state.starknet_wrapper.blocks.get_by_number(block_number) + block = await state.starknet_wrapper.blocks.get_by_number(block_number) if block.transaction_receipts: events.extend(get_events_from_block(block, address, keys)) @@ -109,9 +109,9 @@ async def get_nonce(block_id: BlockId, contract_address: Address) -> Felt: """ Get the nonce associated with the given address in the given block """ - assert_block_id_is_latest_or_pending(block_id) + await assert_block_id_is_latest_or_pending(block_id) - if not state.starknet_wrapper.contracts.is_deployed(int(contract_address, 16)): + if not await state.starknet_wrapper.is_deployed(int(contract_address, 16)): raise RpcError(code=20, message="Contract not found") result = await state.starknet_wrapper.get_nonce( diff --git a/starknet_devnet/blueprints/rpc/routes.py b/starknet_devnet/blueprints/rpc/routes.py index 3dd4fe030..508c29421 100644 --- a/starknet_devnet/blueprints/rpc/routes.py +++ b/starknet_devnet/blueprints/rpc/routes.py @@ -6,6 +6,7 @@ from __future__ import annotations +import inspect from typing import Callable, Dict, Union, List, Tuple from flask import Blueprint @@ -83,6 +84,8 @@ async def base_route(): result = await ( method(*params) if isinstance(params, list) else method(**params) ) + if inspect.iscoroutinefunction(result): + result = await result except TypeError as type_error: return rpc_error(message_id=message_id, code=22, message=str(type_error)) except RpcError as error: diff --git a/starknet_devnet/blueprints/rpc/state.py b/starknet_devnet/blueprints/rpc/state.py index 50c649ec7..015e2cdab 100644 --- a/starknet_devnet/blueprints/rpc/state.py +++ b/starknet_devnet/blueprints/rpc/state.py @@ -17,11 +17,11 @@ async def get_state_update(block_id: BlockId) -> dict: try: if "block_hash" in block_id: - result = state.starknet_wrapper.blocks.get_state_update( + result = await state.starknet_wrapper.blocks.get_state_update( block_hash=block_id["block_hash"] ) else: - result = state.starknet_wrapper.blocks.get_state_update( + result = await state.starknet_wrapper.blocks.get_state_update( block_number=block_id["block_number"] ) except StarknetDevnetException as ex: diff --git a/starknet_devnet/blueprints/rpc/storage.py b/starknet_devnet/blueprints/rpc/storage.py index 91ae4e88c..072d67ddb 100644 --- a/starknet_devnet/blueprints/rpc/storage.py +++ b/starknet_devnet/blueprints/rpc/storage.py @@ -21,9 +21,9 @@ async def get_storage_at( """ Get the value of the storage at the given address and key """ - assert_block_id_is_latest_or_pending(block_id) + await assert_block_id_is_latest_or_pending(block_id) - if not state.starknet_wrapper.contracts.is_deployed(int(contract_address, 16)): + if not await state.starknet_wrapper.is_deployed(int(contract_address, 16)): raise RpcError(code=20, message="Contract not found") storage = await state.starknet_wrapper.get_storage_at( diff --git a/starknet_devnet/blueprints/rpc/structures/responses.py b/starknet_devnet/blueprints/rpc/structures/responses.py index be9ba1e4b..73d051ae2 100644 --- a/starknet_devnet/blueprints/rpc/structures/responses.py +++ b/starknet_devnet/blueprints/rpc/structures/responses.py @@ -105,51 +105,53 @@ class RpcDeployAccountReceipt(RpcBaseTransactionReceipt): contract_address: Felt -def rpc_invoke_receipt(txr: TransactionReceipt) -> RpcInvokeReceipt: +async def rpc_invoke_receipt(txr: TransactionReceipt) -> RpcInvokeReceipt: """ Convert gateway invoke transaction receipt to rpc format """ - return rpc_base_transaction_receipt(txr) + return await rpc_base_transaction_receipt(txr) -def rpc_declare_receipt(txr: TransactionReceipt) -> RpcDeclareReceipt: +async def rpc_declare_receipt(txr: TransactionReceipt) -> RpcDeclareReceipt: """ Convert gateway declare transaction receipt to rpc format """ - return rpc_base_transaction_receipt(txr) + return await rpc_base_transaction_receipt(txr) -def rpc_deploy_receipt(txr: TransactionReceipt) -> RpcDeployReceipt: +async def rpc_deploy_receipt(txr: TransactionReceipt) -> RpcDeployReceipt: """ Convert gateway deploy transaction receipt to rpc format """ - base_receipt = rpc_base_transaction_receipt(txr) - transaction = state.starknet_wrapper.transactions.get_transaction( + base_receipt = await rpc_base_transaction_receipt(txr) + transaction = await state.starknet_wrapper.transactions.get_transaction( hex(txr.transaction_hash) - ).transaction + ) receipt: RpcDeployReceipt = { - "contract_address": rpc_felt(transaction.contract_address), + "contract_address": rpc_felt(transaction.transaction.contract_address), **base_receipt, } return receipt -def rpc_deploy_account_receipt(txr: TransactionReceipt) -> RpcDeployAccountReceipt: +async def rpc_deploy_account_receipt( + txr: TransactionReceipt, +) -> RpcDeployAccountReceipt: """ Convert gateway deploy account transaction receipt to rpc format """ - return rpc_deploy_receipt(txr) + return await rpc_deploy_receipt(txr) -def rpc_l1_handler_receipt(txr: TransactionReceipt) -> RpcL1HandlerReceipt: +async def rpc_l1_handler_receipt(txr: TransactionReceipt) -> RpcL1HandlerReceipt: """ Convert gateway l1 handler transaction receipt to rpc format """ - return rpc_base_transaction_receipt(txr) + return await rpc_base_transaction_receipt(txr) -def rpc_base_transaction_receipt(txr: TransactionReceipt) -> dict: +async def rpc_base_transaction_receipt(txr: TransactionReceipt) -> dict: """ Convert gateway transaction receipt to rpc base transaction receipt """ @@ -188,11 +190,11 @@ def status() -> str: } return mapping[txr.status] - def txn_type() -> RpcTxnType: - transaction = state.starknet_wrapper.transactions.get_transaction( + async def txn_type() -> RpcTxnType: + transaction = await state.starknet_wrapper.transactions.get_transaction( hex(txr.transaction_hash) - ).transaction - return rpc_txn_type(transaction.tx_type.name) + ) + return rpc_txn_type(transaction.transaction.tx_type.name) receipt: RpcBaseTransactionReceipt = { "transaction_hash": rpc_felt(txr.transaction_hash), @@ -202,12 +204,12 @@ def txn_type() -> RpcTxnType: "block_number": txr.block_number or None, "messages_sent": messages_sent(), "events": events(), - "type": txn_type(), + "type": await txn_type(), } return receipt -def rpc_transaction_receipt(txr: TransactionReceipt) -> dict: +async def rpc_transaction_receipt(txr: TransactionReceipt) -> dict: """ Convert gateway transaction receipt to rpc format """ @@ -218,8 +220,8 @@ def rpc_transaction_receipt(txr: TransactionReceipt) -> dict: TransactionType.L1_HANDLER: rpc_l1_handler_receipt, TransactionType.DEPLOY_ACCOUNT: rpc_deploy_account_receipt, } - transaction = state.starknet_wrapper.transactions.get_transaction( + transaction = await state.starknet_wrapper.transactions.get_transaction( hex(txr.transaction_hash) - ).transaction - tx_type = transaction.tx_type - return tx_mapping[tx_type](txr) + ) + tx_type = transaction.transaction.tx_type + return await tx_mapping[tx_type](txr) diff --git a/starknet_devnet/blueprints/rpc/transactions.py b/starknet_devnet/blueprints/rpc/transactions.py index 3ff430e89..db7145efe 100644 --- a/starknet_devnet/blueprints/rpc/transactions.py +++ b/starknet_devnet/blueprints/rpc/transactions.py @@ -52,7 +52,9 @@ async def get_transaction_by_hash(transaction_hash: TxnHash) -> dict: Get the details and status of a submitted transaction """ try: - result = state.starknet_wrapper.transactions.get_transaction(transaction_hash) + result = await state.starknet_wrapper.transactions.get_transaction( + transaction_hash + ) except StarknetDevnetException as ex: raise RpcError(code=25, message="Transaction hash not found") from ex @@ -66,7 +68,7 @@ async def get_transaction_by_block_id_and_index(block_id: BlockId, index: int) - """ Get the details of a transaction by a given block id and index """ - block = get_block_by_block_id(block_id) + block = await get_block_by_block_id(block_id) try: transaction_hash: int = block.transactions[index].transaction_hash @@ -81,7 +83,7 @@ async def get_transaction_receipt(transaction_hash: TxnHash) -> dict: Get the transaction receipt by the transaction hash """ try: - result = state.starknet_wrapper.transactions.get_transaction_receipt( + result = await state.starknet_wrapper.transactions.get_transaction_receipt( tx_hash=transaction_hash ) except StarknetDevnetException as ex: @@ -90,7 +92,7 @@ async def get_transaction_receipt(transaction_hash: TxnHash) -> dict: if result.status == TransactionStatus.NOT_RECEIVED: raise RpcError(code=25, message="Transaction hash not found") - return rpc_transaction_receipt(result) + return await rpc_transaction_receipt(result) async def pending_transactions() -> List[RpcTransaction]: @@ -158,7 +160,7 @@ async def add_deploy_account_transaction( external_tx=deploy_account_tx ) - status_response = state.starknet_wrapper.transactions.get_transaction_status( + status_response = await state.starknet_wrapper.transactions.get_transaction_status( hex(transaction_hash) ) if ( @@ -193,7 +195,7 @@ async def estimate_fee(request: RpcBroadcastedTxn, block_id: BlockId) -> dict: """ Estimate the fee for a given StarkNet transaction """ - assert_block_id_is_latest_or_pending(block_id) + await assert_block_id_is_latest_or_pending(block_id) transaction = make_transaction(request) try: _, fee_response = await state.starknet_wrapper.calculate_trace_and_fee( diff --git a/starknet_devnet/blueprints/rpc/utils.py b/starknet_devnet/blueprints/rpc/utils.py index b51cc0e51..938f0527a 100644 --- a/starknet_devnet/blueprints/rpc/utils.py +++ b/starknet_devnet/blueprints/rpc/utils.py @@ -33,7 +33,7 @@ def block_tag_to_block_number(block_id: BlockId) -> BlockId: return block_id -def get_block_by_block_id(block_id: BlockId) -> dict: +async def get_block_by_block_id(block_id: BlockId) -> dict: """ Get block using different method depending on block_id type """ @@ -42,22 +42,22 @@ def get_block_by_block_id(block_id: BlockId) -> dict: try: if "block_hash" in block_id: - return state.starknet_wrapper.blocks.get_by_hash( + return await state.starknet_wrapper.blocks.get_by_hash( block_hash=block_id["block_hash"] ) - return state.starknet_wrapper.blocks.get_by_number( + return await state.starknet_wrapper.blocks.get_by_number( block_number=block_id["block_number"] ) except StarknetDevnetException as ex: raise RpcError(code=24, message="Block not found") from ex -def assert_block_id_is_latest_or_pending(block_id: BlockId) -> None: +async def assert_block_id_is_latest_or_pending(block_id: BlockId) -> None: """ Assert block_id is "latest"/"pending" or a block hash or number of "latest"/"pending" block and throw RpcError otherwise """ if isinstance(block_id, dict): - last_block = state.starknet_wrapper.blocks.get_last_block() + last_block = await state.starknet_wrapper.blocks.get_last_block() if "block_hash" in block_id and "block_number" in block_id: raise RpcError( diff --git a/starknet_devnet/contract_wrapper.py b/starknet_devnet/contract_wrapper.py deleted file mode 100644 index 3f2d3141a..000000000 --- a/starknet_devnet/contract_wrapper.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Contains code for wrapping StarknetContract instances. -""" - -from typing import List - -from starkware.starknet.services.api.contract_class import ContractClass -from starkware.starknet.testing.contract import StarknetContract - - -class ContractWrapper: - """ - Wraps a StarknetContract, storing its types and code for later use. - """ - - def __init__( - self, - contract: StarknetContract, - contract_class: ContractClass, - deployment_tx_hash: int = None, - ): - self.contract: StarknetContract = contract - self.contract_class = contract_class.remove_debug_info() - self.deployment_tx_hash = deployment_tx_hash - - self.code: dict = { - "abi": contract_class.abi, - "bytecode": self.contract_class.dump()["program"]["data"], - } - - # pylint: disable=too-many-arguments - async def call( - self, - entry_point_selector: int, - calldata: List[int], - caller_address: int, - ): - """ - Calls the function identified with `entry_point_selector`, potentially passing in `calldata` and `signature`. - """ - - call_info = await self.contract.state.copy().execute_entry_point_raw( - contract_address=self.contract.contract_address, - selector=entry_point_selector, - calldata=calldata, - caller_address=caller_address, - ) - - result = list(map(hex, call_info.retdata)) - - return result diff --git a/starknet_devnet/contracts.py b/starknet_devnet/contracts.py deleted file mode 100644 index 24db4d715..000000000 --- a/starknet_devnet/contracts.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Class for storing and handling contracts -""" - -from typing import Dict - -from starkware.starknet.definitions.error_codes import StarknetErrorCode -from starkware.starknet.services.api.contract_class import ContractClass - -from .origin import Origin -from .util import StarknetDevnetException -from .contract_wrapper import ContractWrapper - - -class DevnetContracts: - """ - This class is used to store the deployed contracts of the devnet. - """ - - def __init__(self, origin: Origin): - self.origin = origin - self.__instances: Dict[int, ContractWrapper] = {} - self.__classes: Dict[int, ContractClass] = {} - self.__class_hashes: Dict[int, int] = {} - - def store( - self, address: int, class_hash: int, contract_wrapper: ContractWrapper - ) -> None: - """ - Store the contract wrapper. - """ - self.__instances[address] = contract_wrapper - - self.__classes[class_hash] = contract_wrapper.contract_class - self.__class_hashes[address] = class_hash - - def store_class(self, class_hash: int, contract_class: ContractClass) -> None: - """Store contract class.""" - self.__classes[class_hash] = contract_class.remove_debug_info() - - def is_deployed(self, address: int) -> bool: - """ - Check if the contract is deployed. - """ - return address in self.__instances - - def get_by_address(self, address: int) -> ContractWrapper: - """ - Get the contract wrapper by address. - """ - if not self.is_deployed(address): - message = f"Requested contract address {hex(address)} is not deployed." - raise StarknetDevnetException( - code=StarknetErrorCode.UNINITIALIZED_CONTRACT, message=message - ) - - return self.__instances[address] - - def get_code(self, address: int) -> str: - """ - Get the contract code by address. - """ - if not self.is_deployed(address): - return self.origin.get_code(address) - - return self.__instances[address].code - - def get_full_contract(self, address: int) -> ContractClass: - """ - Get the contract wrapper by address. - """ - contract_wrapper = self.get_by_address(address) - return contract_wrapper.contract_class - - def get_class_by_hash(self, class_hash: int) -> ContractClass: - """Gets the class from the provided class_hash.""" - if class_hash not in self.__classes: - return self.origin.get_class_by_hash(class_hash) - - return self.__classes[class_hash] - - def get_class_hash_at(self, address: int) -> int: - """Gets the class hash at the provided address.""" - if not self.is_deployed(address): - return self.origin.get_class_hash_at(address) - - return self.__class_hashes[address] diff --git a/starknet_devnet/devnet_config.py b/starknet_devnet/devnet_config.py index b63374a1d..74481a306 100644 --- a/starknet_devnet/devnet_config.py +++ b/starknet_devnet/devnet_config.py @@ -1,16 +1,23 @@ """Module for configuration specified by user""" import argparse +import asyncio +import contextlib from enum import Enum, auto import json import os import sys from typing import List +from aiohttp.client_exceptions import ClientConnectorError, InvalidURL from marshmallow.exceptions import ValidationError +from services.external_api.client import BadRequest, RetryConfig from starkware.python.utils import to_bytes from starkware.starknet.core.os.class_hash import compute_class_hash from starkware.starknet.services.api.contract_class import ContractClass +from starkware.starknet.services.api.feeder_gateway.feeder_gateway_client import ( + FeederGatewayClient, +) from .contract_class_wrapper import ( ContractClassWrapper, @@ -28,18 +35,36 @@ ) -# Uncomment this once fork support is added -# def _fork_url(name: str): -# """ -# Return the URL corresponding to the provided name. -# If it's not one of predefined names, assumes it is already a URL. -# """ -# if name in ["alpha", "alpha-goerli"]: -# return "https://alpha4.starknet.io" -# if name == "alpha-mainnet": -# return "https://alpha-mainnet.starknet.io" -# # otherwise a URL; perhaps check validity -# return name +NETWORK_TO_URL = { + "alpha-goerli": "https://alpha4.starknet.io", + "alpha-goerli2": "https://alpha4-2.starknet.io", + "alpha-mainnet": "https://alpha-mainnet.starknet.io", +} +NETWORK_NAMES = ", ".join(NETWORK_TO_URL.keys()) + + +def _fork_network(network_id: str): + """ + Return the URL corresponding to the provided name. + If it's not one of predefined names, assumes it is already a URL. + """ + return NETWORK_TO_URL.get(network_id, network_id) + + +def _fork_block(specifier: str): + """Parse block specifier; allows int and 'latest'""" + if specifier == "latest": + return specifier + + try: + parsed = int(specifier) + assert parsed > 0 + except (AssertionError, ValueError): + sys.exit( + f"The value of --fork-block must be a non-negative integer or 'latest', got: {specifier}" + ) + + return parsed class DumpOn(Enum): @@ -99,6 +124,37 @@ def _parse_account_class(class_path: str) -> ContractClassWrapper: return ContractClassWrapper(contract_class, class_hash_bytes) +def _get_feeder_gateway_client(url: str, block_id: str): + """Construct a feeder gateway client at url and block""" + + feeder_gateway_client = FeederGatewayClient( + url=url, + retry_config=RetryConfig(n_retries=1), + ) + + try: + # FeederGatewayClient is implemented in such a way that it logs and raises; + # this suppreses the logging + with contextlib.redirect_stderr(None): + block = asyncio.run(feeder_gateway_client.get_block(block_number=block_id)) + block_number = block.block_number + except InvalidURL: + sys.exit( + f"Error: Invalid fork-network (must be a URL or one of {{{NETWORK_NAMES}}}). Received: {url}" + ) + except BadRequest as bad_request: + if bad_request.status_code == 404: + msg = f"Error: {url} is not a valid StarkNet sequencer" + else: + msg = f"Error: {bad_request}" + + sys.exit(msg) + except ClientConnectorError as error: + sys.exit(f"Error: {error}") + + return feeder_gateway_client, block_number + + class NonNegativeAction(argparse.Action): """ Action for parsing the non negative int argument. @@ -122,7 +178,7 @@ def parse_args(raw_args: List[str]): Parses CLI arguments. """ parser = argparse.ArgumentParser( - description="Run a local instance of Starknet Devnet" + description="Run a local instance of StarkNet Devnet" ) parser.add_argument( "-v", @@ -134,7 +190,7 @@ def parse_args(raw_args: List[str]): parser.add_argument( "--host", help=f"Specify the address to listen at; defaults to {DEFAULT_HOST} " - + "(use the address the program outputs on start)", + "(use the address the program outputs on start)", default=DEFAULT_HOST, ) parser.add_argument( @@ -156,7 +212,7 @@ def parse_args(raw_args: List[str]): parser.add_argument( "--lite-mode", action="store_true", - help="Introduces speed-up by skipping block hash and deploy transaction hash calculation" + help="Introduces speed-up by skipping block hash calculation" " - applies sequential numbering instead (0x0, 0x1, 0x2, ...).", ) parser.add_argument( @@ -170,7 +226,7 @@ def parse_args(raw_args: List[str]): "-e", action=NonNegativeAction, help="Specify the initial balance of accounts to be predeployed; " - + f"defaults to {DEFAULT_INITIAL_BALANCE:g}", + f"defaults to {DEFAULT_INITIAL_BALANCE:g}", default=DEFAULT_INITIAL_BALANCE, ) parser.add_argument( @@ -210,18 +266,31 @@ def parse_args(raw_args: List[str]): type=_parse_account_class, default=DEFAULT_ACCOUNT_PATH, ) - # Uncomment this once fork support is added - # parser.add_argument( - # "--fork", "-f", - # type=_fork_url, - # help="Specify the network to fork: can be a URL (e.g. https://alpha-mainnet.starknet.io) " + - # "or network name (alpha or alpha-mainnet)", - # ) + parser.add_argument( + "--fork-network", + type=_fork_network, + help="Specify the network to fork: can be a URL (e.g. https://alpha-mainnet.starknet.io) " + f"or network name (valid names: {', '.join(NETWORK_TO_URL.keys())})", + ) + parser.add_argument( + "--fork-block", + type=_fork_block, + help="Specify the block number where the --fork-network is forked; defaults to latest", + ) parsed_args = parser.parse_args(raw_args) if parsed_args.dump_on and not parsed_args.dump_path: sys.exit("Error: --dump-path required if --dump-on present") + if parsed_args.fork_block and not parsed_args.fork_network: + sys.exit("Error: --fork-network required if --fork-block present") + + if parsed_args.fork_network: + parsed_args.fork_block = parsed_args.fork_block or "latest" + parsed_args.fork_network, parsed_args.fork_block = _get_feeder_gateway_client( + parsed_args.fork_network, parsed_args.fork_block + ) + return parsed_args @@ -241,3 +310,5 @@ def __init__(self, args: argparse.Namespace = None): self.lite_mode = self.args.lite_mode self.account_class = self.args.account_class self.hide_predeployed_accounts = self.args.hide_predeployed_accounts + self.fork_network = self.args.fork_network + self.fork_block = self.args.fork_block diff --git a/starknet_devnet/fee_token.py b/starknet_devnet/fee_token.py index 7df330814..c9162ec23 100644 --- a/starknet_devnet/fee_token.py +++ b/starknet_devnet/fee_token.py @@ -2,15 +2,15 @@ Fee token and its predefined constants. """ +from starkware.python.utils import to_bytes from starkware.solidity.utils import load_nearby_contract from starkware.starknet.services.api.contract_class import ContractClass from starkware.starknet.services.api.gateway.transaction import InvokeFunction from starkware.starknet.testing.contract import StarknetContract -from starkware.python.utils import to_bytes from starkware.starknet.compiler.compile import get_selector_from_name from starkware.starknet.testing.starknet import Starknet -from starknet_devnet.sequencer_api_utils import InternalInvokeFunction +from starknet_devnet.sequencer_api_utils import InternalInvokeFunction from starknet_devnet.util import Uint256, str_to_felt @@ -24,10 +24,9 @@ class FeeToken: HASH = 3000409729603134799471314790024123407246450023546294072844903167350593031855 HASH_BYTES = to_bytes(HASH) - # Precalculated to fixed address - # ADDRESS = calculate_contract_address_from_hash(salt=10, class_hash=HASH, - # constructor_calldata=[], deployer_address=0) - ADDRESS = 0x62230EA046A9A5FBC261AC77D03C8D41E5D442DB2284587570AB46455FD2488 + # Taken from + # https://github.com/starknet-community-libs/starknet-addresses/blob/df19b17d2c83f11c30e65e2373e8a0c65446f17c/bridged_tokens/goerli.json + ADDRESS = 0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7 SYMBOL = "ETH" NAME = "ether" @@ -53,10 +52,14 @@ async def deploy(self): await starknet.state.state.set_contract_class( FeeToken.HASH_BYTES, contract_class ) - await starknet.state.state.deploy_contract( - FeeToken.ADDRESS, FeeToken.HASH_BYTES - ) + # pylint: disable=protected-access + starknet.state.state.cache._class_hash_writes[ + FeeToken.ADDRESS + ] = FeeToken.HASH_BYTES + # replace with await starknet.state.state.deploy_contract + + # mimic constructor await starknet.state.state.set_storage_at( FeeToken.ADDRESS, get_selector_from_name("ERC20_name"), @@ -68,7 +71,9 @@ async def deploy(self): str_to_felt(FeeToken.SYMBOL), ) await starknet.state.state.set_storage_at( - FeeToken.ADDRESS, get_selector_from_name("ERC20_decimals"), 18 + FeeToken.ADDRESS, + get_selector_from_name("ERC20_decimals"), + 18, ) self.contract = StarknetContract( @@ -78,10 +83,6 @@ async def deploy(self): deploy_call_info=None, ) - await self.starknet_wrapper.store_contract( - FeeToken.ADDRESS, self.contract, contract_class - ) - async def get_balance(self, address: int) -> int: """Return the balance of the contract under `address`.""" response = await self.contract.balanceOf(address).call() diff --git a/starknet_devnet/forked_state.py b/starknet_devnet/forked_state.py new file mode 100644 index 000000000..3de5739ea --- /dev/null +++ b/starknet_devnet/forked_state.py @@ -0,0 +1,111 @@ +"""Forked state""" + +import contextlib +import json + +from services.external_api.client import BadRequest +from starkware.python.utils import to_bytes +from starkware.starknet.business_logic.state.state import BlockInfo, CachedState +from starkware.starknet.business_logic.state.state_api import StateReader +from starkware.starknet.definitions.constants import UNINITIALIZED_CLASS_HASH +from starkware.starknet.services.api.contract_class import ContractClass +from starkware.starknet.services.api.feeder_gateway.feeder_gateway_client import ( + FeederGatewayClient, +) +from starkware.starknet.testing.starknet import Starknet +from starkware.starknet.testing.state import StarknetState +from starkware.starkware_utils.error_handling import StarkException + +from .block_info_generator import now +from .general_config import DEFAULT_GENERAL_CONFIG + + +def is_originally_starknet_exception(exc: BadRequest): + """ + Return `True` if `exc` matches scheme of a Starknet exception. + Oterhwise return `False`. + """ + try: + loaded = json.loads(exc.text) + assert loaded["code"] + assert loaded["message"] + return True + except (AssertionError, json.decoder.JSONDecodeError): + return False + + +class ForkedStateReader(StateReader): + """State with a fallback to a forked origin""" + + def __init__( + self, + feeder_gateway_client: FeederGatewayClient, + block_number: int, + ): + self.__feeder_gateway_client = feeder_gateway_client + self.__block_number = block_number + + async def get_contract_class(self, class_hash: bytes) -> ContractClass: + try: + with contextlib.redirect_stderr(None): + class_hash_hex = "0x" + class_hash.hex() + contract_class_dict = ( + await self.__feeder_gateway_client.get_class_by_hash(class_hash_hex) + ) + return ContractClass.load(contract_class_dict) + except BadRequest as bad_request: + original_error = StarkException(**json.loads(bad_request.text)) + raise original_error from bad_request + + async def _get_raw_contract_class(self, class_hash: bytes) -> bytes: + raise NotImplementedError + + async def get_class_hash_at(self, contract_address: int) -> bytes: + try: + with contextlib.redirect_stderr(None): + class_hash_hex = await self.__feeder_gateway_client.get_class_hash_at( + contract_address=contract_address, + block_number=self.__block_number, + ) + return to_bytes(int(class_hash_hex, 16)) + except BadRequest as bad_request: + if is_originally_starknet_exception(bad_request): + return UNINITIALIZED_CLASS_HASH + raise + + async def get_nonce_at(self, contract_address: int) -> int: + return await self.__feeder_gateway_client.get_nonce( + contract_address=contract_address, + block_number=self.__block_number, + ) + + async def get_storage_at(self, contract_address: int, key: int) -> int: + storage_hex = await self.__feeder_gateway_client.get_storage_at( + contract_address=contract_address, + key=key, + block_number=self.__block_number, + ) + return int(storage_hex, 16) + + +def get_forked_starknet( + feeder_gateway_client: FeederGatewayClient, block_number: int, gas_price: int +) -> Starknet: + """Return a forked Starknet""" + state_reader = ForkedStateReader( + feeder_gateway_client=feeder_gateway_client, + block_number=block_number, + ) + return Starknet( + state=StarknetState( + state=CachedState( + block_info=BlockInfo.create_for_testing( + block_number=block_number, + block_timestamp=now(), + gas_price=gas_price, + ), + state_reader=state_reader, + ), + general_config=DEFAULT_GENERAL_CONFIG, + ) + ) diff --git a/starknet_devnet/lite_mode/__init__.py b/starknet_devnet/lite_mode/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/starknet_devnet/lite_mode/lite_internal_deploy.py b/starknet_devnet/lite_mode/lite_internal_deploy.py deleted file mode 100644 index bd752f9af..000000000 --- a/starknet_devnet/lite_mode/lite_internal_deploy.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -This module introduces `LiteInternalDeploy`, optimized lite-mode version of InternalDeploy. -""" -from typing import List - -from starkware.starknet.business_logic.transaction.objects import InternalDeploy -from starkware.starknet.services.api.gateway.transaction import ( - Deploy, - Transaction, - EverestTransaction, -) -from starkware.starknet.services.api.contract_class import ContractClass -from starkware.starknet.business_logic.utils import verify_version -from starkware.starknet.core.os.contract_address.contract_address import ( - calculate_contract_address_from_hash, -) -from starkware.starknet.core.os.class_hash import compute_class_hash -from starkware.starknet.business_logic.transaction.objects import InternalTransaction -from starkware.python.utils import to_bytes - -from starknet_devnet.constants import OLD_SUPPORTED_VERSIONS - -# pylint: disable=too-many-ancestors, arguments-renamed, too-many-arguments -class LiteInternalDeploy(InternalDeploy): - """ - The lite version of InternalDeploy which avoid transaction hash a calculation in deploy. - """ - - @classmethod - def _specific_from_external( - cls, - external_tx: Transaction, - tx_number: int, - ) -> "LiteInternalDeploy": - """ - Lite version of _specific_from_external method. - """ - assert isinstance(external_tx, Deploy) - return cls.lite_create( - contract_address_salt=external_tx.contract_address_salt, - contract_class=external_tx.contract_definition, - constructor_calldata=external_tx.constructor_calldata, - version=external_tx.version, - tx_number=tx_number, - ) - - @classmethod - def from_external( - cls, external_tx: EverestTransaction, tx_number: int - ) -> InternalTransaction: - """ - Returns an internal transaction genearated based on an external one. - """ - # Downcast arguments to application-specific types. - assert isinstance(external_tx, Transaction) - - internal_cls = LiteInternalDeploy.external_to_internal_cls.get( - type(external_tx) - ) - if internal_cls is None: - raise NotImplementedError( - f"Unsupported transaction type {type(external_tx).__name__}." - ) - - return LiteInternalDeploy._specific_from_external( - external_tx=external_tx, tx_number=tx_number - ) - - @classmethod - def lite_create( - cls, - contract_address_salt: int, - contract_class: ContractClass, - constructor_calldata: List[int], - version: int, - tx_number: int, - ): - """ - Lite version of create method without hash a calculation. - """ - verify_version( - version=version, - only_query=False, - old_supported_versions=OLD_SUPPORTED_VERSIONS, - ) - class_hash = compute_class_hash(contract_class=contract_class) - contract_address = calculate_contract_address_from_hash( - salt=contract_address_salt, - class_hash=class_hash, - constructor_calldata=constructor_calldata, - deployer_address=0, - ) - - return cls( - contract_address=contract_address, - contract_address_salt=contract_address_salt, - contract_hash=to_bytes(class_hash), - constructor_calldata=constructor_calldata, - version=version, - hash_value=tx_number, - ) diff --git a/starknet_devnet/lite_mode/lite_starknet.py b/starknet_devnet/lite_mode/lite_starknet.py deleted file mode 100644 index 37b8f4fb3..000000000 --- a/starknet_devnet/lite_mode/lite_starknet.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -This module introduces `LiteStarknet`, optimized lite-mode version of Starknet. -""" -from typing import List, Union, Optional - -from starkware.python.utils import as_non_optional -from starkware.starknet.testing.starknet import Starknet -from starkware.starknet.testing.objects import StarknetCallInfo -from starkware.starknet.services.api.contract_class import ContractClass -from starkware.starknet.testing.contract import StarknetContract -from starkware.starknet.testing.contract_utils import ( - get_abi, - get_contract_class, -) - -from .lite_starknet_state import LiteStarknetState - -CastableToAddressSalt = Union[str, int] - -# pylint: disable=too-many-arguments, arguments-differ) -class LiteStarknet(Starknet): - """ - The lite version of Starknet which avoid transaction hash calculation in deploy. - """ - - async def deploy( - self, - starknet: Starknet, - tx_number: int, - source: Optional[str] = None, - contract_class: Optional[ContractClass] = None, - contract_address_salt: Optional[CastableToAddressSalt] = None, - cairo_path: Optional[List[str]] = None, - constructor_calldata: Optional[List[int]] = None, - disable_hint_validation: bool = False, - ) -> StarknetContract: - contract_class = get_contract_class( - source=source, - contract_class=contract_class, - cairo_path=cairo_path, - disable_hint_validation=disable_hint_validation, - ) - - address, execution_info = await LiteStarknetState.deploy( - self, - contract_class=contract_class, - contract_address_salt=contract_address_salt, - constructor_calldata=[] - if constructor_calldata is None - else constructor_calldata, - starknet=starknet, - tx_number=tx_number, - ) - - deploy_call_info = StarknetCallInfo.from_internal( - call_info=as_non_optional(execution_info.call_info), - result=(), - main_call_events=[], - ) - - return StarknetContract( - state=starknet.state, - abi=get_abi(contract_class=contract_class), - contract_address=address, - deploy_call_info=deploy_call_info, - ) diff --git a/starknet_devnet/lite_mode/lite_starknet_state.py b/starknet_devnet/lite_mode/lite_starknet_state.py deleted file mode 100644 index 182ae5f23..000000000 --- a/starknet_devnet/lite_mode/lite_starknet_state.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -This module introduces `LiteStarknetState`, optimized lite-mode version of StarknetState. -""" -from typing import List, Tuple, Union, Optional - -from starkware.starknet.definitions import constants, fields -from starkware.starknet.testing.starknet import ( - Starknet, - StarknetState, -) -from starkware.starknet.services.api.contract_class import ContractClass -from starkware.starknet.business_logic.execution.objects import TransactionExecutionInfo - -from .lite_internal_deploy import LiteInternalDeploy - -CastableToAddressSalt = Union[str, int] - -# pylint: disable=arguments-differ, too-many-arguments -class LiteStarknetState(StarknetState): - """ - The lite version of StarknetState which avoid transaction hash calculation in deploy. - """ - - async def deploy( - self, - contract_class: ContractClass, - constructor_calldata: List[int], - starknet: Starknet, - tx_number: int, - contract_address_salt: Optional[CastableToAddressSalt] = None, - ) -> Tuple[int, TransactionExecutionInfo]: - if contract_address_salt is None: - contract_address_salt = fields.ContractAddressSalt.get_random_value() - if isinstance(contract_address_salt, str): - contract_address_salt = int(contract_address_salt, 16) - assert isinstance(contract_address_salt, int) - - transaction = LiteInternalDeploy.lite_create( - contract_address_salt=contract_address_salt, - constructor_calldata=constructor_calldata, - contract_class=contract_class, - version=constants.TRANSACTION_VERSION, - tx_number=tx_number, - ) - - await starknet.state.state.set_contract_class( - class_hash=transaction.contract_hash, contract_class=contract_class - ) - tx_execution_info = await starknet.state.execute_tx(tx=transaction) - - return transaction.contract_address, tx_execution_info diff --git a/starknet_devnet/origin.py b/starknet_devnet/origin.py index 7b8c2c9f3..d404597e7 100644 --- a/starknet_devnet/origin.py +++ b/starknet_devnet/origin.py @@ -2,8 +2,11 @@ Contains classes that provide the abstraction of L2 blockchain. """ +from services.external_api.client import BadRequest from starkware.starknet.definitions.error_codes import StarknetErrorCode -from starkware.starknet.services.api.contract_class import ContractClass +from starkware.starknet.services.api.feeder_gateway.feeder_gateway_client import ( + FeederGatewayClient, +) from starkware.starknet.services.api.feeder_gateway.response_objects import ( TransactionStatus, TransactionInfo, @@ -11,6 +14,7 @@ TransactionTrace, StarknetBlock, ) +from starknet_devnet.forked_state import is_originally_starknet_exception from starknet_devnet.util import StarknetDevnetException @@ -20,55 +24,37 @@ class Origin: Abstraction of an L2 blockchain. """ - def get_transaction_status(self, transaction_hash: str): + async def get_transaction_status(self, transaction_hash: str): """Returns the status of the transaction.""" raise NotImplementedError - def get_transaction(self, transaction_hash: str) -> TransactionInfo: + async def get_transaction(self, transaction_hash: str) -> TransactionInfo: """Returns the transaction object.""" raise NotImplementedError - def get_transaction_receipt(self, transaction_hash: str) -> TransactionReceipt: + async def get_transaction_receipt( + self, transaction_hash: str + ) -> TransactionReceipt: """Returns the transaction receipt object.""" raise NotImplementedError - def get_transaction_trace(self, transaction_hash: str) -> TransactionTrace: + async def get_transaction_trace(self, transaction_hash: str) -> TransactionTrace: """Returns the transaction trace object.""" raise NotImplementedError - def get_block_by_hash(self, block_hash: str) -> StarknetBlock: + async def get_block_by_hash(self, block_hash: str) -> StarknetBlock: """Returns the block identified with either its hash.""" raise NotImplementedError - def get_block_by_number(self, block_number: int) -> StarknetBlock: + async def get_block_by_number(self, block_number: int) -> StarknetBlock: """Returns the block identified with either its number or the latest block if no number provided.""" raise NotImplementedError - def get_code(self, contract_address: int) -> dict: - """Returns the code of the contract.""" - raise NotImplementedError - - def get_full_contract(self, contract_address: int) -> dict: - """Returns the contract class""" - raise NotImplementedError - - def get_class_by_hash(self, class_hash: int) -> ContractClass: - """Returns the contract class from its hash""" - raise NotImplementedError - - def get_class_hash_at(self, contract_address: int) -> int: - """Returns the class hash at the provided address""" - raise NotImplementedError - - def get_storage_at(self, contract_address: int, key: int) -> str: - """Returns the storage identified with `key` at `contract_address`.""" - raise NotImplementedError - def get_number_of_blocks(self): """Returns the number of blocks stored so far""" raise NotImplementedError - def get_state_update( + async def get_state_update( self, block_hash: str = None, block_number: int = None ) -> dict or None: """ @@ -83,18 +69,20 @@ class NullOrigin(Origin): A default class to comply with the Origin interface. """ - def get_transaction_status(self, transaction_hash: str): + async def get_transaction_status(self, transaction_hash: str): return {"tx_status": TransactionStatus.NOT_RECEIVED.name} - def get_transaction(self, transaction_hash: str) -> TransactionInfo: + async def get_transaction(self, transaction_hash: str) -> TransactionInfo: return TransactionInfo.create( status=TransactionStatus.NOT_RECEIVED, ) - def get_transaction_receipt(self, transaction_hash: str) -> TransactionReceipt: + async def get_transaction_receipt( + self, transaction_hash: str + ) -> TransactionReceipt: return TransactionReceipt( status=TransactionStatus.NOT_RECEIVED, - transaction_hash=int(transaction_hash, 16), + transaction_hash=0, # testnet returns 0 instead of received hash events=[], l2_to_l1_messages=[], block_hash=None, @@ -106,50 +94,29 @@ def get_transaction_receipt(self, transaction_hash: str) -> TransactionReceipt: l1_to_l2_consumed_message=None, ) - def get_transaction_trace(self, transaction_hash: str): + async def get_transaction_trace(self, transaction_hash: str): tx_hash_int = int(transaction_hash, 16) message = f"Transaction corresponding to hash {tx_hash_int} is not found." raise StarknetDevnetException( code=StarknetErrorCode.INVALID_TRANSACTION_HASH, message=message ) - def get_block_by_hash(self, block_hash: str): + async def get_block_by_hash(self, block_hash: str): message = f"Block hash not found; got: {block_hash}." raise StarknetDevnetException( code=StarknetErrorCode.BLOCK_NOT_FOUND, message=message ) - def get_block_by_number(self, block_number: int): + async def get_block_by_number(self, block_number: int): message = "Requested the latest block, but there are no blocks so far." raise StarknetDevnetException( code=StarknetErrorCode.BLOCK_NOT_FOUND, message=message ) - def get_code(self, contract_address: int): - return {"abi": {}, "bytecode": []} - - def get_full_contract(self, contract_address: int) -> dict: - return {"abi": {}, "entry_points_by_type": {}, "program": {}} - - def get_class_by_hash(self, class_hash: int) -> ContractClass: - message = f"Class with hash {hex(class_hash)} is not declared." - raise StarknetDevnetException( - code=StarknetErrorCode.UNDECLARED_CLASS, message=message - ) - - def get_class_hash_at(self, contract_address: int) -> int: - message = f"Contract with address {hex(contract_address)} is not deployed." - raise StarknetDevnetException( - code=StarknetErrorCode.UNINITIALIZED_CONTRACT, message=message - ) - - def get_storage_at(self, contract_address: int, key: int) -> str: - return hex(0) - def get_number_of_blocks(self): return 0 - def get_state_update( + async def get_state_update( self, block_hash: str = None, block_number: int = None ) -> dict or None: if block_hash: @@ -174,44 +141,70 @@ class ForkedOrigin(Origin): Abstracts an origin that the devnet was forked from. """ - def __init__(self, url): - self.url = url - self.number_of_blocks = ... + def __init__( + self, feeder_gateway_client: FeederGatewayClient, last_block_number: int + ): + self.__feeder_gateway_client = feeder_gateway_client + self.__number_of_blocks = last_block_number + 1 - def get_transaction_status(self, transaction_hash: str): - raise NotImplementedError - - def get_transaction(self, transaction_hash: str): - raise NotImplementedError - - def get_transaction_trace(self, transaction_hash: str): - raise NotImplementedError - - def get_block_by_hash(self, block_hash: str): - raise NotImplementedError - - def get_block_by_number(self, block_number: int): - raise NotImplementedError - - def get_code(self, contract_address: int) -> dict: - raise NotImplementedError + async def get_transaction_status(self, transaction_hash: str): + return await self.__feeder_gateway_client.get_transaction_status( + transaction_hash + ) - def get_full_contract(self, contract_address: int) -> dict: - raise NotImplementedError + async def get_transaction(self, transaction_hash: str): + return await self.__feeder_gateway_client.get_transaction(transaction_hash) - def get_class_by_hash(self, class_hash: int) -> ContractClass: - raise NotImplementedError + async def get_transaction_receipt( + self, transaction_hash: str + ) -> TransactionReceipt: + return await self.__feeder_gateway_client.get_transaction_receipt( + transaction_hash + ) - def get_class_hash_at(self, contract_address: int) -> int: - raise NotImplementedError + async def get_transaction_trace(self, transaction_hash: str): + try: + return await self.__feeder_gateway_client.get_transaction_trace( + transaction_hash + ) + except BadRequest as bad_request: + if is_originally_starknet_exception(bad_request): + raise StarknetDevnetException( + code=StarknetErrorCode.INVALID_TRANSACTION_HASH, + message=f"Transaction corresponding to hash {transaction_hash} is not found.", + ) from bad_request + raise + + async def get_block_by_hash(self, block_hash: str): + custom_exception = StarknetDevnetException( + code=StarknetErrorCode.BLOCK_NOT_FOUND, + message=f"Block hash {block_hash} does not exist.", + ) + try: + block = await self.__feeder_gateway_client.get_block(block_hash=block_hash) + if block.block_number > self.get_number_of_blocks(): + raise custom_exception + except BadRequest as bad_request: + if is_originally_starknet_exception(bad_request): + raise custom_exception from bad_request - def get_storage_at(self, contract_address: int, key: int) -> str: - raise NotImplementedError + async def get_block_by_number(self, block_number: int): + return await self.__feeder_gateway_client.get_block(block_number=block_number) def get_number_of_blocks(self): - return self.number_of_blocks + return self.__number_of_blocks - def get_state_update( + async def get_state_update( self, block_hash: str = None, block_number: int = None ) -> dict or None: - raise NotImplementedError + try: + return await self.__feeder_gateway_client.get_state_update( + block_hash=block_hash, + block_number=block_number, + ) + except BadRequest as bad_request: + if is_originally_starknet_exception(bad_request): + raise StarknetDevnetException( + code=StarknetErrorCode.BLOCK_NOT_FOUND, + message=f"Block hash {block_hash} does not exist.", + ) from bad_request diff --git a/starknet_devnet/server.py b/starknet_devnet/server.py index 625d0ee3a..fb496970f 100644 --- a/starknet_devnet/server.py +++ b/starknet_devnet/server.py @@ -82,10 +82,6 @@ def load(self): def main(): """Runs the server.""" - # Uncomment this once fork support is added - # origin = Origin(args.fork) if args.fork else NullOrigin() - # starknet_wrapper.origin = origin - args = parse_args(sys.argv[1:]) try: diff --git a/starknet_devnet/starknet_wrapper.py b/starknet_devnet/starknet_wrapper.py index 271649595..a0079a253 100644 --- a/starknet_devnet/starknet_wrapper.py +++ b/starknet_devnet/starknet_wrapper.py @@ -7,7 +7,6 @@ from typing import Dict, List, Optional, Set, Tuple, Type, Union import cloudpickle as pickle -from starkware.python.utils import as_non_optional from starkware.starknet.business_logic.transaction.fee import calculate_tx_fee from starkware.starknet.business_logic.transaction.objects import ( CallInfo, @@ -23,13 +22,17 @@ from starkware.starknet.core.os.contract_address.contract_address import ( calculate_contract_address_from_hash, ) +from starkware.starknet.core.os.transaction_hash.transaction_hash import ( + calculate_deploy_transaction_hash, +) +from starkware.starknet.definitions.error_codes import StarknetErrorCode from starkware.starknet.services.api.gateway.transaction import ( InvokeFunction, Deploy, DeployAccount, Declare, ) -from starkware.starknet.testing.contract_utils import get_abi +from starkware.starknet.testing.objects import FunctionInvocation from starkware.starknet.testing.starknet import Starknet from starkware.starkware_utils.error_handling import StarkException from starkware.starknet.services.api.contract_class import EntryPointType, ContractClass @@ -40,8 +43,6 @@ from starkware.starknet.services.api.feeder_gateway.response_objects import ( TransactionStatus, ) -from starkware.starknet.testing.contract import StarknetContract -from starkware.starknet.testing.objects import FunctionInvocation, StarknetCallInfo from starkware.starknet.third_party.open_zeppelin.starknet_contracts import ( account_contract as oz_account_class, ) @@ -56,28 +57,23 @@ ) from starkware.starkware_utils.error_handling import StarkErrorCode -from starknet_devnet.util import to_bytes, get_fee_estimation_info -from starknet_devnet.constants import DUMMY_STATE_ROOT, OZ_ACCOUNT_CLASS_HASH - -from .lite_mode.lite_internal_deploy import LiteInternalDeploy -from .lite_mode.lite_starknet import LiteStarknet - from .accounts import Accounts from .blueprints.rpc.structures.types import Felt -from .fee_token import FeeToken +from .constants import DUMMY_STATE_ROOT, OZ_ACCOUNT_CLASS_HASH from .general_config import DEFAULT_GENERAL_CONFIG -from .origin import NullOrigin, Origin +from .fee_token import FeeToken +from .forked_state import get_forked_starknet +from .origin import ForkedOrigin, NullOrigin from .udc import UDC +from .util import to_bytes, get_fee_estimation_info from .util import ( StarknetDevnetException, enable_pickling, get_storage_diffs, get_all_declared_contracts, ) -from .contract_wrapper import ContractWrapper from .postman_wrapper import DevnetL1L2 from .transactions import DevnetTransactions, DevnetTransaction -from .contracts import DevnetContracts from .blocks import DevnetBlocks from .block_info_generator import BlockInfoGenerator from .devnet_config import DevnetConfig @@ -86,6 +82,7 @@ enable_pickling() # pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-public-methods class StarknetWrapper: """ Wraps a Starknet instance and stores data to be returned by the server: @@ -93,13 +90,16 @@ class StarknetWrapper: """ def __init__(self, config: DevnetConfig): - self.origin: Origin = NullOrigin() + self.origin = ( + ForkedOrigin(config.fork_network, config.fork_block) + if config.fork_network + else NullOrigin() + ) """Origin chain that this devnet was forked from.""" self.block_info_generator = BlockInfoGenerator() self.blocks = DevnetBlocks(self.origin, lite=config.lite_mode) self.config = config - self.contracts = DevnetContracts(self.origin) self.l1l2 = DevnetL1L2() self.transactions = DevnetTransactions(self.origin) self.starknet: Starknet = None @@ -112,7 +112,7 @@ def __init__(self, config: DevnetConfig): if config.start_time is not None: self.set_block_time(config.start_time) - self.set_gas_price(config.gas_price) + self.__set_gas_price(config.gas_price) @staticmethod def load(path: str) -> "StarknetWrapper": @@ -151,10 +151,26 @@ async def __init_starknet(self): Create and return underlying Starknet instance """ if not self.starknet: - self.starknet = await Starknet.empty(general_config=DEFAULT_GENERAL_CONFIG) + if self.__is_fork(): + print( + f"Forking {self.config.fork_network.url} from block {self.config.fork_block}" + ) + + self.starknet = get_forked_starknet( + feeder_gateway_client=self.config.fork_network, + block_number=self.config.fork_block, + gas_price=self.block_info_generator.gas_price, + ) + else: + self.starknet = await Starknet.empty( + general_config=DEFAULT_GENERAL_CONFIG + ) return self.starknet + def __is_fork(self): + return bool(self.config.fork_network) + def get_state(self): """ Returns the StarknetState of the underlying Starknet instance. @@ -204,22 +220,6 @@ async def _update_state( state_diff=state_diff, ) - async def store_contract( - self, - address: int, - contract: StarknetContract, - contract_class: ContractClass, - tx_hash: int = None, - ): - """Store the provided data sa wrapped contract""" - class_hash_bytes = await self.starknet.state.state.get_class_hash_at(address) - class_hash = int.from_bytes(class_hash_bytes, "big") - self.contracts.store( - address=address, - class_hash=class_hash, - contract_wrapper=ContractWrapper(contract, contract_class, tx_hash), - ) - async def _store_transaction( self, transaction: DevnetTransaction, @@ -246,13 +246,6 @@ async def _store_transaction( self.transactions.store(tx_hash, transaction) - def set_config(self, config: DevnetConfig): - """ - Sets the configuration of the devnet. - """ - self.config = config - self.blocks.lite = config.lite_mode - async def declare(self, external_tx: Declare) -> Tuple[int, int]: """ Declares the class specified with `declare_transaction` @@ -272,10 +265,11 @@ async def declare(self, external_tx: Declare) -> Tuple[int, int]: tx_handler.explicitly_declared.append(class_hash_int) - # alpha-goerli allows multiple declarations of the same class - self.contracts.store_class(class_hash_int, external_tx.contract_class) + # alpha-goerli allows multiple declarations of the same class. + # Even though execute_tx is performed, class needs to be set explicitly await self.get_state().state.set_contract_class( - tx_handler.internal_tx.class_hash, external_tx.contract_class + class_hash=tx_handler.internal_tx.class_hash, + contract_class=external_tx.contract_class, ) return class_hash_int, tx_handler.internal_tx.hash_value @@ -382,26 +376,12 @@ async def deploy_account(self, external_tx: DeployAccount): tx_handler.internal_tx = InternalDeployAccount.from_external( external_tx, state.general_config ) - tx_handler.execution_info = await state.execute_tx(tx_handler.internal_tx) + + tx_handler.execution_info = await self.__deploy(tx_handler.internal_tx) tx_handler.internal_calls = ( tx_handler.execution_info.call_info.internal_calls ) - contract_class = await state.state.get_contract_class( - to_bytes(external_tx.class_hash) - ) - contract = self.__create_contract( - contract_class, - tx_handler.execution_info.call_info, - address=account_address, - ) - await self.store_contract( - address=account_address, - contract=contract, - contract_class=contract_class, - tx_hash=tx_handler.internal_tx.hash_value, - ) - return ( account_address, tx_handler.internal_tx.hash_value, @@ -413,57 +393,39 @@ async def deploy(self, deploy_transaction: Deploy) -> Tuple[int, int]: Returns (contract_address, transaction_hash). """ - transactions_count = self.transactions.get_count() contract_class = deploy_transaction.contract_definition - if self.config.lite_mode: - internal_tx: LiteInternalDeploy = LiteInternalDeploy.from_external( - deploy_transaction, tx_number=transactions_count - ) - else: - internal_tx: InternalDeploy = InternalDeploy.from_external( - deploy_transaction, self.get_state().general_config - ) + internal_tx: InternalDeploy = InternalDeploy.from_external( + deploy_transaction, self.get_state().general_config + ) contract_address = internal_tx.contract_address - if self.contracts.is_deployed(contract_address): - tx_hash = self.contracts.get_by_address(contract_address).deployment_tx_hash + if await self.is_deployed(contract_address): + tx_hash = calculate_deploy_transaction_hash( + version=deploy_transaction.version, + contract_address=contract_address, + constructor_calldata=deploy_transaction.constructor_calldata, + chain_id=self.get_state().general_config.chain_id.value, + ) return contract_address, tx_hash tx_hash = internal_tx.hash_value async with self.__get_transaction_handler() as tx_handler: tx_handler.internal_tx = internal_tx - - if self.config.lite_mode: - contract = await LiteStarknet.deploy( - self, - contract_class=contract_class, - constructor_calldata=deploy_transaction.constructor_calldata, - contract_address_salt=deploy_transaction.contract_address_salt, - starknet=self.starknet, - tx_number=transactions_count, - ) - else: - contract = await self.__deploy(internal_tx, contract_class) - - tx_handler.execution_info = contract.deploy_call_info - tx_handler.internal_calls = ( - contract.deploy_call_info.call_info.internal_calls + await self.get_state().state.set_contract_class( + class_hash=internal_tx.class_hash, contract_class=contract_class ) - - await self.store_contract( - contract.contract_address, contract, contract_class, tx_hash + tx_handler.execution_info = await self.__deploy(internal_tx) + tx_handler.internal_calls = ( + tx_handler.execution_info.call_info.internal_calls ) - class_hash_bytes = await self.starknet.state.state.get_class_hash_at( - contract_address - ) - class_hash_int = int.from_bytes(class_hash_bytes, "big") tx_handler.deployed_contracts.append( DeployedContract( - address=contract.contract_address, class_hash=class_hash_int + address=contract_address, + class_hash=int.from_bytes(internal_tx.class_hash, "big"), ) ) @@ -489,50 +451,26 @@ async def invoke(self, external_tx: InvokeFunction): async def call(self, transaction: CallFunction): """Perform call according to specifications in `transaction`.""" - contract_wrapper = self.contracts.get_by_address(transaction.contract_address) - adapted_result = await contract_wrapper.call( - entry_point_selector=transaction.entry_point_selector, + state_copy = self.get_state().copy() + call_info = await state_copy.execute_entry_point_raw( + contract_address=transaction.contract_address, + selector=transaction.entry_point_selector, calldata=transaction.calldata, caller_address=0, ) - return {"result": adapted_result} + result = list(map(hex, call_info.retdata)) + return {"result": result} - async def __deploy(self, deploy_tx: InternalDeploy, contract_class: ContractClass): + async def __deploy(self, deploy_tx: Union[InternalDeploy, InternalDeployAccount]): """ Replacement for self.starknet.deploy that allows usage of InternalDeploy right away. This way InternalDeploy doesn't have to be created twice, calculating hash every time. """ state = self.get_state() - await state.state.set_contract_class( - class_hash=deploy_tx.contract_hash, contract_class=contract_class - ) - tx_execution_info = await state.execute_tx(tx=deploy_tx) - - return self.__create_contract( - contract_class=contract_class, - call_info=tx_execution_info.call_info, - address=deploy_tx.contract_address, - ) - - def __create_contract( - self, contract_class: ContractClass, call_info: CallInfo, address: int - ) -> StarknetContract: - - deploy_call_info = StarknetCallInfo.from_internal( - call_info=as_non_optional(call_info), - result=(), - main_call_events=[], - ) - - return StarknetContract( - state=self.get_state(), - abi=get_abi(contract_class=contract_class), - contract_address=address, - deploy_call_info=deploy_call_info, - ) + return tx_execution_info async def _register_new_contracts( self, @@ -542,35 +480,67 @@ async def _register_new_contracts( ): for internal_call in internal_calls: if internal_call.entry_point_type == EntryPointType.CONSTRUCTOR: - state = self.get_state() class_hash_bytes = to_bytes(internal_call.class_hash) class_hash_int = int.from_bytes(class_hash_bytes, "big") - contract_class = await state.state.get_contract_class(class_hash_bytes) - contract = StarknetContract( - state, contract_class.abi, internal_call.contract_address, None - ) - await self.store_contract( - internal_call.contract_address, contract, contract_class, tx_hash - ) deployed_contracts.append( DeployedContract( - address=contract.contract_address, class_hash=class_hash_int + address=internal_call.contract_address, + class_hash=class_hash_int, ) ) await self._register_new_contracts( internal_call.internal_calls, tx_hash, deployed_contracts ) + async def get_class_by_hash(self, class_hash: int) -> ContractClass: + """Return contract class given class hash""" + cached_state = self.get_state().state + class_hash_bytes = to_bytes(class_hash) + return await cached_state.get_contract_class(class_hash_bytes) + + async def get_class_hash_at(self, contract_address: int) -> int: + """Return class hash give the contract address""" + cached_state = self.get_state().state + class_hash_bytes = await cached_state.get_class_hash_at(contract_address) + class_hash_int = int.from_bytes(class_hash_bytes, "big") + + if not class_hash_int: + raise StarknetDevnetException( + code=StarknetErrorCode.UNINITIALIZED_CONTRACT, + message=f"Contract with address {contract_address} is not deployed.", + ) + return class_hash_int + + async def get_class_by_address(self, contract_address: int) -> ContractClass: + """Return contract class given the contract address""" + cached_state = self.get_state().state + class_hash_int = await self.get_class_hash_at(contract_address) + class_hash_bytes = to_bytes(class_hash_int) + return await cached_state.get_contract_class(class_hash_bytes) + + async def get_code(self, contract_address: int) -> dict: + """Return code dict given the contract address""" + try: + contract_class = await self.get_class_by_address(contract_address) + result_dict = { + "abi": contract_class.abi, + "bytecode": contract_class.dump()["program"]["data"], + } + except StarkException as err: + if err.code != StarknetErrorCode.UNINITIALIZED_CONTRACT: + raise + result_dict = {"abi": {}, "bytecode": []} + + return result_dict + async def get_storage_at(self, contract_address: int, key: int) -> Felt: """ Returns the storage identified by `key` from the contract at `contract_address`. """ state = self.get_state().state - if self.contracts.is_deployed(contract_address): - return hex(await state.get_storage_at(contract_address, key)) - return self.origin.get_storage_at(contract_address, key) + return hex(await state.get_storage_at(contract_address, key)) async def load_messaging_contract_in_l1( self, network_url: str, contract_address: str, network_id: str @@ -671,7 +641,7 @@ def set_block_time(self, time_s: int): """Sets the block time to `time_s`.""" self.block_info_generator.set_next_block_time(time_s) - def set_gas_price(self, gas_price: int): + def __set_gas_price(self, gas_price: int): """Sets gas price to `gas_price`.""" self.block_info_generator.set_gas_price(gas_price) @@ -683,3 +653,13 @@ async def __predeclare_oz_account(self): await self.get_state().state.set_contract_class( to_bytes(OZ_ACCOUNT_CLASS_HASH), oz_account_class ) + + async def is_deployed(self, address: int) -> bool: + """ + Check if the contract is deployed. + """ + assert isinstance(address, int) + cached_state = self.get_state().state + class_hash_bytes = await cached_state.get_class_hash_at(address) + class_hash_int = int.from_bytes(class_hash_bytes, "big") + return bool(class_hash_int) diff --git a/starknet_devnet/transactions.py b/starknet_devnet/transactions.py index 91c393510..97741a011 100644 --- a/starknet_devnet/transactions.py +++ b/starknet_devnet/transactions.py @@ -206,25 +206,25 @@ def store(self, tx_hash: int, transaction: DevnetTransaction): """ self.__instances[tx_hash] = transaction - def get_transaction(self, tx_hash: str): + async def get_transaction(self, tx_hash: str): """ Get a transaction info. """ transaction = self.__get_transaction_by_hash(tx_hash) if transaction is None: - return self.origin.get_transaction(tx_hash) + return await self.origin.get_transaction(tx_hash) return transaction.get_tx_info() - def get_transaction_trace(self, tx_hash: str): + async def get_transaction_trace(self, tx_hash: str): """ Get a transaction trace. """ transaction = self.__get_transaction_by_hash(tx_hash) if transaction is None: - return self.origin.get_transaction_trace(tx_hash) + return await self.origin.get_transaction_trace(tx_hash) if transaction.status == TransactionStatus.REJECTED: raise StarknetDevnetException( @@ -234,25 +234,25 @@ def get_transaction_trace(self, tx_hash: str): return transaction.get_trace() - def get_transaction_receipt(self, tx_hash: str): + async def get_transaction_receipt(self, tx_hash: str): """ Get a transaction receipt. """ transaction = self.__get_transaction_by_hash(tx_hash) if transaction is None: - return self.origin.get_transaction_receipt(tx_hash) + return await self.origin.get_transaction_receipt(tx_hash) return transaction.get_receipt() - def get_transaction_status(self, tx_hash: str): + async def get_transaction_status(self, tx_hash: str): """ Get a transaction status. """ transaction = self.__get_transaction_by_hash(tx_hash) if transaction is None: - return self.origin.get_transaction_status(tx_hash) + return await self.origin.get_transaction_status(tx_hash) tx_info = transaction.get_tx_info() diff --git a/starknet_devnet/udc.py b/starknet_devnet/udc.py index d3ec12165..fe5c385d9 100644 --- a/starknet_devnet/udc.py +++ b/starknet_devnet/udc.py @@ -3,7 +3,6 @@ from starkware.python.utils import to_bytes from starkware.solidity.utils import load_nearby_contract from starkware.starknet.services.api.contract_class import ContractClass -from starkware.starknet.testing.contract import StarknetContract from starkware.starknet.testing.starknet import Starknet @@ -40,15 +39,7 @@ async def deploy(self): contract_class = UDC.get_contract_class() await starknet.state.state.set_contract_class(UDC.HASH_BYTES, contract_class) - await starknet.state.state.deploy_contract(UDC.ADDRESS, UDC.HASH_BYTES) - - contract = StarknetContract( - state=starknet.state, - abi=contract_class.abi, - contract_address=UDC.ADDRESS, - deploy_call_info=None, - ) - - await self.starknet_wrapper.store_contract( - UDC.ADDRESS, contract, contract_class - ) + + # pylint: disable=protected-access + starknet.state.state.cache._class_hash_writes[UDC.ADDRESS] = UDC.HASH_BYTES + # replace with await starknet.state.state.deploy_contract diff --git a/starknet_devnet/util.py b/starknet_devnet/util.py index f346dc4ce..76eadfb8f 100644 --- a/starknet_devnet/util.py +++ b/starknet_devnet/util.py @@ -4,6 +4,7 @@ from dataclasses import dataclass import os +import sys from typing import Dict, Union, List, Set from starkware.starknet.definitions.error_codes import StarknetErrorCode @@ -157,3 +158,8 @@ def get_fee_estimation_info(tx_fee: int, gas_price: int): "gas_usage": gas_usage, } ) + + +def warn(msg: str, file=sys.stderr): + """Log a warning""" + print(f"\033[93m{msg}\033[0m", file=file) diff --git a/test/account.py b/test/account.py index 297aa948f..ea55d5296 100644 --- a/test/account.py +++ b/test/account.py @@ -43,10 +43,10 @@ def deploy_account_contract(salt=None): return deploy(ACCOUNT_PATH, inputs=[str(PUBLIC_KEY)], salt=salt) -def get_nonce(account_address: str) -> int: +def get_nonce(account_address: str, feeder_gateway_url=APP_URL) -> int: """Get nonce.""" resp = requests.get( - f"{APP_URL}/feeder_gateway/get_nonce?contractAddress={account_address}" + f"{feeder_gateway_url}/feeder_gateway/get_nonce?contractAddress={account_address}" ) return int(resp.json(), 16) @@ -151,7 +151,11 @@ def _get_transaction_hash( def get_estimated_fee( - calls: List[AccountCall], account_address: str, private_key: str, nonce=None + calls: List[AccountCall], + account_address: str, + private_key: str, + nonce=None, + feeder_gateway_url=APP_URL, ): """Get estimated fee through account.""" @@ -174,6 +178,7 @@ def get_estimated_fee( abi_path=ACCOUNT_ABI_PATH, signature=signature, nonce=nonce, + feeder_gateway_url=feeder_gateway_url, ) @@ -183,11 +188,12 @@ def invoke( private_key: int, nonce=None, max_fee=None, + gateway_url=APP_URL, ): """Invoke __execute__ with correct calldata and signature.""" if nonce is None: - nonce = get_nonce(account_address) + nonce = get_nonce(account_address, feeder_gateway_url=gateway_url) if max_fee is None: max_fee = get_estimated_fee( @@ -195,6 +201,7 @@ def invoke( account_address=account_address, private_key=private_key, nonce=nonce, + feeder_gateway_url=gateway_url, ) signature, execute_calldata = _get_execute_args( @@ -222,7 +229,8 @@ def invoke( *signature, "--max_fee", str(max_fee), - ] + ], + gateway_url=gateway_url, ) print("Invoke sent!") diff --git a/test/expected/invoke_receipt.json b/test/expected/invoke_receipt.json index 9fd17226b..6de317a02 100644 --- a/test/expected/invoke_receipt.json +++ b/test/expected/invoke_receipt.json @@ -10,7 +10,7 @@ "0x16010bdfe1800", "0x0" ], - "from_address": "0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488", + "from_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "keys": [ "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9" ] diff --git a/test/expected/invoke_receipt_account_event.json b/test/expected/invoke_receipt_account_event.json index dc662d03e..b53a35ba2 100644 --- a/test/expected/invoke_receipt_account_event.json +++ b/test/expected/invoke_receipt_account_event.json @@ -17,7 +17,7 @@ "0x5a569a2800", "0x0" ], - "from_address": "0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488", + "from_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "keys": [ "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9" ] diff --git a/test/expected/invoke_receipt_event.json b/test/expected/invoke_receipt_event.json index d445fc0c1..78a42ad5f 100644 --- a/test/expected/invoke_receipt_event.json +++ b/test/expected/invoke_receipt_event.json @@ -17,7 +17,7 @@ "0x160e24a2c4000", "0x0" ], - "from_address": "0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488", + "from_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "keys": [ "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9" ] diff --git a/test/shared.py b/test/shared.py index 976d68b93..1b8914b2e 100644 --- a/test/shared.py +++ b/test/shared.py @@ -48,7 +48,6 @@ GENESIS_BLOCK_NUMBER = 0 GENESIS_BLOCK_HASH = "0x0" INCORRECT_GENESIS_BLOCK_HASH = "0x1" -DEFAULT_GAS_PRICE = int(1e11) SUPPORTED_TX_VERSION = 1 SUPPORTED_RPC_TX_VERSION = 1 @@ -60,8 +59,12 @@ PREDEPLOYED_ACCOUNT_PRIVATE_KEY = 0xBDD640FB06671AD11C80317FA3B1799D EXPECTED_FEE_TOKEN_ADDRESS = ( - "0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488" + "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" ) EXPECTED_UDC_ADDRESS = ( "0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf" ) + +ALPHA_MAINNET_URL = "https://alpha-mainnet.starknet.io" +ALPHA_GOERLI_URL = "https://alpha4.starknet.io" +ALPHA_GOERLI2_URL = "https://alpha4-2.starknet.io" diff --git a/test/test_account.py b/test/test_account.py index 424964e6a..f4f0a851f 100644 --- a/test/test_account.py +++ b/test/test_account.py @@ -62,9 +62,9 @@ def deploy_events_contract(): return deploy(EVENTS_CONTRACT_PATH, salt=SALT) -def get_account_balance(address: str) -> int: +def get_account_balance(address: str, server_url=APP_URL) -> int: """Get balance (wei) of account with `address` (hex).""" - resp = requests.get(f"{APP_URL}/account_balance?address={address}") + resp = requests.get(f"{server_url}/account_balance?address={address}") assert resp.status_code == 200 return int(resp.json()["amount"]) diff --git a/test/test_block_number.py b/test/test_block_number.py index 570afbb11..ebb0caccf 100644 --- a/test/test_block_number.py +++ b/test/test_block_number.py @@ -31,25 +31,24 @@ def my_get_block_number(address: str): @pytest.mark.usefixtures("run_devnet_in_background") @pytest.mark.parametrize( - "run_devnet_in_background, expected_tx_hash", + "run_devnet_in_background", [ - ([*PREDEPLOY_ACCOUNT_CLI_ARGS], EXPECTED_TX_HASH), - ([*PREDEPLOY_ACCOUNT_CLI_ARGS, "--lite-mode"], "0x0"), + PREDEPLOY_ACCOUNT_CLI_ARGS, + [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--lite-mode"], ], indirect=True, ) -def test_block_number_incremented(expected_tx_hash): +def test_block_number_incremented(): """ Tests how block number is incremented in regular mode and lite mode. - In regular mode with salt "0x42" our expected hash is - 0x4506fb016a309c8694a5c862625ba743a3ed2e248bca1ba5aa174ca06381f0f. + In regular mode with salt "0x42" our expected hash is {EXPECTED_TX_HASH}. """ deploy_info = deploy(BLOCK_NUMBER_CONTRACT_PATH, salt="0x42") block_number_before = my_get_block_number(deploy_info["address"]) assert int(block_number_before) == GENESIS_BLOCK_NUMBER + 1 - assert expected_tx_hash == deploy_info["tx_hash"] + assert deploy_info["tx_hash"] == EXPECTED_TX_HASH invoke( calls=[(deploy_info["address"], "write_block_number", [])], diff --git a/test/test_declare.py b/test/test_declare.py index 64a007a14..f32162c89 100644 --- a/test/test_declare.py +++ b/test/test_declare.py @@ -16,11 +16,10 @@ PREDEPLOYED_ACCOUNT_PRIVATE_KEY, ) from .util import ( - assert_contract_class, + assert_class_by_hash, assert_hex_equal, assert_tx_status, devnet_in_background, - get_class_by_hash, ) @@ -60,6 +59,4 @@ def test_declare_happy_path(): class_hash = declare_info["class_hash"] assert_hex_equal(class_hash, EXPECTED_CLASS_HASH) assert_tx_status(declare_info["tx_hash"], "ACCEPTED_ON_L2") - - declared_class = get_class_by_hash(class_hash) - assert_contract_class(declared_class, CONTRACT_PATH) + assert_class_by_hash(class_hash, CONTRACT_PATH) diff --git a/test/test_deploy.py b/test/test_deploy.py index 1c37ce735..c3075037c 100644 --- a/test/test_deploy.py +++ b/test/test_deploy.py @@ -42,14 +42,13 @@ from .account import declare, invoke from .util import ( - assert_contract_class, + assert_class_by_hash, assert_equal, assert_hex_equal, assert_tx_status, call, deploy, devnet_in_background, - get_class_by_hash, get_class_hash_at, get_transaction_receipt, load_contract_class, @@ -85,23 +84,21 @@ def fixture_starknet_wrapper_args(request): @pytest.mark.parametrize( - "starknet_wrapper_args, expected_tx_hash, expected_block_hash", + "starknet_wrapper_args, expected_block_hash", [ ( [*PREDEPLOY_ACCOUNT_CLI_ARGS], - "0x13d4b9f765587296a4f40591efe235a8caf24f0496230f0b13a87f2e4c8150a", "", ), ( [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--lite-mode"], - "0x0", "0x1", ), ], indirect=True, ) @pytest.mark.asyncio -async def test_deploy(starknet_wrapper_args, expected_tx_hash, expected_block_hash): +async def test_deploy(starknet_wrapper_args, expected_block_hash): """ Test the deployment of a contract. """ @@ -121,11 +118,11 @@ async def test_deploy(starknet_wrapper_args, expected_tx_hash, expected_block_ha assert_hex_equal( hex(tx_hash), - expected_tx_hash, + "0x13D4B9F765587296A4F40591EFE235A8CAF24F0496230F0B13A87F2E4C8150A", ) assert contract_address == expected_contract_address - tx_status = devnet.transactions.get_transaction_status(hex(tx_hash)) + tx_status = await devnet.transactions.get_transaction_status(hex(tx_hash)) assert tx_status["tx_status"] == TransactionStatus.ACCEPTED_ON_L2.name if "--lite-mode" in starknet_wrapper_args: @@ -140,6 +137,11 @@ def test_predeployed_oz_account(): @devnet_in_background() def test_deploy_account(): """Test the deployment of an account.""" + deploy_account_test_body() + + +def deploy_account_test_body(): + """The body of account deployment test.""" # the account class should already be declared @@ -251,8 +253,7 @@ def test_deploy_through_deployer_constructor(): class_hash = declare_info["class_hash"] assert_hex_equal(class_hash, EXPECTED_CLASS_HASH) - contract_class = get_class_by_hash(class_hash=class_hash) - assert_contract_class(contract_class, CONTRACT_PATH) + assert_class_by_hash(class_hash, CONTRACT_PATH) # Deploy the deployer - also deploys a contract of the declared class using the deploy syscall initial_balance_in_constructor = "5" @@ -284,7 +285,11 @@ def test_precomputed_udc_address(): @devnet_in_background(*PREDEPLOY_ACCOUNT_CLI_ARGS) def test_deploy_with_udc(): """Test if deploying through UDC works.""" + deploy_with_udc_test_body() + +def deploy_with_udc_test_body(): + """The body of udc deployment test.""" # Declare the class to be deployed declare_info = declare( contract_path=CONTRACT_PATH, @@ -294,8 +299,7 @@ def test_deploy_with_udc(): class_hash = declare_info["class_hash"] assert_hex_equal(class_hash, EXPECTED_CLASS_HASH) - contract_class = get_class_by_hash(class_hash=class_hash) - assert_contract_class(contract_class, CONTRACT_PATH) + assert_class_by_hash(class_hash, CONTRACT_PATH) # Deploy a contract of the declared class through the deployer initial_balance = "10" diff --git a/test/test_endpoints.py b/test/test_endpoints.py index 50189cb09..57992eacc 100644 --- a/test/test_endpoints.py +++ b/test/test_endpoints.py @@ -274,7 +274,10 @@ def test_error_response_class_hash_at(): error_message = resp.json()["message"] assert resp.status_code == 500 - expected_message = f"Contract with address {INVALID_ADDRESS} is not deployed." + expected_message = ( + # alpha-goerli reports a decimal address + f"Contract with address {int(INVALID_ADDRESS, 16)} is not deployed." + ) assert expected_message == error_message diff --git a/test/test_fork.py b/test/test_fork.py new file mode 100644 index 000000000..6aa95a02f --- /dev/null +++ b/test/test_fork.py @@ -0,0 +1,302 @@ +""" +Test the forking feature. +Relying on the fact that devnet doesn't support specifying which block to query +""" + +import pytest + +from starknet_devnet.constants import DEFAULT_INITIAL_BALANCE + +from .account import get_nonce, invoke +from .shared import ( + ABI_PATH, + CONTRACT_PATH, + ALPHA_MAINNET_URL, + PREDEPLOY_ACCOUNT_CLI_ARGS, + PREDEPLOYED_ACCOUNT_ADDRESS, + PREDEPLOYED_ACCOUNT_PRIVATE_KEY, +) +from .settings import APP_URL, bind_free_port, HOST +from .test_account import get_account_balance +from .test_deploy import deploy_account_test_body, deploy_with_udc_test_body +from .testnet_deployment import ( + TESTNET_CONTRACT_ADDRESS, + TESTNET_DEPLOYMENT_BLOCK, + TESTNET_FORK_PARAMS, + TESTNET_URL, +) +from .util import ( + assert_address_has_no_class_hash, + assert_tx_status, + call, + deploy, + devnet_in_background, + mint, +) + +ORIGIN_PORT, ORIGIN_URL = bind_free_port(HOST) +FORK_PORT, FORK_URL = bind_free_port(HOST) + + +def _invoke_on_fork_and_assert_only_fork_changed( + contract_address: str, + initial_balance: str, + fork_url: str, + origin_url: str, +): + + # account nonce - before + origin_nonce_before = get_nonce( + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, feeder_gateway_url=origin_url + ) + assert origin_nonce_before == 0 + fork_nonce_before = get_nonce( + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, feeder_gateway_url=fork_url + ) + assert fork_nonce_before == 0 + + # do the invoke and implicitly estimate fee before that + increase_args = [1, 2] + invoke_tx_hash = invoke( + calls=[(contract_address, "increase_balance", increase_args)], + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, + private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY, + gateway_url=fork_url, + ) + # assert only received on fork + assert_tx_status(invoke_tx_hash, "NOT_RECEIVED", feeder_gateway_url=origin_url) + assert_tx_status(invoke_tx_hash, "ACCEPTED_ON_L2", feeder_gateway_url=fork_url) + # assert only callable + origin_balance_after = call( + function="get_balance", + abi_path=ABI_PATH, + address=contract_address, + feeder_gateway_url=origin_url, + ) + assert origin_balance_after == initial_balance + + fork_balance_after = call( + function="get_balance", + abi_path=ABI_PATH, + address=contract_address, + feeder_gateway_url=fork_url, + ) + expected_balancer_after = str(int(initial_balance) + sum(increase_args)) + assert fork_balance_after == expected_balancer_after + + # account nonce - after + origin_nonce_after = get_nonce( + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, feeder_gateway_url=origin_url + ) + assert origin_nonce_after == 0 + fork_nonce_after = get_nonce( + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, feeder_gateway_url=fork_url + ) + assert fork_nonce_after == 1 + + +def _deploy_on_origin_invoke_on_fork_assert_only_fork_changed( + fork_url: str, + origin_url: str, + initial_balance="10", +): + + deploy_info = deploy( + contract=CONTRACT_PATH, + inputs=[initial_balance], + gateway_url=origin_url, + ) + + _invoke_on_fork_and_assert_only_fork_changed( + contract_address=deploy_info["address"], + initial_balance=initial_balance, + fork_url=fork_url, + origin_url=origin_url, + ) + + +@devnet_in_background("--port", ORIGIN_PORT, *PREDEPLOY_ACCOUNT_CLI_ARGS) +@devnet_in_background( + "--port", FORK_PORT, "--fork-network", ORIGIN_URL, "--accounts", "0" +) +def test_forking_devnet_with_account_on_origin(): + """ + Deploy contract on origin, invoke on fork, rely on account on origin. + Assert only fork changed + """ + + # account balance + origin_balance_before = get_account_balance( + address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=ORIGIN_URL + ) + assert origin_balance_before == DEFAULT_INITIAL_BALANCE + + fork_balance_before = get_account_balance( + # fork has access to balances on origin + address=PREDEPLOYED_ACCOUNT_ADDRESS, + server_url=FORK_URL, + ) + assert fork_balance_before == DEFAULT_INITIAL_BALANCE + + # with goerli, forking would be done here, but having it done beforehand is ok with devnet + _deploy_on_origin_invoke_on_fork_assert_only_fork_changed( + fork_url=FORK_URL, + origin_url=ORIGIN_URL, + ) + + # account balance + origin_balance_after = get_account_balance( + address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=ORIGIN_URL + ) + assert origin_balance_after == DEFAULT_INITIAL_BALANCE + + fork_balance_after = get_account_balance( + address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=FORK_URL + ) + assert fork_balance_after < DEFAULT_INITIAL_BALANCE + + +@devnet_in_background("--port", ORIGIN_PORT, "--accounts", "0") +@devnet_in_background( + "--port", FORK_PORT, "--fork-network", ORIGIN_URL, *PREDEPLOY_ACCOUNT_CLI_ARGS +) +def test_forking_devnet_with_account_on_fork(): + """ + Deploy contract on origin, invoke on fork, rely on account on fork. + Assert only fork changed + """ + + # account balance + origin_balance_before = get_account_balance( + address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=ORIGIN_URL + ) + assert origin_balance_before == 0 + + fork_balance_before = get_account_balance( + address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=FORK_URL + ) + assert fork_balance_before == DEFAULT_INITIAL_BALANCE + + # with goerli, forking would be done here, but having it done beforehand is ok with devnet + _deploy_on_origin_invoke_on_fork_assert_only_fork_changed( + fork_url=FORK_URL, + origin_url=ORIGIN_URL, + ) + + # account balance + origin_balance_after = get_account_balance( + address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=ORIGIN_URL + ) + assert origin_balance_after == 0 + + fork_balance_after = get_account_balance( + address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=FORK_URL + ) + assert fork_balance_after < DEFAULT_INITIAL_BALANCE + + +@pytest.mark.usefixtures("run_devnet_in_background") +@pytest.mark.parametrize( + "run_devnet_in_background", + [ + [*TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK)], + [*TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK + 1)], + [*TESTNET_FORK_PARAMS, "--fork-block", "latest"], + [*TESTNET_FORK_PARAMS], # should default to latest + ], + indirect=True, +) +def test_forking_testnet_from_valid_block(): + """Test forking from various happy path blocks""" + + _invoke_on_fork_and_assert_only_fork_changed( + contract_address=TESTNET_CONTRACT_ADDRESS, + initial_balance="10", + fork_url=APP_URL, + origin_url=TESTNET_URL, + ) + + +@pytest.mark.usefixtures("run_devnet_in_background") +@pytest.mark.parametrize( + "run_devnet_in_background, origin_url", + [ + ( + [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--fork-network", "alpha-mainnet"], + ALPHA_MAINNET_URL, + ), + ([*TESTNET_FORK_PARAMS], TESTNET_URL), + ], + indirect=["run_devnet_in_background"], +) +def test_deploy_on_fork(origin_url): + """ + Deploy on fork, invoke on fork. + Assert usability on fork. Assert no change on origin. + """ + + deploy_info = deploy(contract=CONTRACT_PATH, inputs=["10"]) + contract_address = deploy_info["address"] + + invoke_tx_hash = invoke( + calls=[(contract_address, "increase_balance", [1, 2])], + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, + private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY, + ) + assert_tx_status(invoke_tx_hash, "ACCEPTED_ON_L2") + + balance_after = call( + function="get_balance", + address=contract_address, + abi_path=ABI_PATH, + ) + assert balance_after == "13" + + assert_address_has_no_class_hash(contract_address, origin_url) + + +@devnet_in_background( + *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK - 1) +) +def test_forking_testnet_from_too_early_block(): + """Test forking testnet if not yet deployed""" + + invoke_tx_hash = invoke( + calls=[(TESTNET_CONTRACT_ADDRESS, "increase_balance", [2, 3])], # random values + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, + private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY, + max_fee=int(1e8), # to prevent implicit fee estimation + ) + + # assertions on fork (devnet) + assert_tx_status(invoke_tx_hash, "REJECTED") + assert_address_has_no_class_hash(TESTNET_CONTRACT_ADDRESS) + + # assertions on origin (testnet) + # this will fail if someone invokes `increase_balance(2, 3)` because it will then be REJECTED instead of NOT_RECEIVED + assert_tx_status(invoke_tx_hash, "NOT_RECEIVED", feeder_gateway_url=TESTNET_URL) + + +@devnet_in_background(*TESTNET_FORK_PARAMS) +@pytest.mark.parametrize("lite", [True, False]) +def test_minting(lite: bool): + """Test minting""" + dummy_address = "0x123" + dummy_amount = 100 + + resp = mint(dummy_address, dummy_amount, lite=lite) + assert resp["new_balance"] == dummy_amount + resp = mint(dummy_address, dummy_amount, lite=lite) + assert resp["new_balance"] == dummy_amount * 2 + + +@devnet_in_background(*TESTNET_FORK_PARAMS) +def test_deploy_account(): + """Test that deploy account functionality works when forking""" + deploy_account_test_body() + + +@devnet_in_background(*TESTNET_FORK_PARAMS) +def test_deploy_with_udc(): + """Test that deploying with udc works when forking""" + deploy_with_udc_test_body() diff --git a/test/test_fork_cli_params.py b/test/test_fork_cli_params.py new file mode 100644 index 000000000..cb1b51656 --- /dev/null +++ b/test/test_fork_cli_params.py @@ -0,0 +1,141 @@ +"""Testing fork CLI params""" + +import subprocess + +import pytest + +from .shared import ALPHA_GOERLI_URL, ALPHA_GOERLI2_URL, ALPHA_MAINNET_URL +from .util import DevnetBackgroundProc, read_stream, terminate_and_wait + + +ACTIVE_DEVNET = DevnetBackgroundProc() + + +def test_invalid_fork_network(): + """Test if fork network invalid""" + invalid_name = "alpha-goerli-invalid" + proc = ACTIVE_DEVNET.start( + "--fork-network", + invalid_name, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + assert read_stream(proc.stdout) == "" + assert ( + read_stream(proc.stderr) + == f"Error: Invalid fork-network (must be a URL or one of {{alpha-goerli, alpha-goerli2, alpha-mainnet}}). Received: {invalid_name}\n" + ) + assert proc.returncode == 1 + + +def test_url_not_sequencer(): + """Pass a valid url but not of a StarkNet sequencer""" + invalid_url = "http://google.com" + proc = ACTIVE_DEVNET.start( + "--fork-network", + invalid_url, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + assert read_stream(proc.stdout) == "" + assert ( + read_stream(proc.stderr) + == f"Error: {invalid_url} is not a valid StarkNet sequencer\n" + ) + assert proc.returncode == 1 + + +@pytest.mark.parametrize( + "fork_network, expected_stdout", + [ + ("alpha-mainnet", f"Forking {ALPHA_MAINNET_URL}"), + (ALPHA_MAINNET_URL, f"Forking {ALPHA_MAINNET_URL}"), + ("alpha-goerli", f"Forking {ALPHA_GOERLI_URL}"), + (ALPHA_GOERLI_URL, f"Forking {ALPHA_GOERLI_URL}"), + ("alpha-goerli2", f"Forking {ALPHA_GOERLI2_URL}"), + (ALPHA_GOERLI2_URL, f"Forking {ALPHA_GOERLI2_URL}"), + ], +) +def test_predefined_fork_network_specification( + fork_network: str, + expected_stdout: str, +): + """Test various happy path fork network specification scenarios""" + proc = ACTIVE_DEVNET.start( + "--accounts", + "0", # to reduce output + "--fork-network", + fork_network, + stdout=subprocess.PIPE, + ) + terminate_and_wait(proc) + assert expected_stdout in read_stream(proc.stdout) + assert proc.returncode == 0 + + +def test_block_provided_without_network(): + """Should fail if block provided and network not""" + proc = ACTIVE_DEVNET.start( + "--fork-block", "123", stderr=subprocess.PIPE, stdout=subprocess.PIPE + ) + assert read_stream(proc.stdout) == "" + assert ( + read_stream(proc.stderr) + == "Error: --fork-network required if --fork-block present\n" + ) + assert proc.returncode == 1 + + +@pytest.mark.parametrize("fork_block", ["-1", "piece of invalid text"]) +def test_malformed_block_id(fork_block: str): + """Should exit if provided with a negative block number""" + proc = ACTIVE_DEVNET.start( + "--fork-network", + "alpha-goerli", + "--fork-block", + fork_block, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + assert read_stream(proc.stdout) == "" + assert ( + read_stream(proc.stderr) + == f"The value of --fork-block must be a non-negative integer or 'latest', got: {fork_block}\n" + ) + assert proc.returncode == 1 + + +def test_too_big_block_id(): + """Should exit if fork block number too big""" + too_big_block_id = str(int(1e9)) + proc = ACTIVE_DEVNET.start( + "--fork-network", + "alpha-goerli2", + "--fork-block", + too_big_block_id, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + assert read_stream(proc.stdout) == "" + assert f"Block number {too_big_block_id} was not found." in read_stream(proc.stderr) + + +@pytest.mark.parametrize( + "fork_block", + [ + "latest", # would be hard to assert the block number is correct + "1", # small enough, every chain should have it + ], +) +def test_valid_block_ids(fork_block: str): + """Test some happy path fork block ids""" + proc = ACTIVE_DEVNET.start( + "--fork-network", + "alpha-goerli2", + "--fork-block", + fork_block, + stdout=subprocess.PIPE, + ) + terminate_and_wait(proc) + assert f"Forking {ALPHA_GOERLI2_URL}" in read_stream(proc.stdout) + assert proc.returncode == 0 diff --git a/test/test_fork_feeder_gateway.py b/test/test_fork_feeder_gateway.py new file mode 100644 index 000000000..ffdbdb2d4 --- /dev/null +++ b/test/test_fork_feeder_gateway.py @@ -0,0 +1,377 @@ +"""Test feeder gateway responses of origin and fork""" + +import json + +import requests +from starkware.starknet.definitions.error_codes import StarknetErrorCode +from starkware.starknet.services.api.feeder_gateway.response_objects import ( + BlockIdentifier, +) + +from .account import declare, invoke +from .shared import ( + BALANCE_KEY, + CONTRACT_PATH, + EXPECTED_CLASS_HASH, + PREDEPLOYED_ACCOUNT_ADDRESS, + PREDEPLOYED_ACCOUNT_PRIVATE_KEY, +) +from .settings import APP_URL +from .test_state_update import get_state_update +from .test_transaction_trace import ( + assert_get_block_traces_response, + get_block_traces, + get_transaction_trace_response, +) +from .testnet_deployment import ( + TESTNET_CONTRACT_ADDRESS, + TESTNET_DEPLOYMENT_BLOCK, + TESTNET_FORK_PARAMS, + TESTNET_URL, +) +from .util import ( + assert_address_has_no_class_hash, + assert_class_by_hash, + assert_class_by_hash_not_present, + assert_class_hash_at_address, + assert_contract_code_not_present, + assert_contract_code_present, + assert_full_contract, + assert_full_contract_not_present, + assert_receipt_present, + assert_storage, + assert_transaction, + assert_transaction_not_received, + assert_transaction_receipt_not_received, + assert_tx_status, + deploy, + devnet_in_background, + get_block, +) + + +DEPLOYMENT_INPUT = "10" +EXPECTED_DEPLOYMENT_ADDRESS = ( + "0x007e723b33e317c604b36e17d0a8e7064b08eda39aa9a4d6a94a7626ec432d8a" +) +EXPECTED_INVOKE_HASH = ( + "0x51b501687e77d5433c7fc00b3a6dd25c2f6edf95f506dd9cba2251a4ce9ed43" +) + + +def _deploy_to_expected_address(contract=CONTRACT_PATH): + deploy_info = deploy( + contract=contract, + inputs=[DEPLOYMENT_INPUT], + salt="0x42", + ) + assert int(deploy_info["address"], 16) == int(EXPECTED_DEPLOYMENT_ADDRESS, 16) + + +def _make_expected_invoke(gateway_url=APP_URL): + invoke_tx_hash = invoke( + calls=[(TESTNET_CONTRACT_ADDRESS, "increase_balance", [1, 2])], + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, + private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY, + gateway_url=gateway_url, + ) + assert int(invoke_tx_hash, 16) == int(EXPECTED_INVOKE_HASH, 16) + + +@devnet_in_background( + *TESTNET_FORK_PARAMS, + "--fork-block", + # starting from an earlier block; otherwise the contract class is already present + str(TESTNET_DEPLOYMENT_BLOCK - 1), +) +def test_contract_responses(): + """Assert that get_full_contract only makes sense on fork after deployment""" + # full contract + assert_full_contract_not_present( + address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL + ) + assert_full_contract_not_present( + address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=APP_URL + ) + + # code + assert_contract_code_not_present( + address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL + ) + assert_contract_code_not_present( + address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=APP_URL + ) + + # class hash + assert_address_has_no_class_hash( + contract_address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL + ) + assert_address_has_no_class_hash( + contract_address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=APP_URL + ) + + # storage + assert_storage( + address=EXPECTED_DEPLOYMENT_ADDRESS, + key=BALANCE_KEY, + expected_value="0x0", + feeder_gateway_url=TESTNET_URL, + ) + assert_storage( + address=EXPECTED_DEPLOYMENT_ADDRESS, + key=BALANCE_KEY, + expected_value="0x0", + feeder_gateway_url=APP_URL, + ) + + _deploy_to_expected_address() + + # full contract + assert_full_contract_not_present( + address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL + ) + assert_full_contract( + address=EXPECTED_DEPLOYMENT_ADDRESS, + expected_path=CONTRACT_PATH, + feeder_gateway_url=APP_URL, + ) + + # code + assert_contract_code_not_present( + address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL + ) + assert_contract_code_present( + address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=APP_URL + ) + + assert_address_has_no_class_hash( + contract_address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL + ) + assert_class_hash_at_address( + contract_address=EXPECTED_DEPLOYMENT_ADDRESS, + expected_class_hash=EXPECTED_CLASS_HASH, + feeder_gateway_url=APP_URL, + ) + + # storage + assert_storage( + address=EXPECTED_DEPLOYMENT_ADDRESS, + key=BALANCE_KEY, + expected_value="0x0", + feeder_gateway_url=TESTNET_URL, + ) + assert_storage( + address=EXPECTED_DEPLOYMENT_ADDRESS, + key=BALANCE_KEY, + expected_value=hex(int(DEPLOYMENT_INPUT, 10)), + feeder_gateway_url=APP_URL, + ) + + +@devnet_in_background( + *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK - 1) +) +def test_declare_and_get_class_by_hash(): + """Test class declaration and class getting by hash""" + + assert_class_by_hash_not_present( + class_hash=EXPECTED_CLASS_HASH, feeder_gateway_url=TESTNET_URL + ) + assert_class_by_hash_not_present( + class_hash=EXPECTED_CLASS_HASH, feeder_gateway_url=APP_URL + ) + + declare_info = declare( + contract_path=CONTRACT_PATH, + account_address=PREDEPLOYED_ACCOUNT_ADDRESS, + private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY, + ) + assert int(declare_info["class_hash"], 16) == int(EXPECTED_CLASS_HASH, 16) + assert_tx_status(declare_info["tx_hash"], "ACCEPTED_ON_L2") + + assert_class_by_hash_not_present( + class_hash=EXPECTED_CLASS_HASH, feeder_gateway_url=TESTNET_URL + ) + assert_class_by_hash( + class_hash=EXPECTED_CLASS_HASH, + expected_path=CONTRACT_PATH, + feeder_gateway_url=APP_URL, + ) + + +def _assert_transaction_trace_not_present(tx_hash: str, feeder_gateway_url=APP_URL): + resp = get_transaction_trace_response(tx_hash, server_url=feeder_gateway_url) + assert resp.json()["code"] == str(StarknetErrorCode.INVALID_TRANSACTION_HASH) + assert resp.status_code == 500 + + +def _assert_transaction_trace_present( + tx_hash: str, expected_address: str, feeder_gateway_url=APP_URL +): + resp = get_transaction_trace_response(tx_hash, server_url=feeder_gateway_url) + body = resp.json() + assert body["function_invocation"]["contract_address"] == expected_address + assert resp.status_code == 200 + + +@devnet_in_background( + *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK) +) +def test_transaction_responses(): + """Assert transaction only present on fork after invoking""" + + # tx status + assert_tx_status( + tx_hash=EXPECTED_INVOKE_HASH, + expected_tx_status="NOT_RECEIVED", + feeder_gateway_url=TESTNET_URL, + ) + assert_tx_status( + tx_hash=EXPECTED_INVOKE_HASH, + expected_tx_status="NOT_RECEIVED", + feeder_gateway_url=APP_URL, + ) + + # tx + assert_transaction_not_received( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL + ) + assert_transaction_not_received( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=APP_URL + ) + + # tx receipt + assert_transaction_receipt_not_received( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL + ) + assert_transaction_receipt_not_received( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=APP_URL + ) + + # tx trace + _assert_transaction_trace_not_present( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL + ) + _assert_transaction_trace_not_present( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=APP_URL + ) + + _make_expected_invoke() + + # tx status + assert_tx_status( + tx_hash=EXPECTED_INVOKE_HASH, + expected_tx_status="NOT_RECEIVED", + feeder_gateway_url=TESTNET_URL, + ) + assert_tx_status( + tx_hash=EXPECTED_INVOKE_HASH, + expected_tx_status="ACCEPTED_ON_L2", + feeder_gateway_url=APP_URL, + ) + + # tx + assert_transaction_not_received( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL + ) + assert_transaction( + tx_hash=EXPECTED_INVOKE_HASH, + expected_status="ACCEPTED_ON_L2", + feeder_gateway_url=APP_URL, + ) + + # tx receipt + assert_transaction_receipt_not_received( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL + ) + assert_receipt_present( + tx_hash=EXPECTED_INVOKE_HASH, + expected_status="ACCEPTED_ON_L2", + feeder_gateway_url=APP_URL, + ) + + # tx trace + _assert_transaction_trace_not_present( + tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL + ) + _assert_transaction_trace_present( + tx_hash=EXPECTED_INVOKE_HASH, + expected_address=PREDEPLOYED_ACCOUNT_ADDRESS, + feeder_gateway_url=APP_URL, + ) + + +def _assert_block_artifact_not_found( + method: str, + block_number: BlockIdentifier = None, + block_hash: str = None, + feeder_gateway_url=APP_URL, +): + resp = requests.get( + f"{feeder_gateway_url}/feeder_gateway/{method}", + {"blockNumber": block_number, "blockHash": block_hash}, + ) + assert json.loads(resp.text)["code"] == str(StarknetErrorCode.BLOCK_NOT_FOUND) + assert resp.status_code == 500 + + +@devnet_in_background( + *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK) +) +def test_block_responses(): + """Test how block responses are handled when forking.""" + + origin_block = get_block( + block_number=TESTNET_DEPLOYMENT_BLOCK, + parse=True, + feeder_gateway_url=TESTNET_URL, + ) + fork_block = get_block(block_number=TESTNET_DEPLOYMENT_BLOCK, parse=True) + assert origin_block == fork_block + + # assert block count incremented by one (due to genesis block) + latest_fork_block_before = get_block(block_number="latest", parse=True) + assert latest_fork_block_before["block_number"] == fork_block["block_number"] + 1 + + # assert next block not yet present + next_block_number = str(TESTNET_DEPLOYMENT_BLOCK + 2) + for method in "get_block", "get_block_traces", "get_state_update": + _assert_block_artifact_not_found(method, block_number=next_block_number) + + # invoke + _make_expected_invoke(gateway_url=APP_URL) + + # assert block count incremented by one + latest_fork_block_after = get_block(block_number="latest", parse=True) + assert ( + latest_fork_block_after["block_number"] + == latest_fork_block_before["block_number"] + 1 + ) + + # assert block trace + assert_get_block_traces_response({"blockNumber": "latest"}, EXPECTED_INVOKE_HASH) + + # assert state update + state_update = get_state_update(block_number="latest") + assert TESTNET_CONTRACT_ADDRESS in state_update["state_diff"]["storage_diffs"] + + +@devnet_in_background(*TESTNET_FORK_PARAMS) +def test_block_responses_by_hash(): + """Test error is internally properly handled and that a json is sent""" + dummy_hash = "0x1" + for method in "get_block", "get_block_traces", "get_state_update": + _assert_block_artifact_not_found(method, block_hash=dummy_hash) + + latest_block_by_number = get_block(block_number="latest", parse=True) + latest_block_hash = latest_block_by_number["block_hash"] + latest_block_by_hash = get_block(block_hash=latest_block_hash, parse=True) + assert latest_block_by_number == latest_block_by_hash + + block_traces_by_hash = get_block_traces({"blockHash": latest_block_hash}) + block_traces_by_number = get_block_traces({"blockNumber": "latest"}) + assert block_traces_by_hash == block_traces_by_number + + state_update_by_hash = get_block_traces({"blockHash": latest_block_hash}) + state_update_by_number = get_block_traces({"blockNumber": "latest"}) + assert state_update_by_hash == state_update_by_number diff --git a/test/test_general_workflow.py b/test/test_general_workflow.py index f790732dc..4a2155f32 100644 --- a/test/test_general_workflow.py +++ b/test/test_general_workflow.py @@ -6,12 +6,13 @@ from .account import invoke from .util import ( - assert_contract_class, + assert_class_by_hash, + assert_full_contract, assert_negative_block_input, assert_transaction_not_received, assert_transaction_receipt_not_received, assert_block, - assert_contract_code, + assert_contract_code_present, assert_equal, assert_failing_deploy, assert_receipt, @@ -22,9 +23,7 @@ assert_events, call, deploy, - get_class_by_hash, get_class_hash_at, - get_full_contract, get_block, ) @@ -43,28 +42,25 @@ PREDEPLOYED_ACCOUNT_PRIVATE_KEY, ) -EXPECTED_SALTY_DEPLOY_HASH_LITE_MODE = "0x2" EXPECTED_SALTY_DEPLOY_BLOCK_HASH_LITE_MODE = "0x1" @pytest.mark.usefixtures("run_devnet_in_background") @pytest.mark.parametrize( - "run_devnet_in_background, expected_tx_hash, expected_block_hash", + "run_devnet_in_background, expected_block_hash", [ ( [*PREDEPLOY_ACCOUNT_CLI_ARGS], - EXPECTED_SALTY_DEPLOY_HASH, "", ), ( [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--lite-mode"], - EXPECTED_SALTY_DEPLOY_HASH_LITE_MODE, EXPECTED_SALTY_DEPLOY_BLOCK_HASH_LITE_MODE, ), ], indirect=True, ) -def test_general_workflow(expected_tx_hash, expected_block_hash): +def test_general_workflow(expected_block_hash): """Test devnet with CLI""" deploy_info = deploy(CONTRACT_PATH, inputs=["0"]) @@ -87,16 +83,14 @@ def test_general_workflow(expected_tx_hash, expected_block_hash): assert_transaction_receipt_not_received(NONEXISTENT_TX_HASH) # check code - assert_contract_code(deploy_info["address"]) + assert_contract_code_present(deploy_info["address"]) # check contract class - class_by_address = get_full_contract(deploy_info["address"]) - assert_contract_class(class_by_address, CONTRACT_PATH) + assert_full_contract(address=deploy_info["address"], expected_path=CONTRACT_PATH) # check contract class through class hash class_hash = get_class_hash_at(deploy_info["address"]) - class_by_hash = get_class_by_hash(class_hash) - assert_equal(class_by_address, class_by_hash) + assert_class_by_hash(class_hash, expected_path=CONTRACT_PATH) # increase and assert balance invoke_tx_hash = invoke( @@ -132,7 +126,7 @@ def test_general_workflow(expected_tx_hash, expected_block_hash): inputs=None, expected_status="ACCEPTED_ON_L2", expected_address=EXPECTED_SALTY_DEPLOY_ADDRESS, - expected_tx_hash=expected_tx_hash, + expected_tx_hash=EXPECTED_SALTY_DEPLOY_HASH, ) salty_invoke_tx_hash = invoke( diff --git a/test/test_transaction_trace.py b/test/test_transaction_trace.py index cbe38203d..8598ec638 100644 --- a/test/test_transaction_trace.py +++ b/test/test_transaction_trace.py @@ -26,13 +26,15 @@ ) -def get_transaction_trace_response(tx_hash=None): +def get_transaction_trace_response(tx_hash=None, server_url=APP_URL): """Get transaction trace response""" params = { "transactionHash": tx_hash, } - res = requests.get(f"{APP_URL}/feeder_gateway/get_transaction_trace", params=params) + res = requests.get( + f"{server_url}/feeder_gateway/get_transaction_trace", params=params + ) return res @@ -100,18 +102,23 @@ def test_nonexistent_transaction_hash(): assert res.status_code == 500 -def assert_get_block_traces_response(params, expected_tx_hash): - """Assert response of get_block_traces""" +def get_block_traces(params: dict): + """Get block traces""" block_traces = requests.get( f"{APP_URL}/feeder_gateway/get_block_traces", params=params ).json() # loading to assert valid structure - BlockTransactionTraces.load(block_traces) + return BlockTransactionTraces.load(block_traces) + + +def assert_get_block_traces_response(params: dict, expected_tx_hash: str): + """Assert response of get_block_traces""" + block_traces = get_block_traces(params=params) # index 0 assuming it's the only tx in the response - actual_tx_hash = block_traces["traces"][0]["transaction_hash"] - assert actual_tx_hash == expected_tx_hash + actual_tx_hash = block_traces.traces[0].transaction_hash + assert actual_tx_hash == int(expected_tx_hash, 16) @pytest.mark.transaction_trace diff --git a/test/testnet_deployment.py b/test/testnet_deployment.py new file mode 100644 index 000000000..805ee0761 --- /dev/null +++ b/test/testnet_deployment.py @@ -0,0 +1,16 @@ +"""Contains info about deployment of a test contract on alpha-goerli-2""" + +from .shared import ALPHA_GOERLI2_URL, PREDEPLOY_ACCOUNT_CLI_ARGS + +TESTNET_URL = ALPHA_GOERLI2_URL +TESTNET_CONTRACT_ADDRESS = ( + "0x32320dbdff79639db4ac0ff1f9f8b7450d31fee8ca1bccea7cfa0d7765fe0b2" +) +TESTNET_CONTRACT_SALT = ( + "0x10477367a9748e55196ab3c9ce04be74253cdb974e35a1d52ccda74d6d0e76b" +) +TESTNET_CONTRACT_CLASS_HASH = ( + "0x028c7d54caa154d29953a26857c200623fd185bffa178a185d0ff247d22127a9" +) +TESTNET_DEPLOYMENT_BLOCK = 8827 # this is when the contract was deployed +TESTNET_FORK_PARAMS = [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--fork-network", "alpha-goerli2"] diff --git a/test/util.py b/test/util.py index 518df5ab2..97905da07 100644 --- a/test/util.py +++ b/test/util.py @@ -8,10 +8,11 @@ import re import subprocess import time -from typing import List +from typing import IO, List import requests from starkware.starknet.cli.starknet_cli import get_salt +from starkware.starknet.definitions.error_codes import StarknetErrorCode from starkware.starknet.definitions.transaction_type import TransactionType from starkware.starknet.services.api.contract_class import ContractClass from starkware.starknet.services.api.gateway.transaction import Deploy @@ -37,6 +38,8 @@ def run_devnet_in_background(*args, stderr=None, stdout=None): if "--accounts" not in args: args = [*args, "--accounts", "1"] + port = args[args.index("--port") + 1] if "--port" in args else PORT + command = [ "poetry", "run", @@ -44,13 +47,14 @@ def run_devnet_in_background(*args, stderr=None, stdout=None): "--host", HOST, "--port", - PORT, + port, *args, ] # pylint: disable=consider-using-with proc = subprocess.Popen(command, close_fds=True, stderr=stderr, stdout=stdout) - ensure_server_alive(f"{APP_URL}/is_alive", proc) + healthcheck_url = f"http://{HOST}:{port}/is_alive" + ensure_server_alive(healthcheck_url, proc) return proc @@ -147,11 +151,12 @@ def extract_address(stdout): return extract(r"Contract address: (\w*)", stdout) -def run_starknet(args, raise_on_nonzero=True, add_gateway_urls=True): +def run_starknet(args, raise_on_nonzero=True, gateway_url=APP_URL): """Wrapper around subprocess.run""" my_args = ["poetry", "run", "starknet", *args, "--no_wallet"] - if add_gateway_urls: - my_args.extend(["--gateway_url", APP_URL, "--feeder_gateway_url", APP_URL]) + # there is no case when gateway should not be equal to feeder gateway + my_args.extend(["--gateway_url", gateway_url, "--feeder_gateway_url", gateway_url]) + output = subprocess.run(my_args, encoding="utf-8", check=False, capture_output=True) if output.returncode != 0 and raise_on_nonzero: if output.stderr: @@ -160,20 +165,20 @@ def run_starknet(args, raise_on_nonzero=True, add_gateway_urls=True): return output -def send_tx(transaction: dict, tx_type: TransactionType) -> dict: +def send_tx(transaction: dict, tx_type: TransactionType, gateway_url=APP_URL) -> dict: """ Send transaction. Returns tx hash """ resp = requests.post( - url=f"{APP_URL}/gateway/add_transaction", + url=f"{gateway_url}/gateway/add_transaction", json={**transaction, "type": tx_type.name}, ) assert resp.status_code == 200 return resp.json() -def deploy(contract, inputs=None, salt=None): +def deploy(contract, inputs=None, salt=None, gateway_url=APP_URL): """Wrapper around starknet deploy""" inputs = inputs or [] @@ -185,7 +190,7 @@ def deploy(contract, inputs=None, salt=None): version=SUPPORTED_TX_VERSION, ).dump() - resp = send_tx(deploy_tx, TransactionType.DEPLOY) + resp = send_tx(deploy_tx, TransactionType.DEPLOY, gateway_url) return { "tx_hash": resp["transaction_hash"], @@ -216,9 +221,13 @@ def estimate_message_fee( return extract_fee(output.stdout) -def assert_transaction(tx_hash, expected_status, expected_signature=None): +def assert_transaction( + tx_hash, expected_status, expected_signature=None, feeder_gateway_url=APP_URL +): """Wrapper around starknet get_transaction""" - output = run_starknet(["get_transaction", "--hash", tx_hash]) + output = run_starknet( + ["get_transaction", "--hash", tx_hash], gateway_url=feeder_gateway_url + ) transaction = json.loads(output.stdout) assert_equal(transaction["status"], expected_status, transaction) if expected_signature: @@ -267,29 +276,39 @@ def assert_keys(dictionary, keys): assert dictionary.keys() == expected_set, f"{dictionary.keys()} != {expected_set}" -def assert_transaction_not_received(tx_hash): +def assert_transaction_not_received(tx_hash: str, feeder_gateway_url=APP_URL): """Assert correct tx response when there is no tx with `tx_hash`.""" - output = run_starknet(["get_transaction", "--hash", tx_hash]) + output = run_starknet( + ["get_transaction", "--hash", tx_hash], gateway_url=feeder_gateway_url + ) transaction = json.loads(output.stdout) assert_equal(transaction, {"status": "NOT_RECEIVED"}) -def assert_transaction_receipt_not_received(tx_hash): +def assert_transaction_receipt_not_received(tx_hash: str, feeder_gateway_url=APP_URL): """Assert correct tx receipt response when there is no tx with `tx_hash`.""" - receipt = get_transaction_receipt(tx_hash) + receipt = get_transaction_receipt(tx_hash, feeder_gateway_url=feeder_gateway_url) assert_equal( receipt, { "events": [], "l2_to_l1_messages": [], "status": "NOT_RECEIVED", - "transaction_hash": tx_hash, + "transaction_hash": "0x0", }, ) # pylint: disable=too-many-arguments -def estimate_fee(function, inputs, address, abi_path, signature=None, nonce=None): +def estimate_fee( + function, + inputs, + address, + abi_path, + signature=None, + nonce=None, + feeder_gateway_url=APP_URL, +): """Wrapper around starknet estimate_fee. Returns fee in wei.""" args = [ "invoke", @@ -310,13 +329,15 @@ def estimate_fee(function, inputs, address, abi_path, signature=None, nonce=None if nonce is not None: args.extend(["--nonce", str(nonce)]) - output = run_starknet(args) + output = run_starknet(args, gateway_url=feeder_gateway_url) print("Estimate fee successful!") return extract_fee(output.stdout) -def call(function: str, address: str, abi_path: str, inputs=None): +def call( + function: str, address: str, abi_path: str, inputs=None, feeder_gateway_url=APP_URL +): """Wrapper around starknet call""" args = [ "call", @@ -330,7 +351,7 @@ def call(function: str, address: str, abi_path: str, inputs=None): if inputs: args.extend(["--inputs", *inputs]) - output = run_starknet(args) + output = run_starknet(args, gateway_url=feeder_gateway_url) print("Call successful!") return output.stdout.rstrip() @@ -343,9 +364,11 @@ def load_contract_class(contract_path: str): return ContractClass.load(loaded_contract) -def assert_tx_status(tx_hash, expected_tx_status): +def assert_tx_status(tx_hash, expected_tx_status: str, feeder_gateway_url=APP_URL): """Asserts the tx_status of the tx with tx_hash.""" - output = run_starknet(["tx_status", "--hash", tx_hash]) + output = run_starknet( + ["tx_status", "--hash", tx_hash], gateway_url=feeder_gateway_url + ) response = json.loads(output.stdout) tx_status = response["tx_status"] assert_equal(tx_status, expected_tx_status, response) @@ -354,14 +377,33 @@ def assert_tx_status(tx_hash, expected_tx_status): assert "tx_failure_reason" in response, f"Key not found in {response}" -def assert_contract_code(address): - """Asserts the content of the code of a contract at address.""" - output = run_starknet(["get_code", "--contract_address", address]) +def assert_contract_code_present(address: str, feeder_gateway_url=APP_URL): + """Asserts the content of the code of a contract at `address`.""" + output = run_starknet( + ["get_code", "--contract_address", address], gateway_url=feeder_gateway_url + ) code = json.loads(output.stdout) - # just checking key equality + + assert code["abi"] # assert non-empty + assert code["bytecode"] # assert non-empty + + # assert no other keys assert_equal(sorted(code.keys()), ["abi", "bytecode"]) +def assert_contract_code_not_present(address: str, feeder_gateway_url=APP_URL): + """Assert abi and bytecode empty""" + resp = requests.get( + f"{feeder_gateway_url}/feeder_gateway/get_code?contractAddress={address}" + ) + + code = resp.json() + assert code["abi"] == {} + assert code["bytecode"] == [] + + assert resp.status_code == 200 + + def assert_contract_class(actual_class: ContractClass, expected_class_path: str): """Asserts equality between `actual_class` and class at `expected_class_path`.""" @@ -369,10 +411,13 @@ def assert_contract_class(actual_class: ContractClass, expected_class_path: str) assert_equal(actual_class, loaded_contract_class.remove_debug_info()) -def assert_storage(address, key, expected_value): +def assert_storage( + address: str, key: str, expected_value: str, feeder_gateway_url=APP_URL +): """Asserts the storage value stored at (address, key).""" output = run_starknet( - ["get_storage_at", "--contract_address", address, "--key", key] + ["get_storage_at", "--contract_address", address, "--key", key], + gateway_url=feeder_gateway_url, ) assert_equal(output.stdout.rstrip(), expected_value) @@ -383,28 +428,96 @@ def load_json_from_path(path): return json.load(expected_file) -def get_transaction_receipt(tx_hash: str): +def get_transaction_receipt(tx_hash: str, feeder_gateway_url=APP_URL): """Fetches the transaction receipt of transaction with tx_hash""" - output = run_starknet(["get_transaction_receipt", "--hash", tx_hash]) + output = run_starknet( + ["get_transaction_receipt", "--hash", tx_hash], gateway_url=feeder_gateway_url + ) return json.loads(output.stdout) -def get_full_contract(contract_address: str) -> ContractClass: +def get_full_contract( + contract_address: str, feeder_gateway_url=APP_URL +) -> ContractClass: """Gets contract class by contract address""" - output = run_starknet(["get_full_contract", "--contract_address", contract_address]) + output = run_starknet( + ["get_full_contract", "--contract_address", contract_address], + gateway_url=feeder_gateway_url, + ) return ContractClass.loads(output.stdout) +def assert_full_contract_not_present(address: str, feeder_gateway_url=APP_URL): + """Assert that get_full_contract fails due to uninitialized contract""" + resp = requests.get( + f"{feeder_gateway_url}/feeder_gateway/get_full_contract", + {"contractAddress": address}, + ) + + assert resp.json()["code"] == str(StarknetErrorCode.UNINITIALIZED_CONTRACT) + assert resp.status_code == 500 + + +def assert_full_contract(address: str, expected_path: str, feeder_gateway_url=APP_URL): + """Assert that the provided address has contract from `expected_path` deployed at it.""" + class_by_address = get_full_contract( + contract_address=address, feeder_gateway_url=feeder_gateway_url + ) + assert_contract_class(class_by_address, expected_class_path=expected_path) + + def get_class_hash_at(contract_address: str) -> str: """Gets class hash at given contract address""" output = run_starknet(["get_class_hash_at", "--contract_address", contract_address]) return output.stdout -def get_class_by_hash(class_hash: str): +def assert_address_has_no_class_hash(contract_address: str, feeder_gateway_url=APP_URL): + """There should be no class hash at `contract_address`.""" + resp = requests.get( + f"{feeder_gateway_url}/feeder_gateway/get_class_hash_at", + {"contractAddress": contract_address}, + ) + assert resp.json()["code"] == str(StarknetErrorCode.UNINITIALIZED_CONTRACT) + assert resp.status_code == 500 + + +def assert_class_hash_at_address( + contract_address: str, expected_class_hash: str, feeder_gateway_url=APP_URL +): + """The class hash at `contract_address` should be `expected_class_hash`.""" + resp = requests.get( + f"{feeder_gateway_url}/feeder_gateway/get_class_hash_at", + {"contractAddress": contract_address}, + ) + received_class_hash = int(json.loads(resp.text), 16) + assert received_class_hash == int(expected_class_hash, 16) + assert resp.status_code == 200 + + +def get_class_by_hash(class_hash: str, feeder_gateway_url=APP_URL): """Gets contract class by contract hash""" - output = run_starknet(["get_class_by_hash", "--class_hash", class_hash]) - return ContractClass.loads(output.stdout) + return requests.get( + f"{feeder_gateway_url}/feeder_gateway/get_class_by_hash", + {"classHash": class_hash}, + ) + + +def assert_class_by_hash( + class_hash: str, expected_path: str, feeder_gateway_url=APP_URL +): + """Assert the class at `class_hash` matches what is at `expected_path`.""" + resp = get_class_by_hash(class_hash, feeder_gateway_url=feeder_gateway_url) + class_by_hash = ContractClass.loads(resp.text) + assert_contract_class(class_by_hash, expected_class_path=expected_path) + assert resp.status_code == 200 + + +def assert_class_by_hash_not_present(class_hash: str, feeder_gateway_url=APP_URL): + """Assert the server holds no class at provided `class_hash`.""" + resp = get_class_by_hash(class_hash, feeder_gateway_url=feeder_gateway_url) + assert resp.json()["code"] == str(StarknetErrorCode.UNDECLARED_CLASS) + assert resp.status_code == 500 def assert_receipt(tx_hash, expected_path): @@ -420,6 +533,15 @@ def assert_receipt(tx_hash, expected_path): assert_equal(receipt, expected_receipt) +def assert_receipt_present( + tx_hash: str, expected_status: str, feeder_gateway_url=APP_URL +): + """Asserts the content of the receipt of tx with tx_hash is non-empty""" + receipt = get_transaction_receipt(tx_hash, feeder_gateway_url=feeder_gateway_url) + assert receipt["transaction_hash"] == tx_hash + assert receipt["status"] == expected_status + + def assert_events(tx_hash, expected_path): """Asserts the content of the events element of the receipt of tx with tx_hash.""" receipt = get_transaction_receipt(tx_hash) @@ -427,16 +549,23 @@ def assert_events(tx_hash, expected_path): assert_equal(receipt["events"], expected_receipt["events"]) -def get_block(block_number=None, parse=False): +def get_block( + block_number=None, block_hash=None, parse=False, feeder_gateway_url=APP_URL +): """Get the block with block_number. If no number provided, return the last.""" args = ["get_block"] if block_number: args.extend(["--number", str(block_number)]) + if block_hash: + args.extend(["--hash", str(block_hash)]) + if parse: - output = run_starknet(args, raise_on_nonzero=True) + output = run_starknet( + args, raise_on_nonzero=True, gateway_url=feeder_gateway_url + ) return json.loads(output.stdout) - return run_starknet(args, raise_on_nonzero=False) + return run_starknet(args, raise_on_nonzero=False, gateway_url=feeder_gateway_url) def assert_negative_block_input(): @@ -478,14 +607,6 @@ def assert_block(latest_block_number, latest_tx_hash): assert re.match(r"^[a-fA-F0-9]{64}$", latest_block["state_root"]) -def assert_block_hash(latest_block_number, expected_block_hash): - """Asserts the content of the block with block_number.""" - - block = get_block(block_number=latest_block_number, parse=True) - assert_equal(block["block_hash"], expected_block_hash) - assert_equal(block["status"], "ACCEPTED_ON_L2") - - def assert_salty_deploy( contract_path, inputs, salt, expected_status, expected_address, expected_tx_hash ): @@ -544,3 +665,8 @@ def stop(self): if self.proc: terminate_and_wait(self.proc) self.proc = None + + +def read_stream(stream: IO, encoding="utf-8") -> str: + """Return stdout and stderr of `proc`""" + return stream.read().decode(encoding)