From 790e89a360e479474f480b50db927e14ce7d654a Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Thu, 8 Aug 2024 13:30:09 -0700 Subject: [PATCH] create amm and add assets mod tutorials --- _code-samples/amm/README.md | 3 + _code-samples/amm/js/1.create-an-amm.html | 91 +++ _code-samples/amm/js/1.create-an-amm.js | 185 +++++ _code-samples/amm/js/2.add-assets-to-amm.html | 164 +++++ _code-samples/amm/js/2.add-assets-to-amm.js | 637 ++++++++++++++++++ docs/img/add-assets-to-amm.png | Bin 0 -> 45140 bytes docs/img/create-an-amm.png | Bin 0 -> 18130 bytes .../javascript/amm/add-assets-to-amm.md | 95 +++ .../tutorials/javascript/amm/add-lp-to-amm.md | 0 .../tutorials/javascript/amm/create-an-amm.md | 127 +--- package-lock.json | 56 +- package.json | 2 +- sidebars.yaml | 5 + 13 files changed, 1225 insertions(+), 140 deletions(-) create mode 100644 _code-samples/amm/README.md create mode 100644 _code-samples/amm/js/1.create-an-amm.html create mode 100644 _code-samples/amm/js/1.create-an-amm.js create mode 100644 _code-samples/amm/js/2.add-assets-to-amm.html create mode 100644 _code-samples/amm/js/2.add-assets-to-amm.js create mode 100644 docs/img/add-assets-to-amm.png create mode 100644 docs/img/create-an-amm.png create mode 100644 docs/tutorials/javascript/amm/add-assets-to-amm.md delete mode 100644 docs/tutorials/javascript/amm/add-lp-to-amm.md diff --git a/_code-samples/amm/README.md b/_code-samples/amm/README.md new file mode 100644 index 0000000000..7f789b3000 --- /dev/null +++ b/_code-samples/amm/README.md @@ -0,0 +1,3 @@ +# Quickstart Samples + +Create a test harness for AMM features using JavaScript or Python. \ No newline at end of file diff --git a/_code-samples/amm/js/1.create-an-amm.html b/_code-samples/amm/js/1.create-an-amm.html new file mode 100644 index 0000000000..ab3d40b2ee --- /dev/null +++ b/_code-samples/amm/js/1.create-an-amm.html @@ -0,0 +1,91 @@ + + + + AMM Test Harness + + + + + + + + + + + + + +

AMM Test Harness

+
+ + + + +
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ + + + + +
+ +
+ +
+
+
+ + +
+
+ +

+ +

+
+
+
+ + \ No newline at end of file diff --git a/_code-samples/amm/js/1.create-an-amm.js b/_code-samples/amm/js/1.create-an-amm.js new file mode 100644 index 0000000000..0ae5ce8a5e --- /dev/null +++ b/_code-samples/amm/js/1.create-an-amm.js @@ -0,0 +1,185 @@ +// Define client, network, and explorer. + + const WS_URL = 'wss://s.devnet.rippletest.net:51233' + const EXPLORER = 'https://devnet.xrpl.org' + const client = new xrpl.Client(WS_URL); + let wallet = null + let issuer = null + +// Connect to devnet + + async function getAccount() { + + results = 'Connecting to Devnet ...' + resultField.value = results + + await client.connect() + + results += '\nConnected, funding wallet.' + resultField.value = results + + // Fund a new devnet wallet + wallet = (await client.fundWallet()).wallet + + results += '\nPopulated wallet info.' + resultField.value = results + + // Get wallet info. + accountField.value = wallet.address + seedField.value = wallet.seed + balanceField.value = (await client.getXrpBalance(wallet.address)) + + client.disconnect() + + } + +// Issue FOO tokens to Devnet wallet. + +async function issueFOO() { + + // Create a new issuer on Devnet + + results += '\n\nIssuing FOO tokens ...' + resultField.value = results + + await client.connect() + + issuer = (await client.fundWallet()).wallet + + results += `\n\nCreated issuer account: ${issuer.address}` + resultField.value = results + + // Enable issuer DefaultRipple + const issuer_setup_result = await client.submitAndWait({ + "TransactionType": "AccountSet", + "Account": issuer.address, + "SetFlag": xrpl.AccountSetAsfFlags.asfDefaultRipple + }, {autofill: true, wallet: issuer} ) + + results += `\n\nIssuer DefaultRipple enabled: ${EXPLORER}/transactions/${issuer_setup_result.result.hash}` + resultField.value = results + + // Create trust line from wallet to issuer + const trust_result = await client.submitAndWait({ + "TransactionType": "TrustSet", + "Account": wallet.address, + "LimitAmount": { + "currency": "FOO", + "issuer": issuer.address, + "value": "10000000000" // Large limit, arbitrarily chosen + } + }, {autofill: true, wallet: wallet}) + results += `\n\nTrust line created: ${EXPLORER}/transactions/${trust_result.result.hash}` + resultField.value = results + + // Issue tokens + + results += '\n\nSending 1000 FOO tokens ...' + resultField.value = results + + const issue_result = await client.submitAndWait({ + "TransactionType": "Payment", + "Account": issuer.address, + "Amount": { + "currency": "FOO", + "value": "1000", + "issuer": issuer.address + }, + "Destination": wallet.address + }, {autofill: true, wallet: issuer}) + results += `\nTokens issued: ${EXPLORER}/transactions/${issue_result.result.hash}` + resultField.value = results + + walletBalances = (await client.getBalances(wallet.address)) + + const fooCurrency = walletBalances.find(item => item.currency === 'FOO'); + + const fooValue = fooCurrency ? fooCurrency.value : 'Currency not found'; + fooField.value = fooValue + + client.disconnect() + +} + +// Check if AMM exists. + +async function checkAMM() { + + await client.connect() + + results += '\n\nChecking AMM ...' + resultField.value = results + + const amm_info_request = { + "command": "amm_info", + "asset": { + "currency": "XRP" + }, + "asset2": { + "currency": "FOO", + "issuer": issuer.address + }, + "ledger_index": "validated" + } + try { + const amm_info_result = await client.request(amm_info_request) + results += `\n\n ${JSON.stringify(amm_info_result.result.amm, null, 2)}` + } catch(err) { + if (err.data.error === 'actNotFound') { + results += (`\nNo AMM exists for the pair FOO / XRP.`) + } else { + throw(err) + } + } + + resultField.value = results + + client.disconnect() + +} + +// Create new AMM. + +async function createAMM() { + +// This example assumes that 10 XRP ≈ 100 FOO in value. + + await client.connect() + + results += '\n\nCreating AMM ...' + resultField.value = results + + // AMMCreate requires burning one owner reserve. We can look up that amount + // (in drops) on the current network using server_state: + const ss = await client.request({"command": "server_state"}) + const amm_fee_drops = ss.result.state.validated_ledger.reserve_inc.toString() + + const ammcreate_result = await client.submitAndWait({ + "TransactionType": "AMMCreate", + "Account": wallet.address, + "Amount": { + "currency": "FOO", + "issuer": issuer.address, + "value": "500" + }, + "Amount2": "50000000", // 50 XRP in drops + "TradingFee": 500, // 0.5% + "Fee": amm_fee_drops, + }, {autofill: true, wallet: wallet, fail_hard: true}) + // Use fail_hard so you don't waste the tx cost if you mess up + results += `\n\nAMM created: ${EXPLORER}/transactions/${ammcreate_result.result.hash}` + resultField.value = results + + // Update balances + balanceField.value = (await client.getXrpBalance(wallet.address)) + walletBalances = (await client.getBalances(wallet.address)) + + const fooCurrency = walletBalances.find(item => item.currency === 'FOO'); + + const fooValue = fooCurrency ? fooCurrency.value : 'Currency not found'; + fooField.value = fooValue + + + client.disconnect() + +} \ No newline at end of file diff --git a/_code-samples/amm/js/2.add-assets-to-amm.html b/_code-samples/amm/js/2.add-assets-to-amm.html new file mode 100644 index 0000000000..0b416f77a7 --- /dev/null +++ b/_code-samples/amm/js/2.add-assets-to-amm.html @@ -0,0 +1,164 @@ + + + + AMM Test Harness + + + + + + + + + + + + + +

AMM Test Harness

+
+ + + + +
+ + + + + + +
+ + + + + + + + + + + +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ + + + + +
+ +
+ +
+
+ + +
+ + + + + + + + + + + +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ + + + + +
+ +
+ +
+
+
+ + + + +
+ + + +
+ + +
+ + + + +
+ +
+ +
+ + +
+
+ +

+ +

+ +

+ +

+
+
+
+ + \ No newline at end of file diff --git a/_code-samples/amm/js/2.add-assets-to-amm.js b/_code-samples/amm/js/2.add-assets-to-amm.js new file mode 100644 index 0000000000..88a67b66d6 --- /dev/null +++ b/_code-samples/amm/js/2.add-assets-to-amm.js @@ -0,0 +1,637 @@ +// Define client, network, and explorer. + +const WS_URL = 'wss://s.devnet.rippletest.net:51233' +const EXPLORER = 'https://devnet.xrpl.org' +const client = new xrpl.Client(WS_URL); +let wallet = null +let issuer = null +let wallet2 = null + +// Connect to devnet. + +async function getAccount() { + + results = 'Connecting to Devnet ...' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + await client.connect() + + results += '\n\nConnected, funding wallet.' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + +// Fund a new devnet wallet + wallet = (await client.fundWallet()).wallet + + results += '\n\nPopulated wallet info.' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + +// Get wallet info. + accountField.value = wallet.address + seedField.value = wallet.seed + balanceField.value = (await client.getXrpBalance(wallet.address)) + + client.disconnect() + +} + +// Issue FOO tokens to Devnet wallet. + +async function issueFOO() { + +// Create a new issuer on Devnet + + results += '\n\nIssuing FOO tokens ...' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + await client.connect() + + issuer = (await client.fundWallet()).wallet + + results += `\n\nCreated issuer account: ${issuer.address}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + // Enable issuer DefaultRipple + const issuer_setup_result = await client.submitAndWait({ + "TransactionType": "AccountSet", + "Account": issuer.address, + "SetFlag": xrpl.AccountSetAsfFlags.asfDefaultRipple + }, {autofill: true, wallet: issuer} ) + + results += `\n\nIssuer DefaultRipple enabled: ${EXPLORER}/transactions/${issuer_setup_result.result.hash}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + // Create trust line from wallet to issuer + const trust_result = await client.submitAndWait({ + "TransactionType": "TrustSet", + "Account": wallet.address, + "LimitAmount": { + "currency": "FOO", + "issuer": issuer.address, + "value": "10000000000" // Large limit, arbitrarily chosen + } + }, {autofill: true, wallet: wallet}) + results += `\n\nTrust line created: ${EXPLORER}/transactions/${trust_result.result.hash}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + // Issue tokens + + results += '\n\nSending 1000 FOO tokens ...' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const issue_result = await client.submitAndWait({ + "TransactionType": "Payment", + "Account": issuer.address, + "Amount": { + "currency": "FOO", + "value": "1000", + "issuer": issuer.address + }, + "Destination": wallet.address + }, {autofill: true, wallet: issuer}) + results += `\n\nTokens issued: ${EXPLORER}/transactions/${issue_result.result.hash}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + walletBalances = (await client.getBalances(wallet.address)) + + const fooCurrency = walletBalances.find(item => item.currency === 'FOO'); + + const fooValue = fooCurrency ? fooCurrency.value : 'Currency not found'; + fooField.value = fooValue + + client.disconnect() + +} + +// Check if AMM exists. + +async function checkAMM() { + + await client.connect() + + results += '\n\nUpdating AMM Info ...' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const amm_info_request = { + "command": "amm_info", + "asset": { + "currency": "XRP" + }, + "asset2": { + "currency": "FOO", + "issuer": issuer.address + }, + "ledger_index": "validated" + } + try { + const amm_info_result = await client.request(amm_info_request) + ammInfo = `${JSON.stringify(amm_info_result.result.amm, null, 2)}` + } catch(err) { + if (err.data.error === 'actNotFound') { + ammInfo = (`No AMM exists for the pair FOO / XRP.`) + } else { + results += `\n\n${err}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + } + } + + ammInfoField.value = ammInfo + + // Update AMM info box. + + + + client.disconnect() + +} + +// Create new AMM. + +async function createAMM() { + +// This example assumes that 10 XRP ≈ 100 FOO in value. + + await client.connect() + + results += '\n\nCreating AMM ...' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + // AMMCreate requires burning one owner reserve. We can look up that amount + // (in drops) on the current network using server_state: + const ss = await client.request({"command": "server_state"}) + const amm_fee_drops = ss.result.state.validated_ledger.reserve_inc.toString() + + const ammcreate_result = await client.submitAndWait({ + "TransactionType": "AMMCreate", + "Account": wallet.address, + "Amount": { + "currency": "FOO", + "issuer": issuer.address, + "value": "500" + }, + "Amount2": "50000000", // 50 XRP in drops + "TradingFee": 500, // 0.5% + "Fee": amm_fee_drops, + }, {autofill: true, wallet: wallet, fail_hard: true}) + // Use fail_hard so you don't waste the tx cost if you mess up + results += `\n\nAMM created: ${EXPLORER}/transactions/${ammcreate_result.result.hash}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + // Update balances + balanceField.value = (await client.getXrpBalance(wallet.address)) + walletBalances = (await client.getBalances(wallet.address)) + + const fooCurrency = walletBalances.find(item => item.currency === 'FOO'); + + const fooValue = fooCurrency ? fooCurrency.value : 'Currency not found'; + fooField.value = fooValue + + await updateAMM() + + client.disconnect() + +} + +// Fund second wallet. + +async function getAccount2() { + + await client.connect() + + results += '\n\nFunding second wallet ...' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + +// Fund a new devnet wallet + wallet2 = (await client.fundWallet()).wallet + + results += '\n\nPopulated second wallet info.' + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + +// Get second wallet info. + accountField2.value = wallet2.address + seedField2.value = wallet2.seed + balanceField2.value = (await client.getXrpBalance(wallet2.address)) + + client.disconnect() + +} + +// Helper function to update AMM Info when changes are made. + +async function updateAMM() { + + const amm_info_request = { + "command": "amm_info", + "asset": { + "currency": "XRP" + }, + "asset2": { + "currency": "FOO", + "issuer": issuer.address + }, + "ledger_index": "validated" + } + try { + const amm_info_result = await client.request(amm_info_request) + ammInfo = `${JSON.stringify(amm_info_result.result.amm, null, 2)}` + } catch(err) { + if (err.data.error === 'actNotFound') { + ammInfo = (`No AMM exists for the pair FOO / XRP.`) + } else { + results += `\n\n${err}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + } + } + + ammInfoField.value = ammInfo + +} + +// Buy FOO tokens from the XRP/FOO AMM. + +async function buyFOO() { + + await client.connect() + + // Get trading fee of the AMM. + const amm_info_request = { + "command": "amm_info", + "asset": { + "currency": "XRP" + }, + "asset2": { + "currency": "FOO", + "issuer": issuer.address + }, + "ledger_index": "validated" + } + + const amm_info_result = await client.request(amm_info_request) + ammTradingFee = amm_info_result.result.amm.trading_fee + + // Create offer to buy FOO. + + const offer = { + "TransactionType": "OfferCreate", + "Account": wallet2.address, + "TakerPays": { + currency: "FOO", + issuer: issuer.address, + value: "250" + }, + // Assuming FOO:XRP is 10:1 value, calculation is + // (amount FOO to buy * price per token) + amm trading fees + "TakerGets": xrpl.xrpToDrops(250/10)+ammTradingFee + } + + const prepared_offer = await client.autofill(offer) + results += `\n\nPrepared transaction:\n${JSON.stringify(prepared_offer, null, 2)}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const signed_offer = wallet2.sign(prepared_offer) + results += `\n\nSending OfferCreate transaction...` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const result_offer = await client.submitAndWait(signed_offer.tx_blob) + + if (result_offer.result.meta.TransactionResult == "tesSUCCESS") { + results += `\n\nTransaction succeeded: ${EXPLORER}/transactions/${signed_offer.hash}` + } else { + results += `\n\nError sending transaction: ${result_offer}` + } + + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + // Update second wallet balances. + balanceField2.value = (await client.getXrpBalance(wallet2.address)) + walletBalances2 = (await client.getBalances(wallet2.address)) + + const fooCurrency2 = walletBalances2.find(item => item.currency === 'FOO'); + + const fooValue2 = fooCurrency2 ? fooCurrency2.value : 'Currency not found'; + fooField2.value = fooValue2 + + await updateAMM() + + client.disconnect() + +} + +// Deposit assets to AMM. + +async function addAssets() { + + await client.connect() + + const addXRP = addXRPField.value + const addFOO = addFOOField.value + let ammdeposit = null + + if (addXRP && addFOO) { + + ammdeposit = { + "TransactionType": "AMMDeposit", + "Asset": { + currency: "XRP" + }, + "Asset2": { + currency: "FOO", + issuer: issuer.address + }, + "Account": wallet2.address, + "Amount": xrpl.xrpToDrops(addXRP), + "Amount2": { + currency: "FOO", + issuer: issuer.address, + value: addFOO + }, + "Flags": 0x00100000 + } + + } else if ( addXRP ) { + + ammdeposit = { + "TransactionType": "AMMDeposit", + "Asset": { + currency: "XRP" + }, + "Asset2": { + currency: "FOO", + issuer: issuer.address + }, + "Account": wallet2.address, + "Amount": xrpl.xrpToDrops(addXRP), + "Flags": 0x00080000 + } + + } else if ( addFOO ) { + + ammdeposit = { + "TransactionType": "AMMDeposit", + "Asset": { + currency: "XRP" + }, + "Asset2": { + currency: "FOO", + issuer: issuer.address + }, + "Account": wallet2.address, + "Amount": { + currency: "FOO", + issuer: issuer.address, + value: addFOO + }, + "Flags": 0x00080000 + } + + } else { + + results += `\n\nNo assets selected to add ...` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + } + + const prepared_deposit = await client.autofill(ammdeposit) + results += `\n\nPrepared transaction:\n${JSON.stringify(prepared_deposit, null, 2)}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const signed_deposit = wallet2.sign(prepared_deposit) + results += `\n\nSending AMMDeposit transaction ...` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const lp_deposit = await client.submitAndWait(signed_deposit.tx_blob) + + if (lp_deposit.result.meta.TransactionResult == "tesSUCCESS") { + results += `\n\nTransaction succeeded: ${EXPLORER}/transactions/${signed_deposit.hash}` + } else { + results += `\n\nError sending transaction: ${lp_deposit}` + } + + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + // Update second wallet balances. + balanceField2.value = (await client.getXrpBalance(wallet2.address)) + walletBalances2 = (await client.getBalances(wallet2.address)) + + const fooCurrency2 = walletBalances2.find(item => item.currency === 'FOO'); + + const fooValue2 = fooCurrency2 ? fooCurrency2.value : 'Currency not found'; + fooField2.value = fooValue2 + + // Update LP token value. + + const amm_info_request = { + "command": "amm_info", + "asset": { + "currency": "XRP" + }, + "asset2": { + "currency": "FOO", + "issuer": issuer.address + }, + "ledger_index": "validated" + } + + const amm_info_result = await client.request(amm_info_request) + ammAccount = amm_info_result.result.amm.account + + const lpCurrency2 = walletBalances2.find(item => item.issuer === ammAccount); + + const lpValue2 = lpCurrency2 ? lpCurrency2.value : 'Currency not found'; + lpField2.value = lpValue2 + + + await updateAMM() + + client.disconnect() + +} + +// Vote on fees. +async function voteFees() { + + await client.connect() + + const voteFee = voteFeeField.value + + const ammvote = { + "TransactionType": "AMMVote", + "Asset": { + "currency": "XRP" + }, + "Asset2": { + "currency": "FOO", + "issuer": issuer.address + }, + "Account": wallet2.address, + "TradingFee": Number(voteFee) + } + + const prepared_vote = await client.autofill(ammvote) + results += `\n\nPrepared transaction:\n${JSON.stringify(prepared_vote, null, 2)}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const signed_vote = wallet2.sign(prepared_vote) + results += `\n\nSending AMMVote transaction ...` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const response_vote = await client.submitAndWait(signed_vote.tx_blob) + if (response_vote.result.meta.TransactionResult == "tesSUCCESS") { + results += `\n\nTransaction succeeded: ${EXPLORER}/transactions/${signed_vote.hash}` + } else { + results += `\n\nError sending transaction: ${response_vote}` + } + + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + await updateAMM() + + client.disconnect() + +} + +// Calculate the value of LP tokens. +async function calculateLP() { + + await client.connect() + + const LPTokens = lpField2.value + + const amm_info = (await client.request({ + "command": "amm_info", + "asset": { + "currency": "XRP" + }, + "asset2": { + "currency": "FOO", + "issuer": issuer.address + } + })) + + const my_share = LPTokens / amm_info.result.amm.lp_token.value + + const my_asset1 = (amm_info.result.amm.amount * my_share) / 1000000 + const my_asset2 = amm_info.result.amm.amount2.value * my_share + + results += `\n\nMy ${LPTokens} LP tokens are worth:\n + XRP: ${my_asset1} + ${amm_info.result.amm.amount2.currency}: ${my_asset2}` + + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + client.disconnect() + +} + +// Withdraw by redeeming LP tokens. +async function withdrawLP() { + + await client.connect() + + const LPTokens = lpField2.value + + // Get LP token info. + + const amm_info_request = { + "command": "amm_info", + "asset": { + "currency": "XRP" + }, + "asset2": { + "currency": "FOO", + "issuer": issuer.address + }, + "ledger_index": "validated" + } + + const amm_info_result = await client.request(amm_info_request) + const ammIssuer = amm_info_result.result.amm.lp_token.issuer + const ammCurrency = amm_info_result.result.amm.lp_token.currency + + const ammwithdraw = { + "TransactionType": "AMMWithdraw", + "Asset": { + "currency": "XRP" + }, + "Asset2": { + "currency": "FOO", + "issuer": issuer.address + }, + "Account": wallet2.address, + "LPTokenIn": { + currency: ammCurrency, + issuer: ammIssuer, + value: LPTokens + }, + "Flags": 0x00010000 + } + + const prepared_withdraw = await client.autofill(ammwithdraw) + results += `\n\nPrepared transaction:\n${JSON.stringify(prepared_withdraw, null, 2)}` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const signed_withdraw = wallet2.sign(prepared_withdraw) + results += `\n\nSending AMMWithdraw transaction ...` + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + const response_withdraw = await client.submitAndWait(signed_withdraw.tx_blob) + + if (response_withdraw.result.meta.TransactionResult == "tesSUCCESS") { + results += `\n\nTransaction succeeded: ${EXPLORER}/transactions/${signed_withdraw.hash}` + } else { + results += `\n\nError sending transaction: ${response_withdraw}` + } + + resultField.value = results + resultField.scrollTop = resultField.scrollHeight + + // Update second wallet balances. + balanceField2.value = (await client.getXrpBalance(wallet2.address)) + walletBalances2 = (await client.getBalances(wallet2.address)) + + const fooCurrency2 = walletBalances2.find(item => item.currency === 'FOO'); + + const fooValue2 = fooCurrency2 ? fooCurrency2.value : 'Currency not found'; + fooField2.value = fooValue2 + + // Update LP token value. + ammAccount = amm_info_result.result.amm.account + + const lpCurrency2 = walletBalances2.find(item => item.issuer === ammAccount); + + const lpValue2 = lpCurrency2 ? lpCurrency2.value : 'Currency not found'; + lpField2.value = lpValue2 + + await updateAMM() + + client.disconnect() + +} diff --git a/docs/img/add-assets-to-amm.png b/docs/img/add-assets-to-amm.png new file mode 100644 index 0000000000000000000000000000000000000000..5e39ea6e68c93148b536ce41f24bf19d90e3963c GIT binary patch literal 45140 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 literal 0 HcmV?d00001 diff --git a/docs/img/create-an-amm.png b/docs/img/create-an-amm.png new file mode 100644 index 0000000000000000000000000000000000000000..4b9868b80037bf10a08c666cae8d3fbb82d2d555 GIT binary patch literal 18130 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 +Each AMM pair is unique. This checks to make sure the `XRP/FOO` pair doesn't already exist. In the previous step, we created a new issuer and `FOO` token; even if another `FOO` token already exists, it's considered unique if issued by another account. If the AMM pair exists, this responds with the AMM information, such as token pair, trading fees, etc. + +{% code-snippet file="/_code-samples/amm/js/1.create-an-amm.js" from="// Check if AMM exists." before="// Create new AMM." language="js" /%} -### 5. Look up the AMMCreate transaction cost +### Create AMM -Creating an AMM has a special [transaction cost][] to prevent spam: since it creates objects in the ledger that no one owns, you must burn at least one [owner reserve increment](../../../concepts/accounts/reserves.md) of XRP to send the AMMCreate transaction. The exact value can change due to [fee voting](https://xrpl.org/fee-voting.html), so you should look up the current incremental reserve value using the [server_state method][]. +This send the `AMMCreate` transaction with an initial pairing of 50 `XRP` to 500 `FOO`. -It is also a good practice to display this value and give a human operator a chance to stop before you send the transaction. Burning an owner reserve is typically a much higher cost than sending a normal transaction, so you don't want it to be a surprise. (Currently, on both Mainnet and Devnet, the cost of sending a typical transaction is 0.000010 XRP but the cost of AMMCreate is 2 XRP.) - - -{% code-snippet file="/_code-samples/create-amm/js/create-amm.js" from="// Look up AMM transaction cost" before="// Create AMM" language="js" /%} - - -### 6. Send AMMCreate transaction - -Send an [AMMCreate transaction][] to create the AMM. Important aspects of this transaction include: - -| Field | Value | Description | -|-------|--------|-------------| -| `Asset` | [Currency Amount][] | Starting amount of one asset to deposit in the AMM. | -| `Asset2` | [Currency Amount][] | Starting amount of the other asset to deposit in the AMM. | -| `TradingFee` | Number | The fee to charge when trading against this AMM instance. The maximum value is `1000`, meaning a 1% fee; the minimum value is `0`. If you set this too high, it may be too expensive for users to trade against the AMM; but the lower you set it, the more you expose yourself to currency risk from the AMM's assets changing in value relative to one another. | -| `Fee` | String - XRP Amount | The transaction cost you looked up in a previous step. Client libraries may require that you add a special exception or reconfigure a setting to specify a `Fee` value this high. | - -For the two starting assets, it does not matter which is `Asset` and which is `Asset2`, but you should specify amounts that are about equal in total value, because otherwise other users can profit at your expense by trading against the AMM. - -**Tip:** Use `fail_hard` when submitting this transaction, so you don't have to pay the high transaction cost if the transaction initially fails. (It's still _possible_ that the transaction could tentatively succeed, and then fail and still burn the transaction cost, but this protects you from burning XRP on many of types of failures.) - - -{% code-snippet file="/_code-samples/create-amm/js/create-amm.js" from="// Create AMM" before="// Confirm that AMM exists" language="js" /%} - - -### 7. Check AMM info - -If the AMMCreate transaction succeeded, it creates the AMM and related objects in the ledger. You _could_ check the metadata of the AMMCreate transaction, but it is often easier to call the [amm_info method][] again to get the status of the newly-created AMM. - - -{% code-snippet file="/_code-samples/create-amm/js/create-amm.js" from="// Confirm that AMM exists" before="// Check token balances" language="js" /%} - - -In the result, the `amm` object's `lp_token` field is particularly useful because it includes the issuer and currency code of the AMM's LP Tokens, which you need to know for many other AMM-related transactions. LP Tokens always have a hex currency code starting with `03`, and the rest of the code is derived from the issuers and currency codes of the tokens in the AMM's pool. The issuer of the LP Tokens is the AMM address, which is randomly chosen when you create an AMM. - -Initially, the AMM's total outstanding LP Tokens, reported in the `lp_token` field of the `amm_info` response, match the tokens you hold as its first liquidity provider. However, after other accounts deposit liquidity to the same AMM, the amount shown in `amm_info` updates to reflect the total issued to all liquidity providers. Since others can deposit at any time, even potentially in the same ledger version where the AMM was created, you shouldn't assume that this amount represents your personal LP Tokens balance. - - -### 8. Check trust lines - -You can also use the [account_lines method][] to get an updated view of your token balances. Your balances should be decreased by the amounts you deposited, but you now have a balance of LP Tokens that you received from the AMM. - - -{% code-snippet file="/_code-samples/create-amm/js/create-amm.js" from="// Check token balances" before="// Disconnect" language="js" /%} - - -The `account_lines` response shows only the tokens held by the account you looked up (probably yours). If you have a lot of tokens, you may want to specify the AMM address as the `peer` in the request so you don't have to [paginate](../../../references/http-websocket-apis/api-conventions/markers-and-pagination.md) over multiple requests to find the AMM's LP Tokens. In this tutorial, your account probably only holds the three different tokens, so you can see all three in the same response. - -**Tip:** If one of the assets in the AMM's pool is XRP, you need to call the [account_info method][] on your account to see the difference in your balance (the `Balance` field of the account object). +{% code-snippet file="/_code-samples/amm/js/1.create-an-amm.js" from="// Create new AMM." language="js" /%} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7efaf97062..8a0c2394b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "react18-json-view": "^0.2.6", "smol-toml": "^1.1.3", "use-query-params": "^2.2.1", - "xrpl": "^3.0.0" + "xrpl": "^4.0.0" }, "devDependencies": { "bootstrap": "^4.6.2", @@ -2980,9 +2980,9 @@ } }, "node_modules/@xrplf/isomorphic": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@xrplf/isomorphic/-/isomorphic-1.0.0.tgz", - "integrity": "sha512-IyMsxyjkJK8YWq566KyuFuh/PUiLzQ02RbUO5qa+vEQb6zIAR9MzFwN7wBmBy7wmKkjligcdNDMG5EaBRH8FxQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@xrplf/isomorphic/-/isomorphic-1.0.1.tgz", + "integrity": "sha512-0bIpgx8PDjYdrLFeC3csF305QQ1L7sxaWnL5y71mCvhenZzJgku9QsA+9QCXBC1eNYtxWO/xR91zrXJy2T/ixg==", "dependencies": { "@noble/hashes": "^1.0.0", "eventemitter3": "5.0.1", @@ -4007,33 +4007,6 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" }, - "node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -9170,11 +9143,11 @@ } }, "node_modules/ripple-binary-codec": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-2.0.0.tgz", - "integrity": "sha512-zakENc9A5dlW85uzrmQHrJehymhL59ftggboRNrjxFDJdlNJ6DSE210P3ys/9kL0oVtOzFnTrOPFfxHZeOsA/Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-2.1.0.tgz", + "integrity": "sha512-q0GAx+hj3UVcDbhXVjk7qeNfgUMehlElYJwiCuIBwqs/51GVTOwLr39Ht3eNsX5ow2xPRaC5mqHwcFDvLRm6cA==", "dependencies": { - "@xrplf/isomorphic": "^1.0.0", + "@xrplf/isomorphic": "^1.0.1", "bignumber.js": "^9.0.0", "ripple-address-codec": "^5.0.0" }, @@ -10529,23 +10502,22 @@ } }, "node_modules/xrpl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-3.0.0.tgz", - "integrity": "sha512-QC+dNx3tvMEn9IrxcXFFa0rWwvBwACkGFNKl+W2miMGYnlgSiIsnjdqwtG2WRs0Pyxs5dd9nBTQHyQ1BPxZ78A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-4.0.0.tgz", + "integrity": "sha512-VZm1lQWHQ6PheAAFGdH+ISXKvqB2hZDQ0w4ZcdAEtmqZQXtSIVQHOKPz95rEgGANbos7+XClxJ73++joPhA8Cw==", "dependencies": { "@scure/bip32": "^1.3.1", "@scure/bip39": "^1.2.1", - "@xrplf/isomorphic": "^1.0.0", + "@xrplf/isomorphic": "^1.0.1", "@xrplf/secret-numbers": "^1.0.0", "bignumber.js": "^9.0.0", - "cross-fetch": "^4.0.0", "eventemitter3": "^5.0.1", "ripple-address-codec": "^5.0.0", - "ripple-binary-codec": "^2.0.0", + "ripple-binary-codec": "^2.1.0", "ripple-keypairs": "^2.0.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, "node_modules/xrpl/node_modules/eventemitter3": { diff --git a/package.json b/package.json index ae7867219f..76be6afc78 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "react18-json-view": "^0.2.6", "smol-toml": "^1.1.3", "use-query-params": "^2.2.1", - "xrpl": "^3.0.0" + "xrpl": "^4.0.0" }, "overrides": { "react": "^18.2.0", diff --git a/sidebars.yaml b/sidebars.yaml index 59677e61a6..f62f7eacc1 100644 --- a/sidebars.yaml +++ b/sidebars.yaml @@ -166,6 +166,11 @@ - page: docs/tutorials/javascript/index.md expanded: false items: + - page: docs/tutorials/javascript/amm/index.md + expanded: false + items: + - page: docs/tutorials/javascript/amm/create-an-amm.md + - page: docs/tutorials/javascript/amm/add-assets-to-amm.md - page: docs/tutorials/javascript/send-payments/index.md expanded: false items: