Skip to content

Commit aea18cc

Browse files
authored
feat(SDK): refactor folder structure; add Purse/Accessory classes (#17)
1 parent 743a83d commit aea18cc

17 files changed

Lines changed: 769 additions & 189 deletions

.github/workflows/publish.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [released]
6+
7+
jobs:
8+
deploy:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
id-token: write
12+
steps:
13+
- uses: actions/checkout@v5
14+
- uses: astral-sh/setup-uv@v7
15+
16+
- name: Build
17+
- run: uv venv && uv sync --group build && uv build
18+
19+
- name: Publish
20+
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/test.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ jobs:
1313
functional:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v2
17-
- uses: ApeWorX/github-action@v3
18-
- run: ape compile --size
16+
- uses: actions/checkout@v5
17+
- uses: astral-sh/setup-uv@v7
18+
- run: uv venv && uv sync --group test
19+
- run: uv run ape compile --size
1920
- uses: foundry-rs/foundry-toolchain@v1
20-
- run: ape test -s --gas --coverage
21+
- run: uv run --group test ape test -s --gas --coverage

purse/__init__.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

purse/package.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

pyproject.toml

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,39 @@ requires = ["setuptools", "setuptools-scm"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
6-
name = "purse"
7-
dynamic = ["version"]
8-
description = "Purse: A Simple(r) Smart Wallet"
6+
name = "purse-py"
7+
dynamic = [ "version" ]
8+
description = "Purse: Personalize your Wallet"
99
readme = "README.md"
10-
requires-python = ">=3.9"
10+
requires-python = ">=3.11"
1111
dependencies = [
12-
"eth-ape>=0.8.32",
13-
"snekmate>=0.0.1",
12+
"createx>=0.1.1",
13+
"eth-ape>=0.8.43",
14+
"snekmate>=0.1.2",
15+
]
16+
17+
[dependency-groups]
18+
build = [
19+
"ape-vyper>=0.8.10",
20+
]
21+
test = [
22+
"ape-foundry>=0.8.10",
23+
{include-group = "build"},
24+
]
25+
dev = [
26+
"ape-etherscan>=0.8.5",
27+
{include-group = "test"},
1428
]
1529

1630
[[project.authors]]
1731
name = "ApeWorX LTD"
1832

19-
[tool.setuptools]
20-
packages = ["purse"]
33+
[project.scripts]
34+
purse = "purse.__main__:cli"
35+
36+
[tool.setuptools.packages.find]
37+
where = [ "sdk/py" ]
38+
include = [ "purse" ]
2139

2240
[tool.setuptools_scm]
2341
# NOTE: Required for `setuptools-scm` to function

scripts/deploy.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

sdk/py/purse/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .accessory import Accessory
2+
from .main import Purse
3+
4+
__all__ = [
5+
Accessory.__name__,
6+
Purse.__name__,
7+
]

sdk/py/purse/__main__.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from typing import TYPE_CHECKING
2+
from ape.api.address import Address
3+
import click
4+
5+
from ape.cli import (
6+
ApeCliContextObject,
7+
ConnectedProviderCommand,
8+
account_option,
9+
ape_cli_context,
10+
)
11+
from ape.contracts import ContractContainer
12+
from ape.types import AddressType
13+
from createx import CreateX
14+
from eth_utils.crypto import keccak
15+
from purse import Purse, Accessory
16+
17+
from .package import ACCESSORIES, DEPLOYMENTS, MANIFEST
18+
19+
if TYPE_CHECKING:
20+
from ape.api import AccountAPI
21+
22+
23+
@click.group()
24+
def cli():
25+
"""Commands for managing a Purse-enabled wallet"""
26+
27+
28+
@cli.command(cls=ConnectedProviderCommand)
29+
@ape_cli_context()
30+
@click.argument("address")
31+
def check(cli_ctx: ApeCliContextObject, address: str):
32+
"""Check if ADDRESS has Purse delegate enabled, then check version of accessories."""
33+
34+
if address in cli_ctx.account_manager.aliases:
35+
account = cli_ctx.account_manager.load(address)
36+
37+
else:
38+
account = Address(cli_ctx.conversion_manager.convert(address, AddressType))
39+
40+
if not (delegate := account.delegate):
41+
click.secho("No delegate detected", fg="yellow")
42+
return 1
43+
44+
elif not (singleton := DEPLOYMENTS.get(keccak(delegate.code).hex())):
45+
click.secho("Account is not delegated to Purse", fg="red")
46+
return 1
47+
48+
elif singleton != (latest := list(DEPLOYMENTS.values())[-1]):
49+
click.secho(
50+
f"Not using the latest version of Purse, please upgrade to {latest}",
51+
fg="yellow",
52+
)
53+
54+
else:
55+
click.secho("Delegated to latest version of Purse!", fg="green")
56+
57+
purse = Purse(account)
58+
59+
if not (accessory_deployments := ACCESSORIES.get(singleton, {})):
60+
click.secho(f"No known accessories for version at {singleton}", fg="yellow")
61+
return 1
62+
63+
for accessory_name, accessory_addresses in accessory_deployments.items():
64+
for address in accessory_addresses:
65+
if purse.has_accessory(accessory := Accessory(address)):
66+
if address == (latest := accessory_addresses[-1]):
67+
click.secho(
68+
f"Account has latest accessory '{accessory_name}' for Purse version",
69+
fg="green",
70+
)
71+
else:
72+
click.secho(
73+
f"Account has an older accessory '{accessory_name}'"
74+
f" and should be upgraded to {latest}",
75+
fg="yellow",
76+
)
77+
78+
if not all(
79+
purse.contract.accessoryByMethodId(method.method)
80+
== accessory.address
81+
for method in accessory.methods
82+
):
83+
click.secho(
84+
"Account has not installed all neccessary methods for accessory!",
85+
fg="red",
86+
)
87+
88+
break
89+
90+
else:
91+
click.secho(
92+
f"Account doesn't have accessory '{accessory_name}'", fg="green"
93+
)
94+
95+
96+
@cli.command(cls=ConnectedProviderCommand)
97+
@ape_cli_context()
98+
@account_option()
99+
@click.argument("accessories", nargs=-1)
100+
def enable(cli_ctx, account: "AccountAPI", accessories: list[str]):
101+
"""Enable Purse w/ 1 or more Accessories added"""
102+
103+
singleton = cli_ctx.chain_manager.contracts.instance_at(
104+
list(DEPLOYMENTS.values())[-1],
105+
contract_type=MANIFEST.Purse,
106+
)
107+
valid_choices = ACCESSORIES.get(singleton.address, {})
108+
accessories: list[Accessory] = [
109+
Accessory(valid_choices.get(name, [])[-1]) for name in accessories
110+
]
111+
112+
# TODO: Why doesn't `KeyfileAccount.sign_authorization` display warning?
113+
accessories_str = "\n- " + "\n- ".join(a.address for a in accessories)
114+
if click.confirm(f"Enable {singleton} with accessories:{accessories_str}\n\n"):
115+
Purse.initialize(account, *accessories, singleton=singleton)
116+
117+
118+
@cli.command(cls=ConnectedProviderCommand)
119+
@account_option()
120+
def disable(account: "AccountAPI"):
121+
"""Remove Purse from your account"""
122+
123+
purse = Purse(account)
124+
purse.disable()
125+
126+
127+
@cli.group()
128+
def sudo():
129+
"""Manage System Contracts"""
130+
131+
132+
@sudo.group()
133+
def deploy():
134+
"""Deploy the Purse system contracts and accessories using CreateX"""
135+
136+
137+
@deploy.command(cls=ConnectedProviderCommand)
138+
@account_option()
139+
def singleton(account):
140+
"""Deploy the Purse singleton contract using CreateX"""
141+
142+
try:
143+
createx = CreateX()
144+
except RuntimeError:
145+
createx = CreateX.inject()
146+
147+
deployment = createx.deploy(
148+
ContractContainer(MANIFEST.Purse),
149+
redeploy_protection=False,
150+
sender_protection=False,
151+
sender=account,
152+
salt="Purse",
153+
)
154+
click.secho(f"Purse singleton deployed to {deployment}", fg="green")
155+
156+
157+
@deploy.command(cls=ConnectedProviderCommand)
158+
@account_option()
159+
@click.argument("accessory")
160+
def accessory(account, accessory):
161+
"""Deploy a Purse accessory from this project"""
162+
163+
if not (Accessory := ContractContainer(MANIFEST.get_contract_type(accessory))):
164+
raise click.UsageError(f"'{accessory}' is not a valid accessory.")
165+
166+
try:
167+
createx = CreateX()
168+
except RuntimeError:
169+
createx = CreateX.inject()
170+
171+
deployment = createx.deploy(
172+
Accessory,
173+
redeploy_protection=False,
174+
sender_protection=False,
175+
sender=account,
176+
salt=f"Purse {accessory}",
177+
)
178+
click.secho(f"Accessory '{accessory}' deployed to {deployment}", fg="green")
179+
180+
181+
if __name__ == "__main__":
182+
cli()

0 commit comments

Comments
 (0)