|
| 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)) |
0 commit comments