mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-19 19:25:51 +00:00
Improve & refactor interactive tutorial code
Interactive tutorials: more consistent style Interactive tutorials: Use new generics for send-xrp, use-tickets Interactive tutorials: clean up now-unused code Interactive tutorials: progress & debugging of errors Interactive: Require Destination Tags; and related - Validate addresses in Transaction Sender and warn on Mainnet X-address - Option to load destination address from query param in Tx Sender - Some more/updated helpers in interactive tutorial JS Interactive tutorials: fix JA version Interactive tutorials: readme, include code filter (incomplete) Interactive tutorials: improvements for consistency Interactive Tutorials: finish readme Interactive tutorials: fix syntax errors
This commit is contained in:
@@ -1,5 +1,61 @@
|
||||
// Helper functions for interactive tutorials
|
||||
|
||||
// Locale strings. TODO: maybe move these out to their own file.
|
||||
LOCALES = {
|
||||
"en": {
|
||||
// Leave empty, use the keys provided (in English) by default
|
||||
},
|
||||
"ja": {
|
||||
"Address:": "アドレス:",
|
||||
"Secret:": "シード:",
|
||||
"Balance:": "残高:",
|
||||
"Complete all previous steps first": "前の手順をすべて完了して下さい",
|
||||
"Conection to the XRP Ledger required": "XRP Ledgerの接続が必要です",
|
||||
"Error:": "エラー:",
|
||||
"Populated this page's examples with these credentials.": "このページの例にこのアドレスとシードを入力しました。",
|
||||
"There was an error connecting to the Faucet. Please try again.": "テストネットワークFaucetにエラーが発生しました。もう一度試してください。",
|
||||
"Connecting...": "接続しています...",
|
||||
"Connection required": "接続が必要です",
|
||||
"Connected": "接続されました",
|
||||
"Faucet returned an error:": "テストネットワークFaucetがこのエラーを返しました:",
|
||||
"Validated": "検証済み",
|
||||
"Final Result:": "確定結果:",
|
||||
"(Still pending...)": "(まだ未決…)",
|
||||
"(None)": "(無)",
|
||||
"Prepared transaction:": "準備済みトランザクション:",
|
||||
"Failed to achieve consensus (final)": "検証済みレジャーには含まれません(決定結果)",
|
||||
"Preliminary result:": "予備結果:",
|
||||
"Unknown": "不明",
|
||||
"Couldn't get a valid address/secret value. Check that the previous steps were completed successfully.": "有効なアドレスかシードの取得出来ませんでした。前の手順が完了しましたことを確認して下さい。",
|
||||
"Transaction hash:": "トランザクションハッシュ:"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick-n-dirty localization function. TODO: migrate to a real localization
|
||||
* library, such as https://github.com/wikimedia/jquery.i18n
|
||||
* @param {String} key The string to translate into this page's locale
|
||||
* @return {String} The translated string, if one is available, or the provided
|
||||
* key value if no translation is available.
|
||||
*/
|
||||
const current_locale = $("html").prop("lang")
|
||||
function tl(key) {
|
||||
let mesg = LOCALES[current_locale][key]
|
||||
if (typeof mesg === "undefined") {
|
||||
mesg = key
|
||||
}
|
||||
return mesg
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string of text into a "slug" form that is appropriate for use in
|
||||
* URLs, HTML id and class attributes, and similar. This matches the equivalent
|
||||
* function in filter_interactive_steps.py so that each step's ID is derived
|
||||
* from the given step_name in a consistent way.
|
||||
* @param {String} s The text (step_name or similar) to convert into a
|
||||
* @return {String} The "slug" version of the text, lowercase with no whitespace
|
||||
* and with most non-alphanumeric characters removed.
|
||||
*/
|
||||
function slugify(s) {
|
||||
const unacceptable_chars = /[^A-Za-z0-9._ ]+/g
|
||||
const whitespace_regex = /\s+/g
|
||||
@@ -12,12 +68,70 @@ function slugify(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
function complete_step(step_name) {
|
||||
const step_id = slugify(step_name)
|
||||
$(".bc-"+step_id).removeClass("active").addClass("done")
|
||||
$(".bc-"+step_id).next().removeClass("disabled").addClass("active")
|
||||
|
||||
/**
|
||||
* Check whether a given step has been marked completed already.
|
||||
* @param {String} step_name The exact name of the step, as defined in the
|
||||
* start_step(step_name) function in the MD file.
|
||||
* @return {Boolean} Whether or not this step has been marked complete.
|
||||
*/
|
||||
function is_complete(step_name) {
|
||||
return is_complete_by_id(slugify(step_name))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for is_complete. Also called directly in cases where we only have
|
||||
* the step_id and not the step_name (since that's a one-way conversion).
|
||||
* @param {String} step_id The slugified name of the step.
|
||||
* @return {Boolean} Whether or not this step has been marked complete.
|
||||
*/
|
||||
function is_complete_by_id(step_id) {
|
||||
return $(".bc-"+step_id).hasClass("done")
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a step as done in the breadcrumbs, mark the following step as active,
|
||||
* and enable buttons and such in the following step that have the
|
||||
* "previous-steps-required" class.
|
||||
* @param {String} step_name The exact name of the step, as defined in the
|
||||
* start_step(step_name) function in the MD file.
|
||||
*/
|
||||
function complete_step(step_name) {
|
||||
complete_step_by_id(slugify(step_name))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for complete_step. Also called directly in cases where we only have
|
||||
* the step_id and not the step_name (since that's a one-way conversion).
|
||||
* @param {String} step_id The slugified name of the step.
|
||||
*/
|
||||
function complete_step_by_id(step_id) {
|
||||
$(".bc-"+step_id).removeClass("active").addClass("done")
|
||||
$(".bc-"+step_id).next().removeClass("disabled").addClass("active")
|
||||
|
||||
// Enable follow-up steps that require this step to be done first
|
||||
const next_ui = $(`#interactive-${step_id}`).nextAll(
|
||||
".interactive-block").eq(0).find(".previous-steps-required")
|
||||
next_ui.prop("title", "")
|
||||
next_ui.prop("disabled", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the step_id of the interactive block that contains a given element.
|
||||
* @param {jQuery} jEl The jQuery result representing a single HTML element in
|
||||
* an interactive block.
|
||||
* @return {String} The step_id of the block that contains jEl.
|
||||
*/
|
||||
function get_block_id(jEl) {
|
||||
// Traverse up, then slice "interactive-" off the block's HTML ID
|
||||
return jEl.closest(".interactive-block").prop("id").slice(12)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty-print JSON with standard indentation.
|
||||
* @param {String, Object} j Either a JSON/JSON-like object, or a string
|
||||
containing JSON.
|
||||
*/
|
||||
function pretty_print(j) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(j),null,2)
|
||||
@@ -25,5 +139,387 @@ function pretty_print(j) {
|
||||
// probably already decoded JSON
|
||||
return JSON.stringify(j,null,2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable buttons and such with the "previous-steps-required" class, and give
|
||||
* them an appropriate tooltip message.
|
||||
*/
|
||||
function disable_followup_steps() {
|
||||
$(".previous-steps-required").prop("title", tl("Complete all previous steps first"))
|
||||
$(".previous-steps-required").prop("disabled", true)
|
||||
$(".connection-required").prop("title", tl("Conection to the XRP Ledger required"))
|
||||
$(".connection-required").prop("disabled", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Output an error message in the given area.
|
||||
* @param {jQuery} block The block where this error message should go. This
|
||||
* should be a parent element containing an element with
|
||||
* the "output-area" class.
|
||||
* @param {String} message The HTML contents to put inside the message.
|
||||
*/
|
||||
function show_error(block, message) {
|
||||
block.find(".output-area").html(
|
||||
`<p class="devportal-callout warning"><strong>${tl("Error:")}</strong>
|
||||
${message}</p>`)
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used with _snippets/interactive/tutorials/generate-step.md.
|
||||
* Adds an event to the "Generate" button to call the appropriate faucet
|
||||
* (Testnet or Devnet) and write the credentials to elements that later steps
|
||||
* can use in their examples. Also updates code samples in the current page to
|
||||
* use the generated credentials instead of the placeholder EXAMPLE_ADDR and
|
||||
* EXAMPLE_SECRET.
|
||||
*/
|
||||
const EXAMPLE_ADDR = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
|
||||
const EXAMPLE_SECRET = "s████████████████████████████"
|
||||
function setup_generate_step() {
|
||||
|
||||
$("#generate-creds-button").click( async (event) => {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
block.find(".output-area").html("")
|
||||
block.find(".loader").show()
|
||||
// Get faucet URL (Testnet/Devnet/etc.)
|
||||
const faucet_url = $("#generate-creds-button").data("fauceturl")
|
||||
|
||||
try {
|
||||
const data = await call_faucet(faucet_url)
|
||||
|
||||
block.find(".loader").hide()
|
||||
block.find(".output-area").html(`<div><strong>${tl("Address:")}</strong>
|
||||
<span id="use-address">${data.account.address}</span></div>
|
||||
<div><strong>${tl("Secret:")}</strong>
|
||||
<span id="use-secret">${data.account.secret}</span></div>
|
||||
<strong>${tl("Balance:")}</strong>
|
||||
${Number(data.balance).toLocaleString(current_locale)} XRP`)
|
||||
|
||||
// Automatically populate all examples in the page with the
|
||||
// generated credentials...
|
||||
$("code span:contains('"+EXAMPLE_ADDR+"')").each( function() {
|
||||
let eltext = $(this).text()
|
||||
$(this).text( eltext.replace(EXAMPLE_ADDR, data.account.address) )
|
||||
})
|
||||
$("code span:contains('"+EXAMPLE_SECRET+"')").each( function() {
|
||||
let eltext = $(this).text()
|
||||
$(this).text( eltext.replace(EXAMPLE_SECRET, data.account.secret) )
|
||||
})
|
||||
|
||||
block.find(".output-area").append(`<p>${tl("Populated this page's examples with these credentials.")}</p>`)
|
||||
|
||||
complete_step("Generate")
|
||||
|
||||
} catch(err) {
|
||||
block.find(".loader").hide()
|
||||
block.find(".output-area").html(
|
||||
`<p class="devportal-callout warning"><strong>${tl("Error:")}</strong>
|
||||
${tl("There was an error connecting to the Faucet. Please try again.")}
|
||||
</p>`)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address from the Generate step (snippet), or display an error in
|
||||
* the relevant interactive block if it fails—usually because the user hasn't
|
||||
* cliked the "Get Credentials" button yet.
|
||||
* @return {String, undefined} The address, if available, or undefined if not
|
||||
*/
|
||||
function get_address(event) {
|
||||
const address = $("#use-address").text()
|
||||
if (!address) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
if (!block.length) {return}
|
||||
show_error(block, tl("Couldn't get a valid address/secret value. Check that the previous steps were completed successfully."))
|
||||
}
|
||||
return address
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the secret key from the Generate step (snippet), or display an error in
|
||||
* the relevant interactive block if it fails—usually because the user hasn't
|
||||
* cliked the "Get Credentials" button yet.
|
||||
* @return {String, undefined} The secret key, if available, or undefined if not
|
||||
*/
|
||||
function get_secret(event) {
|
||||
const secret = $("#use-secret").text()
|
||||
if (!secret) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
if (!block.length) {return}
|
||||
show_error(block, tl("Couldn't get a valid address/secret value. Check that the previous steps were completed successfully."))
|
||||
}
|
||||
if (secret == EXAMPLE_SECRET) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
if (!block.length) {return}
|
||||
show_error(block, tl("Can't use the example secret here. Check that the previous steps were completed successfully."))
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for calling the Testnet/Devnet faucet.
|
||||
* @param {String} faucet_url The URL of the faucet to call, for example:
|
||||
* https://faucet.altnet.rippletest.net/accounts
|
||||
*/
|
||||
async function call_faucet(faucet_url, destination) {
|
||||
// Future feature: support the Faucet's optional xrpAmount param
|
||||
const body = {}
|
||||
if (typeof destination != "undefined") {
|
||||
body["destination"] = destination
|
||||
}
|
||||
const response = await fetch(faucet_url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw `${tl("Faucet returned an error:")} ${data.error}`
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used with _snippets/interactive-tutorials/connect-step.md
|
||||
* Adds an event to the "Connect" button to connect to the appropriate
|
||||
* WebSocket server (Testnet, Devnet, maybe Mainnet for some cases).
|
||||
* Also adds an event to re-disable following steps if we get disconnected.
|
||||
*/
|
||||
function setup_connect_step() {
|
||||
if (!$("#connect-button").length) {
|
||||
console.debug("Connect step not included. Skipping related setup.")
|
||||
return
|
||||
}
|
||||
const ws_url = $("#connect-button").data("wsurl")
|
||||
if (!ws_url) {
|
||||
console.error("Interactive Tutorial: WS URL not found. Did you set use_network?")
|
||||
return
|
||||
}
|
||||
api = new ripple.RippleAPI({server: ws_url})
|
||||
api.on('connected', async function() {
|
||||
$("#connection-status").text(tl("Connected"))
|
||||
$("#connect-button").prop("disabled", true)
|
||||
$("#loader-connect").hide()
|
||||
$(".connection-required").prop("disabled", false)
|
||||
$(".connection-required").prop("title", "")
|
||||
|
||||
complete_step("Connect")
|
||||
})
|
||||
api.on('disconnected', (code) => {
|
||||
$("#connection-status").text( tl("Disconnected") +" ("+code+")" )
|
||||
$("#connect-button").prop("disabled", false)
|
||||
$(".connection-required").prop("disabled", true)
|
||||
$(".connection-required").prop("title", tl("Connection required"))
|
||||
|
||||
disable_followup_steps()
|
||||
})
|
||||
$("#connect-button").click(() => {
|
||||
$("#connection-status").text( tl("Connecting...") )
|
||||
$("#loader-connect").show()
|
||||
api.connect()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used with _snippets/interactive-tutorials/wait-step.md
|
||||
* For each wait step in the page, set up a listener that checks for the
|
||||
* transaction shown in that step's "waiting-for-tx" field, as long as that
|
||||
* step's "tx-validation-status" field doesn't have a final result set.
|
||||
* These listeners do very little (just updating the latest validated ledger
|
||||
* index) until you activate one with activate_wait_step(step_name).
|
||||
* Requires ripple-lib to be loaded and instantiated as "api" first.
|
||||
*/
|
||||
function setup_wait_steps() {
|
||||
const wait_steps = $(".wait-step")
|
||||
|
||||
wait_steps.each(async (i, el) => {
|
||||
const wait_step = $(el)
|
||||
const explorer_url = wait_step.data("explorerurl")
|
||||
const status_box = wait_step.find(".tx-validation-status")
|
||||
api.on('ledger', async (ledger) => {
|
||||
// Update the latest validated ledger index in this step's table
|
||||
wait_step.find(".validated-ledger-version").text(ledger.ledgerVersion)
|
||||
if (!status_box.data("status_pending")) {
|
||||
// Before submission or after a final result.
|
||||
// Either way, nothing more to do here.
|
||||
return
|
||||
}
|
||||
|
||||
const transaction = wait_step.find(".waiting-for-tx").text().trim()
|
||||
const min_ledger = parseInt(wait_step.find(".earliest-ledger-version").text())
|
||||
const max_ledger = parseInt(wait_step.find(".lastledgersequence").text())
|
||||
let tx_result
|
||||
try {
|
||||
tx_result = await api.request("tx", {
|
||||
transaction,
|
||||
min_ledger,
|
||||
max_ledger
|
||||
})
|
||||
console.log(tx_result)
|
||||
|
||||
if (tx_result.validated) {
|
||||
status_box.html(
|
||||
`<th>${tl("Final Result:")}</th><td>${tx_result.meta.TransactionResult}
|
||||
(<a href="${explorer_url}/transactions/${transaction}"
|
||||
target="_blank">${tl("Validated")}</a>)</td>`)
|
||||
|
||||
const step_id = get_block_id(wait_step)
|
||||
if (!is_complete_by_id(step_id)) {
|
||||
status_box.data("status_pending", false)
|
||||
complete_step_by_id(step_id)
|
||||
}
|
||||
} else {
|
||||
status_box.html(
|
||||
`<th>${tl("Final Result:")}</th>
|
||||
<td><img class="throbber" src="assets/img/xrp-loader-96.png">
|
||||
${tl("(Still pending...)")}</td>`)
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
if (e.data.error == "txnNotFound" && e.data.searched_all) {
|
||||
status_box.html(
|
||||
`<th>${tl("Final Result:")}</th><td>${tl("Failed to achieve consensus (final)")}</td>`)
|
||||
} else {
|
||||
status_box.html(
|
||||
`<th>${tl("Final Result:")}</th><td>${tl("Unknown")}</td>`)
|
||||
}
|
||||
}
|
||||
}) // end 'ledger' event handler
|
||||
}) // end "each" wait_step
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used with _snippets/interactive-tutorials/wait-step.md
|
||||
* Populate the table of the wait step with the relevant transaction details
|
||||
* and signal this step's listener to look up the relevant transaction.
|
||||
* This function is called by the generic submit handlers that
|
||||
* make_submit_handler() creates.
|
||||
* @param {String} step_name The exact name of the "Wait" step to activate, as
|
||||
* defined in the start_step(step_name) function in
|
||||
* the MD file.
|
||||
* @param {Object} prelim_result The (resolved) return value of submitting a
|
||||
* transaction blob via api.request("submit", {opts})
|
||||
*/
|
||||
async function activate_wait_step(step_name, prelim_result) {
|
||||
const step_id = slugify(step_name)
|
||||
const wait_step = $(`#interactive-${step_id} .wait-step`)
|
||||
const status_box = wait_step.find(".tx-validation-status")
|
||||
const tx_id = prelim_result.tx_json.hash
|
||||
const lls = prelim_result.tx_json.LastLedgerSequence || tl("(None)")
|
||||
|
||||
if (wait_step.find(".waiting-for-tx").text() == tx_id) {
|
||||
// Re-submitting the same transaction? Don't update min_ledger.
|
||||
} else {
|
||||
wait_step.find(".waiting-for-tx").text(tx_id)
|
||||
wait_step.find(".earliest-ledger-version").text(
|
||||
prelim_result.validated_ledger_index
|
||||
)
|
||||
}
|
||||
wait_step.find(".lastledgersequence").text(lls)
|
||||
status_box.html("")
|
||||
status_box.data("status_pending", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for "Send Transaction" buttons to handle the full process of
|
||||
* Prepare → Sign → Submit in one step with appropriate outputs. Assumes you are
|
||||
* also using _snippets/interactive-tutorials/wait-step.md to report the
|
||||
* transaction's final results. Gets important information from data
|
||||
* attributes defined on the button that triggers the event:
|
||||
* data-tx-blob-from="{jQuery selector}" A selector for an element whose .text()
|
||||
* is the blob to submt.
|
||||
* data-wait-step-name="{String}" The exact name of the wait step where this
|
||||
* transaction's results should be reported, as
|
||||
* defined in start_step(step_name) as the MD
|
||||
* This function is meant to be called from within a .click(event) handler, not
|
||||
* directly bound as the click handler.
|
||||
* @param {Event} event The (click) event that this is helping to handle.
|
||||
* @param {Object} tx_json JSON object of transaction instructions to finish
|
||||
* preparing and send.
|
||||
*/
|
||||
async function generic_full_send(event, tx_json) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
const blob_selector = $(event.target).data("txBlobFrom")
|
||||
const wait_step_name = $(event.target).data("waitStepName")
|
||||
block.find(".output-area").html("")
|
||||
const secret = get_secret(event)
|
||||
if (!secret) {return}
|
||||
|
||||
block.find(".loader").show()
|
||||
const prepared = await api.prepareTransaction(tx_json, {
|
||||
maxLedgerVersionOffset: 20
|
||||
})
|
||||
block.find(".output-area").append(
|
||||
`<p>${tl("Prepared transaction:")}</p>
|
||||
<pre><code>${pretty_print(prepared.txJSON)}</code></pre>`)
|
||||
|
||||
const signed = api.sign(prepared.txJSON, secret)
|
||||
block.find(".output-area").append(
|
||||
`<p>${tl("Transaction hash:")} <code id="tx_id">${signed.id}</code></p>`)
|
||||
|
||||
await do_submit(block, {"tx_blob": signed.signedTransaction}, wait_step_name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic event handler for transaction submission buttons. Assumes
|
||||
* you are also using _snippets/interactive-tutorials/wait-step.md to report
|
||||
* the transaction's final results. Gets important information from data
|
||||
* attributes defined on the button that triggers the event:
|
||||
* data-tx-blob-from="{jQuery selector}" A selector for an element whose .text()
|
||||
* is the blob to submt.
|
||||
* data-wait-step-name="{String}" The exact name of the wait step where this
|
||||
* transaction's results should be reported, as
|
||||
* defined in start_step(step_name) as the MD
|
||||
* This function is intended to be bound directly on a submit button as the
|
||||
* click event handler.
|
||||
*/
|
||||
async function submit_handler(event) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
const blob_selector = $(event.target).data("txBlobFrom")
|
||||
const wait_step_name = $(event.target).data("waitStepName")
|
||||
const tx_blob = $(blob_selector).text()
|
||||
do_submit(block, {tx_blob}, wait_step_name)
|
||||
}
|
||||
|
||||
/**
|
||||
* General-purpose transaction submission helper.
|
||||
* @param {jQuery} block Output preliminary results inside this wrapping
|
||||
* .interactive-block's .output-area.
|
||||
* @param {Object} submit_opts The JSON object to be passed to the rippled
|
||||
* submit command. At a minimum, needs "tx_blob"
|
||||
* @param {String} wait_step_name The name of a wait step. Report the final
|
||||
* results of the transaction there. Must be a
|
||||
* _snippets/interactive-tutorials/wait-step.md
|
||||
*/
|
||||
async function do_submit(block, submit_opts, wait_step_name) {
|
||||
block.find(".loader").show()
|
||||
try {
|
||||
// Future feature: support passing in options like fail_hard here.
|
||||
const prelim_result = await api.request("submit", submit_opts)
|
||||
block.find(".output-area").append(
|
||||
`<p>${tl("Preliminary result:")}</p>
|
||||
<pre><code>${pretty_print(prelim_result)}</code></pre>`)
|
||||
|
||||
block.find(".loader").hide()
|
||||
submit_step_id = get_block_id(block)
|
||||
complete_step_by_id(submit_step_id)
|
||||
if (wait_step_name){
|
||||
activate_wait_step(wait_step_name, prelim_result)
|
||||
}
|
||||
} catch(error) {
|
||||
block.find(".loader").hide()
|
||||
show_error(block, error)
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
disable_followup_steps()
|
||||
setup_generate_step()
|
||||
setup_connect_step()
|
||||
setup_wait_steps()
|
||||
})
|
||||
|
||||
137
assets/js/tutorials/require-destination-tags.js
Normal file
137
assets/js/tutorials/require-destination-tags.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// 1. Generate
|
||||
// 2. Connect
|
||||
// The code for these steps is handled by interactive-tutorial.js
|
||||
$(document).ready(() => {
|
||||
|
||||
// 3. Send AccountSet --------------------------------------------------------
|
||||
$("#send-accountset").click( (event) => {
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
|
||||
generic_full_send(event, {
|
||||
"TransactionType": "AccountSet",
|
||||
"Account": address,
|
||||
"SetFlag": 1 // RequireDest
|
||||
})
|
||||
complete_step("Send AccountSet")
|
||||
})
|
||||
|
||||
// 4. Wait for Validation: handled by interactive-tutorial.js and by the
|
||||
// generic full send in the previous step. -----------------------------------
|
||||
|
||||
// 5. Confirm Account Settings -----------------------------------------------
|
||||
$("#confirm-settings").click( async (event) => {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
|
||||
block.find(".output-area").html("")
|
||||
block.find(".loader").show()
|
||||
let account_info = await api.request("account_info", {
|
||||
"account": address,
|
||||
"ledger_index": "validated"
|
||||
})
|
||||
console.log(account_info)
|
||||
const flags = api.parseAccountFlags(account_info.account_data.Flags)
|
||||
block.find(".loader").hide()
|
||||
|
||||
block.find(".output-area").append(
|
||||
`<pre><code>${pretty_print(flags)}</code></pre>`)
|
||||
if (flags.requireDestinationTag) {
|
||||
block.find(".output-area").append(`<p><i class="fa fa-check-circle"></i>
|
||||
Require Destination Tag is enabled.</p>`)
|
||||
} else {
|
||||
block.find(".output-area").append(`<p><i class="fa fa-times-circle"></i>
|
||||
Require Destination Tag is DISABLED.</p>`)
|
||||
}
|
||||
|
||||
complete_step("Confirm Settings")
|
||||
})
|
||||
|
||||
// Send Test Payments --------------------------------------------------------
|
||||
|
||||
// Helper to get an address to send the test payments from. Save the values
|
||||
// from the faucet in data attributes on the block so we don't have to get a
|
||||
// new sending address every time.
|
||||
async function get_test_sender(block) {
|
||||
let address = block.data("testSendAddress")
|
||||
let secret = block.data("testSendSecret")
|
||||
if (!address || !secret) {
|
||||
console.debug("First-time setup for test sender...")
|
||||
const faucet_url = $("#generate-creds-button").data("fauceturl")
|
||||
const data = await call_faucet(faucet_url)
|
||||
address = data.account.classicAddress
|
||||
block.data("testSendAddress", address)
|
||||
secret = data.account.secret
|
||||
block.data("testSendSecret", secret)
|
||||
// First time: Wait for our test sender to be fully funded, so we don't
|
||||
// get the wrong starting sequence number.
|
||||
while (true) {
|
||||
try {
|
||||
await api.request("account_info", {account: address, ledger_index: "validated"})
|
||||
break
|
||||
} catch(e) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
}
|
||||
return {address, secret}
|
||||
}
|
||||
|
||||
// Actual handler for the two buttons in the Send Test Payments block.
|
||||
// Gets the destination tag (or lack thereof) from their data attributes.
|
||||
$(".test-payment").click( async (event) => {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
|
||||
block.find(".loader").show()
|
||||
try {
|
||||
const test_sender = await get_test_sender(block)
|
||||
const tx_json = {
|
||||
"TransactionType": "Payment",
|
||||
"Account": test_sender.address,
|
||||
"Amount": "3152021",
|
||||
"Destination": address
|
||||
}
|
||||
const dt = $(event.target).data("dt")
|
||||
if (dt) {
|
||||
tx_json["DestinationTag"] = parseInt(dt)
|
||||
}
|
||||
|
||||
const prepared = await api.prepareTransaction(tx_json)
|
||||
const signed = api.sign(prepared.txJSON, test_sender.secret)
|
||||
console.debug("Submitting test payment", prepared.txJSON)
|
||||
const prelim_result = await api.request("submit",
|
||||
{"tx_blob": signed.signedTransaction})
|
||||
|
||||
block.find(".loader").hide()
|
||||
block.find(".output-area").append(`<p>${tx_json.TransactionType}
|
||||
${prepared.instructions.sequence} ${(dt?"WITH":"WITHOUT")} Dest. Tag:
|
||||
<a href="https://testnet.xrpl.org/transactions/${signed.id}"
|
||||
target="_blank">${prelim_result.engine_result}</a></p>`)
|
||||
} catch(err) {
|
||||
block.find(".loader").hide()
|
||||
show_error(`An error occurred when sending the test payment: ${err}`)
|
||||
}
|
||||
|
||||
|
||||
// SCRAPPED ALT CODE: using the Faucet as a test sender.
|
||||
// // We can use the Faucet to send a payment to our existing address with a
|
||||
// // destination tag, but only if we roll it into an X-address.
|
||||
// const faucet_url = $("#generate-creds-button").data("fauceturl")
|
||||
// const XCodec = require('xrpl-tagged-address-codec')
|
||||
// const dt = $(event.target).data("dt")
|
||||
// let dest_x_address
|
||||
// if (dt) {
|
||||
// dest_x_address = XCodec.Encode({ account: address, tag: dt, test: true })
|
||||
// } else {
|
||||
// dest_x_address = XCodec.Encode({ account: address, test: true })
|
||||
// }
|
||||
// call_faucet(faucet_url, dest_x_address)
|
||||
//
|
||||
// // TODO: monitor our target address and report when we receive the tx from
|
||||
// // the faucet. (including ✅ or ❌ as appropriate)
|
||||
})
|
||||
|
||||
})
|
||||
100
assets/js/tutorials/send-xrp.js
Normal file
100
assets/js/tutorials/send-xrp.js
Normal file
@@ -0,0 +1,100 @@
|
||||
// Prerequisite: Generate
|
||||
// 1. Connect
|
||||
// The code for these steps is handled by interactive-tutorial.js
|
||||
|
||||
$(document).ready(() => {
|
||||
|
||||
// 2. Prepare Transaction ------------------------------------------------------
|
||||
$("#prepare-button").click( async function(event) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
block.find(".output-area").html("")
|
||||
const send_amount = $("#xrp-amount").val()
|
||||
|
||||
const sender = get_address(event)
|
||||
if (!sender) {return}
|
||||
|
||||
const prepared = await api.prepareTransaction({
|
||||
"TransactionType": "Payment",
|
||||
"Account": sender,
|
||||
"Amount": api.xrpToDrops(send_amount), // Same as "Amount": "22000000"
|
||||
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
|
||||
}, {
|
||||
// Expire this transaction if it doesn't execute within ~5 minutes:
|
||||
"maxLedgerVersionOffset": 75
|
||||
})
|
||||
|
||||
block.find(".output-area").append(
|
||||
`<div><strong>Prepared transaction instructions:</strong>
|
||||
<pre><code id='prepared-tx-json'>${pretty_print(prepared.txJSON)}</code></pre>
|
||||
</div>
|
||||
<div><strong>Transaction cost:</strong> ${prepared.instructions.fee} XRP</div>
|
||||
<div><strong>Transaction expires after ledger:</strong>
|
||||
${prepared.instructions.maxLedgerVersion}</div>`)
|
||||
|
||||
complete_step("Prepare")
|
||||
})
|
||||
|
||||
|
||||
// 3. Sign the transaction -----------------------------------------------------
|
||||
$("#sign-button").click( function(event) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
block.find(".output-area").html("")
|
||||
|
||||
const preparedTxJSON = $("#prepared-tx-json").text()
|
||||
const secret = get_secret(event)
|
||||
if (!secret) {return}
|
||||
|
||||
signResponse = api.sign(preparedTxJSON, secret)
|
||||
|
||||
block.find(".output-area").html(
|
||||
`<div><strong>Signed Transaction blob:</strong>
|
||||
<code id='signed-tx-blob' style='overflow-wrap: anywhere; word-wrap: anywhere'
|
||||
>${signResponse.signedTransaction}</code></div>
|
||||
<div><strong>Identifying hash:</strong> <span id='signed-tx-hash'
|
||||
>${signResponse.id}</span></div>`
|
||||
)
|
||||
|
||||
complete_step("Sign")
|
||||
})
|
||||
|
||||
// 4. Submit the signed transaction --------------------------------------------
|
||||
$("#submit-button").click( submit_handler )
|
||||
|
||||
// 5. Wait for Validation: handled by interactive-tutorial.js and by the
|
||||
// generic submit handler in the previous step. --------------------------------
|
||||
|
||||
|
||||
// 6. Check transaction status -------------------------------------------------
|
||||
$("#get-tx-button").click( async function(event) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
// Wipe previous output
|
||||
block.find(".output-area").html("")
|
||||
|
||||
const txID = $("#signed-tx-hash").text()
|
||||
const earliestLedgerVersion = parseInt(
|
||||
$("#interactive-wait .earliest-ledger-version").text(), 10)
|
||||
const lastLedgerSequence = parseInt(
|
||||
$("#interactive-wait .lastledgersequence").text(), 10)
|
||||
|
||||
try {
|
||||
const tx = await api.getTransaction(txID, {
|
||||
minLedgerVersion: earliestLedgerVersion,
|
||||
maxLedgerVersion: lastLedgerSequence
|
||||
})
|
||||
|
||||
block.find(".output-area").html(
|
||||
"<div><strong>Transaction result:</strong> " +
|
||||
tx.outcome.result + "</div>" +
|
||||
"<div><strong>Balance changes:</strong> <pre><code>" +
|
||||
pretty_print(tx.outcome.balanceChanges) +
|
||||
"</pre></code></div>"
|
||||
)
|
||||
|
||||
complete_step("Check")
|
||||
} catch(error) {
|
||||
show_error(block, "Couldn't get transaction outcome:" + error)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@@ -1,64 +1,36 @@
|
||||
// 1. Generate -----------------------------------------------------------------
|
||||
// The code for this step is provided by the "generate-step.md" snippet
|
||||
|
||||
// 1. Generate
|
||||
// 2. Connect
|
||||
// The code for these steps is handled by interactive-tutorial.js
|
||||
$(document).ready(() => {
|
||||
// 2. Connect ------------------------------------------------------------------
|
||||
// TODO: switch to Testnet when Tickets enabled there
|
||||
api = new ripple.RippleAPI({server: 'wss://s.devnet.rippletest.net:51233'})
|
||||
api.on('connected', async function() {
|
||||
$("#connection-status").text("Connected")
|
||||
$("#connect-button").prop("disabled", true)
|
||||
$("#loader-connect").hide()
|
||||
|
||||
// Update breadcrumbs & activate next step
|
||||
complete_step("Connect")
|
||||
$("#check-sequence").prop("disabled", false)
|
||||
$("#check-sequence").prop("title", "")
|
||||
})
|
||||
api.on('disconnected', (code) => {
|
||||
$("#connection-status").text( "Disconnected ("+code+")" )
|
||||
$("#connect-button").prop("disabled", false)
|
||||
$(".connection-required").prop("disabled", true)
|
||||
$(".connection-required").prop("title", "Connection to Devnet required")
|
||||
})
|
||||
$("#connect-button").click(() => {
|
||||
$("#connection-status").text( "Connecting..." )
|
||||
$("#loader-connect").show()
|
||||
api.connect()
|
||||
})
|
||||
|
||||
// 3. Check Sequence Number
|
||||
$("#check-sequence").click( async function() {
|
||||
const address = $("#use-address").text()
|
||||
|
||||
if (!address) {
|
||||
$("#check-sequence-output").html(
|
||||
`<p class="devportal-callout warning"><strong>Error:</strong>
|
||||
No address. Make sure you <a href="#1-get-credentials">Get Credentials</a> first.</p>`)
|
||||
return;
|
||||
}
|
||||
$("#check-sequence").click( async function(event) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
|
||||
// Wipe previous output
|
||||
$("#check-sequence-output").html("")
|
||||
block.find(".output-area").html("")
|
||||
block.find(".loader").show()
|
||||
const account_info = await api.request("account_info", {"account": address})
|
||||
block.find(".loader").hide()
|
||||
|
||||
$("#check-sequence-output").append(
|
||||
block.find(".output-area").append(
|
||||
`<p>Current sequence:
|
||||
<code id="current_sequence">${account_info.account_data.Sequence}</code>
|
||||
</p>`)
|
||||
|
||||
|
||||
// Update breadcrumbs & activate next step
|
||||
complete_step("Check Sequence")
|
||||
$("#prepare-and-sign").prop("disabled", false)
|
||||
$("#prepare-and-sign").prop("title", "")
|
||||
})
|
||||
|
||||
// 4. Prepare and Sign TicketCreate --------------------------------------------
|
||||
$("#prepare-and-sign").click( async function() {
|
||||
const address = $("#use-address").text()
|
||||
const secret = $("#use-secret").text()
|
||||
let current_sequence;
|
||||
$("#prepare-and-sign").click( async function(event) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
const secret = get_secret(event)
|
||||
if (!secret) {return}
|
||||
let current_sequence
|
||||
try {
|
||||
current_sequence = parseInt($("#current_sequence").text())
|
||||
} catch (e) {
|
||||
@@ -66,13 +38,12 @@ $("#prepare-and-sign").click( async function() {
|
||||
}
|
||||
|
||||
// Wipe previous output
|
||||
$("#prepare-and-sign-output").html("")
|
||||
block.find(".output-area").html("")
|
||||
|
||||
if (!address || !secret || !current_sequence) {
|
||||
$("#prepare-and-sign-output").html(
|
||||
`<p class="devportal-callout warning"><strong>Error:</strong>
|
||||
Couldn't get a valid address/secret/sequence value. Check that the
|
||||
previous steps were completed successfully.</p>`)
|
||||
if (!current_sequence) {
|
||||
show_error(block,
|
||||
`Couldn't get a valid sequence value. Check that the
|
||||
previous steps were completed successfully.`)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,141 +56,63 @@ $("#prepare-and-sign").click( async function() {
|
||||
maxLedgerVersionOffset: 20
|
||||
})
|
||||
|
||||
$("#prepare-and-sign-output").append(
|
||||
block.find(".output-area").append(
|
||||
`<p>Prepared transaction:</p>
|
||||
<pre><code>${pretty_print(prepared.txJSON)}</code></pre>`)
|
||||
$("#lastledgersequence").html(
|
||||
`<code>${prepared.instructions.maxLedgerVersion}</code>`)
|
||||
|
||||
let signed = api.sign(prepared.txJSON, secret)
|
||||
$("#prepare-and-sign-output").append(
|
||||
block.find(".output-area").append(
|
||||
`<p>Transaction hash: <code id="tx_id">${signed.id}</code></p>`)
|
||||
$("#waiting-for-tx").text(signed.id)
|
||||
|
||||
// Reset the "Wait" step to prevent mixups
|
||||
$("#earliest-ledger-version").text("(Not submitted)")
|
||||
$("#tx-validation-status").html("<th>Final Result:</th><td></td>")
|
||||
|
||||
let tx_blob = signed.signedTransaction
|
||||
$("#prepare-and-sign-output").append(
|
||||
`<pre style="visibility: none"><code id="tx_blob">${tx_blob}</code></pre>`)
|
||||
block.find(".output-area").append(
|
||||
`<p>Signed blob:</p><pre class="tx-blob"><code id="tx_blob">${tx_blob}</code></pre>`)
|
||||
|
||||
// Update breadcrumbs & activate next step
|
||||
complete_step("Prepare & Sign")
|
||||
$("#ticketcreate-submit").prop("disabled", false)
|
||||
$("#ticketcreate-submit").prop("title", "")
|
||||
|
||||
})
|
||||
|
||||
// 5. Submit TicketCreate ------------------------------------------------------
|
||||
$("#ticketcreate-submit").click( async function() {
|
||||
const tx_blob = $("#tx_blob").text()
|
||||
// Wipe previous output
|
||||
$("#ticketcreate-submit-output").html("")
|
||||
$("#ticketcreate-submit").click( submit_handler )
|
||||
|
||||
waiting_for_tx = $("#tx_id").text() // next step uses this
|
||||
let prelim_result = await api.request("submit", {"tx_blob": tx_blob})
|
||||
$("#ticketcreate-submit-output").append(
|
||||
`<p>Preliminary result:</p>
|
||||
<pre><code>${pretty_print(prelim_result)}</code></pre>`)
|
||||
|
||||
if ( $("#earliest-ledger-version").text() == "(Not submitted)" ) {
|
||||
// This is the first time we've submitted this transaction, so set the
|
||||
// minimum ledger index for this transaction. Don't overwrite this if this
|
||||
// isn't the first time the transaction has been submitted!
|
||||
$("#earliest-ledger-version").text(prelim_result.validated_ledger_index)
|
||||
}
|
||||
|
||||
// Update breadcrumbs
|
||||
complete_step("Submit")
|
||||
})
|
||||
|
||||
|
||||
// 6. Wait for Validation
|
||||
let waiting_for_tx = null;
|
||||
api.on('ledger', async (ledger) => {
|
||||
$("#current-ledger-version").text(ledger.ledgerVersion)
|
||||
|
||||
let tx_result;
|
||||
let min_ledger = parseInt($("#earliest-ledger-version").text())
|
||||
let max_ledger = parseInt($("#lastledgersequence").text())
|
||||
if (min_ledger > max_ledger) {
|
||||
console.warn("min_ledger > max_ledger")
|
||||
min_ledger = 1
|
||||
}
|
||||
if (waiting_for_tx) {
|
||||
try {
|
||||
tx_result = await api.request("tx", {
|
||||
"transaction": waiting_for_tx,
|
||||
"min_ledger": min_ledger,
|
||||
"max_ledger": max_ledger
|
||||
})
|
||||
if (tx_result.validated) {
|
||||
$("#tx-validation-status").html(
|
||||
`<th>Final Result:</th><td>${tx_result.meta.TransactionResult}
|
||||
(<a href="https://devnet.xrpl.org/transactions/${waiting_for_tx}"
|
||||
target="_blank">Validated</a>)</td>`)
|
||||
waiting_for_tx = null;
|
||||
|
||||
if ( $(".breadcrumb-item.bc-wait").hasClass("active") ) {
|
||||
complete_step("Wait")
|
||||
$("#check-tickets").prop("disabled", false)
|
||||
$("#check-tickets").prop("title", "")
|
||||
$("#intermission-payment").prop("disabled", false)
|
||||
$("#intermission-payment").prop("title", "")
|
||||
$("#intermission-escrowcreate").prop("disabled", false)
|
||||
$("#intermission-escrowcreate").prop("title", "")
|
||||
$("#intermission-accountset").prop("disabled", false)
|
||||
$("#intermission-accountset").prop("title", "")
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
if (e.data.error == "txnNotFound" && e.data.searched_all) {
|
||||
$("#tx-validation-status").html(
|
||||
`<th>Final Result:</th><td>Failed to achieve consensus (final)</td>`)
|
||||
waiting_for_tx = null;
|
||||
} else {
|
||||
$("#tx-validation-status").html(
|
||||
`<th>Final Result:</th><td>Unknown</td>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
// 6. Wait for Validation: handled by interactive-tutorial.js and by the
|
||||
// generic submit handler in the previous step. --------------------------------
|
||||
|
||||
// Intermission ----------------------------------------------------------------
|
||||
async function intermission_submit(tx_json) {
|
||||
const secret = $("#use-secret").text()
|
||||
async function intermission_submit(event, tx_json) {
|
||||
const secret = get_secret(event)
|
||||
if (!secret) {return}
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
let prepared = await api.prepareTransaction(tx_json)
|
||||
let signed = api.sign(prepared.txJSON, secret)
|
||||
let prelim_result = await api.request("submit",
|
||||
{"tx_blob": signed.signedTransaction})
|
||||
|
||||
$("#intermission-output").append(`<p>${tx_json.TransactionType}
|
||||
block.find(".output-area").append(`<p>${tx_json.TransactionType}
|
||||
${prepared.instructions.sequence}:
|
||||
<a href="https://devnet.xrpl.org/transactions/${signed.id}"
|
||||
target="_blank">${prelim_result.engine_result}</a></p>`)
|
||||
}
|
||||
|
||||
$("#intermission-payment").click( async function() {
|
||||
const address = $("#use-address").text()
|
||||
$("#intermission-payment").click( async function(event) {
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
|
||||
intermission_submit({
|
||||
intermission_submit(event, {
|
||||
"TransactionType": "Payment",
|
||||
"Account": address,
|
||||
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", // TestNet Faucet
|
||||
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", // Testnet Faucet
|
||||
"Amount": api.xrpToDrops("201")
|
||||
})
|
||||
|
||||
// Update breadcrumbs; though, this step is optional,
|
||||
// so the previous step already enabled the step after this.
|
||||
complete_step("Intermission")
|
||||
})
|
||||
|
||||
$("#intermission-escrowcreate").click( async function() {
|
||||
const address = $("#use-address").text()
|
||||
$("#intermission-escrowcreate").click( async function(event) {
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
|
||||
intermission_submit({
|
||||
intermission_submit(event, {
|
||||
"TransactionType": "EscrowCreate",
|
||||
"Account": address,
|
||||
"Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", // Genesis acct
|
||||
@@ -227,37 +120,39 @@ $("#intermission-escrowcreate").click( async function() {
|
||||
"FinishAfter": api.iso8601ToRippleTime(Date()) + 30 // 30 seconds from now
|
||||
})
|
||||
|
||||
// Update breadcrumbs; though this step is optional,
|
||||
// so the previous step already enabled the step after this.
|
||||
complete_step("Intermission")
|
||||
})
|
||||
|
||||
$("#intermission-accountset").click( async function() {
|
||||
const address = $("#use-address").text()
|
||||
$("#intermission-accountset").click( async function(event) {
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
|
||||
intermission_submit({
|
||||
intermission_submit(event, {
|
||||
"TransactionType": "AccountSet",
|
||||
"Account": address
|
||||
})
|
||||
|
||||
// Update breadcrumbs; though this step is optional,
|
||||
// so the previous step already enabled the step after this.
|
||||
complete_step("Intermission")
|
||||
})
|
||||
|
||||
// 7. Check Available Tickets --------------------------------------------------
|
||||
$("#check-tickets").click( async function() {
|
||||
const address = $("#use-address").text()
|
||||
$("#check-tickets").click( async function(event) {
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
// Wipe previous output
|
||||
$("#check-tickets-output").html("")
|
||||
block.find(".output-area").html("")
|
||||
block.find(".loader").show()
|
||||
|
||||
let response = await api.request("account_objects", {
|
||||
"account": address,
|
||||
"type": "ticket"
|
||||
})
|
||||
$("#check-tickets-output").html(
|
||||
block.find(".output-area").html(
|
||||
`<pre><code>${pretty_print(response)}</code></pre>`)
|
||||
|
||||
block.find(".loader").hide()
|
||||
|
||||
// Reset the next step's form & add these tickets
|
||||
$("#ticket-selector .form-area").html("")
|
||||
response.account_objects.forEach((ticket, i) => {
|
||||
@@ -269,26 +164,24 @@ $("#check-tickets").click( async function() {
|
||||
for="ticket${i}">${ticket.TicketSequence}</label></div>`)
|
||||
})
|
||||
|
||||
|
||||
// Update breadcrumbs & activate next step
|
||||
complete_step("Check Tickets")
|
||||
$("#prepare-ticketed-tx").prop("disabled", false)
|
||||
$("#prepare-ticketed-tx").prop("title", "")
|
||||
})
|
||||
|
||||
// 8. Prepare Ticketed Transaction ---------------------------------------------
|
||||
$("#prepare-ticketed-tx").click(async function() {
|
||||
$("#prepare-ticketed-tx").click(async function(event) {
|
||||
const address = get_address(event)
|
||||
if (!address) {return}
|
||||
const secret = get_secret(event)
|
||||
if (!secret) {return}
|
||||
|
||||
const block = $(event.target).closest(".interactive-block")
|
||||
block.find(".output-area").html("")
|
||||
const use_ticket = parseInt($('input[name="ticket-radio-set"]:checked').val())
|
||||
if (!use_ticket) {
|
||||
$("#prepare-ticketed-tx-output").append(
|
||||
`<p class="devportal-callout warning"><strong>Error</strong>
|
||||
You must choose a ticket first.</p>`)
|
||||
show_error(block, "You must choose a ticket first.")
|
||||
return
|
||||
}
|
||||
|
||||
const address = $("#use-address").text()
|
||||
const secret = $("#use-secret").text()
|
||||
|
||||
let prepared_t = await api.prepareTransaction({
|
||||
"TransactionType": "AccountSet",
|
||||
"Account": address,
|
||||
@@ -297,81 +190,28 @@ $("#prepare-ticketed-tx").click(async function() {
|
||||
}, {
|
||||
maxLedgerVersionOffset: 20
|
||||
})
|
||||
$("#prepare-ticketed-tx-output").html("")
|
||||
|
||||
$("#prepare-ticketed-tx-output").append(
|
||||
block.find(".output-area").append(
|
||||
`<p>Prepared transaction:</p>
|
||||
<pre><code>${pretty_print(prepared_t.txJSON)}</code></pre>`)
|
||||
$("#lastledgersequence_t").html( //REMEMBER
|
||||
`<code>${prepared_t.instructions.maxLedgerVersion}</code>`)
|
||||
|
||||
let signed_t = api.sign(prepared_t.txJSON, secret)
|
||||
$("#prepare-ticketed-tx-output").append(
|
||||
block.find(".output-area").append(
|
||||
`<p>Transaction hash: <code id="tx_id_t">${signed_t.id}</code></p>`)
|
||||
|
||||
let tx_blob_t = signed_t.signedTransaction
|
||||
$("#prepare-ticketed-tx-output").append(
|
||||
block.find(".output-area").append(
|
||||
`<pre style="visibility: none">
|
||||
<code id="tx_blob_t">${tx_blob_t}</code></pre>`)
|
||||
|
||||
// Update breadcrumbs & activate next step
|
||||
complete_step("Prepare Ticketed Tx")
|
||||
$("#ticketedtx-submit").prop("disabled", false)
|
||||
$("#ticketedtx-submit").prop("title", "")
|
||||
})
|
||||
|
||||
// 9. Submit Ticketed Transaction ----------------------------------------------
|
||||
$("#ticketedtx-submit").click( async function() {
|
||||
const tx_blob = $("#tx_blob_t").text()
|
||||
// Wipe previous output
|
||||
$("#ticketedtx-submit-output").html("")
|
||||
$("#ticketedtx-submit").click( submit_handler )
|
||||
|
||||
waiting_for_tx_t = $("#tx_id_t").text() // next step uses this
|
||||
let prelim_result = await api.request("submit", {"tx_blob": tx_blob})
|
||||
$("#ticketedtx-submit-output").append(
|
||||
`<p>Preliminary result:</p>
|
||||
<pre><code>${pretty_print(prelim_result)}</code></pre>`)
|
||||
$("#earliest-ledger-version_t").text(prelim_result.validated_ledger_index)
|
||||
// 10. Wait for Validation (Again): handled by interactive-tutorial.js and by
|
||||
// the generic submit handler in the previous step. --------------------------------
|
||||
|
||||
// Update breadcrumbs
|
||||
complete_step("Submit Ticketed Tx")
|
||||
})
|
||||
|
||||
// 10. Wait for Validation (again) ---------------------------------------------
|
||||
let waiting_for_tx_t = null;
|
||||
api.on('ledger', async (ledger) => {
|
||||
$("#current-ledger-version_t").text(ledger.ledgerVersion)
|
||||
|
||||
let tx_result;
|
||||
if (waiting_for_tx_t) {
|
||||
try {
|
||||
tx_result = await api.request("tx", {
|
||||
"transaction": waiting_for_tx_t,
|
||||
"min_ledger": parseInt($("#earliest-ledger-version_t").text()),
|
||||
"max_ledger": parseInt($("#lastledgersequence_t").text())
|
||||
})
|
||||
console.log(tx_result)
|
||||
if (tx_result.validated) {
|
||||
$("#tx-validation-status_t").html(
|
||||
`<th>Final Result:</th><td>${tx_result.meta.TransactionResult}
|
||||
(<a href="https://devnet.xrpl.org/transactions/${waiting_for_tx_t}"
|
||||
target="_blank">Validated</a>)</td>`)
|
||||
waiting_for_tx_t = null;
|
||||
|
||||
if ( $(".breadcrumb-item.bc-wait_again").hasClass("active") ) {
|
||||
complete_step("Wait Again")
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
if (e.data.error == "txnNotFound" && e.data.searched_all) {
|
||||
$("#tx-validation-status_t").html(
|
||||
`<th>Final Result:</th><td>Failed to achieve consensus (final)</td>`)
|
||||
waiting_for_tx_t = null;
|
||||
} else {
|
||||
$("#tx-validation-status_t").html(
|
||||
`<th>Final Result:</th><td>Unknown</td>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -239,6 +239,17 @@ const set_up_tx_sender = async function() {
|
||||
}
|
||||
$("#pp_progress .progress-bar").width("20%")
|
||||
|
||||
// Wait for the address's funding to be validated so we don't get the wrong
|
||||
// starting sequence number.
|
||||
while (true) {
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
await api.request("account_info", {account: pp_issuer_address,
|
||||
ledger_index: "validated"})
|
||||
break
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// 2. Set Default Ripple on issuer
|
||||
let resp = await submit_and_verify({
|
||||
TransactionType: "AccountSet",
|
||||
@@ -311,9 +322,32 @@ const set_up_tx_sender = async function() {
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Button Handlers
|
||||
// Button/UI Handlers
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Destination Address box -----------------------------------------------
|
||||
async function on_dest_address_update(event) {
|
||||
const d_a = $("#destination_address").val()
|
||||
if (api.isValidAddress(d_a)) {
|
||||
$("#destination_address").addClass("is-valid").removeClass("is-invalid")
|
||||
if (d_a[0] == "X") {
|
||||
$("#x-address-warning").show()
|
||||
} else {
|
||||
$("#x-address-warning").hide()
|
||||
}
|
||||
} else {
|
||||
$("#destination_address").addClass("is-invalid").removeClass("is-valid")
|
||||
$("#x-address-warning").hide()
|
||||
}
|
||||
}
|
||||
$("#destination_address").change(on_dest_address_update)
|
||||
const search_params = new URLSearchParams(window.location.search)
|
||||
if (search_params.has("destination")) {
|
||||
const d_a = search_params.get("destination")
|
||||
$("#destination_address").val(d_a)
|
||||
on_dest_address_update()
|
||||
}
|
||||
|
||||
// 1. Send XRP Payment Handler -------------------------------------------
|
||||
async function on_click_send_xrp_payment(event) {
|
||||
const destination_address = $("#destination_address").val()
|
||||
|
||||
Reference in New Issue
Block a user