beginner 120 min

Code with XRPL & JavaScript

Getting started coding on the XRP Ledger with JavaScript: create accounts, send XRP, create trustlines, send tokens, and mint NFTs.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Set up a TypeScript project with xrpl.js and run it with tsx.
  • Connect to the XRPL testnet over WebSocket and confirm the connection.
  • Generate and fund a wallet from the testnet faucet, activating an account.
  • Build, autofill, sign, and submit a transaction with submitAndWait.
  • Read the result metadata and confirm tesSUCCESS (vs. a tec/tem error).
Complete this module by peer review. Jump to assessment

Overview

This is the module where you stop reading about the XRP Ledger and start talking to it from code. Every later build module — payments, tokens, NFTs — assumes you can already do four things in your sleep: connect, fund a wallet, submit a transaction, and read the result. That's the whole job here. We keep the surface area deliberately small so the shape of an XRPL interaction becomes muscle memory, not a thing you copy-paste and hope works.

Think of xrpl.js as a translator sitting between your intentions and the ledger. You describe what you want as a plain JavaScript object — "set a flag on my account", "send this amount to that address" — and the library handles the unglamorous wire details: opening a WebSocket, looking up your account's next sequence number, calculating a fee, signing with your keys, broadcasting, and waiting for validators to agree. You think in intent; it thinks in protocol.

Everything here runs against the testnet, a full working copy of the XRP Ledger whose XRP has no monetary value. A faucet hands you free test-XRP on demand, so you can break things, retry, and experiment with zero risk. The exact same code, pointed at a mainnet endpoint and funded with real XRP, would behave identically — which is the quiet superpower of learning here first.

Set up the project

You need almost nothing: one runtime dependency and a TypeScript runner so you can execute .ts files directly without a build step.

npm init -y
npm i xrpl
npm i -D typescript tsx @types/node
  • xrpl is the official JavaScript/TypeScript client for the XRP Ledger. It bundles the WebSocket client, the Wallet class, transaction autofilling, signing, and submission.
  • tsx lets you run TypeScript straight from source — npx tsx index.ts — so there's no separate compile step while you're learning.

Create a file called index.ts. That single file is all you'll need for this module.

Connect to the testnet

The first thing any script does is open a connection. xrpl.js talks to the ledger over a WebSocket — a persistent two-way socket, not one-shot HTTP requests — so the library can stream ledger updates and wait for your transaction to be validated rather than polling for it.

import * as xrpl from 'xrpl'

const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')

async function main() {
  await client.connect()
  console.log('Connected to the XRPL testnet')

  // ... everything below goes inside main() ...

  await client.disconnect()
  console.log('Disconnected')
}

main().catch(console.error)

wss://s.altnet.rippletest.net:51233 is a public testnet endpoint. Run the file with npx tsx index.ts. If you see Connected to the XRPL testnet, your socket is open and you're ready to go. Always pair connect() with disconnect() — an open socket will keep the Node process alive forever otherwise.

Create and fund a wallet

A wallet on the XRP Ledger is just a cryptographic keypair plus the address derived from it. You can generate one offline in a single call:

const fresh = xrpl.Wallet.generate()
console.log(fresh.address, fresh.seed)

But a freshly generated address isn't yet an account. On the XRP Ledger an account only starts existing once it holds at least the base reserve — a small amount of XRP the protocol locks to discourage spam and keep the ledger's account list lean. Until then, the network has never heard of your address.

client.fundWallet() does both jobs at once: it generates a keypair and asks the testnet faucet to send it test-XRP, which activates the account in one step.

const { wallet, balance } = await client.fundWallet()

console.log('Address:', wallet.address)
console.log('Starting balance (XRP):', balance)

The returned wallet object carries the keys you'll sign transactions with. Copy the address into the Testnet Explorer and you'll see the funding transaction already recorded on-chain — your account exists now.

If you ever need the balance again later, you don't have to remember the number fundWallet gave you — ask the ledger directly:

const balanceNow = await client.getXrpBalance(wallet.address)
console.log('Current balance (XRP):', balanceNow)

One subtlety worth internalizing early: the balance the ledger reports is not the same as what the account can spend. The base reserve stays locked to keep the account alive, so an account showing 10 XRP can only move roughly 9 of it. You'll feel this the moment a payment for "all" of an account's balance fails — it's the reserve, doing its job.

In real apps you'd never generate keys this way for anything holding real value, and you'd load a seed from a secure secret rather than printing it. On testnet, throwaway keys are exactly the point.

Build, sign, and submit a transaction

Here's the pattern that underpins every state change on the XRP Ledger, no matter how fancy. You can do almost anything — pay someone, issue a token, mint an NFT, set an account flag — and it's always the same four moves: build → autofill → sign → submit.

A transaction is a plain object. Every transaction has a TransactionType and an Account (the sender, who must sign). We'll use AccountSet, the simplest possible transaction — with no extra fields it's essentially a no-op that still goes through the full lifecycle, which makes it perfect for learning the mechanics.

const tx: xrpl.AccountSet = {
  TransactionType: 'AccountSet',
  Account: wallet.address,
}

This object describes your intent, but it's missing the bookkeeping the network requires: a Fee, the account's next Sequence number, and a LastLedgerSequence (an expiry, so a transaction can't linger and surprise you later). You don't fill these by hand — client.autofill() queries the ledger and adds them for you:

const prepared = await client.autofill(tx)
console.log('Prepared:', prepared)

Next, sign it. Signing produces a tx_blob (the signed transaction as bytes) and the transaction's hash — a unique fingerprint you can use to look it up later. The signature proves you authorized this exact transaction; nothing can change after signing without invalidating it.

const signed = wallet.sign(prepared)
console.log('Tx hash:', signed.hash)

Finally, submit and wait for the network to reach consensus on it. submitAndWait broadcasts the signed blob and then resolves only once the transaction has been included in a validated ledger — so when it returns, the outcome is final.

const result = await client.submitAndWait(signed.tx_blob)

xrpl.js can collapse autofill + sign for you: passing the raw tx plus { autofill: true, wallet } to submitAndWait does it all in one call. Doing it as explicit steps first is worth it once, so you actually understand what that convenience is hiding:

// alternative to the four steps above — use this OR the explicit
// build/autofill/sign/submit, not both (they'd redeclare `result`)
const result = await client.submitAndWait(tx, { autofill: true, wallet })

Read the result

A transaction can be successfully submitted and still fail — the network accepted your message but the ledger rejected the action (insufficient funds, a missing destination, a bad flag). So submission is never the same as success. The truth lives in the transaction's metadata, specifically the engine result code:

const code = result.result.meta?.TransactionResult

if (code === 'tesSUCCESS') {
  console.log('Validated:', result.result.hash)
} else {
  throw new Error(`Transaction failed: ${code}`)
}

tesSUCCESS is the only code that means the ledger applied your transaction. Every other code is a categorized explanation of what happened, and the three-letter prefix tells you the family at a glance:

  • tes — success. There's exactly one: tesSUCCESS.
  • tec — the transaction was included in a ledger and a fee was charged, but the action didn't take effect (e.g. tecUNFUNDED_PAYMENT, tecNO_DST). It cost you a fee but didn't do anything.
  • tem / tef / ter — malformed, failed pre-checks, or retryable; these generally don't make it into a validated ledger at all.

The practical rule: never trust a transaction until you've checked meta.TransactionResult === 'tesSUCCESS'. Treat anything else as an error in your code. Grab the hash from a successful result, paste it into the Testnet Explorer, and watch your transaction sitting in a real validated ledger.

That's the entire core loop. Connect, fund, build, autofill, sign, submit, check the result. The payments module that follows just swaps AccountSet for Payment — and tokens and NFTs later are the same seven steps with a different transaction object in the middle. Once this is reflex, the rest of the track is mostly learning which fields go in that object.

Check

You're done when you have a single script that, in one run: connects to the testnet, funds a wallet via the faucet, builds and submits one transaction (an AccountSet is perfect), and confirms the result is tesSUCCESS by reading meta.TransactionResult. Log the validated transaction hash and confirm it on the Testnet Explorer. Submit a repo or gist containing your index.ts and the broadcast tx hash for a peer to verify.

Assignments

0 of 3 complete