// Source for interactive examples in the "Trade in the Decentralized Exchange" // tutorial. // Get Credentials, Connect steps handled by the snippet $(document).ready(() => { // Look Up Offers -------------------------------------------------------------- function update_exchange_rate(event) { const tpa = $("#taker-pays-amount-1").val() const tga = $("#taker-gets-amount-1").val() const exchange_rate = BigNumber(tga) / BigNumber(tpa) $("#exchange-rate-1").val(`${exchange_rate}`) } $("#taker-pays-amount-1").change(update_exchange_rate) $("#taker-gets-amount-1").change(update_exchange_rate) $("#look-up-offers").click( async (event) => { const block = $(event.target).closest(".interactive-block") block.find(".output-area").html("") const address = get_address(event) if (!address) {return} const we_want = { "currency": $("#taker-pays-currency-1").val(), "issuer": $("#taker-pays-issuer-1").val(), "value": $("#taker-pays-amount-1").val() } const we_spend = { "currency": "XRP", "value": xrpl.xrpToDrops($("#taker-gets-amount-1").val()) } const proposed_quality = BigNumber(we_spend.value) / BigNumber(we_want.value) let orderbook_resp try { orderbook_resp = await api.request({ "command": "book_offers", "taker": address, "ledger_index": "current", "taker_gets": we_want, "taker_pays": we_spend }) block.find(".output-area").append( `
${pretty_print(orderbook_resp.result)}`)
} catch(err) {
show_error(block, err)
block.find(".loader").hide()
return
}
// Estimate whether a proposed Offer would execute immediately, and...
// If so, how much of it? (Partial execution is possible)
// If not, how much liquidity is above it? (How deep in the order book would
// other Offers have to go before ours would get taken?)
// Note: these estimates can be thrown off by rounding if the token issuer
// uses a TickSize setting other than the default (15). In that case, you
// can increase the TakerGets amount of your final offer just a little bit to
// compensate.
const offers = orderbook_resp.result.offers
const want_amt = BigNumber(we_want.value)
let running_total = BigNumber(0)
if (!offers) {
block.find(".output-area").append(`No Offers in the matching book.
Offer probably won't execute immediately.
`)
} else {
for (const o of offers) {
if (o.quality <= proposed_quality) {
block.find(".output-area").append(
`Matching Offer found, funded with
${o.owner_funds} ${we_want.currency}
`)
running_total = running_total.plus(BigNumber(o.owner_funds))
if (running_total >= want_amt) {
block.find(".output-area").append("Full Offer will probably fill.
")
break
}
} else {
// Offers are in ascending quality order, so no others after this
// will match, either
block.find(".output-area").append(`Remaining orders too expensive.
`)
break
}
}
block.find(".output-area").append(`Total matched:
${Math.min(running_total, want_amt)} ${we_want.currency}
`)
if (running_total > 0 && running_total < want_amt) {
block.find(".output-area").append(`Remaining
${want_amt - running_total} ${we_want.currency} would probably be
placed on top of the order book.
`)
}
}
if (running_total == 0) {
// If the Offer would be partly filled, then the rest would be placed
// at the top of the order book. If no part is filled, then there might be
// other Offers going the same direction as ours already on the books with
// an equal or better rate. This code counts how much liquidity is likely to
// be above ours.
// Unlike above, this looks for Offers going the same direction as the
// proposed Offer, so TakerGets and TakerPays are reversed from the previous
// book_offers request.
let orderbook2_resp
try {
orderbook2_resp = await api.request({
"command": "book_offers",
"taker": address,
"ledger_index": "current",
"taker_gets": we_spend,
"taker_pays": we_want
})
block.find(".output-area").append(
`${pretty_print(orderbook2_resp.result)}`)
} catch(err) {
show_error(block, err)
block.find(".loader").hide()
return
}
// Since TakerGets/TakerPays are reversed, the quality is the inverse.
// You could also calculate this as 1/proposed_quality.
const offered_quality = BigNumber(we_want.value) / BigNumber(we_spend.value)
const offers2 = orderbook2_resp.result.offers
let tally_currency = we_spend.currency
if (tally_currency == "XRP") { tally_currency = "drops of XRP" }
let running_total2 = BigNumber(0)
if (!offers2) {
block.find(".output-area").append(`No similar Offers in the book.
Ours would be the first.
`)
} else {
for (const o of offers2) {
if (o.quality <= offered_quality) {
block.find(".output-area").append(`Existing offer found, funded
with ${o.owner_funds} ${tally_currency}
`)
running_total2 = running_total2.plus(BigNumber(o.owner_funds))
} else {
block.find(".output-area").append(`Remaining orders are below where
ours would be placed.
`)
break
}
}
block.find(".output-area").append(`Our Offer would be placed below at
least ${running_total2} ${tally_currency}
`)
if (running_total > 0 && running_total < want_amt) {
block.find(".output-area").append(`Remaining
${want_amt - running_total} ${tally_currency} will probably be
placed on top of the order book.
`)
}
}
}
block.find(".loader").hide()
complete_step("Look Up Offers")
})
// Send OfferCreate Transaction ------------------------------------------------
$("#send-offercreate").click( async (event) => {
const block = $(event.target).closest(".interactive-block")
const address = get_address(event)
if (!address) {return}
block.find(".output-area").html("")
const we_want = {
"currency": $("#taker-pays-currency-1").val(),
"issuer": $("#taker-pays-issuer-1").val(),
"value": $("#taker-pays-amount-1").val()
}
const we_spend = {
"currency": "XRP",
"value": xrpl.xrpToDrops($("#taker-gets-amount-1").val())
}
const tx = {
"TransactionType": "OfferCreate",
"Account": address,
"TakerPays": {
"currency": $("#taker-pays-currency-1").val(),
"issuer": $("#taker-pays-issuer-1").val(),
"value": $("#taker-pays-amount-1").val()
},
"TakerGets": we_spend.value // since it's XRP
}
try {
await generic_full_send(event, tx)
complete_step("Send OfferCreate")
} catch(err) {
block.find(".loader").hide()
show_error(block, err)
}
})
// Wait for Validation: handled by interactive-tutorial.js and by the
// generic full send in the previous step. -------------------------------------
// Check Metadata --------------------------------------------------------------
$("#check-metadata").click(async (event) => {
const block = $(event.target).closest(".interactive-block")
block.find(".loader").show()
block.find(".output-area").html("")
const txid = $(`#interactive-wait`).find(".waiting-for-tx").text().trim()
let result
try {
result = await api.request({"method": "tx", "transaction": txid})
} catch(err) {
show_err(err)
block.find(".loader").hide()
return
}
const balance_changes = xrpl.getBalanceChanges(result.result.meta)
block.find(".output-area").append(`Total balance changes:
${pretty_print(balance_changes)}
`)
// Helper to convert an XRPL amount to a string for display
function amt_str(amt) {
if (typeof amt == "string") {
return `${xrpl.dropsToXrp(amt)} XRP`
} else {
return `${amt.value} ${amt.currency}.${amt.issuer}`
}
}
let offers_affected = 0
for (const affnode of result.result.meta.AffectedNodes) {
if (affnode.hasOwnProperty("ModifiedNode")) {
if (affnode.ModifiedNode.LedgerEntryType == "Offer") {
// Usually a ModifiedNode of type Offer indicates a previous Offer that
// was partially consumed by this one.
offers_affected += 1
}
} else if (affnode.hasOwnProperty("DeletedNode")) {
if (affnode.DeletedNode.LedgerEntryType == "Offer") {
// The removed Offer may have been fully consumed, or it may have been
// found to be expired or unfunded.
offers_affected += 1
}
} else if (affnode.hasOwnProperty("CreatedNode")) {
if (affnode.CreatedNode.LedgerEntryType == "RippleState") {
block.find(".output-area").append("Created a trust line.
")
} else if (affnode.CreatedNode.LedgerEntryType == "Offer") {
const offer = affnode.CreatedNode.NewFields
block.find(".output-area").append(`Created an Offer owned by ${offer.Account} with
TakerGets=${amt_str(offer.TakerGets)} and
TakerPays=${amt_str(offer.TakerPays)}.
`)
}
}
}
block.find(".output-area").append(`Modified or removed ${offers_affected} matching Offer(s).
`)
block.find(".loader").hide()
complete_step("Check Metadata")
})
// Check Balances and Offers
$("#check-balances-and-offers").click( async (event) => {
const block = $(event.target).closest(".interactive-block")
const address = get_address(event)
if (!address) {return}
block.find(".loader").show()
try {
block.find(".output-area").append("Getting address balances as of validated ledger...
")
const balances = await api.request({
command: "account_lines",
account: address,
ledger_index: "validated"
// You could also use ledger_index: "current" to get pending data
})
block.find(".output-area").append(`${pretty_print(balances.result)}
`)
block.find(".output-area").append(`Getting outstanding Offers from ${address} as of validated ledger...
`)
const acct_offers = await api.request({
command: "account_offers",
account: address,
ledger_index: "validated"
})
block.find(".output-area").append(`${pretty_print(acct_offers.result)}
`)
block.find(".loader").hide()
} catch(err) {
show_error(block, err)
block.find(".loader").hide()
}
})
}) // end of $(document).ready()