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 0000000000..5e39ea6e68 Binary files /dev/null and b/docs/img/add-assets-to-amm.png differ diff --git a/docs/img/create-an-amm.png b/docs/img/create-an-amm.png new file mode 100644 index 0000000000..4b9868b800 Binary files /dev/null and b/docs/img/create-an-amm.png differ diff --git a/docs/tutorials/javascript/amm/add-assets-to-amm.md b/docs/tutorials/javascript/amm/add-assets-to-amm.md new file mode 100644 index 0000000000..237b821197 --- /dev/null +++ b/docs/tutorials/javascript/amm/add-assets-to-amm.md @@ -0,0 +1,95 @@ +--- +seo: + description: Provide liquidity for an Automated Market Maker (AMM) and earn income from trading fees. +labels: + - Decentralized Exchange + - Tokens + - AMM +--- +# Add Assets to an AMM + +This example shows how to: + +1. Purchase `FOO` tokens from an AMM. +2. Deposit `XRP` and `FOO` to the AMM and receive LP tokens. +3. Vote on AMM trading fees. +4. Redeem LP Tokens for assets. + + +## Usage + +![Test harness to create AMM](/docs/img/add-assets-to-amm.png) + +Download the [AMM Samples](https://github.com/XRPLF/xrpl-dev-portal/tree/master/_code-samples/amm/js/) to use the AMM test harness in your browser and follow along in this tutorial. + +### Set up an AMM + +1. Click **Get New Devnet Account**. +2. Click **Issue FOO Tokens**. +3. Click **Create AMM**. You can view the AMM pool info in the `AMM Info` box. + + +### Buy FOO tokens + +Buy `FOO` tokens from the established AMM in order to interact with the pool. + +1. Click **Get Second Devnet Account**. +2. Click **Buy FOO Tokens**. To simplify this tutorial, this purchases 250 `FOO`. The AMM starts with a ratio of 10:1 `FOO`:`XRP`. This doesn't translate to a direct 25 `XRP` spend, since the AMM will adjust the cost of `FOO` to be more expensive as you buy more from it. + + +### Add Assets to the AMM + +Add a value in the `XRP` and `FOO` fields, then click **Add Assets**. You can deposit either one or both assets, but depositing only one asset will reduce the amount of LP tokens you receive. + + +### Vote on trading fees + +Add a value in the `Fee` field, then click **Add Vote**. You can view the updated AMM info after doing so and see your submission under trading fees. + +The proposed fee is in units of 1/100,000; a value of 1 is equivalent to 0.001%. The maximum value is 1000, indicating a 1% fee. + + +### Redeem your LP tokens + +1. Click **Calculate LP Value**. +2. Click **Withdraw LP**. This tutorial redeems all your LP tokens to withdraw both assets, but AMM withdrawals can be either both or one, depending on the fields specified in the transaction. + + +## Code Walkthrough + + +### Buy FOO tokens + +Purchases 250 `FOO` tokens by creating an offer on the XRPL DEX. Since the AMM created in the test harness is the only available source of `FOO`, it takes the offer. + +{% code-snippet file="/_code-samples/amm/js/2.add-assets-to-amm.js" language="js" from="// Buy FOO tokens from the XRP/FOO AMM." before="// Deposit assets to AMM." /%} + + +### Deposit assets + +This code checks if you're trying to add one or both assets, and then modifies the `AMMDeposit` transaction to be either a single or double-asset deposit. + +The function to update LP balance checks the AMM to get the unique AMM account, which acts as its own issuer of LP tokens. It then checks your wallet balance and gets the LP token value by matching it with the AMM issuer. + +{% code-snippet file="/_code-samples/amm/js/2.add-assets-to-amm.js" language="js" from="// Deposit assets to AMM." before="// Vote on fees." /%} + + +### Vote on trading fees + +Trading fees are applied to any transaction that interacts with the AMM. The act of voting is straightforward and only requires you to hold the AMM LP tokens before submitting a vote. + +{% code-snippet file="/_code-samples/amm/js/2.add-assets-to-amm.js" language="js" from="// Vote on fees." before="// Calculate the value of LP tokens." /%} + + +### Calculate value of LP tokens + +There isn't a dedicated method to calculate how much you can redeem your LP tokens for, but the math isn't too complicated. The percentage of the total LP tokens you own qualifies you for the same percentage of the total assets in the AMM. + +{% code-snippet file="/_code-samples/amm/js/2.add-assets-to-amm.js" language="js" from="// Calculate the value of LP tokens." before="// Withdraw by redeeming LP tokens." /%} + + +### Redeem your LP tokens + +Redeeming your LP tokens requires you to get the LP token issuer and currency code, both of which you can check using the `amm_info` method. This code redeems assets using all your LP tokens, but you can also specify withdrawals by the amount of an asset you want to take out. + +{% code-snippet file="/_code-samples/amm/js/2.add-assets-to-amm.js" language="js" from="// Withdraw by redeeming LP tokens." /%} diff --git a/docs/tutorials/javascript/amm/add-lp-to-amm.md b/docs/tutorials/javascript/amm/add-lp-to-amm.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/tutorials/javascript/amm/create-an-amm.md b/docs/tutorials/javascript/amm/create-an-amm.md index 8c10ac8c3a..90096a341a 100644 --- a/docs/tutorials/javascript/amm/create-an-amm.md +++ b/docs/tutorials/javascript/amm/create-an-amm.md @@ -2,122 +2,55 @@ This example shows how to: -1. Check if the AMM exists. -2. Create the AMM. -3. Check the AMM info. +1. Issue `FOO` tokens on Devnet. +2. Check if an AMM exists for `XRP/FOO`. +3. Create an AMM for `XRP/FOO`. -**TODO: Create test harness for AMMs.** ## Usage -You can download the [AMM Samples]() archive to try the sample in your own browser. +![Test harness to create AMM](/docs/img/create-an-amm.png) +Download the [AMM Samples](https://github.com/XRPLF/xrpl-dev-portal/tree/master/_code-samples/amm/js/) to use the AMM test harness in your browser and follow along in this tutorial. -### Get Accounts - -1. Open `1.create-amm.html` in a browser. -2. Get test accounts. - - If you have existing Testnet account seeds: - 1. Paste the account seeds in the **Seeds** field. - 2. Click **Get Accounts from Seeds**. - - If you do not have existing Testnet accounts: - 1. Click **Get New Standby Account**. - 2. Click **Get New Operational Account**. - -### Select Assets - -1. Choose the assets you want to create an AMM pair for. -2. Click the `Check AMM`. Since there can only be one AMM for a specific pair of assets, it's best to check first before trying to create one. -3. Click `Check AMM Creation Cost` to look up the fee required for creating the AMM. - -### Create AMM - -1. Click `Create AMM`. - -### Look Up AMM - -1. Click `Look Up AMM`. +1. Open `create-an-amm.html` in a browser. +2. Click **Get New Devnet Account**. +3. Click **Issue FOO Tokens**. +4. Click **Check for AMM**. +5. Click **Create AMM**. +6. Click **Check for AMM**. ## Code Walkthrough -### 3. Select and acquire assets +### Connect to Devnet and generate credentials -As the creator of an AMM, you are also the first liquidity provider and you have to supply it with a starting pool of assets. Other users of the XRP Ledger can also become liquidity providers by supplying assets after the AMM exists. It's crucial to choose assets carefully because, as a liquidity provider for an AMM, you are supplying some amounts of both for users to swap between. If one of the AMM's assets becomes worthless, other users can use the AMM to trade for the other asset, leaving the AMM (and thus, its liquidity providers including you) holding only the worthless one. Technically, the AMM always holds some positive amount of both assets, but the amounts can be very small. +You must be connected to the network to query it and submit transactions. This code connects to Devnet and generates a fresh wallet, funded with 100 XRP. -You can choose any pair of fungible assets in the XRP Ledger, including XRP or tokens, as long as they meet the [restrictions on AMM assets](../../../concepts/tokens/decentralized-exchange/automated-market-makers.md#restrictions-on-assets). - -For each of the two assets, you need to know its currency code and issuer; as an exception, XRP has no issuer. For each of the assets, you must hold a balance of the asset (or _be_ the issuer). The following sample code acquires two assets, "TST" (which it buys using XRP) and "FOO" (which it receives from the issuer). - -{% code-snippet file="/_code-samples/create-amm/js/create-amm.js" from="// Acquire tokens" before="// Check if AMM already exists" language="js" /%} - -This tutorial includes some example code to issue FOO tokens from a second test address. This is not realistic for a production scenario, because tokens do not inherently have value, but it makes it possible to demonstrate creating a new AMM for a unique currency pair. In production, you would acquire a second token in some other way, such as making an off-ledger deposit with the [stablecoin issuer](../../../use-cases/tokenization/stablecoin-issuer.md), or buying it in the [decentralized exchange](../../../concepts/tokens/decentralized-exchange/index.md). - -The helper function for issuing follows an abbreviated version of the steps in the [Issue a Fungible Token](issue-a-fungible-token.md) tutorial: - -{% code-snippet file="/_code-samples/create-amm/js/create-amm.js" from="/* Issue tokens" language="js" /%} +{% code-snippet file="/_code-samples/amm/js/1.create-an-amm.js" from="// Define client, network, and explorer." before="// Issue FOO tokens to Devnet wallet." language="js" /%} -### 4. Check if the AMM exists +### Issue FOO tokens -Since there can only be one AMM for a specific pair of assets, it's best to check first before trying to create one. Use the [amm_info method][] to check whether the AMM already exists. For the request, you specify the two assets. The response should be an `actNotFound` error if the AMM does not exist. +Normally, you'd acquire a second token through a stablecoin issuer, or buying it off an exchange. For this tutorial, we'll go through the process of issuing a brand new FOO token, enabling us to create a unique AMM pair later. Creating a new issuer and issuing a token involves: + +1. Creating a new issuer wallet. +2. Enabling the `DefaultRipple` flag. +3. Creating a trustline from your wallet to the issuer wallet. +4. Sending the `FOO` tokens from the issuer to your wallet. + +{% code-snippet file="/_code-samples/amm/js/1.create-an-amm.js" from="// Issue FOO tokens to Devnet wallet." before="// Check if AMM exists." language="js" /%} -{% code-snippet file="/_code-samples/create-amm/js/create-amm.js" from="// Check if AMM already exists" before="// Look up AMM transaction cost" language="js" /%} +### Check if an AMM already exists. -If the AMM does already exist, you should double-check that you specified the right pair of assets. If someone else has already created this AMM, you can deposit to it instead. +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: