diff --git a/content/_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js b/content/_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js index 3bf139aeec..7f808f3fc6 100644 --- a/content/_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js +++ b/content/_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js @@ -37,7 +37,7 @@ async function main() { // 25 TST * 10 XRP per TST * 15% financial exchange (FX) cost value: xrpl.xrpToDrops(25*10*1.15) } - // "Quality" is defined as TakerPays รท TakerGets. The lower the "quality" + // "Quality" is defined as TakerPays / TakerGets. The lower the "quality" // number, the better the proposed exchange rate is for the taker. // The quality is rounded to a number of significant digits based on the // issuer's TickSize value (or the lesser of the two for token-token trades.) diff --git a/content/_code-samples/trade-in-the-decentralized-exchange/py/README.md b/content/_code-samples/trade-in-the-decentralized-exchange/py/README.md new file mode 100644 index 0000000000..038b492a17 --- /dev/null +++ b/content/_code-samples/trade-in-the-decentralized-exchange/py/README.md @@ -0,0 +1,3 @@ +# Trade in the Decentralized Exchange + +This code demonstrates how to buy a fungible token on the XRP Ledger's decentralized exchange (DEX). For a detailed explanation of how to trade using the DEX, see . diff --git a/content/_code-samples/trade-in-the-decentralized-exchange/py/trade-in-the-dex.py b/content/_code-samples/trade-in-the-decentralized-exchange/py/trade-in-the-dex.py new file mode 100644 index 0000000000..e2fe0e06e2 --- /dev/null +++ b/content/_code-samples/trade-in-the-decentralized-exchange/py/trade-in-the-dex.py @@ -0,0 +1,251 @@ +import asyncio +import pprint +from decimal import Decimal + +from xrpl.asyncio.clients import AsyncWebsocketClient +from xrpl.asyncio.transaction import ( + safe_sign_and_autofill_transaction, + send_reliable_submission, +) +from xrpl.asyncio.wallet import generate_faucet_wallet +from xrpl.models.currencies import ( + IssuedCurrency, + XRP, +) +from xrpl.models.requests import ( + AccountLines, + AccountOffers, + BookOffers, +) +from xrpl.models.transactions import OfferCreate +from xrpl.utils import ( + drops_to_xrp, + get_balance_changes, + xrp_to_drops, +) + + +async def main() -> int: + # Define the network client + async with AsyncWebsocketClient("wss://s.altnet.rippletest.net:51233") as client: + # Get credentials from the Testnet Faucet ----------------------------------- + print("Requesting addresses from the Testnet faucet...") + wallet = await generate_faucet_wallet(client, debug=True) + + # Define the proposed trade. ------------------------------------------------ + # Technically you don't need to specify the amounts (in the "value" field) + # to look up order books using book_offers, but for this tutorial we reuse + # these variables to construct the actual Offer later. + # + # Note that XRP is represented as drops, whereas any other currency is + # represented as a decimal value. + we_want = { + "currency": IssuedCurrency( + currency="TST", + issuer="rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd" + ), + "value": "25", + } + + we_spend = { + "currency": XRP(), + # 25 TST * 10 XRP per TST * 15% financial exchange (FX) cost + "value": xrp_to_drops(25 * 10 * 1.15), + } + + # "Quality" is defined as TakerPays / TakerGets. The lower the "quality" + # number, the better the proposed exchange rate is for the taker. + # The quality is rounded to a number of significant digits based on the + # issuer's TickSize value (or the lesser of the two for token-token trades). + proposed_quality = Decimal(we_spend["value"]) / Decimal(we_want["value"]) + + # Look up Offers. ----------------------------------------------------------- + # To buy TST, look up Offers where "TakerGets" is TST: + print("Requesting orderbook information...") + orderbook_info = await client.request( + BookOffers( + taker=wallet.classic_address, + ledger_index="current", + taker_gets=we_want["currency"], + taker_pays=we_spend["currency"], + limit=10, + ) + ) + print(f"Orderbook:\n{pprint.pformat(orderbook_info.result)}") + + # Estimate whether a proposed Offer would execute immediately, and... + # If so, how much of it? (Partial execution is possible) + # If not, how much liquidity is above it? (How deep in the order book would + # other Offers have to go before ours would get taken?) + # Note: These estimates can be thrown off by rounding if the token issuer + # uses a TickSize setting other than the default (15). In that case, you + # can increase the TakerGets amount of your final Offer to compensate. + + offers = orderbook_info.result.get("offers", []) + want_amt = Decimal(we_want["value"]) + running_total = Decimal(0) + if len(offers) == 0: + print("No Offers in the matching book. Offer probably won't execute immediately.") + else: + for o in offers: + if Decimal(o["quality"]) <= proposed_quality: + print(f"Matching Offer found, funded with {o.get('owner_funds')} " + f"{we_want['currency']}") + running_total += Decimal(o.get("owner_funds", Decimal(0))) + if running_total >= want_amt: + print("Full Offer will probably fill") + break + else: + # Offers are in ascending quality order, so no others after this + # will match either + print("Remaining orders too expensive.") + break + + print(f"Total matched: {min(running_total, want_amt)} {we_want['currency']}") + if 0 < running_total < want_amt: + print(f"Remaining {want_amt - running_total} {we_want['currency']} " + "would probably be placed on top of the order book.") + + if running_total == 0: + # If part of the Offer was expected to cross, then the rest would be placed + # at the top of the order book. If none did, then there might be other + # Offers going the same direction as ours already on the books with an + # equal or better rate. This code counts how much liquidity is likely to be + # above ours. + # + # Unlike above, this time we check for Offers going the same direction as + # ours, so TakerGets and TakerPays are reversed from the previous + # book_offers request. + + print("Requesting second orderbook information...") + orderbook2_info = await client.request( + BookOffers( + taker=wallet.classic_address, + ledger_index="current", + taker_gets=we_spend["currency"], + taker_pays=we_want["currency"], + limit=10, + ) + ) + print(f"Orderbook2:\n{pprint.pformat(orderbook2_info.result)}") + + # Since TakerGets/TakerPays are reversed, the quality is the inverse. + # You could also calculate this as 1 / proposed_quality. + offered_quality = Decimal(we_want["value"]) / Decimal(we_spend["value"]) + + tally_currency = we_spend["currency"] + if isinstance(tally_currency, XRP): + tally_currency = f"drops of {tally_currency}" + + offers2 = orderbook2_info.result.get("offers", []) + running_total2 = Decimal(0) + if len(offers2) == 0: + print("No similar Offers in the book. Ours would be the first.") + else: + for o in offers2: + if Decimal(o["quality"]) <= offered_quality: + print(f"Existing offer found, funded with {o.get('owner_funds')} " + f"{tally_currency}") + running_total2 += Decimal(o.get("owner_funds", Decimal(0))) + else: + print("Remaining orders are below where ours would be placed.") + break + + print(f"Our Offer would be placed below at least {running_total2} " + f"{tally_currency}") + if 0 < running_total2 < want_amt: + print(f"Remaining {want_amt - running_total2} {tally_currency} " + "will probably be placed on top of the order book.") + + # Send OfferCreate transaction ---------------------------------------------- + + # For this tutorial, we already know that TST is pegged to + # XRP at a rate of approximately 10:1 plus spread, so we use + # hard-coded TakerGets and TakerPays amounts. + + tx = OfferCreate( + account=wallet.classic_address, + taker_gets=we_spend["value"], + taker_pays=we_want["currency"].to_amount(we_want["value"]), + ) + + # Sign and autofill the transaction (ready to submit) + signed_tx = await safe_sign_and_autofill_transaction(tx, wallet, client) + print("Transaction:", signed_tx) + + # Submit the transaction and wait for response (validated or rejected) + print("Sending OfferCreate transaction...") + result = await send_reliable_submission(signed_tx, client) + if result.is_successful(): + print(f"Transaction succeeded: " + f"https://testnet.xrpl.org/transactions/{signed_tx.get_hash()}") + else: + raise Exception(f"Error sending transaction: {result}") + + # Check metadata ------------------------------------------------------------ + balance_changes = get_balance_changes(result.result["meta"]) + print(f"Balance Changes:\n{pprint.pformat(balance_changes)}") + + # For educational purposes the transaction metadata is analyzed manually in the + # following section. However, there is also a get_order_book_changes(metadata) + # utility function available in the xrpl library, which is generally the easier + # and preferred choice for parsing the metadata and computing orderbook changes. + + # Helper to convert an XRPL amount to a string for display + def amt_str(amt) -> str: + if isinstance(amt, str): + return f"{drops_to_xrp(amt)} XRP" + else: + return f"{amt['value']} {amt['currency']}.{amt['issuer']}" + + offers_affected = 0 + for affnode in result.result["meta"]["AffectedNodes"]: + if "ModifiedNode" in affnode: + if affnode["ModifiedNode"]["LedgerEntryType"] == "Offer": + # Usually a ModifiedNode of type Offer indicates a previous Offer that + # was partially consumed by this one. + offers_affected += 1 + elif "DeletedNode" in affnode: + if affnode["DeletedNode"]["LedgerEntryType"] == "Offer": + # The removed Offer may have been fully consumed, or it may have been + # found to be expired or unfunded. + offers_affected += 1 + elif "CreatedNode" in affnode: + if affnode["CreatedNode"]["LedgerEntryType"] == "RippleState": + print("Created a trust line.") + elif affnode["CreatedNode"]["LedgerEntryType"] == "Offer": + offer = affnode["CreatedNode"]["NewFields"] + print(f"Created an Offer owned by {offer['Account']} with " + f"TakerGets={amt_str(offer['TakerGets'])} and " + f"TakerPays={amt_str(offer['TakerPays'])}.") + + print(f"Modified or removed {offers_affected} matching Offer(s)") + + # Check balances ------------------------------------------------------------ + print("Getting address balances as of validated ledger...") + balances = await client.request( + AccountLines( + account=wallet.classic_address, + ledger_index="validated", + ) + ) + pprint.pp(balances.result) + + # Check Offers -------------------------------------------------------------- + print(f"Getting outstanding Offers from {wallet.classic_address} " + f"as of validated ledger...") + acct_offers = await client.request( + AccountOffers( + account=wallet.classic_address, + ledger_index="validated", + ) + ) + pprint.pp(acct_offers.result) + + # End main() + return 0 + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/content/tutorials/use-tokens/trade-in-the-decentralized-exchange.md b/content/tutorials/use-tokens/trade-in-the-decentralized-exchange.md index 9e43c04b20..5816f1b719 100644 --- a/content/tutorials/use-tokens/trade-in-the-decentralized-exchange.md +++ b/content/tutorials/use-tokens/trade-in-the-decentralized-exchange.md @@ -14,14 +14,16 @@ This tutorial demonstrates how you can buy and sell tokens in the [decentralized ## Prerequisites +- You need a connection to the XRP Ledger network. As shown in this tutorial, you can use public servers for testing. +- You should be familiar with the Getting Started instructions for your preferred client library. This page provides examples for the following: + - **JavaScript** with the [xrpl.js library](https://github.com/XRPLF/xrpl.js/). See [Get Started Using JavaScript](get-started-using-javascript.html) for setup steps. + - **Python** with the [`xrpl-py` library](https://xrpl-py.readthedocs.io/). See [Get Started using Python](get-started-using-python.html) for setup steps. + - You can also read along and use the interactive steps in your browser without any setup. + -This page provides JavaScript examples that use the [xrpl.js](https://js.xrpl.org/) library. See [Get Started Using JavaScript](get-started-using-javascript.html) for setup instructions. - -Since JavaScript works in the web browser, you can read along and use the interactive steps without any setup. - ## Example Code Complete sample code for all of the steps of this tutorial is available under the [MIT license](https://github.com/XRPLF/xrpl-dev-portal/blob/master/LICENSE). @@ -49,6 +51,10 @@ _JavaScript_ {{ include_code("_code-samples/trade-in-the-decentralized-exchange/js/base-with-bignumber.js", language="js") }} +_Python_ + +{{ include_code("_code-samples/get-started/py/base-async.py", language="py") }} + **Note:** The JavaScript code samples in this tutorial use the [`async`/`await` pattern](https://javascript.info/async-await). Since `await` needs to be used from within an `async` function, the remaining code samples are written to continue inside the `main()` function started here. You can also use Promise methods `.then()` and `.catch()` instead of `async`/`await` if you prefer. @@ -71,6 +77,10 @@ _JavaScript_ {{ include_code("_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js", language="js", start_with="// Get credentials", end_before="// Define the proposed trade") }} +_Python_ + +{{ include_code("_code-samples/trade-in-the-decentralized-exchange/py/trade-in-the-dex.py", language="py", start_with="# Get credentials", end_before="# Define the proposed trade") }} + ### {{n.next()}}. Look Up Offers @@ -87,6 +97,10 @@ _JavaScript_ {{ include_code("_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js", language="js", start_with="// Define the proposed trade", end_before="// Send OfferCreate") }} +_Python_ + +{{ include_code("_code-samples/trade-in-the-decentralized-exchange/py/trade-in-the-dex.py", language="py", start_with="# Define the proposed trade", end_before="# Send OfferCreate") }} + **Note:** Other users of the XRP Ledger can also make trades at any time, so this is only an estimate of what would happen if nothing else changes. The outcome of a transaction is not guaranteed until it is [final](finality-of-results.html). @@ -153,6 +167,10 @@ _JavaScript_ {{ include_code("_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js", language="js", start_with="// Send OfferCreate", end_before="// Check metadata") }} +_Python_ + +{{ include_code("_code-samples/trade-in-the-decentralized-exchange/py/trade-in-the-dex.py", language="py", start_with="# Send OfferCreate", end_before="# Check metadata") }} + You can use this interface to send the transaction specified by the amounts in the previous step: @@ -187,6 +205,10 @@ _JavaScript_ {{ include_code("_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js", language="js", start_with="// Check metadata", end_before="// Check balances") }} +_Python_ + +{{ include_code("_code-samples/trade-in-the-decentralized-exchange/py/trade-in-the-dex.py", language="py", start_with="# Check metadata", end_before="# Check balances") }} + You can use this interface to test it out: @@ -210,6 +232,10 @@ _JavaScript_ {{ include_code("_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js", language="js", start_with="// Check balances", end_before="client.disconnect()") }} +_Python_ + +{{ include_code("_code-samples/trade-in-the-decentralized-exchange/py/trade-in-the-dex.py", language="py", start_with="# Check balances", end_before="# End main()") }} + You can use this interface to test it out: