Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support SNIP-9 #1530

Merged
merged 54 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e25729f
Big things:
baitcode Dec 2, 2024
40b01ed
- Added outside execution model, defined hashing
baitcode Dec 2, 2024
9137b40
* lint
baitcode Dec 5, 2024
ac7b2cc
ok. so I am not allowed to change ci configs
baitcode Dec 8, 2024
6a9fdc9
Merge branch 'development' into feat-snip-9
baitcode Dec 10, 2024
dcb044b
fixing tests
baitcode Dec 10, 2024
0487bd6
lint
baitcode Dec 10, 2024
a79ab75
switched back to increase balance
baitcode Dec 10, 2024
aceded7
tiny revert
baitcode Dec 10, 2024
5851f12
comment documentation test to check if that is what affects test acco…
baitcode Dec 10, 2024
e54e510
Revert "comment documentation test to check if that is what affects t…
baitcode Dec 10, 2024
7ea8760
change for another call in documentation. removed account deployment.
baitcode Dec 10, 2024
14531b1
fix
baitcode Dec 10, 2024
37f24df
fix doctest
baitcode Dec 10, 2024
fb9829e
more fixes
baitcode Dec 10, 2024
4f72882
remove balance change calls
baitcode Dec 10, 2024
1881d84
Fixed naming
baitcode Dec 10, 2024
d369165
fix documentation
baitcode Dec 10, 2024
2c2be9f
added secrets
baitcode Dec 10, 2024
f7867c1
trigger build
baitcode Dec 11, 2024
799b399
Merge branch 'development' into feat-snip-9
baitcode Dec 13, 2024
a6b7774
Review comments fixws
baitcode Dec 14, 2024
92d651c
linter
baitcode Dec 14, 2024
585e8df
fixup
baitcode Dec 14, 2024
9b12024
Added comment
baitcode Dec 14, 2024
81da8fe
fix
baitcode Dec 14, 2024
2bb0282
lost empty line
baitcode Dec 15, 2024
812429d
comments fixes
baitcode Dec 15, 2024
6e2b7f4
Update starknet_py/constants.py
baitcode Dec 15, 2024
44faa3b
comment fixupi
baitcode Dec 15, 2024
37b6ce4
fix wordings
baitcode Dec 15, 2024
6cacd8b
fix
baitcode Dec 15, 2024
11069b4
fixes
baitcode Dec 15, 2024
45644f9
more fixes
baitcode Dec 15, 2024
51f0f7f
more fixes
baitcode Dec 15, 2024
a449d8c
rename
baitcode Dec 15, 2024
a4e75c4
Fix
baitcode Dec 15, 2024
e3a3bed
linter
baitcode Dec 15, 2024
0ac1845
Update starknet_py/tests/e2e/docs/guide/test_account_sign_outside_tra…
baitcode Dec 15, 2024
8d98dbd
Update starknet_py/tests/e2e/docs/guide/test_account_sign_outside_tra…
baitcode Dec 15, 2024
dfdc252
Update starknet_py/net/account/base_account.py
baitcode Dec 15, 2024
c97df0f
a bit more review comment fixes
baitcode Dec 16, 2024
86e8f84
revert execute_v1
baitcode Dec 16, 2024
4b0b0a2
remove auto fee estimation
baitcode Dec 16, 2024
ee9a064
Update docs/guide/account_and_client.rst
baitcode Dec 16, 2024
9776344
Update starknet_py/tests/e2e/docs/guide/test_account_sign_outside_tra…
baitcode Dec 16, 2024
13d8fe0
Merge branch 'development' into feat-snip-9
baitcode Dec 16, 2024
ceaa8ca
import fix
baitcode Dec 16, 2024
a9aa417
fix doctest
baitcode Dec 16, 2024
df4079d
Update starknet_py/tests/e2e/account/outside_execution_test.py
baitcode Dec 16, 2024
817d679
Update starknet_py/tests/e2e/account/outside_execution_test.py
baitcode Dec 16, 2024
b931c5c
changelog
baitcode Dec 16, 2024
e5e76e4
Update docs/migration_guide.rst
baitcode Dec 16, 2024
7434a25
fixing fixes
baitcode Dec 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/guide/account_and_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ Account also provides a way of creating signed transaction without sending them.
:language: python
:dedent: 4

Creating "Outside transaction" and executing it. `SNIP-9 <https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md>`_
---------------------------------------------------------------------------------------------------------------------------

Account also provides a way of creating a call and signing to allow for another account (caller) to execute it later on original account behalf. This will also allow caller to execute calls encoded in that transaction for free (signer will pay the fee).
baitcode marked this conversation as resolved.
Show resolved Hide resolved

.. codesnippet:: ../../starknet_py/tests/e2e/docs/guide/test_account_sign_outside_transaction.py
:language: python
:dedent: 4

Multicall
---------

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ test = [
]

test_ci = ["test_ci_v1", "test_ci_v2"]

franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
test_ci_v1 = "coverage run -a -m pytest --contract_dir=v1 starknet_py --ignore=starknet_py/tests/e2e/docs --ignore=starknet_py/tests/e2e/tests_on_networks"
test_ci_v2 = "coverage run -a -m pytest --contract_dir=v2 starknet_py --ignore=starknet_py/tests/e2e/docs --ignore=starknet_py/tests/e2e/tests_on_networks"

Expand Down
10 changes: 10 additions & 0 deletions starknet_py/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from enum import IntEnum
from pathlib import Path

# Address came from starkware-libs/starknet-addresses repository: https://github.com/starkware-libs/starknet-addresses
Expand Down Expand Up @@ -45,3 +46,12 @@
PUBLIC_KEY_RESPONSE_LENGTH = 65
SIGNATURE_RESPONSE_LENGTH = 65
VERSION_RESPONSE_LENGTH = 3

# SNIP-9 ANY_CALLER
franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
ANY_CALLER = 0x414E595F43414C4C4552


# SNIP-9 INTERFACE_VERSION with ID
class SNIP9InterfaceVersion(IntEnum):
baitcode marked this conversation as resolved.
Show resolved Hide resolved
V1 = 0x68CFD18B92D1907B8BA3CC324900277F5A3622099431EA85DD8089255E4181
V2 = 0x1D1144BB2138366FF28D8E9AB57456B1D332AC42196230C3A602003C89872
118 changes: 118 additions & 0 deletions starknet_py/hash/outside_execution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from starknet_py.constants import SNIP9InterfaceVersion
from starknet_py.net.client_models import OutsideExecution
from starknet_py.net.schemas.common import Revision
from starknet_py.utils import typed_data as td
baitcode marked this conversation as resolved.
Show resolved Hide resolved

SNIP9_INTERFACE_ID_TO_SNIP12_REVISION = {
baitcode marked this conversation as resolved.
Show resolved Hide resolved
SNIP9InterfaceVersion.V1: Revision.V0,
SNIP9InterfaceVersion.V2: Revision.V1,
}


def outside_execution_to_typed_data(
franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
outside_execution: OutsideExecution,
snip9_version: SNIP9InterfaceVersion,
chain_id: int,
) -> td.TypedData:
"""
SNIP-12 Typed Data for OutsideExecution implementation. For revision V0 and V1.
"""

revision = SNIP9_INTERFACE_ID_TO_SNIP12_REVISION[snip9_version]

if revision == Revision.V0:
return td.TypedData.from_dict(
{
"types": {
"StarkNetDomain": [
{"name": "name", "type": "felt"},
{"name": "version", "type": "felt"},
{"name": "chainId", "type": "felt"},
],
"OutsideExecution": [
{"name": "caller", "type": "felt"},
{"name": "nonce", "type": "felt"},
{"name": "execute_after", "type": "felt"},
{"name": "execute_before", "type": "felt"},
{"name": "calls_len", "type": "felt"},
{"name": "calls", "type": "OutsideCall*"},
],
"OutsideCall": [
{"name": "to", "type": "felt"},
{"name": "selector", "type": "felt"},
{"name": "calldata_len", "type": "felt"},
{"name": "calldata", "type": "felt*"},
],
},
"primaryType": "OutsideExecution",
"domain": {
"name": "Account.execute_from_outside",
"version": "1",
"chainId": str(chain_id),
"revision": Revision.V0,
},
"message": {
"caller": outside_execution.caller,
"nonce": outside_execution.nonce,
"execute_after": outside_execution.execute_after,
"execute_before": outside_execution.execute_before,
"calls_len": len(outside_execution.calls),
"calls": [
{
"to": call.to_addr,
"selector": call.selector,
"calldata_len": len(call.calldata),
"calldata": call.calldata,
}
for call in outside_execution.calls
],
},
}
)

# revision == Revision.V1
return td.TypedData.from_dict(
{
"types": {
"StarknetDomain": [
{"name": "name", "type": "shortstring"},
{"name": "version", "type": "shortstring"},
{"name": "chainId", "type": "shortstring"},
{"name": "revision", "type": "shortstring"},
],
"OutsideExecution": [
{"name": "Caller", "type": "ContractAddress"},
{"name": "Nonce", "type": "felt"},
{"name": "Execute After", "type": "u128"},
{"name": "Execute Before", "type": "u128"},
{"name": "Calls", "type": "Call*"},
],
"Call": [
{"name": "To", "type": "ContractAddress"},
{"name": "Selector", "type": "selector"},
{"name": "Calldata", "type": "felt*"},
],
},
"primaryType": "OutsideExecution",
"domain": {
"name": "Account.execute_from_outside",
"version": "2",
"chainId": str(chain_id),
"revision": Revision.V1,
},
"message": {
"Caller": outside_execution.caller,
"Nonce": outside_execution.nonce,
"Execute After": outside_execution.execute_after,
"Execute Before": outside_execution.execute_before,
"Calls": [
{
"To": call.to_addr,
"Selector": call.selector,
"Calldata": call.calldata,
}
for call in outside_execution.calls
],
},
}
)
131 changes: 120 additions & 11 deletions starknet_py/net/account/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union

from starknet_py.common import create_compiled_contract, create_sierra_compiled_contract
from starknet_py.constants import FEE_CONTRACT_ADDRESS, QUERY_VERSION_BASE
from starknet_py.constants import (
ANY_CALLER,
FEE_CONTRACT_ADDRESS,
QUERY_VERSION_BASE,
SNIP9InterfaceVersion,
)
from starknet_py.hash.address import compute_address
from starknet_py.hash.outside_execution import outside_execution_to_typed_data
from starknet_py.hash.selector import get_selector_from_name
from starknet_py.hash.utils import verify_message_signature
from starknet_py.net.account.account_deployment_result import AccountDeploymentResult
from starknet_py.net.account.base_account import BaseAccount
from starknet_py.net.account.base_account import BaseAccount, SNIP9SupportBaseMixin
from starknet_py.net.client import Client
from starknet_py.net.client_models import (
Call,
Calls,
EstimatedFee,
ExecutionTimeBounds,
Hash,
OutsideExecution,
ResourceBounds,
ResourceBoundsMapping,
SentTransactionResponse,
Expand All @@ -39,21 +47,21 @@
from starknet_py.net.models.typed_data import TypedDataDict
from starknet_py.net.signer import BaseSigner
from starknet_py.net.signer.stark_curve_signer import KeyPair, StarkCurveSigner
from starknet_py.serialization.data_serializers.array_serializer import ArraySerializer
from starknet_py.serialization.data_serializers.felt_serializer import FeltSerializer
from starknet_py.serialization.data_serializers.payload_serializer import (
from starknet_py.serialization.data_serializers import (
ArraySerializer,
FeltSerializer,
PayloadSerializer,
)
from starknet_py.serialization.data_serializers.struct_serializer import (
StructSerializer,
UintSerializer,
)
from starknet_py.utils.iterable import ensure_iterable
from starknet_py.utils.sync import add_sync_methods
from starknet_py.utils.typed_data import TypedData


# pylint: disable=too-many-public-methods
@add_sync_methods
class Account(BaseAccount):
class Account(BaseAccount, SNIP9SupportBaseMixin):
"""
Default Account implementation.
"""
Expand Down Expand Up @@ -213,9 +221,7 @@ async def _prepare_invoke(
nonce=nonce,
sender_address=self.address,
)

max_fee = await self._get_max_fee(transaction, max_fee, auto_estimate)

franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
return _add_max_fee_to_transaction(transaction, max_fee)

async def _prepare_invoke_v3(
Expand Down Expand Up @@ -290,6 +296,48 @@ async def get_nonce(
self.address, block_hash=block_hash, block_number=block_number
)

async def _check_snip9_nonce(
self,
nonce: int,
*,
block_hash: Optional[Union[Hash, Tag]] = None,
block_number: Optional[Union[int, Tag]] = None,
) -> bool:
(is_valid,) = await self._client.call_contract(
call=Call(
to_addr=self.address,
selector=get_selector_from_name("is_valid_outside_execution_nonce"),
calldata=[nonce],
),
block_hash=block_hash,
block_number=block_number,
)
return bool(is_valid)

async def get_snip9_nonce(self, retry_count=10) -> int:
while retry_count > 0:
random_stark_address = KeyPair.generate().public_key
if await self._check_snip9_nonce(random_stark_address):
return random_stark_address
retry_count -= 1
raise RuntimeError("Failed to generate a valid nonce")

async def _get_snip9_version(self) -> Union[SNIP9InterfaceVersion, None]:
for version in [SNIP9InterfaceVersion.V1, SNIP9InterfaceVersion.V2]:
if await self.supports_interface(version):
return version
return None

async def supports_interface(self, interface_id: SNIP9InterfaceVersion) -> bool:
(does_support,) = await self._client.call_contract(
Call(
to_addr=self.address,
selector=get_selector_from_name("supports_interface"),
calldata=[interface_id],
)
)
return bool(does_support)

async def get_balance(
self,
token_address: Optional[AddressRepresentation] = None,
Expand Down Expand Up @@ -344,6 +392,54 @@ async def sign_invoke_v1(
signature = self.signer.sign_transaction(execute_tx)
return _add_signature_to_transaction(execute_tx, signature)

async def sign_outside_execution_call(
franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
self,
calls: Calls,
execution_time_bounds: ExecutionTimeBounds,
*,
caller: AddressRepresentation = ANY_CALLER,
nonce: Optional[int] = None,
version: Optional[SNIP9InterfaceVersion] = None,
franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
) -> Call:
if version is None:
version = await self._get_snip9_version()

if version is None:
raise RuntimeError(
"Can't initiate outside execution SNIP-9 is unsupported."
)

if nonce is None:
nonce = await self.get_snip9_nonce()

outside_execution = OutsideExecution(
caller=parse_address(caller),
nonce=nonce,
execute_after=execution_time_bounds.execute_after_timestamp,
execute_before=execution_time_bounds.execute_before_timestamp,
calls=list(ensure_iterable(calls)),
)
chain_id = await self._get_chain_id()
signature = self.signer.sign_message(
outside_execution_to_typed_data(outside_execution, version, chain_id),
self.address,
)
selector_for_version = {
SNIP9InterfaceVersion.V1: "execute_from_outside",
SNIP9InterfaceVersion.V2: "execute_from_outside_v2",
}

return Call(
to_addr=self.address,
selector=get_selector_from_name(selector_for_version[version]),
calldata=_transaction_serialiser.serialize(
{
"external_execution": outside_execution.to_abi_dict(),
baitcode marked this conversation as resolved.
Show resolved Hide resolved
"signature": signature,
}
),
)

async def sign_invoke_v3(
self,
calls: Calls,
Expand Down Expand Up @@ -877,7 +973,6 @@ def _parse_calls_cairo_v1(calls: Iterable[Call]) -> List[Dict]:
calldata=ArraySerializer(_felt_serializer),
)
)

baitcode marked this conversation as resolved.
Show resolved Hide resolved
_execute_payload_serializer_v0 = PayloadSerializer(
OrderedDict(
call_array=ArraySerializer(_call_description_cairo_v0),
Expand All @@ -889,3 +984,17 @@ def _parse_calls_cairo_v1(calls: Iterable[Call]) -> List[Dict]:
calls=ArraySerializer(_call_description_cairo_v1),
)
)
_transaction_serialiser = StructSerializer(
baitcode marked this conversation as resolved.
Show resolved Hide resolved
OrderedDict(
external_execution=StructSerializer(
baitcode marked this conversation as resolved.
Show resolved Hide resolved
OrderedDict(
caller=FeltSerializer(),
nonce=FeltSerializer(),
execute_after=UintSerializer(bits=64),
execute_before=UintSerializer(bits=64),
calls=ArraySerializer(_call_description_cairo_v1),
)
),
signature=ArraySerializer(FeltSerializer()),
)
)
Loading
Loading