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:
mDuo13
2021-03-10 18:33:17 -08:00
parent d98249e984
commit 6d91616a62
37 changed files with 1928 additions and 1031 deletions

View 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)
})
})

View 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)
}
})
})

View File

@@ -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>`)
}
}
}
})
})