Skip to content

Commit 142fc18

Browse files
authored
Feature: New SVMAccount (#213)
* Feature: new SVMAccount * Feature: Add `Eclipse` chain with `SVMAccount` in `chain_account_map`to handle load_account * UnitTest: new unitest for SVM chains
1 parent 0c848b5 commit 142fc18

File tree

3 files changed

+214
-0
lines changed

3 files changed

+214
-0
lines changed

src/aleph/sdk/account.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from aleph.sdk.chains.remote import RemoteAccount
1212
from aleph.sdk.chains.solana import SOLAccount
1313
from aleph.sdk.chains.substrate import DOTAccount
14+
from aleph.sdk.chains.svm import SVMAccount
1415
from aleph.sdk.conf import load_main_configuration, settings
1516
from aleph.sdk.evm_utils import get_chains_with_super_token
1617
from aleph.sdk.types import AccountFromPrivateKey
@@ -39,6 +40,7 @@
3940
Chain.SONIC: EVMAccount,
4041
Chain.WORLDCHAIN: EVMAccount,
4142
Chain.ZORA: EVMAccount,
43+
Chain.ECLIPSE: SVMAccount,
4244
}
4345

4446

src/aleph/sdk/chains/svm.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Optional
2+
3+
from aleph_message.models import Chain
4+
5+
from .solana import SOLAccount
6+
7+
8+
class SVMAccount(SOLAccount):
9+
def __init__(self, private_key: bytes, chain: Optional[Chain] = None):
10+
super().__init__(private_key=private_key)
11+
# Same as EVM ACCOUNT need to decided if we want to send the specified chain or always use SOL
12+
if chain:
13+
self.CHAIN = chain

tests/unit/test_chain_svm.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import json
2+
from dataclasses import asdict, dataclass
3+
from pathlib import Path
4+
from tempfile import NamedTemporaryFile
5+
6+
import base58
7+
import pytest
8+
from aleph_message.models import Chain
9+
from nacl.signing import VerifyKey
10+
11+
from aleph.sdk.chains.common import get_verification_buffer
12+
from aleph.sdk.chains.solana import get_fallback_account as get_solana_account
13+
from aleph.sdk.chains.solana import verify_signature
14+
from aleph.sdk.chains.svm import SVMAccount
15+
from aleph.sdk.exceptions import BadSignatureError
16+
17+
18+
@dataclass
19+
class Message:
20+
chain: str
21+
sender: str
22+
type: str
23+
item_hash: str
24+
25+
26+
@pytest.fixture
27+
def svm_account() -> SVMAccount:
28+
with NamedTemporaryFile(delete=False) as private_key_file:
29+
private_key_file.close()
30+
solana_account = get_solana_account(path=Path(private_key_file.name))
31+
return SVMAccount(private_key=solana_account.private_key)
32+
33+
34+
@pytest.fixture
35+
def svm_eclipse_account() -> SVMAccount:
36+
with NamedTemporaryFile(delete=False) as private_key_file:
37+
private_key_file.close()
38+
solana_account = get_solana_account(path=Path(private_key_file.name))
39+
return SVMAccount(private_key=solana_account.private_key, chain=Chain.ECLIPSE)
40+
41+
42+
def test_svm_account_init():
43+
with NamedTemporaryFile() as private_key_file:
44+
solana_account = get_solana_account(path=Path(private_key_file.name))
45+
account = SVMAccount(private_key=solana_account.private_key)
46+
47+
# Default chain should be SOL
48+
assert account.CHAIN == Chain.SOL
49+
assert account.CURVE == "curve25519"
50+
assert account._signing_key.verify_key
51+
assert isinstance(account.private_key, bytes)
52+
assert len(account.private_key) == 32
53+
54+
# Test with custom chain
55+
account_eclipse = SVMAccount(
56+
private_key=solana_account.private_key, chain=Chain.ECLIPSE
57+
)
58+
assert account_eclipse.CHAIN == Chain.ECLIPSE
59+
60+
61+
@pytest.mark.asyncio
62+
async def test_svm_sign_message(svm_account):
63+
message = asdict(Message("ES", svm_account.get_address(), "SomeType", "ItemHash"))
64+
initial_message = message.copy()
65+
await svm_account.sign_message(message)
66+
assert message["signature"]
67+
68+
address = message["sender"]
69+
assert address
70+
assert isinstance(address, str)
71+
signature = json.loads(message["signature"])
72+
73+
pubkey = base58.b58decode(signature["publicKey"])
74+
assert isinstance(pubkey, bytes)
75+
assert len(pubkey) == 32
76+
77+
verify_key = VerifyKey(pubkey)
78+
verification_buffer = get_verification_buffer(message)
79+
assert get_verification_buffer(initial_message) == verification_buffer
80+
verif = verify_key.verify(
81+
verification_buffer, signature=base58.b58decode(signature["signature"])
82+
)
83+
84+
assert verif == verification_buffer
85+
assert message["sender"] == signature["publicKey"]
86+
87+
pubkey = svm_account.get_public_key()
88+
assert isinstance(pubkey, str)
89+
assert len(pubkey) == 64
90+
91+
92+
@pytest.mark.asyncio
93+
async def test_svm_custom_chain_sign_message(svm_eclipse_account):
94+
message = asdict(
95+
Message(
96+
Chain.ECLIPSE, svm_eclipse_account.get_address(), "SomeType", "ItemHash"
97+
)
98+
)
99+
await svm_eclipse_account.sign_message(message)
100+
assert message["signature"]
101+
102+
# Verify message has correct chain
103+
assert message["chain"] == Chain.ECLIPSE
104+
105+
# Rest of verification is the same
106+
signature = json.loads(message["signature"])
107+
pubkey = base58.b58decode(signature["publicKey"])
108+
verify_key = VerifyKey(pubkey)
109+
verification_buffer = get_verification_buffer(message)
110+
verif = verify_key.verify(
111+
verification_buffer, signature=base58.b58decode(signature["signature"])
112+
)
113+
assert verif == verification_buffer
114+
115+
116+
@pytest.mark.asyncio
117+
async def test_svm_decrypt(svm_account):
118+
assert svm_account.CURVE == "curve25519"
119+
content = b"SomeContent"
120+
121+
encrypted = await svm_account.encrypt(content)
122+
assert isinstance(encrypted, bytes)
123+
decrypted = await svm_account.decrypt(encrypted)
124+
assert isinstance(decrypted, bytes)
125+
assert content == decrypted
126+
127+
128+
@pytest.mark.asyncio
129+
async def test_svm_verify_signature(svm_account):
130+
message = asdict(
131+
Message(
132+
"SVM",
133+
svm_account.get_address(),
134+
"POST",
135+
"SomeHash",
136+
)
137+
)
138+
await svm_account.sign_message(message)
139+
assert message["signature"]
140+
raw_signature = json.loads(message["signature"])["signature"]
141+
assert isinstance(raw_signature, str)
142+
143+
verify_signature(raw_signature, message["sender"], get_verification_buffer(message))
144+
145+
# as bytes
146+
verify_signature(
147+
base58.b58decode(raw_signature),
148+
base58.b58decode(message["sender"]),
149+
get_verification_buffer(message).decode("utf-8"),
150+
)
151+
152+
153+
@pytest.mark.asyncio
154+
async def test_verify_signature_with_forged_signature(svm_account):
155+
message = asdict(
156+
Message(
157+
"SVM",
158+
svm_account.get_address(),
159+
"POST",
160+
"SomeHash",
161+
)
162+
)
163+
await svm_account.sign_message(message)
164+
assert message["signature"]
165+
# create forged 64 bit signature from random bytes
166+
forged = base58.b58encode(bytes(64)).decode("utf-8")
167+
168+
with pytest.raises(BadSignatureError):
169+
verify_signature(forged, message["sender"], get_verification_buffer(message))
170+
171+
172+
@pytest.mark.asyncio
173+
async def test_svm_sign_raw(svm_account):
174+
buffer = b"SomeBuffer"
175+
signature = await svm_account.sign_raw(buffer)
176+
assert signature
177+
assert isinstance(signature, bytes)
178+
179+
verify_signature(signature, svm_account.get_address(), buffer)
180+
181+
182+
def test_svm_with_various_chain_values():
183+
# Test with different chain formats
184+
with NamedTemporaryFile() as private_key_file:
185+
solana_account = get_solana_account(path=Path(private_key_file.name))
186+
187+
# Test with string
188+
account1 = SVMAccount(private_key=solana_account.private_key, chain="ES")
189+
assert account1.CHAIN == Chain.ECLIPSE
190+
191+
# Test with Chain enum if it exists
192+
account2 = SVMAccount(
193+
private_key=solana_account.private_key, chain=Chain.ECLIPSE
194+
)
195+
assert account2.CHAIN == Chain.ECLIPSE
196+
197+
# Test default
198+
account3 = SVMAccount(private_key=solana_account.private_key)
199+
assert account3.CHAIN == Chain.SOL

0 commit comments

Comments
 (0)