Skip to content

Commit 608b0fc

Browse files
committed
Feature: Could not creating Superfluid flows from sdk
Solution: Install and import superfluid.py from PyPI. Add helper methods on EthAccount
1 parent 04622be commit 608b0fc

File tree

5 files changed

+359
-3
lines changed

5 files changed

+359
-3
lines changed

pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ dependencies = [
3131
"jwcrypto==1.5.6",
3232
"python-magic",
3333
"typing_extensions",
34-
"aioresponses>=0.7.6"
34+
"aioresponses>=0.7.6",
35+
"superfluid~=0.2.1",
36+
"eth_typing==4.3.1",
37+
3538
]
3639

3740
[project.optional-dependencies]

src/aleph/sdk/chains/ethereum.py

+95-2
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,77 @@
1+
from decimal import Decimal
12
from pathlib import Path
2-
from typing import Optional, Union
3+
from typing import Awaitable, Dict, Optional, Set, Union
34

5+
from aleph_message.models import Chain
46
from eth_account import Account
57
from eth_account.messages import encode_defunct
68
from eth_account.signers.local import LocalAccount
79
from eth_keys.exceptions import BadSignature as EthBadSignatureError
10+
from superfluid import Web3FlowInfo
811

12+
from ..conf import settings
13+
from ..connectors.superfluid import Superfluid
914
from ..exceptions import BadSignatureError
1015
from ..utils import bytes_from_hex
1116
from .common import BaseAccount, get_fallback_private_key, get_public_key
1217

18+
CHAINS_WITH_SUPERTOKEN: Set[Chain] = {Chain.AVAX}
19+
CHAIN_IDS: Dict[Chain, int] = {
20+
Chain.AVAX: settings.AVAX_CHAIN_ID,
21+
}
22+
23+
24+
def get_rpc_for_chain(chain: Chain):
25+
"""Returns the RPC to use for a given Ethereum based blockchain"""
26+
if not chain:
27+
return None
28+
29+
if chain == Chain.AVAX:
30+
return settings.AVAX_RPC
31+
else:
32+
raise ValueError(f"Unknown RPC for chain {chain}")
33+
34+
35+
def get_chain_id_for_chain(chain: Chain):
36+
"""Returns the chain ID of a given Ethereum based blockchain"""
37+
if not chain:
38+
return None
39+
40+
if chain in CHAIN_IDS:
41+
return CHAIN_IDS[chain]
42+
else:
43+
raise ValueError(f"Unknown RPC for chain {chain}")
44+
1345

1446
class ETHAccount(BaseAccount):
47+
"""Interact with an Ethereum address or key pair"""
1548
CHAIN = "ETH"
1649
CURVE = "secp256k1"
1750
_account: LocalAccount
51+
chain: Optional[Chain]
52+
superfluid_connector: Optional[Superfluid]
1853

19-
def __init__(self, private_key: bytes):
54+
def __init__(
55+
self,
56+
private_key: bytes,
57+
chain: Optional[Chain] = None,
58+
rpc: Optional[str] = None,
59+
chain_id: Optional[int] = None,
60+
):
2061
self.private_key = private_key
2162
self._account = Account.from_key(self.private_key)
63+
self.chain = chain
64+
rpc = rpc or get_rpc_for_chain(chain)
65+
chain_id = chain_id or get_chain_id_for_chain(chain)
66+
self.superfluid_connector = (
67+
Superfluid(
68+
rpc=rpc,
69+
chain_id=chain_id,
70+
account=self._account,
71+
)
72+
if chain in CHAINS_WITH_SUPERTOKEN
73+
else None
74+
)
2275

2376
async def sign_raw(self, buffer: bytes) -> bytes:
2477
"""Sign a raw buffer."""
@@ -37,6 +90,46 @@ def from_mnemonic(mnemonic: str) -> "ETHAccount":
3790
Account.enable_unaudited_hdwallet_features()
3891
return ETHAccount(private_key=Account.from_mnemonic(mnemonic=mnemonic).key)
3992

93+
def create_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]:
94+
"""Creat a Superfluid flow between this account and the receiver address."""
95+
if not self.superfluid_connector:
96+
raise ValueError("Superfluid connector is required to create a flow")
97+
return self.superfluid_connector.create_flow(
98+
sender=self.get_address(), receiver=receiver, flow=flow
99+
)
100+
101+
def get_flow(self, receiver: str) -> Awaitable[Web3FlowInfo]:
102+
"""Get the Superfluid flow between this account and the receiver address."""
103+
if not self.superfluid_connector:
104+
raise ValueError("Superfluid connector is required to get a flow")
105+
return self.superfluid_connector.get_flow(
106+
sender=self.get_address(), receiver=receiver
107+
)
108+
109+
def update_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]:
110+
"""Update the Superfluid flow between this account and the receiver address."""
111+
if not self.superfluid_connector:
112+
raise ValueError("Superfluid connector is required to update a flow")
113+
return self.superfluid_connector.update_flow(
114+
sender=self.get_address(), receiver=receiver, flow=flow
115+
)
116+
117+
def delete_flow(self, receiver: str) -> Awaitable[str]:
118+
"""Delete the Superfluid flow between this account and the receiver address."""
119+
if not self.superfluid_connector:
120+
raise ValueError("Superfluid connector is required to delete a flow")
121+
return self.superfluid_connector.delete_flow(
122+
sender=self.get_address(), receiver=receiver
123+
)
124+
125+
def update_superfluid_connector(self, rpc: str, chain_id: int):
126+
"""Update the Superfluid connector after initialisation."""
127+
self.superfluid_connector = Superfluid(
128+
rpc=rpc,
129+
chain_id=chain_id,
130+
account=self._account,
131+
)
132+
40133

41134
def get_fallback_account(path: Optional[Path] = None) -> ETHAccount:
42135
return ETHAccount(private_key=get_fallback_private_key(path=path))

src/aleph/sdk/conf.py

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class Settings(BaseSettings):
3838

3939
CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists
4040

41+
AVAX_RPC: str = "https://api.avax.network/ext/bc/C/rpc"
42+
AVAX_CHAIN_ID: int = 43114
43+
AVAX_ALEPH_SUPER_TOKEN = "0xc0Fbc4967259786C743361a5885ef49380473dCF" # mainnet
44+
4145
# Dns resolver
4246
DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh"
4347
DNS_PROGRAM_DOMAIN = "program.public.aleph.sh"
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from decimal import Decimal
5+
from typing import TYPE_CHECKING, Optional
6+
7+
from eth_utils import to_normalized_address, to_wei
8+
from superfluid import CFA_V1, Operation, Web3FlowInfo
9+
from web3 import Web3
10+
from web3.types import TxParams
11+
12+
from aleph.sdk.conf import settings
13+
14+
if TYPE_CHECKING:
15+
from aleph.sdk.chains.ethereum import LocalAccount
16+
17+
18+
async def sign_and_send_transaction(
19+
account: LocalAccount, tx_params: TxParams, rpc: str
20+
) -> str:
21+
"""
22+
Sign and broadcast a transaction using the provided ETHAccount
23+
24+
@param tx_params - Transaction parameters
25+
@param rpc - RPC URL
26+
@returns - str - The transaction hash
27+
"""
28+
web3 = Web3(Web3.HTTPProvider(rpc))
29+
30+
def sign_and_send():
31+
signed_txn = account.sign_transaction(tx_params)
32+
transaction_hash = web3.eth.send_raw_transaction(signed_txn.rawTransaction)
33+
return transaction_hash.hex()
34+
35+
# Sending a transaction is done over HTTP(S) and implemented using a blocking
36+
# API in `web3.eth`. This runs it in a non-blocking asyncio executor.
37+
loop = asyncio.get_running_loop()
38+
transaction_hash = await loop.run_in_executor(None, sign_and_send)
39+
return transaction_hash
40+
41+
42+
async def execute_operation_with_account(
43+
account: LocalAccount, operation: Operation
44+
) -> str:
45+
"""
46+
Execute an operation using the provided ETHAccount
47+
48+
@param operation - Operation instance from the library
49+
@returns - str - The transaction hash
50+
@returns - str - The transaction hash
51+
"""
52+
populated_transaction = operation._get_populated_transaction_request(
53+
operation.rpc, account.key
54+
)
55+
transaction_hash = await sign_and_send_transaction(
56+
account, populated_transaction, operation.rpc
57+
)
58+
return transaction_hash
59+
60+
61+
class Superfluid:
62+
"""
63+
Wrapper around the Superfluid APIs in order to CRUD Superfluid flows between two accounts.
64+
"""
65+
account: Optional[LocalAccount]
66+
67+
def __init__(
68+
self,
69+
rpc=settings.AVAX_RPC,
70+
chain_id=settings.AVAX_CHAIN_ID,
71+
account: Optional[LocalAccount] = None,
72+
):
73+
self.cfaV1Instance = CFA_V1(rpc, chain_id)
74+
self.account = account
75+
76+
async def create_flow(self, sender: str, receiver: str, flow: Decimal) -> str:
77+
"""Create a Superfluid flow between two addresses."""
78+
if not self.account:
79+
raise ValueError("An account is required to create a flow")
80+
return await execute_operation_with_account(
81+
account=self.account,
82+
operation=self.cfaV1Instance.create_flow(
83+
sender=to_normalized_address(sender),
84+
receiver=to_normalized_address(receiver),
85+
super_token=settings.AVAX_ALEPH_SUPER_TOKEN,
86+
flow_rate=to_wei(Decimal(flow), "ether"),
87+
),
88+
)
89+
90+
async def get_flow(self, sender: str, receiver: str) -> Web3FlowInfo:
91+
"""Fetch information about the Superfluid flow between two addresses."""
92+
return self.cfaV1Instance.get_flow(
93+
sender=to_normalized_address(sender),
94+
receiver=to_normalized_address(receiver),
95+
super_token=settings.AVAX_ALEPH_SUPER_TOKEN,
96+
)
97+
98+
async def delete_flow(self, sender: str, receiver: str) -> str:
99+
"""Delete the Supefluid flow between two addresses."""
100+
if not self.account:
101+
raise ValueError("An account is required to delete a flow")
102+
return await execute_operation_with_account(
103+
account=self.account,
104+
operation=self.cfaV1Instance.delete_flow(
105+
sender=to_normalized_address(sender),
106+
receiver=to_normalized_address(receiver),
107+
super_token=settings.AVAX_ALEPH_SUPER_TOKEN,
108+
),
109+
)
110+
111+
async def update_flow(self, sender: str, receiver: str, flow: Decimal) -> str:
112+
"""Update the flow of a Superfluid flow between two addresses."""
113+
if not self.account:
114+
raise ValueError("An account is required to update a flow")
115+
return await execute_operation_with_account(
116+
account=self.account,
117+
operation=self.cfaV1Instance.update_flow(
118+
sender=to_normalized_address(sender),
119+
receiver=to_normalized_address(receiver),
120+
super_token=settings.AVAX_ALEPH_SUPER_TOKEN,
121+
flow_rate=to_wei(Decimal(flow), "ether"),
122+
),
123+
)

0 commit comments

Comments
 (0)