Skip to content

Commit fcb3730

Browse files
1yamhoh
authored andcommitted
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 fcb3730

File tree

5 files changed

+361
-3
lines changed

5 files changed

+361
-3
lines changed

pyproject.toml

Lines changed: 4 additions & 1 deletion
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

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,78 @@
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"""
48+
1549
CHAIN = "ETH"
1650
CURVE = "secp256k1"
1751
_account: LocalAccount
52+
chain: Optional[Chain]
53+
superfluid_connector: Optional[Superfluid]
1854

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

2377
async def sign_raw(self, buffer: bytes) -> bytes:
2478
"""Sign a raw buffer."""
@@ -37,6 +91,46 @@ def from_mnemonic(mnemonic: str) -> "ETHAccount":
3791
Account.enable_unaudited_hdwallet_features()
3892
return ETHAccount(private_key=Account.from_mnemonic(mnemonic=mnemonic).key)
3993

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

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

src/aleph/sdk/conf.py

Lines changed: 4 additions & 0 deletions
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"
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
66+
account: Optional[LocalAccount]
67+
68+
def __init__(
69+
self,
70+
rpc=settings.AVAX_RPC,
71+
chain_id=settings.AVAX_CHAIN_ID,
72+
account: Optional[LocalAccount] = None,
73+
):
74+
self.cfaV1Instance = CFA_V1(rpc, chain_id)
75+
self.account = account
76+
77+
async def create_flow(self, sender: str, receiver: str, flow: Decimal) -> str:
78+
"""Create a Superfluid flow between two addresses."""
79+
if not self.account:
80+
raise ValueError("An account is required to create a flow")
81+
return await execute_operation_with_account(
82+
account=self.account,
83+
operation=self.cfaV1Instance.create_flow(
84+
sender=to_normalized_address(sender),
85+
receiver=to_normalized_address(receiver),
86+
super_token=settings.AVAX_ALEPH_SUPER_TOKEN,
87+
flow_rate=to_wei(Decimal(flow), "ether"),
88+
),
89+
)
90+
91+
async def get_flow(self, sender: str, receiver: str) -> Web3FlowInfo:
92+
"""Fetch information about the Superfluid flow between two addresses."""
93+
return self.cfaV1Instance.get_flow(
94+
sender=to_normalized_address(sender),
95+
receiver=to_normalized_address(receiver),
96+
super_token=settings.AVAX_ALEPH_SUPER_TOKEN,
97+
)
98+
99+
async def delete_flow(self, sender: str, receiver: str) -> str:
100+
"""Delete the Supefluid flow between two addresses."""
101+
if not self.account:
102+
raise ValueError("An account is required to delete a flow")
103+
return await execute_operation_with_account(
104+
account=self.account,
105+
operation=self.cfaV1Instance.delete_flow(
106+
sender=to_normalized_address(sender),
107+
receiver=to_normalized_address(receiver),
108+
super_token=settings.AVAX_ALEPH_SUPER_TOKEN,
109+
),
110+
)
111+
112+
async def update_flow(self, sender: str, receiver: str, flow: Decimal) -> str:
113+
"""Update the flow of a Superfluid flow between two addresses."""
114+
if not self.account:
115+
raise ValueError("An account is required to update a flow")
116+
return await execute_operation_with_account(
117+
account=self.account,
118+
operation=self.cfaV1Instance.update_flow(
119+
sender=to_normalized_address(sender),
120+
receiver=to_normalized_address(receiver),
121+
super_token=settings.AVAX_ALEPH_SUPER_TOKEN,
122+
flow_rate=to_wei(Decimal(flow), "ether"),
123+
),
124+
)

0 commit comments

Comments
 (0)