Skip to content

Commit 186d9f5

Browse files
authored
feat: add support for cross-chain account creation (#35)
* add bones of XChainAccountCreate * actually submit min create account to ledger * fix restart bug * clean up stop script * more verboseness in tutorial * get it working * update to witness sending txs * change account reserve to 5 XRP * fix tutorial.sh * automatically calculate create_account_amount from other chain * update scripts
1 parent c5902a4 commit 186d9f5

File tree

7 files changed

+230
-17
lines changed

7 files changed

+230
-17
lines changed

sidechain_cli/bridge/setup.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from xrpl.models import (
99
GenericRequest,
1010
IssuedCurrency,
11+
ServerState,
1112
SignerEntry,
1213
SignerListSet,
1314
XChainCreateBridge,
@@ -114,13 +115,24 @@ def create_bridge(
114115
config["XChainBridge"]["IssuingChainIssue"],
115116
)
116117

118+
chain1 = get_config().get_chain(chains[0])
119+
client1 = chain1.get_client()
120+
chain2 = get_config().get_chain(chains[1])
121+
client2 = chain2.get_client()
122+
server_state1 = client1.request(ServerState())
123+
min_create1 = server_state1.result["state"]["validated_ledger"]["reserve_base"]
124+
server_state2 = client2.request(ServerState())
125+
min_create2 = server_state2.result["state"]["validated_ledger"]["reserve_base"]
126+
print(min_create1, min_create2)
127+
117128
bridge_data: BridgeData = {
118129
"name": name,
119130
"chains": chains,
120131
"witnesses": witnesses,
121132
"door_accounts": doors,
122133
"xchain_currencies": tokens,
123134
"signature_reward": signature_reward,
135+
"create_account_amounts": (str(min_create2), str(min_create1)),
124136
}
125137

126138
if verbose:
@@ -176,13 +188,12 @@ def setup_bridge(bridge: str, bootstrap: str, verbose: int = 0) -> None:
176188
account = client1.request(wallet_propose).result["account_id"]
177189
signer_entries.append(SignerEntry(account=account, signer_weight=1))
178190
bridge_obj = bridge_config.get_bridge()
179-
signature_reward = bridge_config.signature_reward
180191

181192
create_tx1 = XChainCreateBridge(
182193
account=bridge_config.door_accounts[0],
183194
xchain_bridge=bridge_obj,
184-
signature_reward=signature_reward
185-
# TODO: add support for the create account amount
195+
signature_reward=bridge_config.signature_reward,
196+
min_account_create_amount=bridge_config.create_account_amounts[0],
186197
)
187198
submit_tx(create_tx1, client1, bootstrap_config["mainchain_door"]["seed"], verbose)
188199

@@ -198,8 +209,8 @@ def setup_bridge(bridge: str, bootstrap: str, verbose: int = 0) -> None:
198209
create_tx2 = XChainCreateBridge(
199210
account=bridge_config.door_accounts[1],
200211
xchain_bridge=bridge_obj,
201-
signature_reward=signature_reward
202-
# TODO: add support for the create account amount
212+
signature_reward=bridge_config.signature_reward,
213+
min_account_create_amount=bridge_config.create_account_amounts[1],
203214
)
204215
submit_tx(create_tx2, client2, bootstrap_config["sidechain_door"]["seed"], verbose)
205216

sidechain_cli/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import click
44

55
from sidechain_cli.bridge import bridge
6+
from sidechain_cli.misc.create_account import create_xchain_account
67
from sidechain_cli.misc.fund import fund_account
78
from sidechain_cli.server import server
89

@@ -16,6 +17,7 @@ def main() -> None:
1617
main.add_command(server)
1718
main.add_command(bridge)
1819
main.add_command(fund_account)
20+
main.add_command(create_xchain_account)
1921

2022

2123
if __name__ == "__main__":
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Create/fund an account via a cross-chain transfer."""
2+
3+
import time
4+
from pprint import pformat
5+
6+
import click
7+
from xrpl.clients import JsonRpcClient
8+
from xrpl.models import (
9+
AccountInfo,
10+
GenericRequest,
11+
Ledger,
12+
Response,
13+
Transaction,
14+
Tx,
15+
XChainAccountCreateCommit,
16+
)
17+
from xrpl.wallet import Wallet
18+
19+
from sidechain_cli.utils import get_config, submit_tx
20+
21+
_ATTESTATION_TIME_LIMIT = 4 # in seconds
22+
_WAIT_STEP_LENGTH = 0.05
23+
24+
25+
def _submit_tx(
26+
tx: Transaction, client: JsonRpcClient, secret: str, verbose: int
27+
) -> Response:
28+
result = submit_tx(tx, client, secret, verbose)
29+
tx_result = result.result.get("error") or result.result.get("engine_result")
30+
if tx_result != "tesSUCCESS":
31+
raise Exception(
32+
result.result.get("error_message")
33+
or result.result.get("engine_result_message")
34+
)
35+
tx_hash = result.result["tx_json"]["hash"]
36+
return client.request(Tx(transaction=tx_hash))
37+
38+
39+
@click.command(name="create-account")
40+
@click.option(
41+
"--chain",
42+
"from_chain",
43+
required=True,
44+
prompt=True,
45+
type=str,
46+
help="The chain to fund an account from.",
47+
)
48+
@click.option(
49+
"--bridge",
50+
required=True,
51+
prompt=True,
52+
type=str,
53+
help="The bridge across which to create the account.",
54+
)
55+
@click.option(
56+
"--from",
57+
"from_seed",
58+
required=True,
59+
prompt=True,
60+
type=str,
61+
help="The seed of the account that the funds come from.",
62+
)
63+
@click.option(
64+
"--to",
65+
"to_account",
66+
required=True,
67+
prompt=True,
68+
type=str,
69+
help="The account to fund on the opposite chain.",
70+
)
71+
@click.option(
72+
"--verbose", is_flag=True, help="Whether or not to print more verbose information."
73+
)
74+
def create_xchain_account(
75+
from_chain: str, bridge: str, from_seed: str, to_account: str, verbose: bool = False
76+
) -> None:
77+
"""
78+
Create an account on the opposite chain via a cross-chain transfer.
79+
\f
80+
81+
Args:
82+
from_chain: The chain to fund an account from.
83+
bridge: The bridge across which to create the account.
84+
from_seed: The seed of the account that the funds come from.
85+
to_account: The chain to fund an account on.
86+
verbose: Whether or not to print more verbose information.
87+
""" # noqa: D301
88+
# tutorial = True
89+
print_level = 2
90+
91+
bridge_config = get_config().get_bridge(bridge)
92+
from_chain_config = get_config().get_chain(from_chain)
93+
from_client = from_chain_config.get_client()
94+
to_chain = [chain for chain in bridge_config.chains if chain != from_chain][0]
95+
to_chain_config = get_config().get_chain(to_chain)
96+
to_client = to_chain_config.get_client()
97+
create_account_amount = bridge_config.create_account_amounts[
98+
bridge_config.chains.index(from_chain)
99+
]
100+
101+
from_wallet = Wallet(from_seed, 0)
102+
103+
if create_account_amount is None:
104+
click.secho(
105+
"Error: Cannot create a cross-chain account if the create account amount "
106+
"is not set.",
107+
fg="red",
108+
)
109+
return
110+
111+
# submit XChainAccountCreate tx
112+
fund_tx = XChainAccountCreateCommit(
113+
account=from_wallet.classic_address,
114+
xchain_bridge=bridge_config.get_bridge(),
115+
signature_reward=bridge_config.signature_reward,
116+
destination=to_account,
117+
amount=create_account_amount,
118+
)
119+
submit_tx(fund_tx, from_client, from_wallet.seed, print_level)
120+
121+
# wait for attestations
122+
time_count = 0.0
123+
attestation_count = 0
124+
while True:
125+
time.sleep(_WAIT_STEP_LENGTH)
126+
open_ledger = to_client.request(
127+
Ledger(ledger_index="current", transactions=True, expand=True)
128+
)
129+
open_txs = open_ledger.result["ledger"]["transactions"]
130+
for tx in open_txs:
131+
from pprint import pprint
132+
133+
pprint(tx)
134+
if tx["TransactionType"] == "XChainAddAttestation":
135+
batch = tx["XChainAttestationBatch"]
136+
if batch["XChainBridge"] != bridge_config.to_xrpl():
137+
# make sure attestation is for this bridge
138+
continue
139+
attestations = batch["XChainCreateAccountAttestationBatch"]
140+
for attestation in attestations:
141+
element = attestation["XChainCreateAccountAttestationBatchElement"]
142+
# check that the attestation actually matches this transfer
143+
if element["Account"] != from_wallet.classic_address:
144+
continue
145+
if element["Amount"] != create_account_amount:
146+
continue
147+
if element["Destination"] != to_account:
148+
continue
149+
attestation_count += 1
150+
if print_level > 1:
151+
click.echo(pformat(element))
152+
if print_level > 0:
153+
click.secho(
154+
f"Received {attestation_count} attestations",
155+
fg="bright_green",
156+
)
157+
if len(open_txs) > 0:
158+
to_client.request(GenericRequest(method="ledger_accept"))
159+
time_count = 0
160+
else:
161+
time_count += _WAIT_STEP_LENGTH
162+
163+
quorum = max(1, len(bridge_config.witnesses) - 1)
164+
if attestation_count >= quorum:
165+
# received enough attestations for quorum
166+
break
167+
168+
if time_count > _ATTESTATION_TIME_LIMIT:
169+
click.secho("Error: Timeout on attestations.", fg="red")
170+
return
171+
172+
if verbose:
173+
click.echo(pformat(to_client.request(AccountInfo(account=to_account)).result))

sidechain_cli/server/config/templates/rippled.jinja

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ pool.ntp.org
6969
[ssl_verify]
7070
1
7171

72+
[fee_account_reserve]
73+
5000000
74+
75+
[fee_owner_reserve]
76+
1000000
77+
7278
[features]
7379
PayChan
7480
Flow

sidechain_cli/server/start.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
ChainData,
1616
RippledConfig,
1717
ServerConfig,
18+
WitnessConfig,
1819
WitnessData,
1920
add_chain,
2021
add_witness,
@@ -254,13 +255,19 @@ def stop_server(
254255
# to_run = [server.rippled, "--conf", server.config, "stop"]
255256
# subprocess.call(to_run, stdout=fout, stderr=subprocess.STDOUT)
256257
pid = server.pid
257-
os.kill(pid, signal.SIGINT)
258+
try:
259+
os.kill(pid, signal.SIGINT)
260+
except ProcessLookupError:
261+
pass # process already died somehow
258262
else:
259263
# TODO: stop the server with a CLI command
260264
# to_run = [server.witnessd, "--config", server.config, "stop"]
261265
# subprocess.call(to_run, stdout=fout, stderr=subprocess.STDOUT)
262266
pid = server.pid
263-
os.kill(pid, signal.SIGINT)
267+
try:
268+
os.kill(pid, signal.SIGINT)
269+
except ProcessLookupError:
270+
pass # process already died somehow
264271
if verbose:
265272
click.echo(f"Stopped {server.name}")
266273

@@ -298,17 +305,29 @@ def restart_server(
298305

299306
config = get_config()
300307
if restart_all:
301-
chains = config.chains
308+
servers = cast(List[ServerConfig], config.chains) + cast(
309+
List[ServerConfig], config.witnesses
310+
)
302311
else:
303312
assert name is not None
304-
chains = [config.get_chain(name)]
313+
servers = [config.get_server(name)]
305314

306315
ctx.invoke(stop_server, name=name, stop_all=restart_all, verbose=verbose)
307-
for chain in chains:
308-
ctx.invoke(
309-
start_server,
310-
name=chain.name,
311-
rippled=chain.rippled,
312-
config=chain.config,
313-
verbose=verbose,
314-
)
316+
for server in servers:
317+
if isinstance(server, ChainConfig):
318+
ctx.invoke(
319+
start_server,
320+
name=server.name,
321+
exe=server.rippled,
322+
config=server.config,
323+
verbose=verbose,
324+
)
325+
else:
326+
assert isinstance(server, WitnessConfig)
327+
ctx.invoke(
328+
start_server,
329+
name=server.name,
330+
exe=server.witnessd,
331+
config=server.config,
332+
verbose=verbose,
333+
)

sidechain_cli/utils/config_file.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ class BridgeConfig(ConfigItem):
131131
door_accounts: Tuple[str, str]
132132
xchain_currencies: Tuple[Currency, Currency]
133133
signature_reward: str
134+
create_account_amounts: Tuple[str, str]
134135

135136
def get_bridge(self: BridgeConfig) -> XChainBridge:
136137
"""

sidechain_cli/utils/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ class BridgeData(TypedDict):
5050
door_accounts: Tuple[str, str]
5151
xchain_currencies: Tuple[Currency, Currency]
5252
signature_reward: str
53+
create_account_amounts: Tuple[str, str]

0 commit comments

Comments
 (0)