Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Commit

Permalink
feat: replace DeployTransaction with invoke transactions sent to th…
Browse files Browse the repository at this point in the history
…e UDC contract (#118)
  • Loading branch information
antazoey authored Nov 26, 2022
1 parent 92dbe83 commit 5e6c184
Show file tree
Hide file tree
Showing 24 changed files with 371 additions and 485 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,17 @@ jobs:
functional:
runs-on: ${{ matrix.os }}

concurrency:
group: ${{ github.ref }}

strategy:
matrix:
os: [ubuntu-latest, macos-latest] # eventually add `windows-latest`
python-version: [3.8, 3.9]

env:
GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}

steps:
- uses: actions/checkout@v2

Expand All @@ -70,5 +76,4 @@ jobs:
- name: Run Tests
run: |
echo "DEVNET_PORT=8545" >> $GITHUB_ENV
pytest -m "not fuzzing" -n 0 -s --cov
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ repos:
- id: black
name: black

- repo: https://gitlab.com/pycqa/flake8
- repo: https://github.com/pycqa/flake8
rev: 5.0.4
hooks:
- id: flake8

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.982
rev: v0.991
hooks:
- id: mypy
additional_dependencies: [types-PyYAML, types-requests]
additional_dependencies: [types-PyYAML, types-requests, types-setuptools]


default_language_version:
Expand Down
36 changes: 17 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,21 @@ declaration = account.declare(project.MyContract)
print(declaration.class_hash)
```

Then, you can use the class hash in a deploy system call in a factory contract:
Then, you can use the `deploy` method to deploy the contracts.
**NOTE**: The `deploy` method in `ape-starknet` makes an invoke-function call against the Starknet public UDC contract.
Learn more about UDC contracts [here](https://community.starknet.io/t/universal-deployer-contract-proposal/1864).

```python
from ape import accounts, project

# This only works if `project.MyContract` was declared previously.
# The class hash is not necessary as an argument. Ape will look it up.
account = accounts.load("<MY_STARK_ACCOUNT>")
account.deploy(project.MyContact)
```

Alternatively, you can use the class hash in a `deploy()` system call in a local factory contract.
Let's say for example I have the following Cairo factory contract:

```cairo
from starkware.cairo.common.alloc import alloc
Expand All @@ -171,7 +185,8 @@ func deploy_my_contract{
salt.write(value=current_salt + 1)
```

After deploying the factory contract, you can use it to create contract instances:
This contract accepts a class hash of a declared contract deploys it.
The following example shows how to use this factory class to deploy other contracts:

```python
from ape import Contract, accounts, networks, project
Expand All @@ -187,23 +202,6 @@ contract_address = networks.starknet.decode_address(call_result)
contract = Contract(contract_address, contract_type=project.MyContract.contract_type)
```

You can also `deploy()` from the declaration receipt (which uses the legacy deploy transaction):

```python
from ape import accounts, project

declaration = project.provider.declare(project.MyContract)
receipt = declaration.deploy(1, 2, sender=accounts.load("MyAccount"))
```

Otherwise, you can use the legacy deploy system which works the same as Ethereum in ape except no sender is needed:

```python
from ape import project

contract = project.MyContract.deploy()
```

### Contract Interaction

After you have deployed your contracts, you can begin interacting with them.
Expand Down
9 changes: 4 additions & 5 deletions ape_starknet/accounts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ def alias(self) -> Optional[str]:
return self.key_file_path.stem

@cached_property
def class_hash(self) -> int: # type: ignore
def class_hash(self) -> int:
return self.get_account_data().get("class_hash") or OPEN_ZEPPELIN_ACCOUNT_CLASS_HASH

@property
Expand Down Expand Up @@ -721,11 +721,10 @@ def get_deployments(self) -> List[StarknetAccountDeployment]:
return [StarknetAccountDeployment(**d) for d in plugin_key_file_data.get("deployments", [])]

def get_deployment(self, network_name: str) -> Optional[StarknetAccountDeployment]:
# NOTE: d is not None check only because mypy is confused
return next(
(
deployment
for deployment in self.get_deployments()
if deployment.network_name in network_name
filter(
lambda d: d is not None and d.network_name in network_name, self.get_deployments()
),
None,
)
Expand Down
71 changes: 36 additions & 35 deletions ape_starknet/ecosystems.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@
from ethpm_types.abi import ConstructorABI, EventABI, EventABIType, MethodABI
from hexbytes import HexBytes
from pydantic import Field, validator
from starknet_py.constants import OZ_PROXY_STORAGE_KEY
from starknet_py.net.client_models import StarknetBlock as StarknetClientBlock
from starknet_py.net.models.address import parse_address
from starknet_py.net.models.chains import StarknetChainId
from starknet_py.utils.data_transformer.execute_transformer import FunctionCallSerializer
from starkware.starknet.definitions.fields import ContractAddressSalt
from starkware.starknet.core.os.class_hash import compute_class_hash
from starkware.starknet.definitions.transaction_type import TransactionType
from starkware.starknet.public.abi import get_selector_from_name
from starkware.starknet.public.abi import get_selector_from_name, get_storage_var_address
from starkware.starknet.public.abi_structs import identifier_manager_from_abi
from starkware.starknet.services.api.contract_class import ContractClass

Expand All @@ -32,8 +31,6 @@
DeclareTransaction,
DeployAccountReceipt,
DeployAccountTransaction,
DeployReceipt,
DeployTransaction,
InvokeFunctionReceipt,
InvokeFunctionTransaction,
StarknetReceipt,
Expand All @@ -47,6 +44,7 @@
"mainnet": (StarknetChainId.MAINNET.value, StarknetChainId.MAINNET.value),
"testnet": (StarknetChainId.TESTNET.value, StarknetChainId.TESTNET.value),
}
OZ_PROXY_STORAGE_KEY = get_storage_var_address("Proxy_implementation_hash")


class ProxyType(Enum):
Expand Down Expand Up @@ -160,7 +158,7 @@ def encode_calldata(
pre_encoded_arg = self._pre_encode_value(call_arg)

if isinstance(pre_encoded_arg, int):
# 'arr_len' was provided.
# A '_len' arg was provided.
array_index = index + 1
pre_encoded_array = self._pre_encode_array(call_args[array_index])
pre_encoded_args.append(pre_encoded_array)
Expand Down Expand Up @@ -204,7 +202,11 @@ def _pre_encode_struct(self, struct: Dict) -> Dict:
return encoded_struct

def encode_primitive_value(self, value: Any) -> int:
if isinstance(value, int):
if isinstance(value, bool):
# NOTE: bool must come before int.
return int(value)

elif isinstance(value, int):
return value

elif isinstance(value, str) and is_0x_prefixed(value):
Expand All @@ -220,8 +222,6 @@ def decode_receipt(self, data: dict) -> ReceiptAPI:
receipt_cls: Type[StarknetReceipt]
if txn_type == TransactionType.INVOKE_FUNCTION:
receipt_cls = InvokeFunctionReceipt
elif txn_type == TransactionType.DEPLOY:
receipt_cls = DeployReceipt
elif txn_type == TransactionType.DECLARE:
receipt_cls = ContractDeclaration
elif txn_type == TransactionType.DEPLOY_ACCOUNT:
Expand All @@ -247,33 +247,27 @@ def decode_block(self, block: StarknetClientBlock) -> BlockAPI:
def encode_deployment(
self, deployment_bytecode: HexBytes, abi: ConstructorABI, *args, **kwargs
) -> TransactionAPI:
salt = kwargs.get("salt")
if not salt:
salt = ContractAddressSalt.get_random_value()

constructor_args = list(args)
contract = ContractClass.deserialize(deployment_bytecode)
calldata = self.encode_calldata(contract.abi, abi, constructor_args)
return DeployTransaction(
salt=salt,
constructor_calldata=calldata,
contract_code=deployment_bytecode,
token=kwargs.get("token"),
)
contract_class = ContractClass.deserialize(deployment_bytecode)
class_hash = compute_class_hash(contract_class)
contract_type = abi.contract_type
if not contract_type:
raise StarknetEcosystemError(
"Unable to encode deployment - missing full contract type for constructor."
)

constructor_arguments = self.encode_calldata(contract_type.abi, abi, args)
return self.universal_deployer.create_deploy(class_hash, constructor_arguments, **kwargs)

def encode_transaction(
self, address: AddressType, abi: MethodABI, *args, **kwargs
) -> TransactionAPI:
# NOTE: This method only works for invoke-transactions
contract_type = self.starknet_explorer.get_contract_type(address)
contract_type = abi.contract_type or self.starknet_explorer.get_contract_type(address)
if not contract_type:
raise ContractTypeNotFoundError(address)

encoded_calldata = self.encode_calldata(contract_type.abi, abi, list(args))

if "sender" not in kwargs and abi.is_stateful:
raise StarknetEcosystemError("'sender=<account>' required for invoke transactions")

arguments = list(args)
encoded_calldata = self.encode_calldata(contract_type.abi, abi, arguments)
return InvokeFunctionTransaction(
receiver=address,
method_abi=abi,
Expand Down Expand Up @@ -304,8 +298,6 @@ def create_transaction(self, **kwargs) -> TransactionAPI:
invoking = txn_type == TransactionType.INVOKE_FUNCTION
if invoking:
txn_cls = InvokeFunctionTransaction
elif txn_type == TransactionType.DEPLOY:
txn_cls = DeployTransaction
elif txn_type == TransactionType.DECLARE:
txn_cls = DeclareTransaction
elif txn_type == TransactionType.DEPLOY_ACCOUNT:
Expand Down Expand Up @@ -378,18 +370,27 @@ def decode_items(
for abi_type in abi_types:
if abi_type.type == "Uint256":
# Uint256 are stored using 2 slots
decoded.append(from_uint(next(iter_data), next(iter_data)))
next_item_1 = next(iter_data, None)
next_item_2 = next(iter_data, None)
if next_item_1 is not None and next_item_2 is not None:
decoded.append(from_uint(next_item_1, next_item_2))
else:
decoded.append(next(iter_data))
next_item = next(iter_data, None)
if next_item:
decoded.append(next_item)

return decoded

for index, (selector, logs) in enumerate(log_map.items()):
abi = events_by_selector[selector]
if not logs:
continue

for log in logs:
event_args = dict(
zip([a.name for a in abi.inputs], decode_items(abi.inputs, log["data"]))
)
yield ContractLog( # type: ignore
yield ContractLog(
block_hash=log["block_hash"],
block_number=log["block_number"],
contract_address=self.decode_address(log["from_address"]),
Expand All @@ -413,12 +414,12 @@ def _get_proxy_info(
instance = self.chain_manager.contracts.instance_at(address, contract_type=contract_type)
# Legacy proxy check
if "implementation" in contract_type.view_methods:
target = instance.implementation() # type: ignore
target = instance.implementation()
proxy_type = ProxyType.LEGACY

# Argent-X proxy check
elif "get_implementation" in contract_type.view_methods:
target = instance.get_implementation() # type: ignore
target = instance.get_implementation()
proxy_type = ProxyType.ARGENT_X

# OpenZeppelin proxy check
Expand Down
8 changes: 5 additions & 3 deletions ape_starknet/explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ethpm_types import ContractType, HexBytes

from ape_starknet.accounts import BaseStarknetAccount
from ape_starknet.utils import pad_hex_str
from ape_starknet.utils import EXECUTE_METHOD_NAME, pad_hex_str
from ape_starknet.utils.basemodel import StarknetBase


Expand Down Expand Up @@ -52,13 +52,15 @@ def get_contract_type(self, address: AddressType) -> Optional[ContractType]:
"deploymentBytecode": {"bytecode": f"0x{''.join(code_parts)}"},
}

if "__execute__" in [a["name"] for a in code_and_abi.abi]:
if EXECUTE_METHOD_NAME in [a["name"] for a in code_and_abi.abi]:
contract_type_dict["contractName"] = "Account"

return ContractType.parse_obj(contract_type_dict)

@raises_not_implemented
def get_account_transactions(self, address: AddressType) -> Iterator[ReceiptAPI]:
def get_account_transactions( # type: ignore[empty-body]
self, address: AddressType
) -> Iterator[ReceiptAPI]:
# TODO
pass

Expand Down
Loading

0 comments on commit 5e6c184

Please sign in to comment.