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

Commit

Permalink
Merge pull request #30 from ca11ab1e/feat-declare-tx
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Jun 22, 2022
2 parents d127ec0 + be9814e commit f849c33
Show file tree
Hide file tree
Showing 13 changed files with 504 additions and 136 deletions.
78 changes: 74 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ python3 setup.py install

### Account Management

Deploy a new account:
Accounts are used to execute transactions and sign call data.
Accounts are smart contracts in Starknet.

To deploy a new account:

```bash
ape starknet accounts create <ALIAS> --network starknet:testnet
Expand Down Expand Up @@ -77,19 +80,86 @@ ape starknet accounts delete <ALIAS> --network starknet:testnet

**NOTE**: You don't have to specify the network if your account is only deployed to a single network.

### Contract Interaction
### Declare and Deploy Contracts

In Starknet, you can declare contract types by publishing them to the chain.
This allows other contracts to create instances of them using the [deploy system call](https://www.cairo-lang.org/docs/hello_starknet/deploying_from_contracts.html).

To declare a contract using `ape-starknet`, do the following (in a script or console):

```python
from ape import project, networks

provider = networks.active_provider
declaration = provider.declare(project.MyContract)
print(declaration.class_hash)
```

Then, you can use the class hash in a deploy system call in a factory contract:

```cairo
from starkware.cairo.common.alloc import alloc
from starkware.starknet.common.syscalls import deploy
from starkware.cairo.common.cairo_builtins import HashBuiltin
@external
func deploy_my_contract{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr,
}():
let (current_salt) = salt.read()
let (class_hash) = ownable_class_hash.read()
let (calldata_ptr) = alloc()
let (contract_address) = deploy(
class_hash=class_hash,
contract_address_salt=current_salt,
constructor_calldata_size=0,
constructor_calldata=calldata_ptr,
)
salt.write(value=current_salt + 1)
```

After deploying the factory contract, you can use it to create contract instances:

```python
from ape import Contract, project

First, deploy your contract:
declaration = project.provider.declare(project.MyContract)
factory = project.ContractFactory.deploy(declaration.class_hash)
call_result = factory.deploy_my_contract()
contract_address = project.starknet.decode_address(call_result)
contract = Contract(contract_address, contract_address)
```

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()
```

The ``deploy`` method returns a contract instance from which you can call methods on:
### Contract Interaction

After you have deployed your contracts, you can begin interacting with them.
``deploy`` methods return a contract instance from which you can call methods on:

```python
from ape import project

contract = project.MyContract.deploy()

# Interact with deployed contract
receipt = contract.my_mutable_method(123)
value = contract.my_view_method()
```
Expand Down
12 changes: 4 additions & 8 deletions ape_starknet/accounts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from ape.api import AccountAPI, AccountContainerAPI, ReceiptAPI, TransactionAPI
from ape.api.address import BaseAddress
from ape.api.networks import LOCAL_NETWORK_NAME
from ape.contracts import ContractContainer, ContractInstance
from ape.exceptions import AccountsError, ProviderError, SignatureError
from ape.logging import logger
from ape.types import AddressType, SignableMessage, TransactionSignature
Expand All @@ -31,7 +30,7 @@
from ape_starknet.tokens import TokenManager
from ape_starknet.transactions import InvokeFunctionTransaction
from ape_starknet.utils import get_chain_id
from ape_starknet.utils.basemodel import StarknetMixin
from ape_starknet.utils.basemodel import StarknetBase

APP_KEY_FILE_KEY = "ape-starknet"
"""
Expand All @@ -41,7 +40,7 @@
APP_KEY_FILE_VERSION = "0.1.0"


class StarknetAccountContracts(AccountContainerAPI, StarknetMixin):
class StarknetAccountContracts(AccountContainerAPI, StarknetBase):

ephemeral_accounts: Dict[str, Dict] = {}
"""Local-network accounts that do not persist."""
Expand Down Expand Up @@ -211,7 +210,7 @@ def deploy_account(
key_pair = KeyPair.from_private_key(private_key)

contract_address = self.provider._deploy(
COMPILED_ACCOUNT_CONTRACT, key_pair.public_key, token=token
key_pair.public_key, contract_data=COMPILED_ACCOUNT_CONTRACT, token=token
)
self.import_account(alias, network_name, contract_address, key_pair.private_key)
return contract_address
Expand All @@ -233,7 +232,7 @@ class StarknetAccountDeployment:
contract_address: AddressType


class BaseStarknetAccount(AccountAPI, StarknetMixin):
class BaseStarknetAccount(AccountAPI, StarknetBase):
token_manager: TokenManager = TokenManager()

@abstractmethod
Expand Down Expand Up @@ -391,9 +390,6 @@ def transfer(

return self.token_manager.transfer(self.contract_address, receiver, value, **kwargs)

def deploy(self, contract: ContractContainer, *args, **kwargs) -> ContractInstance:
return contract.deploy(sender=self)

def get_deployment(self, network_name: str) -> Optional[StarknetAccountDeployment]:
return next(
(
Expand Down
100 changes: 47 additions & 53 deletions ape_starknet/ecosystems.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Any, Dict, Iterator, List, Tuple, Type, Union

from ape.api import BlockAPI, EcosystemAPI, ReceiptAPI, TransactionAPI
from ape.contracts import ContractContainer
from ape.types import AddressType, ContractLog, RawAddress
from eth_utils import is_0x_prefixed
from ethpm_types import ContractType
from ethpm_types.abi import ConstructorABI, EventABI, MethodABI
from hexbytes import HexBytes
from starknet_py.net.models.address import parse_address # type: ignore
Expand All @@ -16,9 +18,12 @@

from ape_starknet.exceptions import StarknetEcosystemError
from ape_starknet.transactions import (
ContractDeclaration,
DeclareTransaction,
DeployReceipt,
DeployTransaction,
InvocationReceipt,
InvokeFunctionTransaction,
StarknetReceipt,
StarknetTransaction,
)
from ape_starknet.utils import to_checksum_address
Expand Down Expand Up @@ -71,13 +76,19 @@ def serialize_transaction(self, transaction: TransactionAPI) -> bytes:
return starknet_object.deserialize()

def decode_returndata(self, abi: MethodABI, raw_data: List[int]) -> List[Any]: # type: ignore
raw_data = [self.encode_primitive_value(v) if isinstance(v, str) else v for v in raw_data]

def clear_lengths(arr):
arr_len = arr[0]
rest = arr[1:]
num_rest = len(rest)
return clear_lengths(rest) if arr_len == num_rest else arr

is_arr = abi.outputs[0].name == "arr_len" and abi.outputs[1].type == "felt*"
is_arr = (
len(abi.outputs) >= 2
and abi.outputs[0].name == "arr_len"
and abi.outputs[1].type == "felt*"
)
has_leftover_length = len(raw_data) > 1 and not is_arr
if (
len(abi.outputs) == 2
Expand Down Expand Up @@ -176,46 +187,23 @@ def encode_primitive_value(self, value: Any) -> int:
return value

def decode_receipt(self, data: dict) -> ReceiptAPI:
txn_type = data["type"]

if txn_type == TransactionType.INVOKE_FUNCTION.value:
data["receiver"] = data.pop("contract_address")

max_fee = data.get("max_fee", 0) or 0
if isinstance(max_fee, str):
max_fee = int(max_fee, 16)

receiver = data.get("receiver")
if receiver:
receiver = self.decode_address(receiver)

# 'contract_address' is for deploy-txns and refers to the new contract.
contract_address = data.get("contract_address")
if contract_address:
contract_address = self.decode_address(contract_address)

block_hash = data.get("block_hash")
if block_hash:
block_hash = HexBytes(block_hash).hex()

return StarknetReceipt(
provider=data.get("provider"),
type=data["type"],
transaction_hash=HexBytes(data["transaction_hash"]).hex(),
status=data["status"].value,
block_number=data["block_number"],
block_hash=block_hash,
events=data.get("events", []),
receiver=receiver,
contract_address=contract_address,
actual_fee=data.get("actual_fee", 0),
max_fee=max_fee,
)
txn_type = TransactionType(data["type"])
cls: Union[Type[ContractDeclaration], Type[DeployReceipt], Type[InvocationReceipt]]
if txn_type == TransactionType.INVOKE_FUNCTION:
cls = InvocationReceipt
elif txn_type == TransactionType.DEPLOY:
cls = DeployReceipt
elif txn_type == TransactionType.DECLARE:
cls = ContractDeclaration
else:
raise ValueError(f"Unable to handle contract type '{txn_type.value}'.")

return cls.parse_obj(data)

def decode_block(self, data: dict) -> BlockAPI:
return StarknetBlock(
number=data["block_number"],
hash=HexBytes(data["block_hash"]),
number=data["block_number"],
parentHash=HexBytes(data["parent_block_hash"]),
size=len(data["transactions"]), # TODO: Figure out size
timestamp=data["timestamp"],
Expand Down Expand Up @@ -244,6 +232,7 @@ def encode_transaction(
# NOTE: This method only works for invoke-transactions
contract_type = self.chain_manager.contracts[address]
encoded_calldata = self.encode_calldata(contract_type.abi, abi, list(args))

return InvokeFunctionTransaction(
contract_address=address,
method_abi=abi,
Expand All @@ -252,14 +241,32 @@ def encode_transaction(
max_fee=kwargs.get("max_fee", 0),
)

def encode_contract_declaration(
self, contract: Union[ContractContainer, ContractType], *args, **kwargs
) -> DeclareTransaction:
contract_type = (
contract.contract_type if isinstance(contract, ContractContainer) else contract
)
code = (
(contract_type.deployment_bytecode.bytecode or 0)
if contract_type.deployment_bytecode
else 0
)
starknet_contract = ContractClass.deserialize(HexBytes(code))
return DeclareTransaction(contract_type=contract_type, data=starknet_contract.dumps())

def create_transaction(self, **kwargs) -> TransactionAPI:
txn_type = kwargs.pop("type", kwargs.pop("tx_type", ""))
txn_cls: Union[Type[InvokeFunctionTransaction], Type[DeployTransaction]]
txn_type = TransactionType(kwargs.pop("type", kwargs.pop("tx_type", "")))
txn_cls: Union[
Type[InvokeFunctionTransaction], Type[DeployTransaction], Type[DeclareTransaction]
]
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

txn_data: Dict[str, Any] = {**kwargs, "signature": None}
if "chain_id" not in txn_data and self.network_manager.active_provider:
Expand All @@ -274,19 +281,6 @@ def create_transaction(self, **kwargs) -> TransactionAPI:

""" ~ Invoke transactions ~ """

if "receiver" in txn_data:
# Model expects 'contract_address' key during serialization.
# NOTE: Deploy transactions have a different 'contract_address' and that is handled
# above before getting to the 'Invoke transactions' section.
txn_data["contract_address"] = self.decode_address(txn_data["receiver"])

if (
"max_fee" in txn_data
and not isinstance(txn_data["max_fee"], int)
and txn_data["max_fee"] is not None
):
txn_data["max_fee"] = self.encode_primitive_value(txn_data["max_fee"])

if "method_abi" not in txn_data:
contract_int = txn_data["contract_address"]
contract_str = self.decode_address(contract_int)
Expand Down
4 changes: 2 additions & 2 deletions ape_starknet/explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from ape.types import AddressType
from ethpm_types import ContractType

from ape_starknet.utils.basemodel import StarknetMixin
from ape_starknet.utils.basemodel import StarknetBase


class StarknetExplorer(ExplorerAPI, StarknetMixin):
class StarknetExplorer(ExplorerAPI, StarknetBase):
BASE_URIS = {
"testnet": "https://goerli.voyager.online",
"mainnet": "https://voyager.online",
Expand Down
Loading

0 comments on commit f849c33

Please sign in to comment.