const set_up_tx_sender = async function() { ////////////////////////////////////////////////////////////////////////////// // Notification helpers ////////////////////////////////////////////////////////////////////////////// function successNotif(msg) { $.bootstrapGrowl(msg, { delay: 7000, offset: {from: 'bottom', amount: 68}, type: 'success', width: 'auto' }) } function errorNotif(msg) { $.bootstrapGrowl(msg, { delay: 7000, offset: {from: 'bottom', amount: 68}, type: 'danger', width: 'auto' }) } function logTx(txtype, hash, result) { let classes let icon const txlink = "https://testnet.xrpl.org/transactions/" + hash if (result === "tesSUCCESS") { classes = "text-muted" icon = '' } else { classes = "list-group-item-danger" icon = '' } const li = `
  • ${icon} ${txtype}: ${hash}
  • ` $("#tx-sender-history ul").prepend(li) } ////////////////////////////////////////////////////////////////////////////// // Connection / Setup ////////////////////////////////////////////////////////////////////////////// const FAUCET_URL = "https://faucet.altnet.rippletest.net/accounts" const TESTNET_URL = "wss://s.altnet.rippletest.net:51233" let connection_ready = false api = new xrpl.Client(TESTNET_URL) let sending_wallet let xrp_balance = "TBD" function enable_buttons_if_ready() { if ( (typeof sending_wallet) === "undefined") { console.debug("No sending address yet...") return false } if (!connection_ready) { console.debug("API not connected yet...") return false } $(".needs-connection").prop("disabled", false) $(".needs-connection").removeClass("disabled") set_up_for_partial_payments() return true } $("#init_button").click(async (evt) => { console.log("Connecting to Testnet WebSocket...") await api.connect() console.debug("Getting a sending address from the faucet...") try { const fund_response = await api.fundWallet() sending_wallet = fund_response.wallet xrp_balance = xrpl.dropsToXrp(fund_response.balance) } catch(error) { console.error(error) errorNotif("There was an error with the XRP Ledger Testnet Faucet. Reload this page to try again.") return } $("#balance-item").text(xrp_balance) $(".sending-address-item").text(sending_wallet.address) $("#init_button").prop("disabled", "disabled") $("#init_button").addClass("disabled") $("#init_button").attr("title", "Done") $("#init_button").append(' ') enable_buttons_if_ready() }) api.on('connected', () => { connection_ready = true $("#connection-status-item").text("Connected") $("#connection-status-item").removeClass("disabled").addClass("active") enable_buttons_if_ready() }) api.on('disconnected', (code) => { connection_ready = false $("#connection-status-item").text("Not connected") $("#connection-status-item").removeClass("active").addClass("disabled") }) ////////////////////////////////////////////////////////////////////////////// // Generic Transaction Submission ////////////////////////////////////////////////////////////////////////////// // Helper function for await-able timeouts function timeout(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function update_xrp_balance() { balances = await api.getBalances(sending_wallet.address, {currency: "XRP"}) $("#balance-item").text(balances[0].value) } async function submit_and_notify(tx_object, use_wallet, silent) { if (use_wallet === undefined) { use_wallet = sending_wallet } try { // Auto-fill fields like Fee and Sequence prepared = await api.autofill(tx_object) console.debug("Prepared:", prepared) } catch(error) { console.log(error) if (!silent) { errorNotif("Error preparing tx: "+error) } return } try { const {tx_blob, hash} = use_wallet.sign(prepared) const final_result_data = await api.submitAndWait(tx_blob) console.log("final_result_data is", final_result_data) let final_result = final_result_data.result.meta.TransactionResult if (!silent) { if (final_result === "tesSUCCESS") { successNotif(`${tx_object.TransactionType} tx succeeded (hash: ${hash})`) } else { errorNotif(`${tx_object.TransactionType} tx failed w/ code ${final_result} (hash: ${hash})`) } logTx(tx_object.TransactionType, hash, final_result) } update_xrp_balance() return final_result_data } catch (error) { console.log(error) if (!silent) { errorNotif(`Error signing & submitting ${tx_object.TransactionType} tx: ${error}`) } return } } ////////////////////////////////////////////////////////////////////////////// // Issuer Setup for Partial Payments // (Partial payments must involve at least one issued currency, so we set up // an issuer for a fake currency to ripple through.) ////////////////////////////////////////////////////////////////////////////// let pp_issuer_wallet let pp_sending_currency = "BAR" async function set_up_for_partial_payments() { while (!connection_ready) { console.debug("... waiting for connection before doing partial payment setup") await timeout(200) } console.debug("Starting partial payment setup...") $("#pp_progress .progress-bar").addClass("progress-bar-animated") // 1. Get a funded address to use as issuer try { const fund_response = await api.fundWallet() pp_issuer_wallet = fund_response.wallet } catch(error) { console.log("Error getting issuer address for partial payments:", error) return } $("#pp_progress .progress-bar").width("20%") // 2. Set Default Ripple on issuer let resp = await submit_and_notify({ TransactionType: "AccountSet", Account: pp_issuer_wallet.address, SetFlag: xrpl.AccountSetAsfFlags.asfDefaultRipple }, pp_issuer_wallet, true) if (resp === undefined) { console.log("Couldn't set Default Ripple for partial payment issuer") return } $("#pp_progress .progress-bar").width("40%") // 3. Make a trust line from sending address to issuer resp = await submit_and_notify({ TransactionType: "TrustSet", Account: sending_wallet.address, LimitAmount: { currency: pp_sending_currency, value: "1000000000", // arbitrarily, 1 billion fake currency issuer: pp_issuer_wallet.address } }, sending_wallet, true) if (resp === undefined) { console.log("Error making trust line to partial payment issuer") return } $("#pp_progress .progress-bar").width("60%") // 4. Issue fake currency to main sending address resp = await submit_and_notify({ TransactionType: "Payment", Account: pp_issuer_wallet.address, Destination: sending_wallet.address, Amount: { currency: pp_sending_currency, value: "1000000000", issuer: pp_issuer_wallet.address } }, pp_issuer_wallet, true) if (resp === undefined) { console.log("Error sending fake currency from partial payment issuer") return } $("#pp_progress .progress-bar").width("80%") // 5. Place offer to buy issued currency for XRP // When sending the partial payment, the sender consumes their own offer (!) // so they end up paying themselves issued currency then delivering XRP. resp = await submit_and_notify({ TransactionType: "OfferCreate", Account: sending_wallet.address, TakerGets: "1000000000000000", // 1 billion XRP TakerPays: { currency: pp_sending_currency, value: "1000000000", issuer: pp_issuer_wallet.address } }, sending_wallet, true) if (resp === undefined) { console.log("Error placing order to enable partial payments") return } $("#pp_progress .progress-bar").width("100%").removeClass("progress-bar-animated") $("#pp_progress").hide() // Done. Enable "Send Partial Payment" button console.log("Done getting ready to send partial payments.") $("#send_partial_payment button").prop("disabled",false) $("#send_partial_payment button").attr("title", "") } ////////////////////////////////////////////////////////////////////////////// // Button/UI Handlers ////////////////////////////////////////////////////////////////////////////// // Destination Address box ----------------------------------------------- async function on_dest_address_update(event) { const d_a = $("#destination_address").val() if (xrpl.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() const xrp_drops_input = $("#send_xrp_payment_amount").val() $("#send_xrp_payment .loader").show() $("#send_xrp_payment button").prop("disabled","disabled") await submit_and_notify({ TransactionType: "Payment", Account: sending_wallet.address, Destination: destination_address, Amount: xrp_drops_input }) $("#send_xrp_payment .loader").hide() $("#send_xrp_payment button").prop("disabled",false) } $("#send_xrp_payment button").click(on_click_send_xrp_payment) // 2. Send Partial Payment Handler --------------------------------------- async function on_click_send_partial_payment(event) { const destination_address = $("#destination_address").val() $("#send_partial_payment .loader").show() $("#send_partial_payment button").prop("disabled","disabled") await submit_and_notify({ TransactionType: "Payment", Account: sending_wallet.address, Destination: destination_address, Amount: "1000000000000000", // 1 billion XRP SendMax: { value: (Math.random()*.01).toPrecision(15), // random very small amount currency: pp_sending_currency, issuer: pp_issuer_wallet.address }, Flags: xrpl.PaymentFlags.tfPartialPayment }) $("#send_partial_payment .loader").hide() $("#send_partial_payment button").prop("disabled",false) } $("#send_partial_payment button").click(on_click_send_partial_payment) // 3. Create Escrow Handler ---------------------------------------------- async function on_click_create_escrow(event) { const destination_address = $("#destination_address").val() const duration_seconds_txt = $("#create_escrow_duration_seconds").val() const release_auto = $("#create_escrow_release_automatically").prop("checked") const duration_seconds = parseInt(duration_seconds_txt, 10) if (duration_seconds === NaN || duration_seconds < 1) { errorNotif("Error: Escrow duration must be a positive number of seconds") return } const finish_after = xrpl.isoTimeToRippleTime(Date()) + duration_seconds $("#create_escrow .loader").show() $("#create_escrow button").prop("disabled","disabled") const escrowcreate_tx_data = await submit_and_notify({ TransactionType: "EscrowCreate", Account: sending_wallet.address, Destination: destination_address, Amount: "1000000", FinishAfter: finish_after }) if (escrowcreate_tx_data && release_auto) { // Wait until there's a ledger with a close time > FinishAfter // to submit the EscrowFinish $("#escrow_progress .progress-bar").width("0%").addClass("progress-bar-animated") $("#escrow_progress").show() let seconds_left let pct_done let latestCloseTimeRipple while (true) { seconds_left = (finish_after - xrpl.isoTimeToRippleTime(Date())) pct_done = Math.min(99, Math.max(0, (1-(seconds_left / duration_seconds)) * 100)) $("#escrow_progress .progress-bar").width(pct_done+"%") if (seconds_left <= 0) { // System time has advanced past FinishAfter. But is there a new // enough validated ledger? latest_close_time = (await api.request({ command: "ledger", "ledger_index": "validated"} )).result.ledger.close_time if (latest_close_time > finish_after) { $("#escrow_progress .progress-bar").width("100%").removeClass("progress-bar-animated") break } } // Update the progress bar & check again in 1 second. await timeout(1000) } $("#escrow_progress").hide() // Now submit the EscrowFinish // Future feature: submit from a different sender, just to prove that // escrows can be finished by a third party await submit_and_notify({ Account: sending_wallet.address, TransactionType: "EscrowFinish", Owner: sending_wallet.address, OfferSequence: escrowcreate_tx_data.result.Sequence }) } $("#create_escrow .loader").hide() $("#create_escrow button").prop("disabled",false) } $("#create_escrow button").click(on_click_create_escrow) // 4. Create Payment Channel Handler ------------------------------------- async function on_click_create_payment_channel(event) { const destination_address = $("#destination_address").val() const xrp_drops_input = $("#create_payment_channel_amount").val() const pubkey = sending_wallet.publicKey $("#create_payment_channel .loader").show() $("#create_payment_channel button").prop("disabled","disabled") await submit_and_notify({ TransactionType: "PaymentChannelCreate", Account: sending_wallet.address, Destination: destination_address, Amount: xrp_drops_input, SettleDelay: 30, PublicKey: pubkey }) $("#create_payment_channel .loader").hide() $("#create_payment_channel button").prop("disabled",false) // Future feature: figure out channel ID and enable a button that creates // valid claims for the given payment channel to help test redeeming } $("#create_payment_channel button").click(on_click_create_payment_channel) // 5. Send Issued Currency Handler --------------------------------------- async function on_click_send_issued_currency(event) { const destination_address = $("#destination_address").val() const issue_amount = $("#send_issued_currency_amount").val() const issue_code = $("#send_issued_currency_code").text() $("#send_issued_currency .loader").show() $("#send_issued_currency button").prop("disabled","disabled") // Future feature: cross-currency sending with paths? await submit_and_notify({ TransactionType: "Payment", Account: sending_wallet.address, Destination: destination_address, Amount: { "currency": issue_code, "value": issue_amount, "issuer": sending_wallet.address } }) $("#send_issued_currency .loader").hide() $("#send_issued_currency button").prop("disabled",false) } $("#send_issued_currency button").click(on_click_send_issued_currency) // 6. Trust For Handler async function on_trust_for(event) { const destination_address = $("#destination_address").val() const trust_limit = $("#trust_for_amount").val() const trust_currency_code = $("#trust_for_currency_code").text() $("#trust_for .loader").show() $("#trust_for button").prop("disabled","disabled") await submit_and_notify({ TransactionType: "TrustSet", Account: sending_wallet.address, LimitAmount: { currency: trust_currency_code, value: trust_limit, issuer: destination_address } }) $("#trust_for .loader").hide() $("#trust_for button").prop("disabled",false) } $("#trust_for button").click(on_trust_for) } $(document).ready( function() { set_up_tx_sender() } )