Skip to content

Commit

Permalink
Mint tx v1 (0xSpaceShard#384)
Browse files Browse the repository at this point in the history
- Introduce chargeable account
- Refactor account utilities
  • Loading branch information
FabijanC committed Jan 17, 2023
1 parent 9b37076 commit b6947da
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 150 deletions.
13 changes: 13 additions & 0 deletions page/docs/guide/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@ After adding a new cairo-lang version, you will probably want to recompile contr
8. Update expected test paths and addresses
9. Update docs

## Development - predeployment

Several things are preconfigured on startup to be available on the first user interaction with Devnet. This is done in the `initialize` method of `StarknetWrapper`. The following is currently executed:

- Deployment of
- Fee token contract
- User accounts
- Chargeable account
- for e.g. signing minting txs
- UDC
- supports contract deployment ever since deploy txs have been deprecated
- Declaration of the account class used by Starknet CLI

## Development - Build

You don't need to build anything to be able to run locally, but if you need the `*.whl` or `*.tar.gz` artifacts, run
Expand Down
20 changes: 4 additions & 16 deletions starknet_devnet/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
Account class and its predefined constants.
"""

from starkware.cairo.lang.vm.crypto import pedersen_hash
from starkware.starknet.core.os.contract_address.contract_address import (
calculate_contract_address_from_hash,
)
from starkware.starknet.public.abi import get_selector_from_name
from starkware.starknet.testing.contract import StarknetContract
from starkware.starknet.testing.starknet import Starknet

from starknet_devnet.account_util import set_balance
from starknet_devnet.contract_class_wrapper import ContractClassWrapper
from starknet_devnet.util import Uint256


class Account:
Expand All @@ -36,7 +35,7 @@ def __init__(
# the only thing that affects the account address
self.address = calculate_contract_address_from_hash(
salt=20,
class_hash=1803505466663265559571280894381905521939782500874858933595227108099796801620,
class_hash=0x3FCBF77B28C96F4F2FB5BD2D176AB083A12A5E123ADEB0DE955D7EE228C9854,
constructor_calldata=[public_key],
deployer_address=0,
)
Expand All @@ -52,7 +51,7 @@ def to_json(self):
}

async def deploy(self) -> StarknetContract:
"""Deploy this account."""
"""Deploy this account and set its balance."""
starknet: Starknet = self.starknet_wrapper.starknet
contract_class = self.contract_class
await starknet.state.state.set_contract_class(
Expand All @@ -64,15 +63,4 @@ async def deploy(self) -> StarknetContract:
self.address, get_selector_from_name("Account_public_key"), self.public_key
)

# set initial balance
fee_token_address = starknet.state.general_config.fee_token_address
balance_address = pedersen_hash(
get_selector_from_name("ERC20_balances"), self.address
)
initial_balance_uint256 = Uint256.from_felt(self.initial_balance)
await starknet.state.state.set_storage_at(
fee_token_address, balance_address, initial_balance_uint256.low
)
await starknet.state.state.set_storage_at(
fee_token_address, balance_address + 1, initial_balance_uint256.high
)
await set_balance(starknet.state, self.address, self.initial_balance)
131 changes: 131 additions & 0 deletions starknet_devnet/account_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Utilities for OZ (not Starknet CLI) implementation of Starknet Account
Latest changes based on https://github.com/OpenZeppelin/nile/pull/184
"""

from typing import List, NamedTuple, Sequence, Tuple

from starkware.cairo.lang.vm.crypto import pedersen_hash
from starkware.crypto.signature.signature import sign
from starkware.starknet.core.os.transaction_hash.transaction_hash import (
TransactionHashPrefix,
calculate_transaction_hash_common,
)
from starkware.starknet.definitions.general_config import StarknetChainId
from starkware.starknet.public.abi import get_selector_from_name
from starkware.starknet.testing.starknet import StarknetState

from .util import Uint256


class AccountCall(NamedTuple):
"""Things needed to interact through Account"""

to_address: str
"""The address of the called contract"""

function: str
inputs: List[str]


def _from_call_to_call_array(calls: List[AccountCall]):
"""Transforms calls to call_array and calldata."""
call_array = []
calldata = []

for call_tuple in calls:
call_tuple = AccountCall(*call_tuple)

entry = (
int(call_tuple.to_address, 16),
get_selector_from_name(call_tuple.function),
len(calldata),
len(call_tuple.inputs),
)
call_array.append(entry)
calldata.extend(int(data) for data in call_tuple.inputs)

return (call_array, calldata)


def _get_execute_calldata(call_array, calldata):
"""Get calldata for __execute__."""
return [
len(call_array),
*[x for t in call_array for x in t],
len(calldata),
*calldata,
]


# pylint: disable=too-many-arguments
def get_execute_args(
calls: List[AccountCall],
account_address: str,
private_key: int,
nonce: int,
version: int,
max_fee=None,
chain_id=StarknetChainId.TESTNET,
):
"""Returns signature and execute calldata"""

# get execute calldata
(call_array, calldata) = _from_call_to_call_array(calls)
execute_calldata = _get_execute_calldata(call_array, calldata)

# get signature
message_hash = _get_transaction_hash(
contract_address=int(account_address, 16),
calldata=execute_calldata,
nonce=nonce,
version=version,
max_fee=max_fee,
chain_id=chain_id,
)
signature = _get_signature(message_hash, private_key)

return signature, execute_calldata


def _get_transaction_hash(
contract_address: int,
calldata: Sequence[int],
nonce: int,
version: int,
max_fee: int,
chain_id=StarknetChainId.TESTNET,
) -> str:
"""Get transaction hash for execute transaction."""
return calculate_transaction_hash_common(
tx_hash_prefix=TransactionHashPrefix.INVOKE,
version=version,
contract_address=contract_address,
entry_point_selector=0,
calldata=calldata,
max_fee=max_fee,
chain_id=chain_id.value,
additional_data=[nonce],
)


def _get_signature(message_hash: int, private_key: int) -> Tuple[str, str]:
"""Get signature from message hash and private key."""
sig_r, sig_s = sign(message_hash, private_key)
return [str(sig_r), str(sig_s)]


async def set_balance(state: StarknetState, address: int, balance: int):
"""Modify `state` so that `address` has `balance`"""

fee_token_address = state.general_config.fee_token_address

balance_address = pedersen_hash(get_selector_from_name("ERC20_balances"), address)
balance_uint256 = Uint256.from_felt(balance)

await state.state.set_storage_at(
fee_token_address, balance_address, balance_uint256.low
)
await state.state.set_storage_at(
fee_token_address, balance_address + 1, balance_uint256.high
)
25 changes: 25 additions & 0 deletions starknet_devnet/chargeable_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Account that is charged with a fee when nobody else can be charged.
"""

from starknet_devnet.account import Account


class ChargeableAccount(Account):
"""
A well-funded account that can be charged with a fee when no other account can.
E.g. for signing mint txs. Can also be useful in tests.
"""

PRIVATE_KEY = 0x5FB2959E3011A873A7160F5BB32B0ECE
PUBLIC_KEY = 0x4C37AB4F0994879337BFD4EAD0800776DB57DA382B8ED8EFAA478C5D3B942A4
ADDRESS = 0x1CAF2DF5ED5DDE1AE3FAEF4ACD72522AC3CB16E23F6DC4C7F9FAED67124C511

def __init__(self, starknet_wrapper):
super().__init__(
starknet_wrapper,
private_key=ChargeableAccount.PRIVATE_KEY,
public_key=ChargeableAccount.PUBLIC_KEY,
initial_balance=2**251, # loads of cash
account_class_wrapper=starknet_wrapper.config.account_class,
)
54 changes: 42 additions & 12 deletions starknet_devnet/fee_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from starkware.starknet.testing.contract import StarknetContract
from starkware.starknet.testing.starknet import Starknet

from starknet_devnet.account_util import get_execute_args
from starknet_devnet.chargeable_account import ChargeableAccount
from starknet_devnet.constants import SUPPORTED_TX_VERSION
from starknet_devnet.sequencer_api_utils import InternalInvokeFunction
from starknet_devnet.util import Uint256, str_to_felt

Expand All @@ -21,7 +24,7 @@ class FeeToken:

# Precalculated
# HASH = to_bytes(compute_class_hash(contract_class=FeeToken.get_contract_class()))
HASH = 3000409729603134799471314790024123407246450023546294072844903167350593031855
HASH = 0x6A22BF63C7BC07EFFA39A25DFBD21523D211DB0100A0AFD054D172B81840EAF
HASH_BYTES = to_bytes(HASH)

# Taken from
Expand Down Expand Up @@ -75,6 +78,11 @@ async def deploy(self):
get_selector_from_name("ERC20_decimals"),
18,
)
await starknet.state.state.set_storage_at(
FeeToken.ADDRESS,
get_selector_from_name("Ownable_owner"),
ChargeableAccount.ADDRESS,
)

self.contract = StarknetContract(
state=starknet.state,
Expand All @@ -92,18 +100,40 @@ async def get_balance(self, address: int) -> int:
).to_felt()
return balance

@classmethod
def get_mint_transaction(cls, to_address: int, amount: Uint256):
async def get_mint_transaction(self, fundable_address: int, amount: Uint256):
"""Construct a transaction object representing minting request"""

starknet: Starknet = self.starknet_wrapper.starknet
calldata = [
str(fundable_address),
str(amount.low),
str(amount.high),
]

version = SUPPORTED_TX_VERSION
max_fee = int(1e18) # big enough

# we need a funded account for this since the tx has to be signed and a fee will be charged
# a user-intedded predeployed account cannot be used for this
nonce = await starknet.state.state.get_nonce_at(ChargeableAccount.ADDRESS)
chargeable_address = hex(ChargeableAccount.ADDRESS)
signature, execute_calldata = get_execute_args(
calls=[(hex(FeeToken.ADDRESS), "mint", calldata)],
account_address=chargeable_address,
private_key=ChargeableAccount.PRIVATE_KEY,
nonce=nonce,
version=version,
max_fee=max_fee,
chain_id=starknet.state.general_config.chain_id,
)

transaction_data = {
"entry_point_selector": hex(get_selector_from_name("mint")),
"calldata": [
str(to_address),
str(amount.low),
str(amount.high),
],
"signature": [],
"contract_address": hex(cls.ADDRESS),
"calldata": [str(v) for v in execute_calldata],
"contract_address": chargeable_address,
"nonce": hex(nonce),
"max_fee": hex(max_fee),
"signature": signature,
"version": hex(version),
}
return InvokeFunction.load(transaction_data)

Expand All @@ -115,7 +145,7 @@ async def mint(self, to_address: int, amount: int, lite: bool):
amount_uint256 = Uint256.from_felt(amount)

tx_hash = None
transaction = self.get_mint_transaction(to_address, amount_uint256)
transaction = await self.get_mint_transaction(to_address, amount_uint256)
starknet: Starknet = self.starknet_wrapper.starknet
if lite:
internal_tx = InternalInvokeFunction.from_external(
Expand Down
10 changes: 10 additions & 0 deletions starknet_devnet/starknet_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from .block_info_generator import BlockInfoGenerator
from .blocks import DevnetBlocks
from .blueprints.rpc.structures.types import Felt
from .chargeable_account import ChargeableAccount
from .constants import DUMMY_STATE_ROOT, OZ_ACCOUNT_CLASS_HASH
from .devnet_config import DevnetConfig
from .fee_token import FeeToken
Expand All @@ -74,6 +75,7 @@
get_fee_estimation_info,
get_storage_diffs,
to_bytes,
warn,
)

enable_pickling()
Expand Down Expand Up @@ -124,6 +126,7 @@ async def initialize(self):

await self.fee_token.deploy()
await self.accounts.deploy()
await self.__deploy_chargeable_account()
await self.__predeclare_oz_account()
await self.__udc.deploy()

Expand Down Expand Up @@ -697,10 +700,17 @@ async def get_nonce(self, contract_address: int):
return await self.get_state().state.get_nonce_at(contract_address)

async def __predeclare_oz_account(self):
"""Predeclares the account class used by Starknet CLI"""
await self.get_state().state.set_contract_class(
to_bytes(OZ_ACCOUNT_CLASS_HASH), oz_account_class
)

async def __deploy_chargeable_account(self):
if await self.is_deployed(ChargeableAccount.ADDRESS):
warn("Chargeable account already deployed")
else:
await ChargeableAccount(self).deploy()

async def is_deployed(self, address: int) -> bool:
"""
Check if the contract is deployed.
Expand Down
Loading

0 comments on commit b6947da

Please sign in to comment.