From 1c205a1c25a7e1fc0700f5d6cc3067b809d2dbab Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Fri, 5 Apr 2024 16:53:38 -0700 Subject: [PATCH 1/8] Partial code/tutorial for calculating AMM bid savings --- .gitignore | 2 + .../amm-calculator/js/amm-calculator.js | 130 ++++++++++++++++++ _code-samples/amm-calculator/js/index.html | 0 _code-samples/amm-calculator/js/package.json | 6 + .../use-amm-auction-slot-for-lower-fees.md | 66 +++++++++ 5 files changed, 204 insertions(+) create mode 100644 _code-samples/amm-calculator/js/amm-calculator.js create mode 100644 _code-samples/amm-calculator/js/index.html create mode 100644 _code-samples/amm-calculator/js/package.json create mode 100644 docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md diff --git a/.gitignore b/.gitignore index 2e76687a3a..c285d68523 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,7 @@ yarn-error.log *.iml .venv/ +_code-samples/*/js/package-lock.json + # PHP composer.lock diff --git a/_code-samples/amm-calculator/js/amm-calculator.js b/_code-samples/amm-calculator/js/amm-calculator.js new file mode 100644 index 0000000000..701e8bac38 --- /dev/null +++ b/_code-samples/amm-calculator/js/amm-calculator.js @@ -0,0 +1,130 @@ +const xrpl = require('xrpl') +const BigNumber = require('bignumber.js') + +/* + * Convert a trading fee to a value that can be multiplied + * or divided for fee calculations. + * @param tFee int {0, 1000} + * 1 = 1/100,000 or 1000 = 1% fee + * @returns BigNumber (1 - fee) as a decimal + */ +function feeMult(tFee) { + const AUCTION_SLOT_FEE_SCALE_FACTOR = 100000 + return BigNumber(1).minus( (BigNumber(tFee).dividedBy(AUCTION_SLOT_FEE_SCALE_FACTOR)) ) +} + +/* + * Implements the AMM SwapOut formula, as defined in XLS-30 section 2.4 AMM + * Swap, formula 10. The asset weights WA/WB are currently always 1/1 so + * they're canceled out. + * C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/AMMHelpers.h#L253-L258 + * @param asset_out_bn BigNumber - The target amount to receive from the AMM. + * @param pool_in_bn BigNumber - The amount of the input asset in the AMM's + * pool before the swap. + * @param pool_out_bn BigNumber - The amount of the output asset in the AMM's + * pool before the swap. + * @param trading_fee int - The trading fee as an integer {0, 1000} where 1000 + * represents a 1% fee. + * @returns BigNumber - The amount of the input asset that must be swapped in + * to receive the target output amount. Unrounded, because + * the number of decimals depends on if this is drops of + * XRP or a decimal amount of a token; since this is a + * theoretical input to the pool, it should be rounded + * up (ceiling) to preserve the pool's constant product. + */ +function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) { + return ( ( pool_in_bn.multipliedBy(pool_out_bn) ).dividedBy( + pool_out_bn.minus(asset_out_bn) + ).minus(pool_in_bn) + ).dividedBy(feeMult(trading_fee)) +} + +/* + * Calculates the necessary bid to win the AMM Auction slot, per the pricing + * algorithm defined in XLS-30 section 4.1.1. + */ +function auctionPrice(old_bid, time_interval) { + const min_bid = 123456 // @@TODO: figure out real min bid + if (time_interval >= 20) { + return min_bid + } + + if (time_interval <= 1) { + return (old_bid * 1.05) + min_bid + } + + const t60 = BigNumber(0.05).multipliedBy(time_interval).exponentiatedBy(60) + return B * 1.05 * (1 - t60) + min_bid +} + +async function main() { + // Connect ---------------------------------------------------------------- + const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233') + console.log("Connecting to Testnet...") + await client.connect() + + // Get credentials from the Testnet Faucet -------------------------------- + // console.log("Requesting address from the Testnet faucet...") + // const wallet = (await client.fundWallet()).wallet + // console.log(`Got address ${wallet.address}.`) + + // Look up the AMM + const from_asset = { + "currency": "XRP" + } + const to_asset = { + "currency": "TST", + "issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd" + } + + const amm_info = (await client.request({"command": "amm_info", "asset": from_asset, "asset2": to_asset})) + console.dir(amm_info, {depth: null}) + const amm_account = amm_info.result.amm.account + const lpt_code = amm_info.result.amm.lp_token.currency + const pool_drops = amm_info.result.amm.amount // XRP is always first if the pool is token←→XRP + const pool_tst = amm_info.result.amm.amount2 + const full_trading_fee = amm_info.result.amm.trading_fee + const discounted_trading_fee = amm_info.result.amm.auction_slot.discounted_fee + + // Calculate price in XRP to get 10 TST from the AMM ---------------------- + // TODO: first calculate how much will be fulfilled by the order book before getting to the AMM. + + const to_amount = { + "currency": to_asset.currency, + "issuer": to_asset.issuer, + "value": "10.0" + } + + // Convert values to BigNumbers with the appropriate precision ------------ + // Tokens always have 15 significant digits; + // XRP is precise to integer drops, which can be as high as 10^17 + const asset_out_bn = BigNumber(to_amount.value).precision(15) + const pool_in_bn = BigNumber(pool_drops).precision(17) + const pool_out_bn = BigNumber(pool_tst.value).precision(15) + + // Use AMM's SwapOut formula to figure out how much XRP we have to pay + // to receive the target amount of TST, under the current trading fee. + const unrounded_amount = swapOut(asset_out_bn, pool_in_bn, pool_out_bn, full_trading_fee) + const from_amount = unrounded_amount.dp(0, BigNumber.ROUND_CEIL) // Round XRP to integer drops. + console.log(`Expected cost of ${to_amount.value} ${to_amount.currency}: ${xrpl.dropsToXrp(from_amount)} XRP`) + + // Same calculation, but assume we have access to the discounted trading + // fee from the auction slot. + const unrounded_amount_discounted = swapOut(asset_out_bn, pool_in_bn, pool_out_bn, discounted_trading_fee) + const discounted_from_amount = unrounded_amount_discounted.dp(0, BigNumber.ROUND_CEIL) + console.log(`Expected cost with auction slot discount: ${xrpl.dropsToXrp(discounted_from_amount)} XRP`) + + // The potential savings is the difference between the necessary input + // amounts with the full vs discounted fee. + const potential_savings = from_amount - discounted_from_amount + console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`) + + // Calculate the cost of winning the auction slot, in LP Tokens. + const auction_price = auctionPrice(old_bid, time_interval) + + + // Done. + client.disconnect() +} // End of main() + +main() diff --git a/_code-samples/amm-calculator/js/index.html b/_code-samples/amm-calculator/js/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/_code-samples/amm-calculator/js/package.json b/_code-samples/amm-calculator/js/package.json new file mode 100644 index 0000000000..1b96fc2efc --- /dev/null +++ b/_code-samples/amm-calculator/js/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "xrpl": "^3.0.0", + "bignumber.js": "^9.0.0" + } +} diff --git a/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md b/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md new file mode 100644 index 0000000000..b9556d387f --- /dev/null +++ b/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md @@ -0,0 +1,66 @@ +# Use the AMM Auction Slot for Lower Fees + +When trading on the XRP Ledger's decentralized exchange (DEX), you can sometimes save on trading fees by buying the auction slot built into an [Automated Market Maker (AMM)](../../../concepts/tokens/decentralized-exchange/automated-market-makers.md). This tutorial shows how to identify when the auction slot can save you money and what transactions to send to use it, with example code in JavaScript. + +For a simpler example of trading currencies in the XRP Ledger's DEX, see [Trade in the Decentralized Exchange](../../how-tos/use-tokens/trade-in-the-decentralized-exchange.md). + +This tutorial does not exhaustively cover all possible market conditions and circumstances. Always exercise caution and trade at your own risk. + +## 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 basic usage of the [xrpl.js client library](https://github.com/XRPLF/xrpl.js/). See [Get Started Using JavaScript](../build-apps/get-started.md) for setup steps. +- The AMM for the asset pair you want to trade must already be created. This tutorial uses an AMM on Testnet which has been set up in advance. For instructions on creating an AMM for a different currency pair, see [Create an Automated Market Maker](../../how-tos/use-tokens/create-an-automated-market-maker.md). + + +## Background + +This tutorial uses an AMM instance that has been set up on the XRP Ledger Testnet in advance, connecting the following assets: + +| Currency Code | Issuer | Notes | +|---|---|---| +| XRP | N/A | Testnet XRP is functionally similar to XRP, but holds no real-world value. You can get it for free from a [faucet](/resources/dev-tools/xrp-faucets). +| TST | `rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd` | A test token pegged to XRP at a rate of approximately 10 XRP per 1 TST. The issuer has existing Offers on the XRP Ledger Testnet to buy and sell these tokens. | + +TST can be issued and redeemed through _backstop_ Offers, managed by its issuer, in the order book part of the DEX. The backstop offers have a spread of ±15%, meaning that it takes 11.5 XRP to buy 1 TST, but it takes 1.15 TST to buy back 10 XRP. The backstop offers have very large quantity, so they are unlikely to be used up, meaning that these represent the worst case scenario exchange rate between these two assets. Also, rP9j... does not have a [Tick Size](../../../concepts/tokens/decentralized-exchange/ticksize.md) set, so exchange rates can use the full 15 digits of precision. If one were set, you would have to do more rounding when calculating what exchange rate you would get. + +## Steps + +At a high level, the steps involved in using an AMM auction slot to save money on trading are as follows: + +1. Check the current XRP Ledger state to estimate how much your desired trade would cost in AMM trading fees. +2. Compare against the cost to win the current auction slot. +3. If winning the auction slot is cheaper, use AMMDeposit to acquire some LPTokens and then use AMMBid to spend those tokens on winning the auction slot. +4. Make the intended trade using an OfferCreate transaction. (You could also use a cross-currency Payment transaction, but this tutorial uses OfferCreate.) + +This tutorial assumes that you have XRP and want to acquire a fixed amount of TST. The details of the steps will vary in other scenarios, such as if you want to spend a fixed amount of XRP to acquire as much TST as you can, or if a token you are buying or selling has a [transfer fee](../../../concepts/tokens/transfer-fees.md). + +### 1. Setup + +The following code imports necessary libraries, connects to the ledger, and instantiates an XRP Ledger wallet. For this use case, you need a high-precision number library such as Bignumber.js for correctly performing calculations on currency amounts you may find in the ledger. + +@@TODO + +### 2. Estimate the cost of AMM trading fees + +Estimating the cost of AMM trading fees can be tricky and the details vary depending on the type of trade you are performing. For example, a "buy" trade that stops after receiving a target amount of an asset should be calculated differently than a "sell" trade, which continues until a target amount of an asset has been spent. Similarly, options such as immediate-or-cancel or limit quality may affect your calculations. + +Also, the XRP Ledger's decentralized exchange is always open to other traders using it too, so new transactions can change the current state at the same time that you are doing these calculations and sending your transactions. You must always allow for some amount of _slippage_ (the change in rates between when you checked them and when your transaction executes) as well as the possibility that your transactions may not succeed at all. (For example, someone else might bid a higher amount for the auction slot right as you place your bid.) + +The following code estimates the amount that would be paid to an AMM's trading fee for a specific trade (selling XRP, buying TST.rP9j...): + +@@TODO: + 1. Calculate how much of the trade will use the AMM (as opposed to CLOB DEX) + 2. Calculate how much will be swapped into the AMM + 3. Calculate the full trading fee for the amount to be swapped in + 4. Calculate the reduced trading fee for the amount to be swapped in if you hold the auction slot + 5. Calculate the difference between the full trading fee and the reduced trading fee, in other words your potential savings + +### 3. Compare with the cost of winning the auction slot + +The cost to win the auction slot depends on how long the current holder has held it and how much they paid, but it's always denominated in LP tokens. To win the auction slot if you currently only have XRP, you'll have to make a single-asset deposit to get LP Tokens. So, the comparison we want to make here is: + +@@TODO + 1. Calculate quantity of LP Tokens needed to win the auction slot + 2. Calculate single-asset amount needed to deposit to receive that many LP Tokens + 3. Compare the cost of winning the auction slot vs potential savings calculated in the previous step From 43ebddb05ae34dd41dde9c1b6ee14d079f89d7a1 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Wed, 10 Apr 2024 15:55:05 -0700 Subject: [PATCH 2/8] Incomplete AMM calculations sample code --- .../amm-calculator/js/amm-calculator.js | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/_code-samples/amm-calculator/js/amm-calculator.js b/_code-samples/amm-calculator/js/amm-calculator.js index 701e8bac38..ce1cc5fcd4 100644 --- a/_code-samples/amm-calculator/js/amm-calculator.js +++ b/_code-samples/amm-calculator/js/amm-calculator.js @@ -3,14 +3,24 @@ const BigNumber = require('bignumber.js') /* * Convert a trading fee to a value that can be multiplied - * or divided for fee calculations. + * by a total to "subtract" the fee from the total. * @param tFee int {0, 1000} - * 1 = 1/100,000 or 1000 = 1% fee + * such that 1 = 1/100,000 and 1000 = 1% fee * @returns BigNumber (1 - fee) as a decimal */ function feeMult(tFee) { + return BigNumber(1).minus( feeDecimal(tFee) ) +} +/* + * Convert a trading fee to a decimal BigNumber value, + * for example 1000 becomes 0.01 + * @param tFee int {0, 1000} + * such that 1 = 1/100,000 and 1000 = 1% fee + * @returns BigNumber(fee) as a decimal + */ +function feeDecimal(tFee) { const AUCTION_SLOT_FEE_SCALE_FACTOR = 100000 - return BigNumber(1).minus( (BigNumber(tFee).dividedBy(AUCTION_SLOT_FEE_SCALE_FACTOR)) ) + return BigNumber(tFee).dividedBy(AUCTION_SLOT_FEE_SCALE_FACTOR) } /* @@ -42,19 +52,22 @@ function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) { /* * Calculates the necessary bid to win the AMM Auction slot, per the pricing * algorithm defined in XLS-30 section 4.1.1. + * @returns BigNumber - the minimum amount of LP tokens to win the auction slot */ -function auctionPrice(old_bid, time_interval) { - const min_bid = 123456 // @@TODO: figure out real min bid +function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) { + const tfee_decimal = feeDecimal(trading_fee) + const min_bid = BigNumber(lpt_balance).multipliedBy(tfee_decimal).dividedBy(25) + const b = BigNumber(old_bid) if (time_interval >= 20) { return min_bid } if (time_interval <= 1) { - return (old_bid * 1.05) + min_bid + return b.multipliedBy(BigNumber("1.05")).plus(min_bid) } - const t60 = BigNumber(0.05).multipliedBy(time_interval).exponentiatedBy(60) - return B * 1.05 * (1 - t60) + min_bid + const t60 = BigNumber("0.05").multipliedBy(time_interval).exponentiatedBy(60) + return b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60)).plus(min_bid) } async function main() { @@ -80,7 +93,7 @@ async function main() { const amm_info = (await client.request({"command": "amm_info", "asset": from_asset, "asset2": to_asset})) console.dir(amm_info, {depth: null}) const amm_account = amm_info.result.amm.account - const lpt_code = amm_info.result.amm.lp_token.currency + const lpt = amm_info.result.amm.lp_token const pool_drops = amm_info.result.amm.amount // XRP is always first if the pool is token←→XRP const pool_tst = amm_info.result.amm.amount2 const full_trading_fee = amm_info.result.amm.trading_fee @@ -119,8 +132,10 @@ async function main() { const potential_savings = from_amount - discounted_from_amount console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`) - // Calculate the cost of winning the auction slot, in LP Tokens. - const auction_price = auctionPrice(old_bid, time_interval) + // Calculate the cost of winning the auction slot, then convert it to XRP + const auction_price = auctionPrice(old_bid, time_interval, full_trading_fee, lpt.value) + console.log(`Auction price: ${auction_price} LP Tokens`) + // @@TODO: figure out how to convert auction_price from LPT to input asset. // Done. From 6613ed98f0011d56c4c8f9ab2bd7250b54d60920 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Tue, 16 Apr 2024 01:49:39 -0700 Subject: [PATCH 3/8] AMM auction slot tutorial progress --- .../amm-calculator/js/amm-calculator.js | 112 ++++++++++++++++-- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/_code-samples/amm-calculator/js/amm-calculator.js b/_code-samples/amm-calculator/js/amm-calculator.js index ce1cc5fcd4..eb1e4e07f7 100644 --- a/_code-samples/amm-calculator/js/amm-calculator.js +++ b/_code-samples/amm-calculator/js/amm-calculator.js @@ -11,6 +11,17 @@ const BigNumber = require('bignumber.js') function feeMult(tFee) { return BigNumber(1).minus( feeDecimal(tFee) ) } + +/* + * Same as feeMult, but with half the trading fee. + * @param tFee int {0, 1000} + * such that 1 = 1/100,000 and 1000 = 1% fee + * @returns BigNumber (1 - (fee/2)) as a decimal + */ +function feeMultHalf(tFee) { + return BigNumber(1).minus( feeDecimal(tFee).dividedBy(2) ) +} + /* * Convert a trading fee to a decimal BigNumber value, * for example 1000 becomes 0.01 @@ -49,6 +60,43 @@ function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) { ).dividedBy(feeMult(trading_fee)) } +/* + * Computes the quadratic formula. Helper function for ammAssetIn. + * Params and return value are BigNumber instances. + */ +function solveQuadraticEq(a,b,c) { + const b2minus4ac = b.multipliedBy(b).minus( a.multipliedBy(c).multipliedBy(4) ) + return ( b.negated().plus(b2minus4ac.sqrt()) ).dividedBy(a.multipliedBy(2)); +} + +/* + * Implements the AMM single-asset deposit formula to calculate how much to + * put in so that you receive a specific number of LP Tokens back. + * C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/impl/AMMHelpers.cpp#L55-L83 + * @param pool_in string - Quantity of input asset the pool already has + * @param lpt_balance string - Quantity of LP Tokens already issued by the AMM + * @param desired_lpt string - Quantity of new LP Tokens you want to receive + * @param trading_fee int - The trading fee as an integer {0,1000} where 1000 + * represents a 1% fee. + */ +function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) { + // TODO: refactor to take pool_in as a BigNumber so precision can be set based on XRP/drops? + // convert inputs to BigNumber + const lpTokens = BigNumber(desired_lpt) + const lptAMMBalance = BigNumber(lpt_balance) + const asset1Balance = BigNumber(pool_in) + + const f1 = feeMult(trading_fee) + const f2 = feeMultHalf(trading_fee).dividedBy(f1) + const t1 = lpTokens.dividedBy(lptAMMBalance) + const t2 = t1.plus(1) + const d = f2.minus( t1.dividedBy(t2) ) + const a = BigNumber(1).dividedBy( t2.multipliedBy(t2)) + const b = BigNumber(2).multipliedBy(d).dividedBy(t2).minus( BigNumber(1).dividedBy(f1) ) + const c = d.multipliedBy(d).minus( f2.multipliedBy(f2) ) + return asset1Balance.multipliedBy(solveQuadraticEq(a,b,c)) +} + /* * Calculates the necessary bid to win the AMM Auction slot, per the pricing * algorithm defined in XLS-30 section 4.1.1. @@ -60,26 +108,27 @@ function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) { const b = BigNumber(old_bid) if (time_interval >= 20) { return min_bid - } - if (time_interval <= 1) { + } else if (time_interval > 1) { + const t60 = BigNumber("0.05").multipliedBy(time_interval).exponentiatedBy(60) + return b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60)).plus(min_bid) + + } else { // time_interval <= 1 return b.multipliedBy(BigNumber("1.05")).plus(min_bid) } - const t60 = BigNumber("0.05").multipliedBy(time_interval).exponentiatedBy(60) - return b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60)).plus(min_bid) } async function main() { // Connect ---------------------------------------------------------------- - const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233') + const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233') console.log("Connecting to Testnet...") await client.connect() // Get credentials from the Testnet Faucet -------------------------------- - // console.log("Requesting address from the Testnet faucet...") - // const wallet = (await client.fundWallet()).wallet - // console.log(`Got address ${wallet.address}.`) + console.log("Requesting address from the Testnet faucet...") + const wallet = (await client.fundWallet()).wallet + console.log(`Got address ${wallet.address}.`) // Look up the AMM const from_asset = { @@ -98,6 +147,8 @@ async function main() { const pool_tst = amm_info.result.amm.amount2 const full_trading_fee = amm_info.result.amm.trading_fee const discounted_trading_fee = amm_info.result.amm.auction_slot.discounted_fee + const old_bid = amm_info.result.amm.auction_slot.price.value + const time_interval = amm_info.result.amm.auction_slot.time_interval // Calculate price in XRP to get 10 TST from the AMM ---------------------- // TODO: first calculate how much will be fulfilled by the order book before getting to the AMM. @@ -132,11 +183,50 @@ async function main() { const potential_savings = from_amount - discounted_from_amount console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`) - // Calculate the cost of winning the auction slot, then convert it to XRP - const auction_price = auctionPrice(old_bid, time_interval, full_trading_fee, lpt.value) + // Calculate the cost of winning the auction slot, in LP Tokens + const auction_price = auctionPrice(old_bid, time_interval, full_trading_fee, lpt.value).precision(15) console.log(`Auction price: ${auction_price} LP Tokens`) - // @@TODO: figure out how to convert auction_price from LPT to input asset. + // Figure out how much XRP we need to deposit to receive the auction price + const deposit_for_bid = ammAssetIn(pool_in_bn, lpt.value, auction_price, full_trading_fee).dp(0, BigNumber.ROUND_CEIL) + console.log(`Auction price as XRP single-asset deposit amount: ${xrpl.dropsToXrp(deposit_for_bid)} XRP`) + const SLIPPAGE_MULT = BigNumber(1.01) // allow up to 1% more than estimated amounts. TODO: also allow slippage on auction price? + const deposit_max = deposit_for_bid.multipliedBy(SLIPPAGE_MULT).dp(0).toString() + + // TODO: compare price of deposit+bid with potential savings. Don't forget XRP burned as transaction costs + + const auction_bid = { + "currency": lpt.currency, + "issuer": lpt.issuer, + "value": auction_price.toString() + } + // Do a single-asset deposit to get LP Tokens to bid on the auction slot + const deposit_result = await client.submitAndWait({ + // const deposit_autofill = await client.autofill({ + "TransactionType": "AMMDeposit", + "Account": wallet.address, + "Asset": from_asset, + "Asset2": to_asset, + "Amount": deposit_max, + "LPTokenOut": auction_bid, + "Flags": xrpl.AMMDepositFlags.tfOneAssetLPToken + }, {autofill: true, wallet: wallet} + ) + console.log("Deposit result:") + console.dir(deposit_result, {depth: null}) + + + // Bid on the auction slot + const bid_result = await client.submitAndWait({ + "TransactionType": "AMMBid", + "Account": wallet.address, + "Asset": from_asset, + "Asset2": to_asset, + "BidMax": auction_bid // TODO: try w/ BidMin + BidMax w/ slippage + }, {autofill: true, wallet: wallet} + ) + console.log("Bid result:") + console.dir(bid_result, {depth: null}) // Done. client.disconnect() From 40e2e6341ef6ec17d1b382b514e6b9f8af3fff86 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Tue, 16 Apr 2024 01:50:58 -0700 Subject: [PATCH 4/8] Auction slot tutorial: rename files to match function --- .../js/amm-calculator.js => auction-slot/js/auction-slot.js} | 0 _code-samples/{amm-calculator => auction-slot}/js/index.html | 0 _code-samples/{amm-calculator => auction-slot}/js/package.json | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename _code-samples/{amm-calculator/js/amm-calculator.js => auction-slot/js/auction-slot.js} (100%) rename _code-samples/{amm-calculator => auction-slot}/js/index.html (100%) rename _code-samples/{amm-calculator => auction-slot}/js/package.json (100%) diff --git a/_code-samples/amm-calculator/js/amm-calculator.js b/_code-samples/auction-slot/js/auction-slot.js similarity index 100% rename from _code-samples/amm-calculator/js/amm-calculator.js rename to _code-samples/auction-slot/js/auction-slot.js diff --git a/_code-samples/amm-calculator/js/index.html b/_code-samples/auction-slot/js/index.html similarity index 100% rename from _code-samples/amm-calculator/js/index.html rename to _code-samples/auction-slot/js/index.html diff --git a/_code-samples/amm-calculator/js/package.json b/_code-samples/auction-slot/js/package.json similarity index 100% rename from _code-samples/amm-calculator/js/package.json rename to _code-samples/auction-slot/js/package.json From 46d1add256b2bc4b3601c879ed12cc4d1c0d1311 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Thu, 2 May 2024 15:37:41 -0700 Subject: [PATCH 5/8] Auction slot: update sample code --- _code-samples/auction-slot/js/auction-slot.js | 204 +++++++++++++----- 1 file changed, 154 insertions(+), 50 deletions(-) diff --git a/_code-samples/auction-slot/js/auction-slot.js b/_code-samples/auction-slot/js/auction-slot.js index eb1e4e07f7..d5b6968f96 100644 --- a/_code-samples/auction-slot/js/auction-slot.js +++ b/_code-samples/auction-slot/js/auction-slot.js @@ -65,8 +65,10 @@ function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) { * Params and return value are BigNumber instances. */ function solveQuadraticEq(a,b,c) { - const b2minus4ac = b.multipliedBy(b).minus( a.multipliedBy(c).multipliedBy(4) ) - return ( b.negated().plus(b2minus4ac.sqrt()) ).dividedBy(a.multipliedBy(2)); + const b2minus4ac = b.multipliedBy(b).minus( + a.multipliedBy(c).multipliedBy(4) + ) + return ( b.negated().plus(b2minus4ac.sqrt()) ).dividedBy(a.multipliedBy(2)) } /* @@ -80,7 +82,6 @@ function solveQuadraticEq(a,b,c) { * represents a 1% fee. */ function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) { - // TODO: refactor to take pool_in as a BigNumber so precision can be set based on XRP/drops? // convert inputs to BigNumber const lpTokens = BigNumber(desired_lpt) const lptAMMBalance = BigNumber(lpt_balance) @@ -92,43 +93,85 @@ function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) { const t2 = t1.plus(1) const d = f2.minus( t1.dividedBy(t2) ) const a = BigNumber(1).dividedBy( t2.multipliedBy(t2)) - const b = BigNumber(2).multipliedBy(d).dividedBy(t2).minus( BigNumber(1).dividedBy(f1) ) + const b = BigNumber(2).multipliedBy(d).dividedBy(t2).minus( + BigNumber(1).dividedBy(f1) + ) const c = d.multipliedBy(d).minus( f2.multipliedBy(f2) ) return asset1Balance.multipliedBy(solveQuadraticEq(a,b,c)) } /* * Calculates the necessary bid to win the AMM Auction slot, per the pricing - * algorithm defined in XLS-30 section 4.1.1. + * algorithm defined in XLS-30 section 4.1.1, if you already hold LP Tokens. + * Not useful in the case where you need to make a deposit to get LP Tokens, + * because doing so causes more LP Tokens to be issued, changing the min bid. * @returns BigNumber - the minimum amount of LP tokens to win the auction slot */ function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) { const tfee_decimal = feeDecimal(trading_fee) - const min_bid = BigNumber(lpt_balance).multipliedBy(tfee_decimal).dividedBy(25) + const lptokens = BigNumber(lpt_balance) + const min_bid = lptokens.multipliedBy(tfee_decimal).dividedBy(25) const b = BigNumber(old_bid) - if (time_interval >= 20) { - return min_bid - - } else if (time_interval > 1) { - const t60 = BigNumber("0.05").multipliedBy(time_interval).exponentiatedBy(60) - return b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60)).plus(min_bid) - - } else { // time_interval <= 1 - return b.multipliedBy(BigNumber("1.05")).plus(min_bid) + let new_bid = min_bid + + if (time_interval == 0) { + new_bid = b.multipliedBy("1.05").plus(min_bid) + } else if (time_interval <= 19) { + const t60 = BigNumber(time_interval).multipliedBy("0.05" + ).exponentiatedBy(60) + new_bid = b.multipliedBy("1.05").multipliedBy( + BigNumber(1).minus(t60) + ).plus(min_bid) } + const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING + ).minus(lptokens).precision(15, BigNumber.FLOOR) + return rounded_bid } + +/* + * Calculates how much to deposit, in terms of LP Tokens out, to be able to win + * the auction slot. This is based on the slot pricing algorithm defined in + * XLS-30 section 4.1.1, but factors in the increase in the minimum bid as a + * result of having new LP Tokens issued to you from your deposit. + */ +function auctionDeposit(old_bid, time_interval, trading_fee, lpt_balance) { + const tfee_decimal = feeDecimal(trading_fee) + const lptokens = BigNumber(lpt_balance) + const b = BigNumber(old_bid) + let outbidAmount = BigNumber(0) // This is the case if time_interval >= 20 + if (time_interval == 0) { + outbidAmount = b.multipliedBy("1.05") + } else if (time_interval <= 19) { + const t60 = BigNumber(time_interval).multipliedBy("0.05").exponentiatedBy(60) + outbidAmount = b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60)) + } + + const new_bid = lptokens.plus(outbidAmount).dividedBy( + BigNumber(25).dividedBy(tfee_decimal).minus(1) + ).plus(outbidAmount) + + // Significant digits for the deposit are limited by total LPTokens issued + // so we calculate lptokens + deposit - lptokens to determine where the + // rounding occurs. We use ceiling/floor to make sure the amount we receive + // after rounding is still enough to win the auction slot. + const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING + ).minus(lptokens).precision(15, BigNumber.FLOOR) + return rounded_bid +} + + async function main() { // Connect ---------------------------------------------------------------- - const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233') + const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233') console.log("Connecting to Testnet...") await client.connect() - // Get credentials from the Testnet Faucet -------------------------------- - console.log("Requesting address from the Testnet faucet...") + // // Get credentials from the faucet ------------------------------------- + console.log("Requesting test XRP from the faucet...") const wallet = (await client.fundWallet()).wallet - console.log(`Got address ${wallet.address}.`) + console.log(`Got address ${wallet.address} / seed ${wallet.seed}.`) // Look up the AMM const from_asset = { @@ -138,76 +181,123 @@ async function main() { "currency": "TST", "issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd" } - - const amm_info = (await client.request({"command": "amm_info", "asset": from_asset, "asset2": to_asset})) + const amm_info = (await client.request({ + "command": "amm_info", + "asset": from_asset, + "asset2": to_asset + })) console.dir(amm_info, {depth: null}) - const amm_account = amm_info.result.amm.account const lpt = amm_info.result.amm.lp_token - const pool_drops = amm_info.result.amm.amount // XRP is always first if the pool is token←→XRP + // XRP is always first if the pool is token←→XRP. + // For a token←→token AMM, you'd need to figure out which asset is first. + const pool_drops = amm_info.result.amm.amount const pool_tst = amm_info.result.amm.amount2 const full_trading_fee = amm_info.result.amm.trading_fee - const discounted_trading_fee = amm_info.result.amm.auction_slot.discounted_fee + const discounted_fee = amm_info.result.amm.auction_slot.discounted_fee const old_bid = amm_info.result.amm.auction_slot.price.value const time_interval = amm_info.result.amm.auction_slot.time_interval // Calculate price in XRP to get 10 TST from the AMM ---------------------- - // TODO: first calculate how much will be fulfilled by the order book before getting to the AMM. - + // Note, this ignores Offers from the non-AMM part of the DEX. const to_amount = { "currency": to_asset.currency, "issuer": to_asset.issuer, "value": "10.0" } - // Convert values to BigNumbers with the appropriate precision ------------ + // Convert values to BigNumbers with the appropriate precision. // Tokens always have 15 significant digits; // XRP is precise to integer drops, which can be as high as 10^17 const asset_out_bn = BigNumber(to_amount.value).precision(15) const pool_in_bn = BigNumber(pool_drops).precision(17) const pool_out_bn = BigNumber(pool_tst.value).precision(15) - + + if (to_amount.value > pool_out_bn) { + console.log(`Requested ${to_amount.value} ${to_amount.currency} ` + + `but AMM only holds ${pool_tst.value}. Quitting.`) + client.disconnect() + return + } + // Use AMM's SwapOut formula to figure out how much XRP we have to pay // to receive the target amount of TST, under the current trading fee. - const unrounded_amount = swapOut(asset_out_bn, pool_in_bn, pool_out_bn, full_trading_fee) - const from_amount = unrounded_amount.dp(0, BigNumber.ROUND_CEIL) // Round XRP to integer drops. - console.log(`Expected cost of ${to_amount.value} ${to_amount.currency}: ${xrpl.dropsToXrp(from_amount)} XRP`) + const unrounded_amount = swapOut(asset_out_bn, pool_in_bn, + pool_out_bn, full_trading_fee) + // Round XRP to integer drops. Round ceiling to make you pay in enough. + const from_amount = unrounded_amount.dp(0, BigNumber.ROUND_CEIL) + console.log(`Expected cost of ${to_amount.value} ${to_amount.currency}: ` + + `${xrpl.dropsToXrp(from_amount)} XRP`) // Same calculation, but assume we have access to the discounted trading // fee from the auction slot. - const unrounded_amount_discounted = swapOut(asset_out_bn, pool_in_bn, pool_out_bn, discounted_trading_fee) - const discounted_from_amount = unrounded_amount_discounted.dp(0, BigNumber.ROUND_CEIL) - console.log(`Expected cost with auction slot discount: ${xrpl.dropsToXrp(discounted_from_amount)} XRP`) + const raw_discounted = swapOut(asset_out_bn, pool_in_bn, pool_out_bn, + discounted_fee) + const discounted_from_amount = raw_discounted.dp(0, BigNumber.ROUND_CEIL) + console.log(`Expected cost with auction slot discount: `+ + `${xrpl.dropsToXrp(discounted_from_amount)} XRP`) // The potential savings is the difference between the necessary input // amounts with the full vs discounted fee. - const potential_savings = from_amount - discounted_from_amount + const potential_savings = from_amount.minus(discounted_from_amount) console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`) + + // Calculate the cost of winning the auction slot, in LP Tokens ----------- - // Calculate the cost of winning the auction slot, in LP Tokens - const auction_price = auctionPrice(old_bid, time_interval, full_trading_fee, lpt.value).precision(15) - console.log(`Auction price: ${auction_price} LP Tokens`) - // Figure out how much XRP we need to deposit to receive the auction price - const deposit_for_bid = ammAssetIn(pool_in_bn, lpt.value, auction_price, full_trading_fee).dp(0, BigNumber.ROUND_CEIL) - console.log(`Auction price as XRP single-asset deposit amount: ${xrpl.dropsToXrp(deposit_for_bid)} XRP`) + // The price is slightly different if you already hold LP Tokens vs if you + // have to make a deposit, because the deposit causes more LP Tokens to be + // issued, which increases the minimum bid. + const lp_auction_price = auctionPrice(old_bid, time_interval, + full_trading_fee, lpt.value + ).precision(15) + console.log(`Auction price for current LPs: ${lp_auction_price} LP Tokens`) - const SLIPPAGE_MULT = BigNumber(1.01) // allow up to 1% more than estimated amounts. TODO: also allow slippage on auction price? - const deposit_max = deposit_for_bid.multipliedBy(SLIPPAGE_MULT).dp(0).toString() + const auction_price = auctionDeposit(old_bid, time_interval, + full_trading_fee, lpt.value + ).precision(15) + console.log(`Auction price after deposit: ${auction_price} LP Tokens`) + + // Calculate how much XRP to deposit to receive that many LP Tokens + const deposit_for_bid = ammAssetIn(pool_in_bn, lpt.value, auction_price, + full_trading_fee + ).dp(0, BigNumber.ROUND_CEIL) + console.log(`Auction price as XRP single-asset deposit amount: `+ + `${xrpl.dropsToXrp(deposit_for_bid)} XRP`) - // TODO: compare price of deposit+bid with potential savings. Don't forget XRP burned as transaction costs + // Optional. Allow for costs to be 1% greater than estimated, in case other + // transactions affect the same AMM during this time. + const SLIPPAGE_MULT = BigNumber(1.01) + const deposit_max = deposit_for_bid.multipliedBy(SLIPPAGE_MULT).dp(0) + // Compare price of deposit+bid with potential savings. ------------------- + // Don't forget XRP burned as transaction costs. + const fee_response = (await client.request({"command":"fee"})) + const tx_cost_drops = BigNumber(fee_response.result.drops.minimum_fee + ).multipliedBy(client.feeCushion).dp(0) + const net_savings = potential_savings.minus( + tx_cost_drops.multipliedBy(2).plus(deposit_max) + ) + if (net_savings > 0) { + console.log(`Estimated net savings from the auction slot: ` + + `${xrpl.dropsToXrp(net_savings)} XRP`) + } else { + console.log(`Estimated the auction slot to be MORE EXPENSIVE by `+ + `${xrpl.dropsToXrp(net_savings.negated())} XRP. Quitting.`) + client.disconnect() + return + } + + // Do a single-asset deposit to get LP Tokens to bid on the auction slot -- const auction_bid = { "currency": lpt.currency, "issuer": lpt.issuer, "value": auction_price.toString() } - // Do a single-asset deposit to get LP Tokens to bid on the auction slot const deposit_result = await client.submitAndWait({ - // const deposit_autofill = await client.autofill({ "TransactionType": "AMMDeposit", "Account": wallet.address, "Asset": from_asset, "Asset2": to_asset, - "Amount": deposit_max, + "Amount": deposit_max.toString(), "LPTokenOut": auction_bid, "Flags": xrpl.AMMDepositFlags.tfOneAssetLPToken }, {autofill: true, wallet: wallet} @@ -215,19 +305,33 @@ async function main() { console.log("Deposit result:") console.dir(deposit_result, {depth: null}) - - // Bid on the auction slot + // Actually bid on the auction slot --------------------------------------- const bid_result = await client.submitAndWait({ "TransactionType": "AMMBid", "Account": wallet.address, "Asset": from_asset, "Asset2": to_asset, - "BidMax": auction_bid // TODO: try w/ BidMin + BidMax w/ slippage + "BidMax": auction_bid, + "BidMin": auction_bid, // So rounding doesn't leave dust amounts of LPT }, {autofill: true, wallet: wallet} ) console.log("Bid result:") console.dir(bid_result, {depth: null}) + // Trade using the discount ----------------------------------------------- + const spend_drops = discounted_from_amount.multipliedBy(SLIPPAGE_MULT + ).dp(0).toString() + const offer_result = await client.submitAndWait({ + "TransactionType": "OfferCreate", + "Account": wallet.address, + "TakerPays": to_amount, + "TakerGets": spend_drops + }, {autofill: true, wallet: wallet}) + console.log("Offer result:") + console.dir(offer_result, {depth: null}) + console.log("Offer balance changes summary:") + console.dir(xrpl.getBalanceChanges(offer_result.result.meta), {depth:null}) + // Done. client.disconnect() } // End of main() From 8b687e98eae4b1f5c251e18c287ec987f6d82624 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Tue, 7 May 2024 19:10:07 -0700 Subject: [PATCH 6/8] Finish draft of AMM tutorial --- _code-samples/auction-slot/js/auction-slot.js | 103 ++++++------- .../use-amm-auction-slot-for-lower-fees.md | 143 +++++++++++++++--- 2 files changed, 169 insertions(+), 77 deletions(-) diff --git a/_code-samples/auction-slot/js/auction-slot.js b/_code-samples/auction-slot/js/auction-slot.js index d5b6968f96..9835117f1e 100644 --- a/_code-samples/auction-slot/js/auction-slot.js +++ b/_code-samples/auction-slot/js/auction-slot.js @@ -1,8 +1,7 @@ const xrpl = require('xrpl') const BigNumber = require('bignumber.js') -/* - * Convert a trading fee to a value that can be multiplied +/* Convert a trading fee to a value that can be multiplied * by a total to "subtract" the fee from the total. * @param tFee int {0, 1000} * such that 1 = 1/100,000 and 1000 = 1% fee @@ -12,8 +11,7 @@ function feeMult(tFee) { return BigNumber(1).minus( feeDecimal(tFee) ) } -/* - * Same as feeMult, but with half the trading fee. +/* Same as feeMult, but with half the trading fee. * @param tFee int {0, 1000} * such that 1 = 1/100,000 and 1000 = 1% fee * @returns BigNumber (1 - (fee/2)) as a decimal @@ -22,8 +20,7 @@ function feeMultHalf(tFee) { return BigNumber(1).minus( feeDecimal(tFee).dividedBy(2) ) } -/* - * Convert a trading fee to a decimal BigNumber value, +/* Convert a trading fee to a decimal BigNumber value, * for example 1000 becomes 0.01 * @param tFee int {0, 1000} * such that 1 = 1/100,000 and 1000 = 1% fee @@ -34,8 +31,7 @@ function feeDecimal(tFee) { return BigNumber(tFee).dividedBy(AUCTION_SLOT_FEE_SCALE_FACTOR) } -/* - * Implements the AMM SwapOut formula, as defined in XLS-30 section 2.4 AMM +/* Implement the AMM SwapOut formula, as defined in XLS-30 section 2.4 AMM * Swap, formula 10. The asset weights WA/WB are currently always 1/1 so * they're canceled out. * C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/AMMHelpers.h#L253-L258 @@ -60,8 +56,7 @@ function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) { ).dividedBy(feeMult(trading_fee)) } -/* - * Computes the quadratic formula. Helper function for ammAssetIn. +/* Compute the quadratic formula. Helper function for ammAssetIn. * Params and return value are BigNumber instances. */ function solveQuadraticEq(a,b,c) { @@ -71,8 +66,7 @@ function solveQuadraticEq(a,b,c) { return ( b.negated().plus(b2minus4ac.sqrt()) ).dividedBy(a.multipliedBy(2)) } -/* - * Implements the AMM single-asset deposit formula to calculate how much to +/* Implement the AMM single-asset deposit formula to calculate how much to * put in so that you receive a specific number of LP Tokens back. * C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/impl/AMMHelpers.cpp#L55-L83 * @param pool_in string - Quantity of input asset the pool already has @@ -100,38 +94,7 @@ function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) { return asset1Balance.multipliedBy(solveQuadraticEq(a,b,c)) } -/* - * Calculates the necessary bid to win the AMM Auction slot, per the pricing - * algorithm defined in XLS-30 section 4.1.1, if you already hold LP Tokens. - * Not useful in the case where you need to make a deposit to get LP Tokens, - * because doing so causes more LP Tokens to be issued, changing the min bid. - * @returns BigNumber - the minimum amount of LP tokens to win the auction slot - */ -function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) { - const tfee_decimal = feeDecimal(trading_fee) - const lptokens = BigNumber(lpt_balance) - const min_bid = lptokens.multipliedBy(tfee_decimal).dividedBy(25) - const b = BigNumber(old_bid) - let new_bid = min_bid - - if (time_interval == 0) { - new_bid = b.multipliedBy("1.05").plus(min_bid) - } else if (time_interval <= 19) { - const t60 = BigNumber(time_interval).multipliedBy("0.05" - ).exponentiatedBy(60) - new_bid = b.multipliedBy("1.05").multipliedBy( - BigNumber(1).minus(t60) - ).plus(min_bid) - } - - const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING - ).minus(lptokens).precision(15, BigNumber.FLOOR) - return rounded_bid -} - - -/* - * Calculates how much to deposit, in terms of LP Tokens out, to be able to win +/* Calculate how much to deposit, in terms of LP Tokens out, to be able to win * the auction slot. This is based on the slot pricing algorithm defined in * XLS-30 section 4.1.1, but factors in the increase in the minimum bid as a * result of having new LP Tokens issued to you from your deposit. @@ -173,7 +136,7 @@ async function main() { const wallet = (await client.fundWallet()).wallet console.log(`Got address ${wallet.address} / seed ${wallet.seed}.`) - // Look up the AMM + // Look up AMM status ----------------------------------------------------- const from_asset = { "currency": "XRP" } @@ -242,21 +205,12 @@ async function main() { console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`) // Calculate the cost of winning the auction slot, in LP Tokens ----------- - - // The price is slightly different if you already hold LP Tokens vs if you - // have to make a deposit, because the deposit causes more LP Tokens to be - // issued, which increases the minimum bid. - const lp_auction_price = auctionPrice(old_bid, time_interval, - full_trading_fee, lpt.value - ).precision(15) - console.log(`Auction price for current LPs: ${lp_auction_price} LP Tokens`) - const auction_price = auctionDeposit(old_bid, time_interval, full_trading_fee, lpt.value ).precision(15) console.log(`Auction price after deposit: ${auction_price} LP Tokens`) - // Calculate how much XRP to deposit to receive that many LP Tokens + // Calculate how much XRP to deposit to receive that many LP Tokens ------- const deposit_for_bid = ammAssetIn(pool_in_bn, lpt.value, auction_price, full_trading_fee ).dp(0, BigNumber.ROUND_CEIL) @@ -337,3 +291,42 @@ async function main() { } // End of main() main() + +/// TODO: move everything below here to a separate code sample with minimal setup to make it work: + +// /* Calculate the necessary bid to win the AMM Auction slot, per the pricing +// * algorithm defined in XLS-30 section 4.1.1, if you already hold LP Tokens. +// * Not useful in the case where you need to make a deposit to get LP Tokens, +// * because doing so causes more LP Tokens to be issued, changing the min bid. +// * @returns BigNumber - the minimum amount of LP tokens to win the auction slot +// */ +// function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) { +// const tfee_decimal = feeDecimal(trading_fee) +// const lptokens = BigNumber(lpt_balance) +// const min_bid = lptokens.multipliedBy(tfee_decimal).dividedBy(25) +// const b = BigNumber(old_bid) +// let new_bid = min_bid + +// if (time_interval == 0) { +// new_bid = b.multipliedBy("1.05").plus(min_bid) +// } else if (time_interval <= 19) { +// const t60 = BigNumber(time_interval).multipliedBy("0.05" +// ).exponentiatedBy(60) +// new_bid = b.multipliedBy("1.05").multipliedBy( +// BigNumber(1).minus(t60) +// ).plus(min_bid) +// } + +// const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING +// ).minus(lptokens).precision(15, BigNumber.FLOOR) +// return rounded_bid +// } +// +// +// // The price is slightly different if you already hold LP Tokens vs if you +// // have to make a deposit, because the deposit causes more LP Tokens to be +// // issued, which increases the minimum bid. +// const lp_auction_price = auctionPrice(old_bid, time_interval, +// full_trading_fee, lpt.value +// ).precision(15) +// console.log(`Auction price for current LPs: ${lp_auction_price} LP Tokens`) diff --git a/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md b/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md index b9556d387f..5843122863 100644 --- a/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md +++ b/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md @@ -10,7 +10,7 @@ This tutorial does not exhaustively cover all possible market conditions and cir - 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 basic usage of the [xrpl.js client library](https://github.com/XRPLF/xrpl.js/). See [Get Started Using JavaScript](../build-apps/get-started.md) for setup steps. -- The AMM for the asset pair you want to trade must already be created. This tutorial uses an AMM on Testnet which has been set up in advance. For instructions on creating an AMM for a different currency pair, see [Create an Automated Market Maker](../../how-tos/use-tokens/create-an-automated-market-maker.md). +- The AMM for the asset pair you want to trade must already exist in the ledger. This tutorial uses an AMM on Testnet which has been set up in advance. For instructions on creating an AMM for a different currency pair, see [Create an Automated Market Maker](../../how-tos/use-tokens/create-an-automated-market-maker.md). ## Background @@ -20,9 +20,8 @@ This tutorial uses an AMM instance that has been set up on the XRP Ledger Testne | Currency Code | Issuer | Notes | |---|---|---| | XRP | N/A | Testnet XRP is functionally similar to XRP, but holds no real-world value. You can get it for free from a [faucet](/resources/dev-tools/xrp-faucets). -| TST | `rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd` | A test token pegged to XRP at a rate of approximately 10 XRP per 1 TST. The issuer has existing Offers on the XRP Ledger Testnet to buy and sell these tokens. | +| TST | `rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd` | A test token pegged to XRP at a rate of approximately 10 XRP per 1 TST. The issuer has existing Offers on the XRP Ledger Testnet to buy and sell these tokens. This token has no [transfer fee](../../../concepts/tokens/transfer-fees.md) or [Tick Size](../../../concepts/tokens/decentralized-exchange/ticksize.md) set. | -TST can be issued and redeemed through _backstop_ Offers, managed by its issuer, in the order book part of the DEX. The backstop offers have a spread of ±15%, meaning that it takes 11.5 XRP to buy 1 TST, but it takes 1.15 TST to buy back 10 XRP. The backstop offers have very large quantity, so they are unlikely to be used up, meaning that these represent the worst case scenario exchange rate between these two assets. Also, rP9j... does not have a [Tick Size](../../../concepts/tokens/decentralized-exchange/ticksize.md) set, so exchange rates can use the full 15 digits of precision. If one were set, you would have to do more rounding when calculating what exchange rate you would get. ## Steps @@ -33,34 +32,134 @@ At a high level, the steps involved in using an AMM auction slot to save money o 3. If winning the auction slot is cheaper, use AMMDeposit to acquire some LPTokens and then use AMMBid to spend those tokens on winning the auction slot. 4. Make the intended trade using an OfferCreate transaction. (You could also use a cross-currency Payment transaction, but this tutorial uses OfferCreate.) -This tutorial assumes that you have XRP and want to acquire a fixed amount of TST. The details of the steps will vary in other scenarios, such as if you want to spend a fixed amount of XRP to acquire as much TST as you can, or if a token you are buying or selling has a [transfer fee](../../../concepts/tokens/transfer-fees.md). +For simplicity, this tutorial assumes that you have XRP, you want to acquire a fixed amount of TST in a single trade, and that the entire trade will execute using the AMM. Real-life situations are more complicated. For example, part of your trade may execute by consuming Offers rather than using the AMM, or you may want to do a series of trades over a period of time. If one or both of the assets you are trading has a transfer fee or tick size set, those details can also affect the calculations. + ### 1. Setup -The following code imports necessary libraries, connects to the ledger, and instantiates an XRP Ledger wallet. For this use case, you need a high-precision number library such as Bignumber.js for correctly performing calculations on currency amounts you may find in the ledger. +For this use case, you need a high-precision number library such as Bignumber.js for correctly performing calculations on currency amounts you may find in the ledger. -@@TODO +The following `package.json` file lists the necessary dependencies: -### 2. Estimate the cost of AMM trading fees +{% code-snippet file="/_code-samples/auction-slot/js/package.json" language="json" /%} -Estimating the cost of AMM trading fees can be tricky and the details vary depending on the type of trade you are performing. For example, a "buy" trade that stops after receiving a target amount of an asset should be calculated differently than a "sell" trade, which continues until a target amount of an asset has been spent. Similarly, options such as immediate-or-cancel or limit quality may affect your calculations. +After copying this file, run `npm install` from the same folder to download the appropriate dependencies. -Also, the XRP Ledger's decentralized exchange is always open to other traders using it too, so new transactions can change the current state at the same time that you are doing these calculations and sending your transactions. You must always allow for some amount of _slippage_ (the change in rates between when you checked them and when your transaction executes) as well as the possibility that your transactions may not succeed at all. (For example, someone else might bid a higher amount for the auction slot right as you place your bid.) +Start your JavaScript file with the following code to import dependencies: -The following code estimates the amount that would be paid to an AMM's trading fee for a specific trade (selling XRP, buying TST.rP9j...): +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" before="/* Convert" /%} -@@TODO: - 1. Calculate how much of the trade will use the AMM (as opposed to CLOB DEX) - 2. Calculate how much will be swapped into the AMM - 3. Calculate the full trading fee for the amount to be swapped in - 4. Calculate the reduced trading fee for the amount to be swapped in if you hold the auction slot - 5. Calculate the difference between the full trading fee and the reduced trading fee, in other words your potential savings +After that, you should connect to the network and set up a `Wallet` instance in the usual way. For example: -### 3. Compare with the cost of winning the auction slot +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="async function main()" before="// Look up AMM status" /%} -The cost to win the auction slot depends on how long the current holder has held it and how much they paid, but it's always denominated in LP tokens. To win the auction slot if you currently only have XRP, you'll have to make a single-asset deposit to get LP Tokens. So, the comparison we want to make here is: -@@TODO - 1. Calculate quantity of LP Tokens needed to win the auction slot - 2. Calculate single-asset amount needed to deposit to receive that many LP Tokens - 3. Compare the cost of winning the auction slot vs potential savings calculated in the previous step +### 2. Look Up AMM Status + +To determine if your trade could benefit from the reduced fees of the AMM auction slot, you must first look up the current state of the AMM. To get the latest information, use the `amm_info` method and query the `current` (pending) ledger version. + +**Caution:** The `current` ledger incorporates recently-sent transactions that are likely to be confirmed; it is the most up-to-date picture of the ledger state, but the details may change when the ledger is validated. You can also use the `validated` ledger, which returns only the latest confirmed data. + +The following code snippet reads the `amm_info` result and saves some of the details for later use: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Look up AMM status" before="// Calculate price in XRP" /%} + +### 3. Estimate the cost of AMM trading fees + +Estimating the cost of AMM trading fees can be tricky and the details vary depending on the type of trade you are performing. For example, a "buy" trade that stops after receiving a target amount of an asset should be calculated differently than a "sell" trade, which continues until a target amount of an asset has been spent. + +This tutorial shows how to estimate the cost, in XRP, to buy TST using the AMM. First, define the target amount and check that the AMM can even fulfill the trade: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Calculate price in XRP" before="// Use AMM's SwapOut formula" /%} + +Then, you use the _Swap Out_ formula, defined in the XRPL-0030 specification as formula 10, to estimate how much of one asset you have to swap in to the AMM to receive a target amount out of the other asset. The following function implements the formula: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Implement the AMM SwapOut formula" before="/* Compute the quadratic formula" /%} + +The following helper functions are also needed for Swap Out and other formulas later in the tutorial: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Convert a trading fee to a value that" before="/* Implement the AMM SwapOut formula" /%} + +Then, to estimate the cost of trading fees, calculate the Swap Out value twice: once with the full fee, and once with the discounted fee of the auction slot. The difference between the two results represents the maximum possible savings from the auction slot for this trade, not including the costs of winning the auction slot. + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Use AMM's SwapOut formula" before="// Calculate the cost of winning" /%} + +**Note:** For illustrative purposes, this code assumes that the entire trade will execute using the AMM and not by consuming Offers. In reality, a trade can use both in combination to achieve a better rate. + + +### 4. Calculate the cost of winning the auction slot + +The cost to win the auction slot depends on how long the current holder has held it and how much they paid, but it's always denominated in LP tokens. If you currently only have XRP and you want to win the auction slot, you must first deposit some of your XRP to get LP Tokens. + +The price of winning the auction slot is defined in XLS-0030 section 4.1.1. However, the minimum bid scales with the number of LP Tokens issued. If you calculate the auction price and _then_ deposit exactly enough to pay for it, the auction price increases proportionally to the new LP Tokens you gained, so you need to invert the formula. + +This is similar to cases where you want to deliver exactly $100 after subtracting a 3% fee. If you calculate $100 + (0.03 * $100) = $103, only $99.91 will arrive because the extra $3 is also subject to the fee. Instead, you divide 100 ÷ 0.97 ≈ $103.10 (rounding up to make sure). + +The following function represents the inverted formula, which calculates the total amount of LP Tokens you need to receive to match the auction price: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Calculate how much to deposit" before="async function main()" /%} + +Then, you call the function like this: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Calculate the cost of winning the auction slot" before="// Calculate how much XRP to deposit" /%} + +### 5. Compare the costs and savings + +The previous step gives a cost for the auction slot in the form of LP Tokens. To compare against your potential savings, you need to convert this to the XRP cost of the deposit. If the XRP cost of making the deposit and winning the auction slot is greater than your savings, then you should not go through with it. + +To do the conversion, you use the AMM _Asset In_ formula, which is implemented as follows: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Implement the AMM single-asset deposit" before="/* Calculate the necessary bid" /%} + +In addition to the `feeMult(t)` and `feeMultHalf(t)` helper functions defined earlier, this formula needs to solve the quadratic equation. (You may remember the quadratic formula from high-school mathematics.) The following code implements a helper function for this: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Compute the quadratic formula." before="/* Implement the AMM single-asset deposit" /%} + +The following code uses the Asset In formula to estimate the cost of the deposit: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Calculate how much XRP to deposit" before="// Optional. Allow for costs" /%} + +Since the XRP Ledger's decentralized exchange is always open to other traders using it too, new transactions can change the current state at the same time that you are doing these calculations and sending your own transactions. You should allow for some amount of _slippage_, the change in rates between when you checked them and when your transaction executes. The following example allows up to 1% slippage: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Optional. Allow for costs" before="// Compare price of deposit+bid" /%} + +Finally, you take the slippage-adjusted cost in XRP, add the transaction costs in XRP burned for sending two transactions, and compare that total to the potential savings calculated back in step 3. If the total cost is higher than the savings, you won't save money using the auction slot, so you stop here. + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Compare price of deposit+bid" before="// Do a single-asset deposit" /%} + +**Tip:** You may still be able to save money if you plan to do additional trades for a larger total amount. Also, if the auction slot is currently occupied, the cost of outbidding the current slot holder decreases over 24 hours, so you can wait and try again later. + +### 6. Send an AMMDeposit transaction to get LP Tokens + +Assuming you determined that you could make money, it's now time to send actual transactions to the XRP Ledger, starting with an [AMMDeposit transaction][] to get the LP Tokens that you'll bid on the auction slot. + +The "One Asset LP Token" deposit type is most convenient here since you can specify exactly how many LP Tokens you want to receive, and you only need to deposit XRP. The following code creates and sends the transaction: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Do a single-asset deposit" before="// Actually bid" /%} + +After the transaction is (or isn't) confirmed by the consensus process, the code displays the results to the console. + +### 7. Send an AMMBid transaction to win the auction slot + +Assuming the previous transaction was successful, the next step is to use the LP Tokens to bid on the auction slot. To do this, you send an [AMMBid transaction][] with the slippage-adjusted bid amount you calculated earlier in the `BidMax` field, as in the following code: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Actually bid" before="// Trade using the discount" /%} + +**Tip:** The amounts that LP Tokens get rounded can be surprising. If you don't plan on holding LP Tokens after bidding, you should set `BidMin` to the same as `BidMax` so that you aren't left with a trust line that contains a very tiny amount of LP Tokens that weren't spent on the auction price, and you don't have to meet the XRP reserve for that trust line. + +### 8. Trade using the discount + +If your previous transaction was successful, you should now be the auction slot holder until you are outbid or 24 hours have passed, whichever comes first. You can immediately use this opportunity to make the XRP for TST trade that you wanted to make in the first place. There are several ways to make trades in the XRP Ledger, but sending an OfferCreate transaction is the most conventional, as in the following code: + +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Trade using the discount" before="// Done" /%} + +**Tip:** If you are outbid before the auction slot expires, you'll get some of your LP Tokens refunded based on how much time you had left. You can use an [AMMWithdraw transaction][] to convert those to additional XRP, TST, or both as desired. + +## Conclusion + +Trading assets is never risk-free, and Automated Market Makers are no exception. However, the XRP Ledger's fast, low-cost transactions can be helpful in reducing the costs and fees associated with currency exchange. This tutorial demonstrates a minimal approach to using the auction slot to save money, but of course more creative uses are possible. + +The details depend on your specific circumstances and the types of trades you are doing, or expect to do in the future, as well as the state of the market, the XRP Ledger network, and the AMM instances in particular. See the [Code Samples](/resources/code-samples) for additional related use cases, and feel free to contribute your own samples as well to show the community what can be done on the XRP Ledger. + + +{% raw-partial file="/docs/_snippets/common-links.md" /%} From 169e5349ec5af4be6b726e2001d594f3bcf52033 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Wed, 8 May 2024 17:20:42 -0700 Subject: [PATCH 7/8] Auction slot tutorial: improve structure --- _code-samples/auction-slot/js/amm-formulas.js | 163 +++++++++++++++++ _code-samples/auction-slot/js/auction-slot.js | 164 +----------------- .../javascript/trade-on-ledger/index.md | 9 + .../use-amm-auction-slot-for-lower-fees.md | 118 +++++++++---- sidebars.yaml | 4 + 5 files changed, 264 insertions(+), 194 deletions(-) create mode 100644 _code-samples/auction-slot/js/amm-formulas.js create mode 100644 docs/tutorials/javascript/trade-on-ledger/index.md diff --git a/_code-samples/auction-slot/js/amm-formulas.js b/_code-samples/auction-slot/js/amm-formulas.js new file mode 100644 index 0000000000..82c89923d0 --- /dev/null +++ b/_code-samples/auction-slot/js/amm-formulas.js @@ -0,0 +1,163 @@ +const BigNumber = require('bignumber.js') + +/* Convert a trading fee to a value that can be multiplied + * by a total to "subtract" the fee from the total. + * @param tFee int {0, 1000} + * such that 1 = 1/100,000 and 1000 = 1% fee + * @returns BigNumber (1 - fee) as a decimal + */ +function feeMult(tFee) { + return BigNumber(1).minus( feeDecimal(tFee) ) +} + +/* Same as feeMult, but with half the trading fee. Single-asset deposits and + * withdrawals use this because half of the deposit is treated as being + * "swapped" for the other asset in the AMM's pool. + * @param tFee int {0, 1000} + * such that 1 = 1/100,000 and 1000 = 1% fee + * @returns BigNumber (1 - (fee/2)) as a decimal + */ +function feeMultHalf(tFee) { + return BigNumber(1).minus( feeDecimal(tFee).dividedBy(2) ) +} + +/* Convert a trading fee to a decimal BigNumber value, + * for example 1000 becomes 0.01 + * @param tFee int {0, 1000} + * such that 1 = 1/100,000 and 1000 = 1% fee + * @returns BigNumber(fee) as a decimal + */ +function feeDecimal(tFee) { + const AUCTION_SLOT_FEE_SCALE_FACTOR = 100000 + return BigNumber(tFee).dividedBy(AUCTION_SLOT_FEE_SCALE_FACTOR) +} + +/* Implement the AMM SwapOut formula, as defined in XLS-30 section 2.4 AMM + * Swap, formula 10. The asset weights WA/WB are currently always 1/1 so + * they're canceled out. + * C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/AMMHelpers.h#L253-L258 + * @param asset_out_bn BigNumber - The target amount to receive from the AMM. + * @param pool_in_bn BigNumber - The amount of the input asset in the AMM's + * pool before the swap. + * @param pool_out_bn BigNumber - The amount of the output asset in the AMM's + * pool before the swap. + * @param trading_fee int - The trading fee as an integer {0, 1000} where 1000 + * represents a 1% fee. + * @returns BigNumber - The amount of the input asset that must be swapped in + * to receive the target output amount. Unrounded, because + * the number of decimals depends on if this is drops of + * XRP or a decimal amount of a token; since this is a + * theoretical input to the pool, it should be rounded + * up (ceiling) to preserve the pool's constant product. + */ +function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) { + return ( ( pool_in_bn.multipliedBy(pool_out_bn) ).dividedBy( + pool_out_bn.minus(asset_out_bn) + ).minus(pool_in_bn) + ).dividedBy(feeMult(trading_fee)) +} + +/* Compute the quadratic formula. Helper function for ammAssetIn. + * Params and return value are BigNumber instances. + */ +function solveQuadraticEq(a,b,c) { + const b2minus4ac = b.multipliedBy(b).minus( + a.multipliedBy(c).multipliedBy(4) + ) + return ( b.negated().plus(b2minus4ac.sqrt()) ).dividedBy(a.multipliedBy(2)) +} + +/* Implement the AMM single-asset deposit formula to calculate how much to + * put in so that you receive a specific number of LP Tokens back. + * C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/impl/AMMHelpers.cpp#L55-L83 + * @param pool_in string - Quantity of input asset the pool already has + * @param lpt_balance string - Quantity of LP Tokens already issued by the AMM + * @param desired_lpt string - Quantity of new LP Tokens you want to receive + * @param trading_fee int - The trading fee as an integer {0,1000} where 1000 + * represents a 1% fee. + */ +function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) { + // convert inputs to BigNumber + const lpTokens = BigNumber(desired_lpt) + const lptAMMBalance = BigNumber(lpt_balance) + const asset1Balance = BigNumber(pool_in) + + const f1 = feeMult(trading_fee) + const f2 = feeMultHalf(trading_fee).dividedBy(f1) + const t1 = lpTokens.dividedBy(lptAMMBalance) + const t2 = t1.plus(1) + const d = f2.minus( t1.dividedBy(t2) ) + const a = BigNumber(1).dividedBy( t2.multipliedBy(t2)) + const b = BigNumber(2).multipliedBy(d).dividedBy(t2).minus( + BigNumber(1).dividedBy(f1) + ) + const c = d.multipliedBy(d).minus( f2.multipliedBy(f2) ) + return asset1Balance.multipliedBy(solveQuadraticEq(a,b,c)) +} + +/* Calculate how much to deposit, in terms of LP Tokens out, to be able to win + * the auction slot. This is based on the slot pricing algorithm defined in + * XLS-30 section 4.1.1, but factors in the increase in the minimum bid as a + * result of having new LP Tokens issued to you from your deposit. + */ +function auctionDeposit(old_bid, time_interval, trading_fee, lpt_balance) { + const tfee_decimal = feeDecimal(trading_fee) + const lptokens = BigNumber(lpt_balance) + const b = BigNumber(old_bid) + let outbidAmount = BigNumber(0) // This is the case if time_interval >= 20 + if (time_interval == 0) { + outbidAmount = b.multipliedBy("1.05") + } else if (time_interval <= 19) { + const t60 = BigNumber(time_interval).multipliedBy("0.05").exponentiatedBy(60) + outbidAmount = b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60)) + } + + const new_bid = lptokens.plus(outbidAmount).dividedBy( + BigNumber(25).dividedBy(tfee_decimal).minus(1) + ).plus(outbidAmount) + + // Significant digits for the deposit are limited by total LPTokens issued + // so we calculate lptokens + deposit - lptokens to determine where the + // rounding occurs. We use ceiling/floor to make sure the amount we receive + // after rounding is still enough to win the auction slot. + const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING + ).minus(lptokens).precision(15, BigNumber.FLOOR) + return rounded_bid +} + +/* Calculate the necessary bid to win the AMM Auction slot, per the pricing + * algorithm defined in XLS-30 section 4.1.1, if you already hold LP Tokens. + * + * NOT USED in the Auction Slot tutorial, which assumes the user does not hold + * any LP Tokens. + * + * @returns BigNumber - the minimum amount of LP tokens to win the auction slot + */ +function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) { + const tfee_decimal = feeDecimal(trading_fee) + const lptokens = BigNumber(lpt_balance) + const min_bid = lptokens.multipliedBy(tfee_decimal).dividedBy(25) + const b = BigNumber(old_bid) + let new_bid = min_bid + + if (time_interval == 0) { + new_bid = b.multipliedBy("1.05").plus(min_bid) + } else if (time_interval <= 19) { + const t60 = BigNumber(time_interval).multipliedBy("0.05" + ).exponentiatedBy(60) + new_bid = b.multipliedBy("1.05").multipliedBy( + BigNumber(1).minus(t60) + ).plus(min_bid) + } + + const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING + ).minus(lptokens).precision(15, BigNumber.FLOOR) + return rounded_bid +} + +module.exports = { + "auctionDeposit": auctionDeposit, + "auctionPrice": auctionPrice, + "ammAssetIn": ammAssetIn, + "swapOut": swapOut, +} diff --git a/_code-samples/auction-slot/js/auction-slot.js b/_code-samples/auction-slot/js/auction-slot.js index 9835117f1e..5d1b0d2e7e 100644 --- a/_code-samples/auction-slot/js/auction-slot.js +++ b/_code-samples/auction-slot/js/auction-slot.js @@ -1,129 +1,6 @@ const xrpl = require('xrpl') const BigNumber = require('bignumber.js') - -/* Convert a trading fee to a value that can be multiplied - * by a total to "subtract" the fee from the total. - * @param tFee int {0, 1000} - * such that 1 = 1/100,000 and 1000 = 1% fee - * @returns BigNumber (1 - fee) as a decimal - */ -function feeMult(tFee) { - return BigNumber(1).minus( feeDecimal(tFee) ) -} - -/* Same as feeMult, but with half the trading fee. - * @param tFee int {0, 1000} - * such that 1 = 1/100,000 and 1000 = 1% fee - * @returns BigNumber (1 - (fee/2)) as a decimal - */ -function feeMultHalf(tFee) { - return BigNumber(1).minus( feeDecimal(tFee).dividedBy(2) ) -} - -/* Convert a trading fee to a decimal BigNumber value, - * for example 1000 becomes 0.01 - * @param tFee int {0, 1000} - * such that 1 = 1/100,000 and 1000 = 1% fee - * @returns BigNumber(fee) as a decimal - */ -function feeDecimal(tFee) { - const AUCTION_SLOT_FEE_SCALE_FACTOR = 100000 - return BigNumber(tFee).dividedBy(AUCTION_SLOT_FEE_SCALE_FACTOR) -} - -/* Implement the AMM SwapOut formula, as defined in XLS-30 section 2.4 AMM - * Swap, formula 10. The asset weights WA/WB are currently always 1/1 so - * they're canceled out. - * C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/AMMHelpers.h#L253-L258 - * @param asset_out_bn BigNumber - The target amount to receive from the AMM. - * @param pool_in_bn BigNumber - The amount of the input asset in the AMM's - * pool before the swap. - * @param pool_out_bn BigNumber - The amount of the output asset in the AMM's - * pool before the swap. - * @param trading_fee int - The trading fee as an integer {0, 1000} where 1000 - * represents a 1% fee. - * @returns BigNumber - The amount of the input asset that must be swapped in - * to receive the target output amount. Unrounded, because - * the number of decimals depends on if this is drops of - * XRP or a decimal amount of a token; since this is a - * theoretical input to the pool, it should be rounded - * up (ceiling) to preserve the pool's constant product. - */ -function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) { - return ( ( pool_in_bn.multipliedBy(pool_out_bn) ).dividedBy( - pool_out_bn.minus(asset_out_bn) - ).minus(pool_in_bn) - ).dividedBy(feeMult(trading_fee)) -} - -/* Compute the quadratic formula. Helper function for ammAssetIn. - * Params and return value are BigNumber instances. - */ -function solveQuadraticEq(a,b,c) { - const b2minus4ac = b.multipliedBy(b).minus( - a.multipliedBy(c).multipliedBy(4) - ) - return ( b.negated().plus(b2minus4ac.sqrt()) ).dividedBy(a.multipliedBy(2)) -} - -/* Implement the AMM single-asset deposit formula to calculate how much to - * put in so that you receive a specific number of LP Tokens back. - * C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/impl/AMMHelpers.cpp#L55-L83 - * @param pool_in string - Quantity of input asset the pool already has - * @param lpt_balance string - Quantity of LP Tokens already issued by the AMM - * @param desired_lpt string - Quantity of new LP Tokens you want to receive - * @param trading_fee int - The trading fee as an integer {0,1000} where 1000 - * represents a 1% fee. - */ -function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) { - // convert inputs to BigNumber - const lpTokens = BigNumber(desired_lpt) - const lptAMMBalance = BigNumber(lpt_balance) - const asset1Balance = BigNumber(pool_in) - - const f1 = feeMult(trading_fee) - const f2 = feeMultHalf(trading_fee).dividedBy(f1) - const t1 = lpTokens.dividedBy(lptAMMBalance) - const t2 = t1.plus(1) - const d = f2.minus( t1.dividedBy(t2) ) - const a = BigNumber(1).dividedBy( t2.multipliedBy(t2)) - const b = BigNumber(2).multipliedBy(d).dividedBy(t2).minus( - BigNumber(1).dividedBy(f1) - ) - const c = d.multipliedBy(d).minus( f2.multipliedBy(f2) ) - return asset1Balance.multipliedBy(solveQuadraticEq(a,b,c)) -} - -/* Calculate how much to deposit, in terms of LP Tokens out, to be able to win - * the auction slot. This is based on the slot pricing algorithm defined in - * XLS-30 section 4.1.1, but factors in the increase in the minimum bid as a - * result of having new LP Tokens issued to you from your deposit. - */ -function auctionDeposit(old_bid, time_interval, trading_fee, lpt_balance) { - const tfee_decimal = feeDecimal(trading_fee) - const lptokens = BigNumber(lpt_balance) - const b = BigNumber(old_bid) - let outbidAmount = BigNumber(0) // This is the case if time_interval >= 20 - if (time_interval == 0) { - outbidAmount = b.multipliedBy("1.05") - } else if (time_interval <= 19) { - const t60 = BigNumber(time_interval).multipliedBy("0.05").exponentiatedBy(60) - outbidAmount = b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60)) - } - - const new_bid = lptokens.plus(outbidAmount).dividedBy( - BigNumber(25).dividedBy(tfee_decimal).minus(1) - ).plus(outbidAmount) - - // Significant digits for the deposit are limited by total LPTokens issued - // so we calculate lptokens + deposit - lptokens to determine where the - // rounding occurs. We use ceiling/floor to make sure the amount we receive - // after rounding is still enough to win the auction slot. - const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING - ).minus(lptokens).precision(15, BigNumber.FLOOR) - return rounded_bid -} - +const {auctionDeposit, ammAssetIn, swapOut} = require("./amm-formulas.js") async function main() { // Connect ---------------------------------------------------------------- @@ -291,42 +168,3 @@ async function main() { } // End of main() main() - -/// TODO: move everything below here to a separate code sample with minimal setup to make it work: - -// /* Calculate the necessary bid to win the AMM Auction slot, per the pricing -// * algorithm defined in XLS-30 section 4.1.1, if you already hold LP Tokens. -// * Not useful in the case where you need to make a deposit to get LP Tokens, -// * because doing so causes more LP Tokens to be issued, changing the min bid. -// * @returns BigNumber - the minimum amount of LP tokens to win the auction slot -// */ -// function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) { -// const tfee_decimal = feeDecimal(trading_fee) -// const lptokens = BigNumber(lpt_balance) -// const min_bid = lptokens.multipliedBy(tfee_decimal).dividedBy(25) -// const b = BigNumber(old_bid) -// let new_bid = min_bid - -// if (time_interval == 0) { -// new_bid = b.multipliedBy("1.05").plus(min_bid) -// } else if (time_interval <= 19) { -// const t60 = BigNumber(time_interval).multipliedBy("0.05" -// ).exponentiatedBy(60) -// new_bid = b.multipliedBy("1.05").multipliedBy( -// BigNumber(1).minus(t60) -// ).plus(min_bid) -// } - -// const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING -// ).minus(lptokens).precision(15, BigNumber.FLOOR) -// return rounded_bid -// } -// -// -// // The price is slightly different if you already hold LP Tokens vs if you -// // have to make a deposit, because the deposit causes more LP Tokens to be -// // issued, which increases the minimum bid. -// const lp_auction_price = auctionPrice(old_bid, time_interval, -// full_trading_fee, lpt.value -// ).precision(15) -// console.log(`Auction price for current LPs: ${lp_auction_price} LP Tokens`) diff --git a/docs/tutorials/javascript/trade-on-ledger/index.md b/docs/tutorials/javascript/trade-on-ledger/index.md new file mode 100644 index 0000000000..73dd4176c9 --- /dev/null +++ b/docs/tutorials/javascript/trade-on-ledger/index.md @@ -0,0 +1,9 @@ +--- +metadata: + indexPage: true +--- +# Trade On-Ledger + +Use the XRP Ledger's built-in Decentralized Exchange (DEX) to trade assets. + +{% child-pages /%} diff --git a/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md b/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md index 5843122863..5e9b02a324 100644 --- a/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md +++ b/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md @@ -30,14 +30,18 @@ At a high level, the steps involved in using an AMM auction slot to save money o 1. Check the current XRP Ledger state to estimate how much your desired trade would cost in AMM trading fees. 2. Compare against the cost to win the current auction slot. 3. If winning the auction slot is cheaper, use AMMDeposit to acquire some LPTokens and then use AMMBid to spend those tokens on winning the auction slot. -4. Make the intended trade using an OfferCreate transaction. (You could also use a cross-currency Payment transaction, but this tutorial uses OfferCreate.) +4. Make the intended trade using an OfferCreate transaction. For simplicity, this tutorial assumes that you have XRP, you want to acquire a fixed amount of TST in a single trade, and that the entire trade will execute using the AMM. Real-life situations are more complicated. For example, part of your trade may execute by consuming Offers rather than using the AMM, or you may want to do a series of trades over a period of time. If one or both of the assets you are trading has a transfer fee or tick size set, those details can also affect the calculations. +### Source Code + +See {% repo-link path="_code-samples/auction-slot/js/" %}Code Samples: Auction Slot{% /repo-link %} for the full source code for this tutorial. + ### 1. Setup -For this use case, you need a high-precision number library such as Bignumber.js for correctly performing calculations on currency amounts you may find in the ledger. +For this use case, you need a high-precision number library such as [Bignumber.js](https://mikemcl.github.io/bignumber.js/) for correctly performing calculations on currency amounts you may find in the ledger. The following `package.json` file lists the necessary dependencies: @@ -47,7 +51,9 @@ After copying this file, run `npm install` from the same folder to download the Start your JavaScript file with the following code to import dependencies: -{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" before="/* Convert" /%} +{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" before="async function main()" /%} + +You should also create the `amm-formulas.js` file, which defines several AMM-related helper functions. See the [Appendix](#appendix-amm-formulas) for the implementations of these functions. After that, you should connect to the network and set up a `Wallet` instance in the usual way. For example: @@ -58,64 +64,52 @@ After that, you should connect to the network and set up a `Wallet` instance in To determine if your trade could benefit from the reduced fees of the AMM auction slot, you must first look up the current state of the AMM. To get the latest information, use the `amm_info` method and query the `current` (pending) ledger version. -**Caution:** The `current` ledger incorporates recently-sent transactions that are likely to be confirmed; it is the most up-to-date picture of the ledger state, but the details may change when the ledger is validated. You can also use the `validated` ledger, which returns only the latest confirmed data. +{% admonition type="warning" name="Caution" %}The `current` ledger incorporates recently-sent transactions that are likely to be confirmed; it is the most up-to-date picture of the ledger state, but the details may change when the ledger is validated. You can also use the `validated` ledger, which returns only the latest confirmed data.{% /admonition %} The following code snippet reads the `amm_info` result and saves some of the details for later use: {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Look up AMM status" before="// Calculate price in XRP" /%} + ### 3. Estimate the cost of AMM trading fees -Estimating the cost of AMM trading fees can be tricky and the details vary depending on the type of trade you are performing. For example, a "buy" trade that stops after receiving a target amount of an asset should be calculated differently than a "sell" trade, which continues until a target amount of an asset has been spent. +This tutorial shows how to estimate the cost, in XRP, to buy a fixed amount of TST using the AMM. The calculations are different for other types of trades, such as a "sell" trade that buys as much TST as possible with a fixed amount of XRP, or for assets with other complications such as transfer fees. -This tutorial shows how to estimate the cost, in XRP, to buy TST using the AMM. First, define the target amount and check that the AMM can even fulfill the trade: +First, define the target amount of TST and check that the AMM can even fulfill the trade: {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Calculate price in XRP" before="// Use AMM's SwapOut formula" /%} -Then, you use the _Swap Out_ formula, defined in the XRPL-0030 specification as formula 10, to estimate how much of one asset you have to swap in to the AMM to receive a target amount out of the other asset. The following function implements the formula: +Then, you use the AMM _SwapOut_ formula to calculate how much XRP you need to pay to receive the target amount of TST out. See [SwapOut in the Appendix](#swapout) for the implementation of this formula. -{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Implement the AMM SwapOut formula" before="/* Compute the quadratic formula" /%} - -The following helper functions are also needed for Swap Out and other formulas later in the tutorial: - -{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Convert a trading fee to a value that" before="/* Implement the AMM SwapOut formula" /%} - -Then, to estimate the cost of trading fees, calculate the Swap Out value twice: once with the full fee, and once with the discounted fee of the auction slot. The difference between the two results represents the maximum possible savings from the auction slot for this trade, not including the costs of winning the auction slot. +To estimate the cost of trading fees, call SwapOut twice: once with the full fee, and once with the discounted fee of the auction slot. The difference between the two represents the maximum possible savings from the auction slot for this trade, not including the costs of winning the auction slot. {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Use AMM's SwapOut formula" before="// Calculate the cost of winning" /%} -**Note:** For illustrative purposes, this code assumes that the entire trade will execute using the AMM and not by consuming Offers. In reality, a trade can use both in combination to achieve a better rate. +{% admonition type="info" name="Note" %}For illustrative purposes, this code assumes that the entire trade will execute using the AMM and not by consuming Offers. In reality, a trade can use both in combination to achieve a better rate.{% /admonition %} ### 4. Calculate the cost of winning the auction slot The cost to win the auction slot depends on how long the current holder has held it and how much they paid, but it's always denominated in LP tokens. If you currently only have XRP and you want to win the auction slot, you must first deposit some of your XRP to get LP Tokens. -The price of winning the auction slot is defined in XLS-0030 section 4.1.1. However, the minimum bid scales with the number of LP Tokens issued. If you calculate the auction price and _then_ deposit exactly enough to pay for it, the auction price increases proportionally to the new LP Tokens you gained, so you need to invert the formula. +The price of winning the auction slot is defined in XLS-0030 section 4.1.1. However, the minimum bid scales with the number of LP Tokens issued. If you calculate the auction price and _then_ deposit exactly enough to pay for it, the auction price increases proportionally to the new LP Tokens you gained. This is similar to cases where you want to deliver exactly $100 after subtracting a 3% fee. If you calculate $100 + (0.03 * $100) = $103, only $99.91 will arrive because the extra $3 is also subject to the fee. Instead, you divide 100 ÷ 0.97 ≈ $103.10 (rounding up to make sure). -The following function represents the inverted formula, which calculates the total amount of LP Tokens you need to receive to match the auction price: +The _AuctionDeposit_ formula represents the inverted form of the auction price formula such so that you can calulate how much to deposit to match the auction price. See [Appendix: AuctionDeposit](#auctiondeposit) for the implementation. -{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Calculate how much to deposit" before="async function main()" /%} - -Then, you call the function like this: +You use the function like this: {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Calculate the cost of winning the auction slot" before="// Calculate how much XRP to deposit" /%} + ### 5. Compare the costs and savings The previous step gives a cost for the auction slot in the form of LP Tokens. To compare against your potential savings, you need to convert this to the XRP cost of the deposit. If the XRP cost of making the deposit and winning the auction slot is greater than your savings, then you should not go through with it. -To do the conversion, you use the AMM _Asset In_ formula, which is implemented as follows: +You use the AMM _AssetIn_ formula to estimate how much XRP you have to deposit to receive the target amount of TST. See [Appendix: AssetIn](#assetin) for the implementation. -{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Implement the AMM single-asset deposit" before="/* Calculate the necessary bid" /%} - -In addition to the `feeMult(t)` and `feeMultHalf(t)` helper functions defined earlier, this formula needs to solve the quadratic equation. (You may remember the quadratic formula from high-school mathematics.) The following code implements a helper function for this: - -{% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="/* Compute the quadratic formula." before="/* Implement the AMM single-asset deposit" /%} - -The following code uses the Asset In formula to estimate the cost of the deposit: +The following code uses AssetIn to estimate the cost of the deposit: {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Calculate how much XRP to deposit" before="// Optional. Allow for costs" /%} @@ -127,7 +121,8 @@ Finally, you take the slippage-adjusted cost in XRP, add the transaction costs i {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Compare price of deposit+bid" before="// Do a single-asset deposit" /%} -**Tip:** You may still be able to save money if you plan to do additional trades for a larger total amount. Also, if the auction slot is currently occupied, the cost of outbidding the current slot holder decreases over 24 hours, so you can wait and try again later. +{% admonition type="success" name="Tip" %} You may still be able to save money if you plan to do additional trades for a larger total amount. Also, if the auction slot is currently occupied, the cost of outbidding the current slot holder decreases over 24 hours, so you can wait and try again later.{% /admonition %} + ### 6. Send an AMMDeposit transaction to get LP Tokens @@ -139,13 +134,15 @@ The "One Asset LP Token" deposit type is most convenient here since you can spec After the transaction is (or isn't) confirmed by the consensus process, the code displays the results to the console. + ### 7. Send an AMMBid transaction to win the auction slot Assuming the previous transaction was successful, the next step is to use the LP Tokens to bid on the auction slot. To do this, you send an [AMMBid transaction][] with the slippage-adjusted bid amount you calculated earlier in the `BidMax` field, as in the following code: {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Actually bid" before="// Trade using the discount" /%} -**Tip:** The amounts that LP Tokens get rounded can be surprising. If you don't plan on holding LP Tokens after bidding, you should set `BidMin` to the same as `BidMax` so that you aren't left with a trust line that contains a very tiny amount of LP Tokens that weren't spent on the auction price, and you don't have to meet the XRP reserve for that trust line. +{% admonition type="success" name="Tip" %}The amounts that LP Tokens get rounded can be surprising. If you don't plan on holding LP Tokens after bidding, you should set `BidMin` to the same as `BidMax` so that you aren't left with a trust line that contains a very tiny amount of LP Tokens that weren't spent on the auction price, and you don't have to meet the XRP reserve for that trust line.{% /admonition %} + ### 8. Trade using the discount @@ -153,7 +150,66 @@ If your previous transaction was successful, you should now be the auction slot {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Trade using the discount" before="// Done" /%} -**Tip:** If you are outbid before the auction slot expires, you'll get some of your LP Tokens refunded based on how much time you had left. You can use an [AMMWithdraw transaction][] to convert those to additional XRP, TST, or both as desired. +{% admonition type="success" name="Tip" %}If you are outbid before the auction slot expires, you'll get some of your LP Tokens refunded based on how much time you had left. You can use an [AMMWithdraw transaction][] to convert those to additional XRP, TST, or both as desired.{% /admonition %} + + +## Appendix: AMM Formulas + +The above sample code shows how to use various AMM-related formulas in a particular workflow. Your code must also have implementations of those formulas, which can be in the same file or imported from a separate one as you desire. + +The file {% repo-link path="_code-samples/auction-slot/js/amm-formulas.js" %}`amm-formulas.js`{% /repo-link %} contains the source code for all these formulas. + +### AssetIn + +The _AssetIn_ formula calculates how much of an asset must be deposited to receive a target amount of LP Tokens. + +The following function implements AssetIn in JavaScript: + +{% code-snippet file="/_code-samples/auction-slot/js/amm-formulas.js" language="js" from="/* Implement the AMM single-asset deposit" before="/* Calculate how much to deposit" /%} + +This function depends on the `feeMult`, `feeMultHalf`, and `solveQuadraticEq` helper functions. + +### AuctionDeposit + +The _AuctionDeposit_ function is an inverted form of the formula for the AMM's auction price, taking into account how the minimum bid value changes as LP Tokens are issued. AuctionDeposit calculates the total amount of LP Tokens you need to receive in a deposit if you want to match the auction price: + +{% code-snippet file="/_code-samples/auction-slot/js/amm-formulas.js" language="js" from="/* Calculate how much to deposit" before="/* Calculate the necessary bid" /%} + +### AuctionPrice + +The _AuctionPrice_ function calculates the cost of the auction slot for current LP Token holders. **It is not used in this tutorial,** which assumes the user does not hold LP Tokens, but is presented here for completeness: + +{% code-snippet file="/_code-samples/auction-slot/js/amm-formulas.js" language="js" from="/* Calculate the necessary bid" before="module.exports =" /%} + + +### SwapOut + +The _SwapOut_ formula, defined in the XRPL-0030 specification as formula 10, calculates how much of one asset you have to swap in to the AMM to receive a target amount of the other asset out from the AMM. + +The following function implements SwapOut in JavaScript: + +{% code-snippet file="/_code-samples/auction-slot/js/amm-formulas.js" language="js" from="/* Implement the AMM SwapOut formula" before="/* Compute the quadratic formula" /%} + +### feeDecimal, feeMult, and feeMultHalf + +These helper functions are used in other AMM formulas to convert from a trading fee value in the ledger (integer from 0 to 1000) to a decimal representation that can be multiplied by a total to apply the fee. + +The following functions implement `feeMult`, `feeMultHalf`, and `feeDecimal` in JavaScript: + +{% code-snippet file="/_code-samples/auction-slot/js/amm-formulas.js" language="js" from="/* Convert a trading fee to a value that" before="/* Implement the AMM SwapOut formula" /%} + +The AssetIn and SwapOut functions depend on these helper functions. + +### solveQuadraticEq + +This helper function is solves the quadratic equation, which you may remember from high-school mathematics. + +The following function implements the quadratic equation in JavaScript: + +{% code-snippet file="/_code-samples/auction-slot/js/amm-formulas.js" language="js" from="/* Compute the quadratic formula." before="/* Implement the AMM single-asset deposit" /%} + +The AssetIn formula depends on this. + ## Conclusion diff --git a/sidebars.yaml b/sidebars.yaml index 2b650cbf9f..bcae3fd2f1 100644 --- a/sidebars.yaml +++ b/sidebars.yaml @@ -184,6 +184,10 @@ - page: docs/tutorials/javascript/build-apps/get-started.md - page: docs/tutorials/javascript/build-apps/build-a-browser-wallet-in-javascript.md - page: docs/tutorials/javascript/build-apps/build-a-desktop-wallet-in-javascript.md + - page: docs/tutorials/javascript/trade-on-ledger/index.md + expanded: false + items: + - page: docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md - page: docs/tutorials/python/index.md expanded: false items: From 73586ffade3d735f1f68b3c91504c2e064ff7156 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Fri, 21 Jun 2024 17:30:00 -0700 Subject: [PATCH 8/8] Auction slot tutorial: apply edits per reviews --- .../use-amm-auction-slot-for-lower-fees.md | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md b/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md index 5e9b02a324..4c86f8dc90 100644 --- a/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md +++ b/docs/tutorials/javascript/trade-on-ledger/use-amm-auction-slot-for-lower-fees.md @@ -4,41 +4,40 @@ When trading on the XRP Ledger's decentralized exchange (DEX), you can sometimes For a simpler example of trading currencies in the XRP Ledger's DEX, see [Trade in the Decentralized Exchange](../../how-tos/use-tokens/trade-in-the-decentralized-exchange.md). +{% admonition type="warning" name="Caution" %} This tutorial does not exhaustively cover all possible market conditions and circumstances. Always exercise caution and trade at your own risk. +{% /admonition %} ## 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 basic usage of the [xrpl.js client library](https://github.com/XRPLF/xrpl.js/). See [Get Started Using JavaScript](../build-apps/get-started.md) for setup steps. -- The AMM for the asset pair you want to trade must already exist in the ledger. This tutorial uses an AMM on Testnet which has been set up in advance. For instructions on creating an AMM for a different currency pair, see [Create an Automated Market Maker](../../how-tos/use-tokens/create-an-automated-market-maker.md). +- The AMM for the asset pair you want to trade must already exist in the ledger. This tutorial uses an AMM instance that has been set up on the XRP Ledger Testnet in advance, connecting the following assets: + | Currency Code | Issuer | Notes | + |---|---|---| + | XRP | N/A | Testnet XRP is functionally similar to XRP, but holds no real-world value. You can get it for free from a [faucet](/resources/dev-tools/xrp-faucets). + | TST | `rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd` | A test token pegged to XRP at a rate of approximately 10 XRP per 1 TST. The issuer has existing Offers on the XRP Ledger Testnet to buy and sell these tokens. This token has no [transfer fee](../../../concepts/tokens/transfer-fees.md) or [Tick Size](../../../concepts/tokens/decentralized-exchange/ticksize.md) set. | -## Background + For instructions on creating an AMM for a different currency pair, see [Create an Automated Market Maker](../../how-tos/use-tokens/create-an-automated-market-maker.md). -This tutorial uses an AMM instance that has been set up on the XRP Ledger Testnet in advance, connecting the following assets: +## Source Code -| Currency Code | Issuer | Notes | -|---|---|---| -| XRP | N/A | Testnet XRP is functionally similar to XRP, but holds no real-world value. You can get it for free from a [faucet](/resources/dev-tools/xrp-faucets). -| TST | `rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd` | A test token pegged to XRP at a rate of approximately 10 XRP per 1 TST. The issuer has existing Offers on the XRP Ledger Testnet to buy and sell these tokens. This token has no [transfer fee](../../../concepts/tokens/transfer-fees.md) or [Tick Size](../../../concepts/tokens/decentralized-exchange/ticksize.md) set. | +See {% repo-link path="_code-samples/auction-slot/js/" %}Code Samples: Auction Slot{% /repo-link %} for the full source code for this tutorial. ## Steps At a high level, the steps involved in using an AMM auction slot to save money on trading are as follows: -1. Check the current XRP Ledger state to estimate how much your desired trade would cost in AMM trading fees. -2. Compare against the cost to win the current auction slot. -3. If winning the auction slot is cheaper, use AMMDeposit to acquire some LPTokens and then use AMMBid to spend those tokens on winning the auction slot. -4. Make the intended trade using an OfferCreate transaction. +- **Step 1:** Connect to the ledger so can query the current state. +- **Steps 2-4:** Estimate how much your desired trade would cost in AMM trading fees. +- **Step 5:** Compare against the cost to win the current auction slot. +- **Steps 6-7:** If winning the auction slot is cheaper, use AMMDeposit to acquire some LPTokens and then use AMMBid to spend those tokens on winning the auction slot. +- **Step 8:** Make the intended trade using an OfferCreate transaction. For simplicity, this tutorial assumes that you have XRP, you want to acquire a fixed amount of TST in a single trade, and that the entire trade will execute using the AMM. Real-life situations are more complicated. For example, part of your trade may execute by consuming Offers rather than using the AMM, or you may want to do a series of trades over a period of time. If one or both of the assets you are trading has a transfer fee or tick size set, those details can also affect the calculations. -### Source Code - -See {% repo-link path="_code-samples/auction-slot/js/" %}Code Samples: Auction Slot{% /repo-link %} for the full source code for this tutorial. - - ### 1. Setup For this use case, you need a high-precision number library such as [Bignumber.js](https://mikemcl.github.io/bignumber.js/) for correctly performing calculations on currency amounts you may find in the ledger. @@ -81,7 +80,7 @@ First, define the target amount of TST and check that the AMM can even fulfill t Then, you use the AMM _SwapOut_ formula to calculate how much XRP you need to pay to receive the target amount of TST out. See [SwapOut in the Appendix](#swapout) for the implementation of this formula. -To estimate the cost of trading fees, call SwapOut twice: once with the full fee, and once with the discounted fee of the auction slot. The difference between the two represents the maximum possible savings from the auction slot for this trade, not including the costs of winning the auction slot. +To estimate the cost of trading fees, call SwapOut twice: once with the full fee, and once with the discounted fee of the auction slot. The difference between the two represents the maximum possible savings from the auction slot for this trade. The actual savings will be less because of the costs of winning the auction slot. {% code-snippet file="/_code-samples/auction-slot/js/auction-slot.js" language="js" from="// Use AMM's SwapOut formula" before="// Calculate the cost of winning" /%} @@ -96,7 +95,7 @@ The price of winning the auction slot is defined in XLS-0030 section 4.1.1. Howe This is similar to cases where you want to deliver exactly $100 after subtracting a 3% fee. If you calculate $100 + (0.03 * $100) = $103, only $99.91 will arrive because the extra $3 is also subject to the fee. Instead, you divide 100 ÷ 0.97 ≈ $103.10 (rounding up to make sure). -The _AuctionDeposit_ formula represents the inverted form of the auction price formula such so that you can calulate how much to deposit to match the auction price. See [Appendix: AuctionDeposit](#auctiondeposit) for the implementation. +The _AuctionDeposit_ formula represents the inverted form of the auction price formula so that you can calulate how much to deposit to match the auction price. See [Appendix: AuctionDeposit](#auctiondeposit) for the implementation. You use the function like this: @@ -202,9 +201,7 @@ The AssetIn and SwapOut functions depend on these helper functions. ### solveQuadraticEq -This helper function is solves the quadratic equation, which you may remember from high-school mathematics. - -The following function implements the quadratic equation in JavaScript: +This helper function implements the quadratic equation in JavaScript: {% code-snippet file="/_code-samples/auction-slot/js/amm-formulas.js" language="js" from="/* Compute the quadratic formula." before="/* Implement the AMM single-asset deposit" /%}