Compare commits

..

16 Commits

Author SHA1 Message Date
oeggert
aca16f3609 Merge pull request #3625 from XRPLF/clarify-oracle-scale
Clarify AssetPrice and Scale interactions with new vs existing pairs
2026-04-24 12:06:15 -07:00
oeggert
187542fef5 Update docs/references/protocol/transactions/types/oracleset.md
Co-authored-by: Amarantha Kulkarni <amarantha-k@users.noreply.github.com>
2026-04-24 12:00:36 -07:00
oeggert
0972abb0b6 Merge pull request #3599 from XRPLF/ai-resources
Add AI Tools page
2026-04-24 11:40:10 -07:00
Amarantha Kulkarni
6256e1d7c8 Merge pull request #3606 from XRPLF/VODF-172
VODF-172 - Add Batch transaction integration considerations
2026-04-24 11:39:06 -07:00
oeggert
9f7c675517 Update resources/dev-tools/ai-tools.md
Co-authored-by: Amarantha Kulkarni <amarantha-k@users.noreply.github.com>
2026-04-24 11:39:04 -07:00
Maria Shodunke
202a16288c Address review comments 2026-04-24 15:37:46 +01:00
Oliver Eggert
5e63775a97 clarify AssetPrice and Scale interactions with new vs existing pairs 2026-04-23 16:18:16 -07:00
rachelflynn
3e96de6323 Merge pull request #3586 from rachelflynn/fix-checks-js-code-samples
Fix JS code samples for checks tutorials and add set up scripts
2026-04-23 15:57:41 -04:00
rachelflynn
f60393e9fa Addressed PR review feedback 2026-04-17 17:14:40 -04:00
Maria Shodunke
d945d6a5d6 Tutorials landing page v2 (#3572) 2026-04-17 11:52:23 +01:00
Maria Shodunke
9b72e6c6ff VODF-172 - Add Batch transaction integration considerations 2026-04-14 12:40:11 +01:00
Oliver Eggert
39f5b9ab66 fix spelling, grammar, and formatting 2026-04-10 16:04:06 -07:00
Oliver Eggert
3d3ac6adb3 second iteration 2026-04-09 16:52:21 -07:00
Oliver Eggert
539cef2510 initial draft 2026-04-09 10:25:00 -07:00
rachelflynn
0da70afdff Added set up scripts for updated code samples 2026-04-09 11:24:15 -04:00
rachelflynn
513c86dff3 Fix JS code samples for Use Checks tutorials: update to ES modules, fix syntax highlighting and indentation rendering, and improve code consistency 2026-04-07 16:47:25 -04:00
31 changed files with 1853 additions and 1306 deletions

1
.gitignore vendored
View File

@@ -9,7 +9,6 @@ yarn-error.log
.venv/
_code-samples/*/js/package-lock.json
_code-samples/*/go/go.sum
_code-samples/*/java/target/
_code-samples/*/*/*[Ss]etup.json
# PHP

View File

@@ -1,13 +1,15 @@
import { indexPages } from './plugins/index-pages.js';
import { codeSamples } from './plugins/code-samples.js';
import { blogPosts } from './plugins/blog-posts.js';
import { tutorialLanguages } from './plugins/tutorial-languages.js'
import { tutorialLanguages } from './plugins/tutorial-languages.js';
import { tutorialMetadata } from './plugins/tutorial-metadata.js';
export default function customPlugin() {
const indexPagesInst = indexPages();
const codeSamplesInst = codeSamples();
const blogPostsInst = blogPosts();
const tutorialLanguagesInst = tutorialLanguages();
const tutorialMetadataInst = tutorialMetadata();
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
@@ -18,12 +20,14 @@ export default function customPlugin() {
await codeSamplesInst.processContent?.(content, actions);
await blogPostsInst.processContent?.(content, actions);
await tutorialLanguagesInst.processContent?.(content, actions);
await tutorialMetadataInst.processContent?.(content, actions);
},
afterRoutesCreated: async (content, actions) => {
await indexPagesInst.afterRoutesCreated?.(content, actions);
await codeSamplesInst.afterRoutesCreated?.(content, actions);
await blogPostsInst.afterRoutesCreated?.(content, actions);
await tutorialLanguagesInst.processContent?.(content, actions);
await tutorialMetadataInst.processContent?.(content, actions);
},
};

View File

@@ -1,7 +1,15 @@
// @ts-check
import { getInnerText } from '@redocly/realm/dist/markdoc/helpers/get-inner-text.js'
/**
* Plugin to detect languages supported in tutorial pages by scanning for tab labels.
* Plugin to detect languages supported in tutorial pages.
*
* Detection methods (in priority order):
* 1. Tab labels in the markdown (for multi-language tutorials)
* 2. Filename patterns like "-js.md", "-py.md" (for single-language tutorials)
* 3. Title containing language name (for single-language tutorials)
*
* This creates shared data that maps tutorial paths to their supported languages.
*/
export function tutorialLanguages() {
@@ -21,7 +29,18 @@ export function tutorialLanguages() {
for (const { relativePath } of tutorialFiles) {
try {
const { data } = await cache.load(relativePath, 'markdown-ast')
const languages = extractLanguagesFromAst(data.ast)
// Try to detect languages from tab labels first
let languages = extractLanguagesFromAst(data.ast)
// Fallback: detect language from filename/title for single-language tutorials
if (languages.length === 0) {
const title = extractFirstHeading(data.ast) || ''
const fallbackLang = detectLanguageFromPathAndTitle(relativePath, title)
if (fallbackLang) {
languages = [fallbackLang]
}
}
if (languages.length > 0) {
// Convert file path to URL path
@@ -54,16 +73,31 @@ function extractLanguagesFromAst(ast) {
const languages = new Set()
visit(ast, (node) => {
// Look for tab nodes with a label attribute
if (isNode(node) && node.type === 'tag' && node.tag === 'tab') {
if (!isNode(node)) return
// Detect languages from tab labels
if (node.type === 'tag' && node.tag === 'tab') {
const label = node.attributes?.label
if (label) {
const normalizedLang = normalizeLanguage(label)
if (normalizedLang) {
languages.add(normalizedLang)
}
const normalized = normalizeLanguage(label)
if (normalized) languages.add(normalized)
}
}
// Detect languages from code-snippet language attributes
if (node.type === 'tag' && node.tag === 'code-snippet') {
const lang = node.attributes?.language
if (lang) {
const normalized = normalizeLanguage(lang)
if (normalized) languages.add(normalized)
}
}
// Detect languages from fenced code blocks (```js, ```python, etc.)
if (node.type === 'fence' && node.attributes?.language) {
const normalized = normalizeLanguage(node.attributes.language)
if (normalized) languages.add(normalized)
}
})
return Array.from(languages)
@@ -98,6 +132,70 @@ function normalizeLanguage(label) {
return null
}
/**
* Detect language from file path and title for single-language tutorials.
* This is a fallback when no tab labels are found in the markdown.
*/
function detectLanguageFromPathAndTitle(relativePath, title) {
const pathLower = relativePath.toLowerCase()
const titleLower = (title || '').toLowerCase()
// Check filename suffixes like "-js.md", "-py.md"
if (pathLower.endsWith('-js.md') || pathLower.includes('-javascript.md') || pathLower.includes('-in-javascript.md')) {
return 'javascript'
}
if (pathLower.endsWith('-py.md') || pathLower.includes('-python.md') || pathLower.includes('-in-python.md')) {
return 'python'
}
if (pathLower.endsWith('-java.md') || pathLower.includes('-in-java.md')) {
return 'java'
}
if (pathLower.endsWith('-go.md') || pathLower.includes('-in-go.md') || pathLower.includes('-golang.md')) {
return 'go'
}
if (pathLower.endsWith('-php.md') || pathLower.includes('-in-php.md')) {
return 'php'
}
// Check title for language indicators
if (titleLower.includes('javascript') || titleLower.includes(' js ') || titleLower.endsWith(' js')) {
return 'javascript'
}
if (titleLower.includes('python')) {
return 'python'
}
if (titleLower.includes('java') && !titleLower.includes('javascript')) {
return 'java'
}
if (titleLower.includes('golang') || (titleLower.includes(' go ') || titleLower.endsWith(' go') || titleLower.includes('using go'))) {
return 'go'
}
if (titleLower.includes('php')) {
return 'php'
}
return null
}
const EXIT = Symbol('Exit visitor')
/**
* Extract the first heading from the markdown AST
*/
function extractFirstHeading(ast) {
let heading = null
visit(ast, (node) => {
if (!isNode(node)) return
if (node.type === 'heading') {
heading = getInnerText([node])
return EXIT
}
})
return heading
}
function isNode(value) {
return !!(value?.$$mdtype === 'Node')
}
@@ -105,14 +203,16 @@ function isNode(value) {
function visit(node, visitor) {
if (!node) return
visitor(node)
const res = visitor(node)
if (res === EXIT) return res
if (node.children) {
for (const child of node.children) {
if (!child || typeof child === 'string') {
continue
}
visit(child, visitor)
const res = visit(child, visitor)
if (res === EXIT) return res
}
}
}

View File

@@ -0,0 +1,207 @@
// @ts-check
import { getInnerText } from '@redocly/realm/dist/markdoc/helpers/get-inner-text.js';
import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = resolve(__dirname, '../..');
/**
* Plugin to extract tutorial metadata including last modified dates.
* Uses Redocly's built-in git integration for dates (same as "Last updated" display).
* Only includes tutorials that appear in the sidebar navigation (sidebars.yaml).
* This creates shared data for displaying "What's New" tutorials and
* auto-generating tutorial sections on the landing page.
*/
export function tutorialMetadata() {
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
const instance = {
processContent: async (actions, { fs, cache }) => {
try {
// Extract tutorial paths and categories from sidebars.yaml.
// Only tutorials present in the sidebar are included.
const { pageCategory, categories } = extractSidebarData();
/** @type {Array<{path: string, title: string, description: string, lastModified: string, category: string}>} */
const tutorials = [];
const allFiles = await fs.scan();
// Find all markdown files in tutorials directory
const tutorialFiles = allFiles.filter((file) =>
file.relativePath.match(/^docs[\/\\]tutorials[\/\\].*\.md$/)
);
for (const { relativePath } of tutorialFiles) {
try {
// Skip tutorials not present in sidebar navigation
const category = pageCategory.get(relativePath);
if (!category) continue;
const { data: { ast } } = await cache.load(relativePath, 'markdown-ast');
const { data: { frontmatter } } = await cache.load(relativePath, 'markdown-frontmatter');
// Get last modified date using Redocly's built-in git integration
const lastModified = await fs.getLastModified(relativePath);
if (!lastModified) continue; // Skip files without dates
// Extract title from first heading
const title = extractFirstHeading(ast) || '';
if (!title) continue;
// Get description from frontmatter or first paragraph
const description = frontmatter?.seo?.description || '';
// Convert file path to URL path
const urlPath = '/' + relativePath
.replace(/[\/\\]index\.md$/, '/')
.replace(/\.md$/, '/')
.replace(/\\/g, '/');
tutorials.push({
path: urlPath,
title,
description,
lastModified,
category,
});
} catch (err) {
continue; // Skip files that can't be parsed
}
}
// Sort by last modified date (newest first) for "What's New"
tutorials.sort((a, b) =>
new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
);
// Create shared data including sidebar-derived categories
actions.createSharedData('tutorial-metadata', { tutorials, categories });
actions.addRouteSharedData('/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
actions.addRouteSharedData('/ja/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
actions.addRouteSharedData('/es-es/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
} catch (e) {
console.log('[tutorial-metadata] Error:', e);
}
},
};
return instance;
}
/**
* Extract the first heading from the markdown AST
*/
function extractFirstHeading(ast) {
let heading = null;
visit(ast, (node) => {
if (!isNode(node)) return;
if (node.type === 'heading') {
heading = getInnerText([node]);
return EXIT;
}
});
return heading;
}
function isNode(value) {
return !!(value?.$$mdtype === 'Node');
}
const EXIT = Symbol('Exit visitor');
function visit(node, visitor) {
if (!node) return;
const res = visitor(node);
if (res === EXIT) return res;
if (node.children) {
for (const child of node.children) {
if (!child || typeof child === 'string') continue;
const res = visit(child, visitor);
if (res === EXIT) return res;
}
}
}
/**
* Extract tutorial page paths and categories from sidebars.yaml.
*
* Returns:
* - pageCategory: Map of relativePath to category id (slug)
* - categories: Array of { id, title } in sidebar display order
*
* Top-level groups under the tutorials section become categories.
* Pages not inside a group (e.g. public-servers.md) are skipped.
*/
function extractSidebarData() {
/** @type {Map<string, string>} */
const pageCategory = new Map();
/** @type {Array<{id: string, title: string}>} */
const categories = [];
try {
const content = readFileSync(resolve(PROJECT_ROOT, 'sidebars.yaml'), 'utf-8');
const lines = content.split('\n');
let inTutorials = false;
let entryIndent = -1; // indent of the tutorials entry itself
let topItemIndent = -1; // indent of direct children (groups/pages)
let currentCategory = null; // current top-level group { id, title }
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
const indent = line.search(/\S/);
// Detect the tutorials section
if (trimmed.includes('page: docs/tutorials/index.page.tsx')) {
inTutorials = true;
entryIndent = indent;
continue;
}
if (!inTutorials) continue;
// Exit tutorials when we reach a sibling entry at the same indent
if (indent <= entryIndent && trimmed.startsWith('- ')) {
break;
}
// Detect the indent of top-level items (first `- ` under tutorials items)
if (topItemIndent === -1 && trimmed.startsWith('- ')) {
topItemIndent = indent;
}
// Top-level group - start a new category
if (indent === topItemIndent && trimmed.startsWith('- group:')) {
const title = trimmed.replace('- group:', '').trim();
const id = title.toLowerCase().replace(/\s+/g, '-');
currentCategory = { id, title };
categories.push(currentCategory);
continue;
}
// Top-level page (no group, e.g. public-servers.md) - reset current category
if (indent === topItemIndent && trimmed.startsWith('- page:')) {
currentCategory = null;
continue;
}
// Nested page under a group - assign to current category
if (currentCategory) {
const pageMatch = trimmed.match(/^- page:\s+(docs\/tutorials\/\S+\.md)/);
if (pageMatch) {
pageCategory.set(pageMatch[1], currentCategory.id);
}
}
}
} catch (err) {
console.log('[tutorial-metadata] Warning: Could not read sidebars.yaml:', String(err));
}
return { pageCategory, categories };
}

View File

@@ -1,56 +1,66 @@
'use strict'
const xrpl = require('xrpl')
import xrpl from 'xrpl'
import { execSync } from 'child_process'
import fs from 'fs'
// Define parameters. Edit this snippet with your values before running it.
const secret = "s████████████████████████████" // Replace with your secret
const check_id = "" // Replace with your Check ID
// Looks for setup data required to run the checks tutorials.
// If missing, checks-setup.js will generate the data.
async function main() {
try {
// Connect ----------------------------------------------------------------
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
// Instantiate wallet from secret. ----------------------------------------
const wallet = await xrpl.Wallet.fromSeed(secret)
console.log("Wallet address: ", wallet.address)
// Check if the check ID is provided --------------------------------------
if (check_id.length === 0) {
console.log("Please edit this snippet to provide a check ID. You can get a check ID by running create-check.js.");
return;
}
// Prepare the transaction ------------------------------------------------
const checkcancel = {
"TransactionType": "CheckCancel",
"Account": wallet.address,
"CheckID": check_id
};
// Submit the transaction -------------------------------------------------
const tx = await client.submitAndWait(
checkcancel,
{ autofill: true,
wallet: wallet }
)
// Confirm results --------------------------------------------------------
console.log(`Transaction result: ${JSON.stringify(tx, null, 2)}`)
if (tx.result.meta.TransactionResult === "tesSUCCESS") {
// submitAndWait() only returns when the transaction's outcome is final,
// so you don't also have to check for validated: true.
console.log("Transaction was successful.")
}
// Disconnect -------------------------------------------------------------
await client.disconnect()
} catch (error) {
console.error(`Error: ${error}`)
}
if (!fs.existsSync('checks-setup.json')) {
console.log(`\n=== Checks tutorial data doesn't exist. Running setup script... ===\n`)
execSync('node checks-setup.js', { stdio: 'inherit' })
}
main()
// Load setup data --------------------------------------------------------
const setupData = JSON.parse(fs.readFileSync('checks-setup.json', 'utf8'))
const wallet = xrpl.Wallet.fromSeed(setupData.sender.seed)
const checkID = setupData.checkIDs.cancel
console.log(`Wallet address: ${wallet.address}`)
console.log(`Check ID to cancel: ${checkID}`)
// Connect to Testnet -----------------------------------------------------
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
// Prepare the transaction ------------------------------------------------
console.log(`\n=== Preparing CheckCancel transaction ===\n`)
const checkCancel = {
TransactionType: 'CheckCancel',
Account: wallet.address,
CheckID: checkID
}
// Validate the transaction before submitting -----------------------------
xrpl.validate(checkCancel)
console.log(JSON.stringify(checkCancel, null, 2))
// Submit the transaction -------------------------------------------------
console.log(`\n=== Submitting CheckCancel transaction ===\n`)
const tx = await client.submitAndWait(
checkCancel,
{ autofill: true,
wallet }
)
// Confirm transaction result ---------------------------------------------
const resultCode = tx.result.meta.TransactionResult
if (resultCode !== 'tesSUCCESS') {
console.error('Unable to cancel check:', resultCode)
await client.disconnect()
process.exit(1)
}
const deletedCheck = tx.result.meta.AffectedNodes.find(node =>
node.DeletedNode?.LedgerEntryType === 'Check')
console.log(`Check canceled successfully.`)
console.log(`Deleted check:\n`, JSON.stringify(deletedCheck.DeletedNode.FinalFields, null, 2))
// Disconnect -------------------------------------------------------------
await client.disconnect()

View File

@@ -1,62 +1,68 @@
'use strict'
const xrpl = require('xrpl')
import xrpl from 'xrpl'
import { execSync } from 'child_process'
import fs from 'fs'
// Define parameters. Edit this code with your values before running it.
const secret = "s████████████████████████████" // Replace with your secret
const check_id = "49D339B76FAB3FE3C9DFAD32EB7DB9269FD07B07E165DD7BAFDF68D14CE6CAB8"
const amount = "30000000" // Replace with the amount you want to cash
// String for XRP in drops
// {currency, issuer, value} object for token amount
// Looks for setup data required to run the checks tutorials.
// If missing, checks-setup.js will generate the data.
async function main() {
try {
// Connect to Testnet
const client = new xrpl.Client("wss://s.altnet.rippletest.net:51233")
await client.connect()
// Instantiate a wallet -----------------------------------------------
const wallet = xrpl.Wallet.fromSeed(secret)
console.log("Wallet address: ", wallet.address)
// Check if the check ID is provided ----------------------------------
if (check_id == "49D339B76FAB3FE3C9DFAD32EB7DB9269FD07B07E165DD7BAFDF68D14CE6CAB8") {
console.log("Please edit this snippet to provide your own check ID. You can get a check ID by running create-check.js.")
return
}
// Prepare the transaction ------------------------------------------------
const checkcash = {
TransactionType: "CheckCash",
Account: wallet.address,
CheckID: check_id,
Amount: amount
}
// Submit the transaction -------------------------------------------------
const tx = await client.submitAndWait(
checkcash,
{ autofill: true,
wallet: wallet }
)
// Confirm transaction results --------------------------------------------
console.log(`Transaction result: ${JSON.stringify(tx, null, 2)}`)
if (tx.result.meta.TransactionResult === "tesSUCCESS") {
// submitAndWait() only returns when the transaction's outcome is final,
// so you don't also have to check for validated: true.
console.log("Transaction was successful.")
console.log("Balance changes:",
JSON.stringify(xrpl.getBalanceChanges(tx.result.meta), null, 2)
)
}
// Disconnect -------------------------------------------------------------
await client.disconnect()
} catch (error) {
console.log("Error: ", error)
}
if (!fs.existsSync('checks-setup.json')) {
console.log(`\n=== Checks tutorial data doesn't exist. Running setup script... ===\n`)
execSync('node checks-setup.js', { stdio: 'inherit' })
}
main()
// Load setup data --------------------------------------------------------
const setupData = JSON.parse(fs.readFileSync('checks-setup.json', 'utf8'))
const wallet = xrpl.Wallet.fromSeed(setupData.recipient.seed)
const checkID = setupData.checkIDs.exact
const amount = xrpl.xrpToDrops(30)
console.log(`Wallet address: ${wallet.address}`)
console.log(`Check ID to cash: ${checkID}`)
console.log(`Amount to cash: ${amount}`)
// Connect to Testnet -----------------------------------------------------
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
// Prepare the transaction ------------------------------------------------
const checkCash = {
TransactionType: "CheckCash",
Account: wallet.address,
CheckID: checkID,
Amount: amount
}
// Validate the transaction before submitting -----------------------------
xrpl.validate(checkCash)
console.log(JSON.stringify(checkCash, null, 2))
// Submit the transaction -------------------------------------------------
console.log(`\n=== Submitting CheckCash transaction ===\n`)
const tx = await client.submitAndWait(
checkCash,
{ autofill: true,
wallet }
)
// Confirm transaction result ---------------------------------------------
const resultCode = tx.result.meta.TransactionResult
if (resultCode !== 'tesSUCCESS') {
console.error('Unable to cash check:', resultCode)
await client.disconnect()
process.exit(1)
}
console.log('Check cashed successfully.')
console.log('Balance changes:',
JSON.stringify(xrpl.getBalanceChanges(tx.result.meta), null, 2)
)
// Disconnect ------------------------------------------------------------
await client.disconnect()

View File

@@ -1,62 +1,68 @@
'use strict'
const xrpl = require('xrpl')
import xrpl from 'xrpl'
import { execSync } from 'child_process'
import fs from 'fs'
// Define parameters. Edit this code with your values before running it.
const secret = "s████████████████████████████" // Replace with your secret
const check_id = "5C5E9F39A92908BBA7B85AECD9457E9616AD36DF1895074723253B767A380D14"
const deliver_min = "20000000" // Replace with the minimum amount to receive
// String for XRP in drops
// {currency, issuer, value} object for token amount
// Looks for setup data required to run the checks tutorials.
// If missing, checks-setup.js will generate the data.
async function main() {
try {
// Connect to Testnet
const client = new xrpl.Client("wss://s.altnet.rippletest.net:51233")
await client.connect()
// Instantiate a wallet -----------------------------------------------
const wallet = xrpl.Wallet.fromSeed(secret)
console.log("Wallet address: ", wallet.address)
// Check if the check ID is provided ----------------------------------
if (check_id.length === 0) {
console.log("Please edit this snippet to provide a check ID. You can get a check ID by running create-check.js.")
return
}
// Prepare the transaction ------------------------------------------------
const checkcash = {
TransactionType: "CheckCash",
Account: wallet.address,
CheckID: check_id,
DeliverMin: deliver_min
}
// Submit the transaction -------------------------------------------------
const tx = await client.submitAndWait(
checkcash,
{ autofill: true,
wallet: wallet }
)
// Confirm transaction results --------------------------------------------
console.log(`Transaction result: ${JSON.stringify(tx, null, 2)}`)
if (tx.result.meta.TransactionResult === "tesSUCCESS") {
// submitAndWait() only returns when the transaction's outcome is final,
// so you don't also have to check for validated: true.
console.log("Transaction was successful.")
console.log("Balance changes:",
JSON.stringify(xrpl.getBalanceChanges(tx.result.meta), null, 2)
)
}
// Disconnect -------------------------------------------------------------
await client.disconnect()
} catch (error) {
console.log("Error: ", error)
}
if (!fs.existsSync('checks-setup.json')) {
console.log(`\n=== Checks tutorial data doesn't exist. Running setup script... ===\n`)
execSync('node checks-setup.js', { stdio: 'inherit' })
}
main()
// Load setup data --------------------------------------------------------
const setupData = JSON.parse(fs.readFileSync('checks-setup.json', 'utf8'))
const wallet = xrpl.Wallet.fromSeed(setupData.recipient.seed)
const checkID = setupData.checkIDs.flexible
const deliverMin = xrpl.xrpToDrops(20)
console.log(`Wallet address: ${wallet.address}`)
console.log(`Check ID to cash: ${checkID}`)
console.log(`Deliver minimum: ${deliverMin}`)
// Connect to Testnet -----------------------------------------------------
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
// Prepare the transaction ------------------------------------------------
const checkCash = {
TransactionType: "CheckCash",
Account: wallet.address,
CheckID: checkID,
DeliverMin: deliverMin
}
// Validate the transaction before submitting -----------------------------
xrpl.validate(checkCash)
console.log(JSON.stringify(checkCash, null, 2))
// Submit the transaction -------------------------------------------------
console.log(`\n=== Submitting CheckCash transaction ===\n`)
const tx = await client.submitAndWait(
checkCash,
{ autofill: true,
wallet }
)
// Confirm transaction result ---------------------------------------------
const resultCode = tx.result.meta.TransactionResult
if (resultCode !== 'tesSUCCESS') {
console.error('Unable to cash check:', resultCode)
await client.disconnect()
process.exit(1)
}
console.log('Check cashed successfully.')
console.log('Balance changes:',
JSON.stringify(xrpl.getBalanceChanges(tx.result.meta), null, 2)
)
// Disconnect -------------------------------------------------------------
await client.disconnect()

View File

@@ -0,0 +1,121 @@
import xrpl from 'xrpl'
import fs from 'fs'
// Helper to extract ticket sequences from TicketCreate result
function getTicketSequences(ticketCreateResult) {
return ticketCreateResult.result.meta.AffectedNodes
.filter(node => node.CreatedNode?.LedgerEntryType === 'Ticket')
.map(node => node.CreatedNode.NewFields.TicketSequence)
}
// Helper to extract check ID from CheckCreate result
function getCheckId(checkCreateResult) {
const checkNode = checkCreateResult.result.meta.AffectedNodes.find(
node => node.CreatedNode?.LedgerEntryType === 'Check'
)
return checkNode.CreatedNode.LedgerIndex
}
// Connect ----------------------
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
// Fund sender and recipient wallets ----------------------
process.stdout.write('Setting up tutorial: 0/3\r')
const [{ wallet: sender }, { wallet: recipient }] = await Promise.all([
client.fundWallet(),
client.fundWallet()
])
// Create tickets for sender to submit checks in parallel ----------------------
process.stdout.write('Setting up tutorial: 1/3\r')
const ticketCreateResult = await client.submitAndWait(
{
TransactionType: 'TicketCreate',
Account: sender.address,
TicketCount: 4
},
{ wallet: sender, autofill: true }
)
const ticketSequences = getTicketSequences(ticketCreateResult)
// Create four checks in parallel ----------------------
process.stdout.write('Setting up tutorial: 2/3\r')
const [exactResult, flexibleResult, cancelResult, sampleResult] = await Promise.all([
client.submitAndWait(
{
TransactionType: 'CheckCreate',
Account: sender.address,
Destination: recipient.address,
SendMax: xrpl.xrpToDrops(30),
TicketSequence: ticketSequences[0],
Sequence: 0
},
{ wallet: sender, autofill: true }
),
client.submitAndWait(
{
TransactionType: 'CheckCreate',
Account: sender.address,
Destination: recipient.address,
SendMax: xrpl.xrpToDrops(100),
TicketSequence: ticketSequences[1],
Sequence: 0
},
{ wallet: sender, autofill: true }
),
client.submitAndWait(
{
TransactionType: 'CheckCreate',
Account: sender.address,
Destination: recipient.address,
SendMax: xrpl.xrpToDrops(30),
TicketSequence: ticketSequences[2],
Sequence: 0
},
{ wallet: sender, autofill: true }
),
client.submitAndWait(
{
TransactionType: 'CheckCreate',
Account: sender.address,
Destination: recipient.address,
SendMax: xrpl.xrpToDrops(50),
TicketSequence: ticketSequences[3],
Sequence: 0
},
{ wallet: sender, autofill: true }
)
])
// Save setup data to file ----------------------
process.stdout.write('Setting up tutorial: 3/3\r')
const setupData = {
sender: {
address: sender.address,
seed: sender.seed
},
recipient: {
address: recipient.address,
seed: recipient.seed
},
checkIDs: {
exact: getCheckId(exactResult),
flexible: getCheckId(flexibleResult),
cancel: getCheckId(cancelResult),
sample: getCheckId(sampleResult)
}
}
fs.writeFileSync('checks-setup.json', JSON.stringify(setupData, null, 2))
process.stdout.write('Setting up tutorial: Complete!\n')
// Disconnect ----------------------
await client.disconnect()

View File

@@ -1,65 +1,60 @@
'use strict'
const xrpl = require('xrpl')
import xrpl from 'xrpl'
async function main() {
try {
// Connect to the XRP Ledger Test Net -------------------------------------
console.log("Connecting to Testnet...")
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
console.log("Connected.")
// Connect to the Testnet -------------------------------------------------
// Get a new wallet ---------------------------------------------------
console.log("Generating new wallet...")
const wallet = (await client.fundWallet()).wallet
console.log(" Address:", wallet.address)
console.log(" Seed:", wallet.seed)
console.log('Connecting to Testnet...')
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
console.log('Connected.')
// Prepare the transaction --------------------------------------------
const checkcreate = {
"TransactionType": "CheckCreate",
"Account": wallet.address,
"Destination": "rGPnRH1EBpHeTF2QG8DCAgM7z5pb75LAis",
"SendMax": xrpl.xrpToDrops(120), // Can be more than you have
"InvoiceID": "46060241FABCF692D4D934BA2A6C4427CD4279083E38C77CBE642243E43BE291"
}
// Get a new wallet -------------------------------------------------------
// Submit the transaction ---------------------------------------------
console.log("Submitting transaction...")
const tx = await client.submitAndWait(
checkcreate,
{ autofill: true,
wallet: wallet }
)
console.log('Generating new wallet...')
const wallet = (await client.fundWallet()).wallet
console.log(' Address:', wallet.address)
console.log(' Seed:', wallet.seed)
// Get transaction result and Check ID---------------------------------
console.log(`Transaction: ${JSON.stringify(tx, null, 2)}`)
// Prepare the transaction ------------------------------------------------
if (tx.result.meta.TransactionResult === "tesSUCCESS") {
let checkID = null
for (const node of tx.result.meta.AffectedNodes) {
if (node?.CreatedNode &&
node.CreatedNode?.LedgerEntryType == "Check") {
checkID = node.CreatedNode.LedgerIndex
break
}
}
if (checkID) {
console.log(`Check ID: ${checkID}`)
} else {
console.log("Unable to find the CheckID from parsing the metadata. Look for the LedgerIndex of the 'Check' object within 'meta'.")
}
} else {
console.log("Transaction failed with result code "+
tx.result.meta.TransactionResult)
}
// Disconnect ---------------------------------------------------------
await client.disconnect()
} catch (error) {
console.error(`Error: ${error}`)
}
const checkCreate = {
TransactionType: 'CheckCreate',
Account: wallet.address,
Destination: 'rGPnRH1EBpHeTF2QG8DCAgM7z5pb75LAis',
SendMax: xrpl.xrpToDrops(120), // Can be more than you have
InvoiceID: '46060241FABCF692D4D934BA2A6C4427CD4279083E38C77CBE642243E43BE291'
}
main()
// Validate the transaction before submitting -----------------------------
xrpl.validate(checkCreate)
console.log(JSON.stringify(checkCreate, null, 2))
// Submit the transaction -------------------------------------------------
console.log('Submitting transaction...')
const tx = await client.submitAndWait(
checkCreate,
{ autofill: true,
wallet }
)
// Confirm transaction result and get check ID ------------------------------------
const resultCode = tx.result.meta.TransactionResult
if (resultCode !== 'tesSUCCESS') {
console.error('Unable to create check:', resultCode)
await client.disconnect()
process.exit(1)
}
const node = tx.result.meta.AffectedNodes.find(node =>
node.CreatedNode?.LedgerEntryType === 'Check'
).CreatedNode
console.log('Check created successfully.')
console.log(`Check details:\n`, JSON.stringify(node.NewFields, null, 2))
console.log(`Check ID: ${node.LedgerIndex}`)
// Disconnect -------------------------------------------------------------
await client.disconnect()

View File

@@ -1,57 +1,66 @@
'use strict'
const xrpl = require('xrpl')
import xrpl from 'xrpl'
import { execSync } from 'child_process'
import fs from 'fs'
async function main() {
try {
// Connect ----------------------------------------------------------------
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
// Looks for setup data required to run the checks tutorials.
// If missing, checks-setup.js will generate the data.
// Loop through account objects until marker is undefined -----------------
let current_marker = null
let checks_found = []
do {
const request = {
"command": "account_objects",
"account": "rGPnRH1EBpHeTF2QG8DCAgM7z5pb75LAis",
"ledger_index": "validated",
"type": "check"
}
if (!fs.existsSync('checks-setup.json')) {
console.log(`\n=== Checks tutorial data doesn't exist. Running setup script... ===\n`)
execSync('node checks-setup.js', { stdio: 'inherit' })
}
if (current_marker) {
request.marker = current_marker
}
// Load setup data ----------------------
const response = await client.request(request)
checks_found = checks_found.concat(response.result.account_objects)
current_marker = response.result.marker
const setupData = JSON.parse(fs.readFileSync('checks-setup.json', 'utf8'))
const address = setupData.recipient.address
} while (current_marker)
// Connect ----------------------
// Filter results by recipient --------------------------------------------
// To filter by sender, check Account field instead of Destination
const checks_by_recipient = []
for (const check of checks_found) {
if (check.Destination == "rGPnRH1EBpHeTF2QG8DCAgM7z5pb75LAis") {
checks_by_recipient.push(check)
}
}
// Print results ----------------------------------------------------------
if (checks_by_recipient.length === 0) {
console.log("No checks found.")
} else {
console.log("Checks: \n", JSON.stringify(checks_by_recipient, null, 2))
}
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
// Disconnect -------------------------------------------------------------
await client.disconnect()
// Loop through account objects until marker is undefined ----------------------
} catch (error) {
console.log(error)
process.exit(1)
let currentMarker = null
let checksFound = []
do {
const request = {
command: 'account_objects',
account: address,
ledger_index: 'validated',
type: 'check'
}
if (currentMarker) {
request.marker = currentMarker
}
const response = await client.request(request)
checksFound = checksFound.concat(response.result.account_objects)
currentMarker = response.result.marker
} while (currentMarker)
// Filter results by recipient ----------------------
// To filter by sender, check Account field instead of Destination
const checksByRecipient = []
for (const check of checksFound) {
if (check.Destination == address) {
checksByRecipient.push(check)
}
}
main()
// Print results ----------------------
if (checksByRecipient.length === 0) {
console.log('No checks found.')
} else {
console.log('Checks: \n', JSON.stringify(checksByRecipient, null, 2))
}
// Disconnect ----------------------
await client.disconnect()

View File

@@ -3,7 +3,8 @@
"description": "Example code for signing and submitting Checks",
"version": "0.0.2",
"license": "MIT",
"type": "module",
"dependencies": {
"xrpl": "^4.0.0"
"xrpl": "^4.4.0"
}
}

View File

@@ -1,82 +0,0 @@
# Credential Example (Java)
This directory contains a Java example demonstrating how to issue a credential, accept a credential, and delete a credential.
## Setup
Install dependencies before running any examples:
```sh
mvn install
```
---
## Manage Credentials
```sh
mvn exec:java -Dexec.mainClass=com.example.xrpl.ManageCredentials
```
The script should output two newly funded accounts, the CredentialCreate transaction, CredentialAccept transaction, and CredentialDelete transaction. Each successful transaction submission includes a link to the transaction metadata on the XRPL Explorer.
```sh
=== Funding issuer and subject accounts on Testnet ===
Issuer: r446kRqJA1XGo1zGMiC2RAebtWbQa4duHL
Subject: rs8vtTf3aLZW7XRCh398SUkuuAWBn7q49y
=== Preparing CredentialCreate transaction ===
{
"Account" : "r446kRqJA1XGo1zGMiC2RAebtWbQa4duHL",
"TransactionType" : "CredentialCreate",
"Fee" : "15",
"Sequence" : 16795444,
"LastLedgerSequence" : 16795464,
"SigningPubKey" : "EDC2C03C393852514C40CCCCF34CB61A8DDB4AECC6C95271468DDF13DE0979DCC7",
"Subject" : "rs8vtTf3aLZW7XRCh398SUkuuAWBn7q49y",
"CredentialType" : "6B79632D747261646572"
}
=== Submitting CredentialCreate transaction ===
CredentialCreate succeeded!
Explorer: https://testnet.xrpl.org/transactions/D7A00CFC8DFFE384F7A5D2DF14B3AC5629E8F8DBFD8BD06BC389363782F296B3
=== Preparing CredentialAccept transaction ===
{
"Account" : "rs8vtTf3aLZW7XRCh398SUkuuAWBn7q49y",
"TransactionType" : "CredentialAccept",
"Fee" : "15",
"Sequence" : 16795444,
"LastLedgerSequence" : 16795466,
"SigningPubKey" : "EDBED812587E0D7D9F965EFE63F4F2B2BB2EB559AD7D1FA9250C239C235CE62726",
"Issuer" : "r446kRqJA1XGo1zGMiC2RAebtWbQa4duHL",
"CredentialType" : "6B79632D747261646572"
}
=== Submitting CredentialAccept transaction ===
CredentialAccept succeeded!
Explorer: https://testnet.xrpl.org/transactions/C9E55B0A5270FEB37C18E710BDB01C46480530673FE8E4FC39FE3D6B036DF8F5
=== Preparing CredentialDelete transaction ===
{
"Account" : "rs8vtTf3aLZW7XRCh398SUkuuAWBn7q49y",
"TransactionType" : "CredentialDelete",
"Fee" : "15",
"Sequence" : 16795445,
"LastLedgerSequence" : 16795468,
"SigningPubKey" : "EDBED812587E0D7D9F965EFE63F4F2B2BB2EB559AD7D1FA9250C239C235CE62726",
"Issuer" : "r446kRqJA1XGo1zGMiC2RAebtWbQa4duHL",
"CredentialType" : "6B79632D747261646572"
}
=== Submitting CredentialDelete transaction ===
CredentialDelete succeeded!
Explorer: https://testnet.xrpl.org/transactions/0755B4FED0A646D5FB3698891D25DC0374C521DEF00D85D8FCFF58EB09CAB4FE
```

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>credential-samples</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.xrpl</groupId>
<artifactId>xrpl4j-client</artifactId>
<version>6.0.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,292 +0,0 @@
package com.example.xrpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.primitives.UnsignedInteger;
import okhttp3.HttpUrl;
import org.xrpl.xrpl4j.client.JsonRpcClientErrorException;
import org.xrpl.xrpl4j.client.XrplClient;
import org.xrpl.xrpl4j.client.faucet.FaucetClient;
import org.xrpl.xrpl4j.client.faucet.FundAccountRequest;
import org.xrpl.xrpl4j.crypto.keys.KeyPair;
import org.xrpl.xrpl4j.crypto.keys.PrivateKey;
import org.xrpl.xrpl4j.crypto.keys.Seed;
import org.xrpl.xrpl4j.crypto.signing.SignatureService;
import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction;
import org.xrpl.xrpl4j.crypto.signing.bc.BcSignatureService;
import org.xrpl.xrpl4j.model.client.accounts.AccountInfoRequestParams;
import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult;
import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier;
import org.xrpl.xrpl4j.model.client.fees.FeeUtils;
import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams;
import org.xrpl.xrpl4j.model.client.transactions.SubmitResult;
import org.xrpl.xrpl4j.model.client.transactions.TransactionRequestParams;
import org.xrpl.xrpl4j.model.client.transactions.TransactionResult;
import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory;
import org.xrpl.xrpl4j.model.transactions.Address;
import org.xrpl.xrpl4j.model.transactions.CredentialAccept;
import org.xrpl.xrpl4j.model.transactions.CredentialCreate;
import org.xrpl.xrpl4j.model.transactions.CredentialDelete;
import org.xrpl.xrpl4j.model.transactions.CredentialType;
import org.xrpl.xrpl4j.model.transactions.Transaction;
import org.xrpl.xrpl4j.model.transactions.TransactionResultCodes;
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
/**
* This code sample demonstrates the Credential lifecycle on the XRPL.
* It issues a credential to a subject, accepts the credential, and then deletes it.
*/
public class ManageCredentials {
private static final HttpUrl NETWORK_URL = HttpUrl.get("https://s.altnet.rippletest.net:51234/");
private static final HttpUrl FAUCET_URL = HttpUrl.get("https://faucet.altnet.rippletest.net");
private static final String EXPLORER_BASE = "https://testnet.xrpl.org/transactions/";
private static final CredentialType CREDENTIAL_TYPE = CredentialType.ofPlainText("kyc-trader");
public static void main(String[] args) {
try {
run();
} catch (Exception e) {
// Unwrap CompletionException so async failures print the same clean message
// as sync failures. CompletableFuture.join() wraps exceptions in CompletionException
Throwable cause = (e instanceof CompletionException && e.getCause() != null)
? e.getCause() : e;
System.err.println("Error: " + cause.getMessage());
System.exit(1);
}
}
private static void run() {
// ----- Connect to Testnet and fund accounts -----
XrplClient xrplClient = new XrplClient(NETWORK_URL);
System.out.println("\n=== Funding issuer and subject accounts on Testnet ===\n");
CompletableFuture<KeyPair> issuerFuture = CompletableFuture.supplyAsync(
() -> createAndFundWallet(xrplClient));
CompletableFuture<KeyPair> subjectFuture = CompletableFuture.supplyAsync(
() -> createAndFundWallet(xrplClient));
CompletableFuture.allOf(issuerFuture, subjectFuture).join();
KeyPair issuer = issuerFuture.join();
KeyPair subject = subjectFuture.join();
Address issuerAddress = issuer.publicKey().deriveAddress();
Address subjectAddress = subject.publicKey().deriveAddress();
System.out.println("Issuer: " + issuerAddress);
System.out.println("Subject: " + subjectAddress);
// ----- Prepare CredentialCreate transaction -----
System.out.println("\n=== Preparing CredentialCreate transaction ===\n");
CredentialCreate createTx = CredentialCreate.builder()
.account(issuerAddress)
.subject(subjectAddress)
.credentialType(CREDENTIAL_TYPE)
.sequence(accountSequence(xrplClient, issuerAddress))
.fee(recommendedFee(xrplClient))
.lastLedgerSequence(lastLedgerSequence(xrplClient))
.signingPublicKey(issuer.publicKey())
.build();
printTransactionJson(createTx);
// ----- Sign, submit, and wait for CredentialCreate validation -----
System.out.println("\n=== Submitting CredentialCreate transaction ===\n");
TransactionResult<CredentialCreate> createResult = signSubmitAndWait(
xrplClient, issuer, createTx, CredentialCreate.class);
requireSuccess(createResult);
// ----- Prepare CredentialAccept transaction -----
System.out.println("\n=== Preparing CredentialAccept transaction ===\n");
CredentialAccept acceptTx = CredentialAccept.builder()
.account(subjectAddress)
.issuer(issuerAddress)
.credentialType(CREDENTIAL_TYPE)
.sequence(accountSequence(xrplClient, subjectAddress))
.fee(recommendedFee(xrplClient))
.lastLedgerSequence(lastLedgerSequence(xrplClient))
.signingPublicKey(subject.publicKey())
.build();
printTransactionJson(acceptTx);
// ----- Sign, Submit, and wait for CredentialAccept validation -----
System.out.println("\n=== Submitting CredentialAccept transaction ===\n");
TransactionResult<CredentialAccept> acceptResult = signSubmitAndWait(
xrplClient, subject, acceptTx, CredentialAccept.class);
requireSuccess(acceptResult);
// ----- Prepare CredentialDelete transaction -----
System.out.println("\n=== Preparing CredentialDelete transaction ===\n");
CredentialDelete deleteTx = CredentialDelete.builder()
.account(subjectAddress)
.issuer(issuerAddress)
.credentialType(CREDENTIAL_TYPE)
.sequence(accountSequence(xrplClient, subjectAddress))
.fee(recommendedFee(xrplClient))
.lastLedgerSequence(lastLedgerSequence(xrplClient))
.signingPublicKey(subject.publicKey())
.build();
printTransactionJson(deleteTx);
// ----- Sign, Submit, and wait for CredentialDelete validation -----
System.out.println("\n=== Submitting CredentialDelete transaction ===\n");
TransactionResult<CredentialDelete> deleteResult = signSubmitAndWait(
xrplClient, subject, deleteTx, CredentialDelete.class);
requireSuccess(deleteResult);
}
// ===== Helper functions =====
// Generates a new Ed25519 keypair, funds it from the Testnet faucet, and
// returns the keypair once the account is visible on a validated ledger.
private static KeyPair createAndFundWallet(XrplClient xrplClient) {
KeyPair keyPair = Seed.ed25519Seed().deriveKeyPair();
Address address = keyPair.publicKey().deriveAddress();
FaucetClient faucetClient = FaucetClient.construct(FAUCET_URL);
faucetClient.fundAccount(FundAccountRequest.of(address));
for (int attempt = 0; attempt < 20; attempt++) {
try {
xrplClient.accountInfo(AccountInfoRequestParams.builder()
.account(address)
.ledgerSpecifier(LedgerSpecifier.VALIDATED)
.build());
return keyPair;
} catch (JsonRpcClientErrorException notYetVisible) {
try {
Thread.sleep(1_000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Account polling interrupted for " + address + ". " + e.getMessage(), e);
}
}
}
throw new IllegalStateException("Faucet funding for " + address + " did not confirm in time.");
}
// Fetches the next transaction sequence number of an address from
// the latest validated ledger.
private static UnsignedInteger accountSequence(XrplClient xrplClient, Address address) {
try {
AccountInfoResult info = xrplClient.accountInfo(AccountInfoRequestParams.builder()
.account(address)
.ledgerSpecifier(LedgerSpecifier.VALIDATED)
.build());
return info.accountData().sequence();
} catch (JsonRpcClientErrorException e) {
throw new RuntimeException("Failed to fetch account sequence for " + address + ". " + e.getMessage(), e);
}
}
// Fetches the current network fee and returns the recommended fee for
// a standard (non-multisig, non-batch) transaction.
private static XrpCurrencyAmount recommendedFee(XrplClient xrplClient) {
try {
return FeeUtils.computeNetworkFees(xrplClient.fee()).recommendedFee();
} catch (JsonRpcClientErrorException e) {
throw new RuntimeException("Failed to fetch network fee. " + e.getMessage(), e);
}
}
// Computes a safe LastLedgerSequence for a new transaction. The
// latest validated ledger index plus a small buffer (20 ledgers).
private static UnsignedInteger lastLedgerSequence(XrplClient xrplClient) {
try {
UnsignedInteger validatedLedger = xrplClient.ledger(LedgerRequestParams.builder()
.ledgerSpecifier(LedgerSpecifier.VALIDATED)
.build())
.ledgerIndexSafe()
.unsignedIntegerValue();
return validatedLedger.plus(UnsignedInteger.valueOf(20));
} catch (JsonRpcClientErrorException e) {
throw new RuntimeException("Failed to compute LastLedgerSequence. " + e.getMessage(), e);
}
}
// Prints a transaction as a formatted JSON.
private static void printTransactionJson(Transaction tx) {
try {
System.out.println(ObjectMapperFactory.create().writerWithDefaultPrettyPrinter().writeValueAsString(tx));
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize transaction JSON. " + e.getMessage(), e);
}
}
// Signs and submits a transaction, then polls the network until
// the transaction reaches a validated state.
private static <T extends Transaction> TransactionResult<T> signSubmitAndWait(
XrplClient xrplClient,
KeyPair signer,
T transaction,
Class<T> transactionType
) {
SignatureService<PrivateKey> signatureService = new BcSignatureService();
UnsignedInteger lastLedgerSequence = transaction.lastLedgerSequence()
.orElseThrow(() -> new IllegalArgumentException(
"Must set LastLedgerSequence for polling expiration"));
try {
SingleSignedTransaction<T> signed = signatureService.sign(signer.privateKey(), transaction);
SubmitResult<T> submit = xrplClient.submit(signed);
if (!TransactionResultCodes.TES_SUCCESS.equals(submit.engineResult())) {
throw new IllegalStateException(
"Submission rejected. " + submit.engineResult() + "" + submit.engineResultMessage());
}
while (true) {
Thread.sleep(1_000L);
// Poll network for validated status using tx hash
try {
TransactionResult<T> result = xrplClient.transaction(
TransactionRequestParams.of(signed.hash()), transactionType);
if (result.validated()) {
return result;
}
} catch (JsonRpcClientErrorException e) {
// Transaction not found; keep polling.
}
// Check if transaction expired before polling again
UnsignedInteger currentLedger = xrplClient.ledger(LedgerRequestParams.builder()
.ledgerSpecifier(LedgerSpecifier.VALIDATED)
.build())
.ledgerIndexSafe()
.unsignedIntegerValue();
if (currentLedger.compareTo(lastLedgerSequence) > 0) {
throw new IllegalStateException("Transaction expired. Current ledger " + currentLedger
+ " passed LastLedgerSequence " + lastLedgerSequence);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Transaction polling interrupted. " + e.getMessage(), e);
} catch (JsonRpcClientErrorException | JsonProcessingException e) {
throw new RuntimeException("Transaction processing failed. " + e.getMessage(), e);
}
}
// Checks for a tesSUCCESS result code. If true, prints an explorer
// link. Otherwise, throws an error.
private static void requireSuccess(TransactionResult<?> result) {
String code = result.metadata().get().transactionResult();
String txType = result.transaction().transactionType().value();
if (!TransactionResultCodes.TES_SUCCESS.equals(code)) {
throw new IllegalStateException(txType + " failed with error code " + code);
}
System.out.println(txType + " succeeded!");
System.out.println("Explorer: " + EXPLORER_BASE + result.hash());
}
}

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Quiets xrpl4j's DEBUG chatter so tutorial output stays readable.
Raise xrpl4j to DEBUG to see wire-level transaction details. -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.xrpl.xrpl4j" level="WARN"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

View File

@@ -373,7 +373,6 @@
[get_counts command]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/get_counts.md
[get_counts method]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/get_counts.md
[Get Started Using Go]: /docs/tutorials/get-started/get-started-go.md
[Get Started Using Java]: /docs/tutorials/get-started/get-started-java.md
[Get Started Using JavaScript]: /docs/tutorials/get-started/get-started-javascript.md
[Get Started Using Python]: /docs/tutorials/get-started/get-started-python.md
[hexadecimal]: https://en.wikipedia.org/wiki/Hexadecimal
@@ -491,7 +490,6 @@
[vault_info method]: /docs/references/http-websocket-apis/public-api-methods/vault-methods/vault_info.md
[wallet_propose command]: /docs/references/http-websocket-apis/admin-api-methods/key-generation-methods/wallet_propose.md
[wallet_propose method]: /docs/references/http-websocket-apis/admin-api-methods/key-generation-methods/wallet_propose.md
[xrpl4j library]: https://github.com/XRPLF/xrpl4j
[xrpl-go library]: https://github.com/XRPLF/xrpl-go
[xrpl.js library]: https://github.com/XRPLF/xrpl.js
[xrpl-py library]: https://github.com/XRPLF/xrpl-py

View File

@@ -55,7 +55,7 @@ Each inner transaction:
- Must set the `tfInnerBatchTxn` flag.
- Must not have a fee. It must use a fee value of _0_.
- Must not be signed (the global transaction is already signed by all relevant parties). They must instead have an empty string ("") in the `SigningPubKey` and `TxnSignature` fields.
- Must not be signed (the global transaction is already signed by all relevant parties). It should instead have an empty string (`""`) in the `SigningPubKey` field and must not include the `TxnSignature` or `Signers` fields.
A transaction is considered a failure if it receives any result that is not `tesSUCCESS`.
@@ -130,7 +130,7 @@ Each outer transaction contains the metadata for its sequence and fee processing
Each inner transaction contains the metadata for its own processing. Only the inner transactions that are actually committed to the ledger are included. This makes it easier for legacy systems to process `Batch` transactions as if they were normal.
There is also a pointer back to the parent outer transaction (`ParentBatchID`).
There is also a pointer back to the outer transaction (`ParentBatchID`).
## Transaction Common Fields
@@ -168,3 +168,40 @@ If Alice just signs her part of the Batch transaction, Bob can modify his transa
An inner batch transaction is a special case. It doesn't include a signature or a fee (since those are both included in the outer transaction). Therefore, they must be handled carefully to ensure that someone can't somehow directly submit an inner `Batch` transaction without it being included in an outer transaction.
Inner transactions cannot be broadcast (and won't be accepted if they happen to be broadcast, for example, from a malicious node). They must be generated from the `Batch` outer transaction instead. Inner transactions cannot be directly submitted via the submit RPC.
## Integration Considerations
`Batch` transactions have some unique integration considerations:
- Since the outer transaction returns `tesSUCCESS` even when inner transactions fail (see [Metadata](#metadata)), you must check each inner transaction's metadata and result codes to determine its actual outcome.
- If inner transactions are validated, they are included in the same ledger as the outer transaction. If an inner transaction appears in a different ledger, it is likely a fraud attempt.
- Systems that don't specifically handle `Batch` transactions should be able to support them without any changes, since each inner transaction is a valid transaction on its own. All inner transactions that have a `tes` (success) or `tec` result code are accessible via standard transaction-fetching mechanisms such as [`tx`](/docs/references/http-websocket-apis/public-api-methods/transaction-methods/tx.md) and [`account_tx`](/docs/references/http-websocket-apis/public-api-methods/account-methods/account_tx.md).
- In a multi-account `Batch` transaction, only the inner transactions and batch mode flags are signed by all parties. This means the submitter of the outer transaction can adjust the sequence number and fee of the outer transaction as needed, without coordinating with the other parties.
The following sections cover additional recommendations for specific types of integrations.
### Client Libraries
Client libraries that implement `Batch` transaction support should:
- Provide a helper method to calculate the fee for a `Batch` transaction, since the fee includes the sum of all inner transaction fees. See [XRPL Batch Transaction Fees](#xrpl-batch-transaction-fees).
- Provide a helper method to construct and sign multi-account `Batch` transactions, where one party signs the outer transaction and the other parties sign the inner transactions.
- Provide an auto-fill method that sets each inner transaction's `Fee` to `"0"` and the `SigningPubKey` to an empty string (`""`), while omitting the `TxnSignature` field.
### Wallets
Wallets that display or sign `Batch` transactions should:
- Clearly display all inner transactions to users before requesting a signature, so users understand the full scope of what they are approving.
- For multi-account `Batch` transactions, provide a workflow for users to review and sign their portion of the batch, then export it for other parties to sign.
- Warn users if they are signing a `Batch` transaction that includes inner transactions from other accounts, since they are approving the entire batch.
- Display the [batch mode](#xrpl-batch-transaction-modes) and explain its implications.
- Avoid auto-incrementing sequence numbers after successes or failures, since the number of validated transactions depends on the batch mode and which inner transactions succeed. Instead, wait for the outer `Batch` transaction to be validated and check the result of each inner transaction to determine which sequences were consumed.
### Explorers and Indexers
Explorers and indexers that display `Batch` transactions should:
- Display the relationship between outer `Batch` transactions and their inner transactions using the `ParentBatchID` field in the inner transaction metadata.
- Show inner transactions in context with their outer `Batch` transaction, rather than as standalone transactions.
- Consider grouping inner transactions with their outer transaction in transaction lists for clarity.

View File

@@ -105,24 +105,26 @@ Create or update a [price oracle](../../../../concepts/decentralized-storage/pri
| Field | JSON Type | Internal Type | Required? | Description |
|---------------------|-----------|---------------|-----------|-------------|
| `BaseAsset` | String | Currency | Yes | The primary asset in a trading pair. Any valid identifier, such as a stock symbol, bond CUSIP, or currency code is allowed. For example, in the BTC/USD pair, BTC is the base asset; in 912810RR9/BTC, 912810RR9 is the base asset. |
| `QuoteAsset` | String | Currency | Yes | The quote asset in a trading pair. The quote asset denotes the price of one unit of the base asset. For example, in the BTC/USD pair, BTC is the base asset; in 912810RR9/BTC, 912810RR9 is the base asset. |
| `AssetPrice` | String | UInt64 | No | The asset price after applying the `Scale` precision level. It's not included if the last update transaction didn't include the `BaseAsset`/`QuoteAsset` pair. It's recommended you provide this value as a hexadecimal, but [client libraries](https://xrpl.org/docs/references#client-libraries) will accept decimal numbers and convert to hexadecimal strings. |
| `Scale` | Number | UInt8 | No | The scaling factor to apply to an asset price. For example, if `Scale` is 6 and original price is 0.155, then the scaled price is 155000. Valid scale ranges are 0-10. It's not included if the last update transaction didn't include the `BaseAsset`/`QuoteAsset` pair.|
| `QuoteAsset` | String | Currency | Yes | The quote asset in a trading pair. The quote asset denotes the price of one unit of the base asset. For example, in the BTC/USD pair, USD is the quote asset; in 912810RR9/BTC, BTC is the quote asset. |
| `AssetPrice` | String | UInt64 | No | The asset price after applying the `Scale` precision level. It's recommended you provide this value as a hexadecimal, but [client libraries](https://xrpl.org/docs/references#client-libraries) will accept decimal numbers and convert to hexadecimal strings. |
| `Scale` | Number | UInt8 | No | The scaling factor to apply to an asset price. For example, if `Scale` is 6 and original price is 0.155, then the scaled price is 155000. Valid scale ranges are 0-10. |
`PriceData` is created or updated, following these rules:
`PriceData` is created or updated based on whether the token pair is new or already exists on the oracle entry, and which fields are included in the `OracleSet` transaction. The table below describes the possible outcomes:
- New token pairs in the transaction are added to the object.
- Token pairs in the transaction overwrite corresponding token pairs in the object.
- Token pairs in the transaction with a missing `AssetPrice` field delete corresponding token pairs in the object.
- Token pairs that only appear in the object have `AssetPrice` and `Scale` removed to signify that the price is outdated.
| Token pair state and transaction fields | Outcome |
|:--------------------------------------------------------|:--------|
| New pair, including `AssetPrice` | The asset pair is added to the oracle entry with `AssetPrice`. `Scale` is set with a default value of `0` if not set. |
| New pair, excluding `AssetPrice` | `temMALFORMED` if creating a new oracle entry; `tecTOKEN_PAIR_NOT_FOUND` if updating an existing oracle entry. |
| Existing pair, including `AssetPrice` and `Scale` | `AssetPrice` and `Scale` are updated for the asset pair. |
| Existing pair, including `AssetPrice` but _not_ `Scale` | `AssetPrice` is updated. `Scale` is reset to the default value of 0. |
| Existing pair, excluding `AssetPrice` | The asset pair is deleted from the oracle entry. |
| Existing pair excluded from the transaction | The existing asset pair remains in the oracle entry, but its `AssetPrice` and `Scale` are cleared to signal the price is outdated. |
When updating fewer entries than the existing oracle contains, the `LastUpdateTime` applies to all entries. Entries not included in the update have their prices removed to indicate they are out of date for the given `LastUpdateTime`. To access historical price data for these entries, you can:
The `LastUpdateTime` field applies to all entries in the `PriceDataSeries` array. Existing asset pairs not included in an `OracleSet` update transaction have their prices removed to indicate they are out of date for the given `LastUpdateTime`. To access historical price data for these entries, you can:
- Use the `ledger_entry` method with `PreviousTxnLgrSeq` to traverse previous Oracle objects
- Use the `tx` method with `PreviousTxnID` to find historical transactions
This design choice saves space by having a single `LastUpdateTime` for all entries rather than tracking update times per token pair.
{% admonition type="info" name="Note" %}
The order of token pairs in the transaction isn't important because each token pair uniquely identifies the location of the `PriceData` object in the `PriceDataSeries`.
{% /admonition %}

View File

@@ -1,151 +0,0 @@
---
seo:
description: Issue, accept, and delete a credential on the XRP Ledger.
metadata:
indexPage: true
labels:
- Credentials
---
# Manage Credentials
This tutorial shows you how to manage the full lifecycle of [Credentials][] on the XRP Ledger: issuing a credential to a subject, accepting the credential, and deleting it.
{% amendment-disclaimer name="Credentials" /%}
## Goals
By the end of this tutorial, you will be able to:
- Issue a credential to a subject account.
- Accept a credential as the subject.
- Delete a credential from the ledger.
## Prerequisites
To complete this tutorial, you should:
- Have a basic understanding of the XRP Ledger.
- Have an XRP Ledger client library set up in your development environment. This page provides examples for the following:
- **Java** with the [xrpl4j library][]. See [Get Started Using Java][] for setup steps.
## Source Code
You can find the complete source code for this tutorial's examples in the {% repo-link path="_code-samples/credential/" %}code samples section of this website's repository{% /repo-link %}.
## Steps
### 1. Install dependencies
{% tabs %}
{% tab label="Java" %}
From the code sample folder, use `mvn` to install dependencies.
```bash
mvn install
```
{% /tab %}
{% /tabs %}
### 2. Set up client and fund accounts
To get started, import the necessary libraries and instantiate a client to connect to the XRPL Testnet. This example imports:
{% tabs %}
{% tab label="Java" %}
- `xrpl4j`: Used for XRPL client connection, transaction submission, and wallet handling.
- `OkHttp`, `Guava`, `Jackson`: Used for HTTP URL construction, unsigned integer arithmetic, and JSON serialization.
- `java.util.concurrent`: Used for async operations.
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" before="// ----- Prepare CredentialCreate" /%}
The `createAndFundWallet()` helper generates an Ed25519 keypair, funds it from the Testnet faucet, and polls Testnet until the account is visible on a validated ledger.
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// Generates a new Ed25519 keypair" before="// Fetches the next transaction sequence number" /%}
{% /tab %}
{% /tabs %}
### 3. Prepare CredentialCreate transaction
Create the [CredentialCreate transaction][] object.
{% tabs %}
{% tab label="Java" %}
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Prepare CredentialCreate" before="// ----- Sign, submit, and wait for CredentialCreate" /%}
{% /tab %}
{% /tabs %}
The credential is identified by the issuer, subject, and credential type (written as a hexadecimal string).
### 4. Submit CredentialCreate transaction
Sign and submit the `CredentialCreate` transaction to the XRP Ledger.
{% tabs %}
{% tab label="Java" %}
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Sign, submit, and wait for CredentialCreate" before="// ----- Prepare CredentialAccept" /%}
The `signSubmitAndWait()` helper signs a transaction, submits it, and polls Testnet until it reaches a validated ledger.
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// Signs and submits a transaction" before="// Checks for a tesSUCCESS result code" /%}
The `requireSuccess` helper verifies that the transaction succeeded with a `tesSUCCESS` result code and posts a link to the transaction metadata on the XRPL Explorer.
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// Checks for a tesSUCCESS result code" /%}
{% /tab %}
{% /tabs %}
### 5. Prepare CredentialAccept transaction
Create the [CredentialAccept transaction][] object. The subject account must accept the credential to make it valid.
{% tabs %}
{% tab label="Java" %}
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Prepare CredentialAccept" before="// ----- Sign, Submit, and wait for CredentialAccept" /%}
{% /tab %}
{% /tabs %}
### 6. Submit CredentialAccept transaction
Sign and submit the `CredentialAccept` transaction to the XRP Ledger.
{% tabs %}
{% tab label="Java" %}
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Sign, Submit, and wait for CredentialAccept" before="// ----- Prepare CredentialDelete" /%}
{% /tab %}
{% /tabs %}
### 7. Prepare CredentialDelete transaction
Create the [CredentialDelete transaction][] object. Either the issuer or the subject can delete a credential.
{% tabs %}
{% tab label="Java" %}
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Prepare CredentialDelete" before="// ----- Sign, Submit, and wait for CredentialDelete" /%}
{% /tab %}
{% /tabs %}
### 8. Submit CredentialDelete transaction
Sign and submit the `CredentialDelete` transaction to the XRP Ledger.
{% tabs %}
{% tab label="Java" %}
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Sign, Submit, and wait for CredentialDelete" before="// ===== Helper functions" /%}
{% /tab %}
{% /tabs %}
## See Also
**Concepts**:
- [Credentials][]
**Tutorials**:
- [Verify Credentials](./verify-credentials.md)
**References**:
- [CredentialCreate transaction][]
- [CredentialAccept transaction][]
- [CredentialDelete transaction][]
{% raw-partial file="/docs/_snippets/common-links.md" /%}

View File

@@ -1,5 +1,52 @@
import { useThemeHooks } from "@redocly/theme/core/hooks"
import { Link } from "@redocly/theme/components/Link/Link"
import { useRef, useState } from "react"
type TutorialLanguagesMap = Record<string, string[]>
interface TutorialMetadataItem {
path: string
title: string
description: string
lastModified: string
category: string
}
interface Tutorial {
title: string
description?: string
path: string
// External community contribution fields (optional)
author?: { name: string; url: string }
github?: string
externalUrl?: string
}
interface TutorialSection {
id: string
title: string
description: string
tutorials: Tutorial[]
showFooter?: boolean
}
// External community contribution - manually curated with author/repo/demo info
interface PinnedExternalTutorial {
title: string
description: string
author: { name: string; url: string }
github: string
url?: string
}
// Pinned tutorial entry:
// - string: internal path (uses frontmatter title/description)
// - object with `path`: internal path with optional description override
// - PinnedExternalTutorial: external community contribution with author/repo/demo
type PinnedTutorial = string | { path: string; description?: string } | PinnedExternalTutorial
const MAX_WHATS_NEW = 3
const MAX_TUTORIALS_PER_SECTION = 6
export const frontmatter = {
seo: {
@@ -19,242 +66,90 @@ const langIcons: Record<string, { src: string; alt: string }> = {
xrpl: { src: "/img/logos/xrp-mark.svg", alt: "XRP Ledger" },
}
// Type for the tutorial languages map from the plugin
type TutorialLanguagesMap = Record<string, string[]>
interface Tutorial {
title: string
body?: string
path: string
icon?: string // Single language icon (for single-language tutorials)
}
interface TutorialSection {
id: string
title: string
description: string
tutorials: Tutorial[]
}
// Get Started tutorials -----------------
const getStartedTutorials: Tutorial[] = [
{
title: "JavaScript",
body: "Using the xrpl.js client library.",
path: "/docs/tutorials/get-started/get-started-javascript/",
icon: "javascript",
// ── Section configuration -----------------------------------------------------------
// Categories and their titles are auto-detected by the tutorial-metadata plugin.
// Use the config to customize the category titles, add descriptions, change the default category order, and pin tutorials.
const sectionConfig: Record<string, {
title?: string
description?: string
pinned?: PinnedTutorial[]
showFooter?: boolean
}> = {
"whats-new": {
title: "What's New",
description: "Recently added/updated tutorials to help you build on the XRP Ledger.",
},
{
title: "Python",
body: "Using xrpl.py, a pure Python library.",
path: "/docs/tutorials/get-started/get-started-python/",
icon: "python",
"get-started": {
showFooter: true,
title: "Get Started with SDKs",
description: "These tutorials walk you through the basics of building a very simple XRP Ledger-connected application using your favorite programming language.",
pinned: [
{ path: "/docs/tutorials/get-started/get-started-javascript/", description: "Using the xrpl.js client library." },
{ path: "/docs/tutorials/get-started/get-started-python/", description: "Using xrpl.py, a pure Python library." },
{ path: "/docs/tutorials/get-started/get-started-go/", description: "Using xrpl-go, a pure Go library." },
{ path: "/docs/tutorials/get-started/get-started-java/", description: "Using xrpl4j, a pure Java library." },
{ path: "/docs/tutorials/get-started/get-started-php/", description: "Using the XRPL_PHP client library." },
{ path: "/docs/tutorials/get-started/get-started-http-websocket-apis/", description: "Access the XRP Ledger directly through the APIs of its core server." },
],
},
{
title: "Go",
body: "Using xrpl-go, a pure Go library.",
path: "/docs/tutorials/get-started/get-started-go/",
icon: "go",
},
{
title: "Java",
body: "Using xrpl4j, a pure Java library.",
path: "/docs/tutorials/get-started/get-started-java/",
icon: "java",
},
{
title: "PHP",
body: "Using the XRPL_PHP client library.",
path: "/docs/tutorials/get-started/get-started-php/",
icon: "php",
},
{
title: "HTTP & WebSocket APIs",
body: "Access the XRP Ledger directly through the APIs of its core server.",
path: "/docs/tutorials/get-started/get-started-http-websocket-apis/",
icon: "http",
},
]
// Other tutorial sections -----------------
// Languages are auto-detected from the markdown files by the tutorial-languages plugin.
// Only specify `icon` for single-language tutorials without tabs.
const sections: TutorialSection[] = [
{
id: "tokens",
title: "Tokens",
"tokens": {
description: "Create and manage tokens on the XRP Ledger.",
tutorials: [
{
title: "Issue a Multi-Purpose Token",
body: "Issue new tokens using the v2 fungible token standard.",
path: "/docs/tutorials/tokens/mpts/issue-a-multi-purpose-token/",
},
{
title: "Issue a Fungible Token",
body: "Issue new tokens using the v1 fungible token standard.",
path: "/docs/tutorials/tokens/fungible-tokens/issue-a-fungible-token/",
},
{
title: "Mint and Burn NFTs Using JavaScript",
body: "Create new NFTs, retrieve existing tokens, and burn the ones you no longer need.",
path: "/docs/tutorials/tokens/nfts/mint-and-burn-nfts-js/",
icon: "javascript",
},
pinned: [
{ path: "/docs/tutorials/tokens/mpts/issue-a-multi-purpose-token/", description: "Issue new tokens using the v2 fungible token standard." },
{ path: "/docs/tutorials/tokens/fungible-tokens/issue-a-fungible-token/", description: "Issue new tokens using the v1 fungible token standard." },
{ path: "/docs/tutorials/tokens/nfts/mint-and-burn-nfts-js/", description: "Create new NFTs, retrieve existing tokens, and burn the ones you no longer need." },
"/docs/tutorials/tokens/mpts/sending-mpts-in-javascript/",
],
},
{
id: "payments",
title: "Payments",
"payments": {
description: "Transfer XRP and issued currencies using various payment types.",
tutorials: [
{
title: "Send XRP",
body: "Send a direct XRP payment to another account.",
path: "/docs/tutorials/payments/send-xrp/",
},
{
title: "Sending MPTs in JavaScript",
body: "Send a Multi-Purpose Token (MPT) to another account with the JavaScript SDK.",
path: "/docs/tutorials/tokens/mpts/sending-mpts-in-javascript/",
icon: "javascript",
},
{
title: "Create Trust Line and Send Currency in JavaScript",
body: "Set up trust lines and send issued currencies with the JavaScript SDK.",
path: "/docs/tutorials/payments/create-trust-line-send-currency-in-javascript/",
icon: "javascript",
},
{
title: "Create Trust Line and Send Currency in Python",
body: "Set up trust lines and send issued currencies with the Python SDK.",
path: "/docs/tutorials/payments/create-trust-line-send-currency-in-python/",
icon: "python",
},
{
title: "Send a Conditional Escrow",
body: "Send an escrow that can be released when a specific crypto-condition is fulfilled.",
path: "/docs/tutorials/payments/send-a-conditional-escrow/",
},
{
title: "Send a Timed Escrow",
body: "Send an escrow whose only condition for release is that a specific time has passed.",
path: "/docs/tutorials/payments/send-a-timed-escrow/",
},
pinned: [
"/docs/tutorials/payments/send-xrp/",
"/docs/tutorials/payments/create-trust-line-send-currency-in-javascript/",
"/docs/tutorials/payments/send-a-conditional-escrow/",
"/docs/tutorials/payments/send-a-timed-escrow/",
],
},
{
id: "defi",
title: "DeFi",
"defi": {
description: "Trade, provide liquidity, and lend using native XRP Ledger DeFi features.",
tutorials: [
{
title: "Create an Automated Market Maker",
body: "Set up an AMM for a token pair and provide liquidity.",
path: "/docs/tutorials/defi/dex/create-an-automated-market-maker/",
},
{
title: "Trade in the Decentralized Exchange",
body: "Buy and sell tokens in the Decentralized Exchange (DEX).",
path: "/docs/tutorials/defi/dex/trade-in-the-decentralized-exchange/",
},
{
title: "Create a Loan Broker",
body: "Set up a loan broker to create and manage loans.",
path: "/docs/tutorials/defi/lending/use-the-lending-protocol/create-a-loan-broker/",
},
{
title: "Create a Loan",
body: "Create a loan on the XRP Ledger.",
path: "/docs/tutorials/defi/lending/use-the-lending-protocol/create-a-loan/",
},
{
title: "Create a Single Asset Vault",
body: "Create a single asset vault on the XRP Ledger.",
path: "/docs/tutorials/defi/lending/use-single-asset-vaults/create-a-single-asset-vault/",
},
{
title: "Deposit into a Vault",
body: "Deposit assets into a vault and receive shares.",
path: "/docs/tutorials/defi/lending/use-single-asset-vaults/deposit-into-a-vault/",
},
pinned: [
"/docs/tutorials/defi/dex/create-an-automated-market-maker/",
"/docs/tutorials/defi/dex/trade-in-the-decentralized-exchange/",
"/docs/tutorials/defi/lending/use-the-lending-protocol/create-a-loan/",
"/docs/tutorials/defi/lending/use-single-asset-vaults/create-a-single-asset-vault/",
],
},
{
id: "best-practices",
title: "Best Practices",
"best-practices": {
description: "Learn recommended patterns for building reliable, secure applications on the XRP Ledger.",
tutorials: [
{
title: "API Usage",
body: "Best practices for using XRP Ledger APIs.",
path: "/docs/tutorials/best-practices/api-usage/",
},
{
title: "Use Tickets",
body: "Use tickets to send transactions out of the normal order.",
path: "/docs/tutorials/best-practices/transaction-sending/use-tickets/",
},
{
title: "Send a Single Account Batch Transaction",
body: "Group multiple transactions together and execute them as a single atomic operation.",
path: "/docs/tutorials/best-practices/transaction-sending/send-a-single-account-batch-transaction/",
},
{
title: "Assign a Regular Key Pair",
body: "Assign a regular key pair for signing transactions.",
path: "/docs/tutorials/best-practices/key-management/assign-a-regular-key-pair/",
},
{
title: "Set Up Multi-Signing",
body: "Configure multi-signing for enhanced security.",
path: "/docs/tutorials/best-practices/key-management/set-up-multi-signing/",
},
{
title: "Send a Multi-Signed Transaction",
body: "Send a transaction with multiple signatures.",
path: "/docs/tutorials/best-practices/key-management/send-a-multi-signed-transaction/",
},
pinned: [
"/docs/tutorials/best-practices/api-usage/",
],
},
{
id: "sample-apps",
title: "Sample Apps",
"compliance-features": {
title: "Compliance",
description: "Implement compliance controls like destination tags, credentials, and permissioned domains.",
},
"programmability": {
description: "Set up cross-chain bridges and submit interoperability transactions.",
},
"advanced-developer-topics": {
description: "Explore advanced topics like WebSocket monitoring and testing Devnet features.",
},
"sample-apps": {
description: "Build complete, end-to-end applications like wallets and credential services.",
tutorials: [
pinned: [
{
title: "Build a Browser Wallet in JavaScript",
body: "Build a browser wallet for the XRP Ledger using JavaScript and various libraries.",
path: "/docs/tutorials/sample-apps/build-a-browser-wallet-in-javascript/",
icon: "javascript",
},
{
title: "Build a Desktop Wallet in JavaScript",
body: "Build a desktop wallet for the XRP Ledger using JavaScript, the Electron Framework, and various libraries.",
path: "/docs/tutorials/sample-apps/build-a-desktop-wallet-in-javascript/",
icon: "javascript",
},
{
title: "Build a Desktop Wallet in Python",
body: "Build a desktop wallet for the XRP Ledger using Python and various libraries.",
path: "/docs/tutorials/sample-apps/build-a-desktop-wallet-in-python/",
icon: "python",
},
{
title: "Credential Issuing Service in JavaScript",
body: "Build a credential issuing service using the JavaScript SDK.",
path: "/docs/tutorials/sample-apps/credential-issuing-service-in-javascript/",
icon: "javascript",
},
{
title: "Credential Issuing Service in Python",
body: "Build a credential issuing service using the Python SDK.",
path: "/docs/tutorials/sample-apps/credential-issuing-service-in-python/",
icon: "python",
title: "XRPL Lending Protocol Demo",
description: "A full-stack web application that demonstrates the end-to-end flow of the Lending Protocol and Single Asset Vaults.",
author: { name: "Aaditya-T", url: "https://github.com/Aaditya-T" },
github: "https://github.com/Aaditya-T/lending_test",
url: "https://lending-test-lovat.vercel.app/",
},
],
},
]
}
// ── Components ──────────────────────────────────────────────────────────────
function TutorialCard({
tutorial,
@@ -267,12 +162,10 @@ function TutorialCard({
showFooter?: boolean
translate: (text: string) => string
}) {
// Get icons: manual icon takes priority, then auto-detected languages, then XRPL fallback
const icons = tutorial.icon && langIcons[tutorial.icon]
? [langIcons[tutorial.icon]]
: detectedLanguages && detectedLanguages.length > 0
? detectedLanguages.map((lang) => langIcons[lang]).filter(Boolean)
: [langIcons.xrpl]
// Get icons from auto-detected languages, or fallback to XRPL icon.
const icons = detectedLanguages && detectedLanguages.length > 0
? detectedLanguages.map((lang) => langIcons[lang]).filter(Boolean)
: [langIcons.xrpl]
return (
<Link to={tutorial.path} className="card">
@@ -285,13 +178,220 @@ function TutorialCard({
</div>
<div className="card-body">
<h4 className="card-title h5">{translate(tutorial.title)}</h4>
{tutorial.body && <p className="card-text">{translate(tutorial.body)}</p>}
{tutorial.description && <p className="card-text">{translate(tutorial.description)}</p>}
</div>
{showFooter && <div className="card-footer"></div>}
</Link>
)
}
// Inline meta link used in ContributionCard
function MetaLink({ href, icon, label }: {
href: string
icon: string
label: string
}) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" className="meta-link">
<i className={`fa fa-${icon}`} aria-hidden="true" />
{label}
</a>
)
}
// Community Contribution Card
function ContributionCard({
tutorial,
translate,
}: {
tutorial: Tutorial
translate: (text: string) => string
}) {
const primaryUrl = tutorial.externalUrl || tutorial.github!
const handleCardClick = (e: React.MouseEvent | React.KeyboardEvent) => {
if ((e.target as HTMLElement).closest(".card-meta-row")) return
window.open(primaryUrl, "_blank", "noopener,noreferrer")
}
return (
<div
className="card contribution-card"
onClick={handleCardClick}
onKeyDown={(e) => { if (e.key === "Enter") handleCardClick(e) }}
role="link"
tabIndex={0}
>
<div className="card-header contribution-header">
<span className="circled-logo contribution-icon">
<i className="fa fa-users" aria-hidden="true" />
</span>
<div className="card-meta-row">
{tutorial.author && (
<>
<MetaLink href={tutorial.author.url} icon="user" label={tutorial.author.name} />
<span className="meta-dot" aria-hidden="true">·</span>
</>
)}
<MetaLink href={tutorial.github!} icon="github" label={translate("GitHub")} />
</div>
</div>
<div className="card-body">
<h4 className="card-title h5">
{translate(tutorial.title)}
<span className="card-external-icon" aria-label={translate("External link")}>
<i className="fa fa-external-link" aria-hidden="true" />
</span>
</h4>
{tutorial.description && <p className="card-text">{translate(tutorial.description)}</p>}
</div>
</div>
)
}
// Reusable section block for rendering tutorial sections
function TutorialSectionBlock({
id,
title,
description,
tutorials,
tutorialLanguages,
showFooter = false,
maxTutorials,
className = "",
translate,
}: {
id: string
title: string
description: string
tutorials: Tutorial[]
tutorialLanguages: TutorialLanguagesMap
showFooter?: boolean
maxTutorials?: number
className?: string
translate: (text: string) => string
}) {
const [expanded, setExpanded] = useState(false)
const sectionRef = useRef<HTMLElement>(null)
const hasMore = maxTutorials ? tutorials.length > maxTutorials : false
const displayTutorials = maxTutorials && !expanded ? tutorials.slice(0, maxTutorials) : tutorials
const handleToggle = () => {
if (expanded && sectionRef.current) {
const offsetTop = sectionRef.current.getBoundingClientRect().top + window.scrollY
setExpanded(false)
requestAnimationFrame(() => {
window.scrollTo({ top: offsetTop - 20 })
})
} else {
setExpanded(true)
}
}
return (
<section ref={sectionRef} className={`container-new pt-10 pb-14 ${className}`.trim()} id={id}>
<div className="col-12 col-xl-8 p-0">
<h3 className="h4 mb-3">{translate(title)}</h3>
<p className="mb-4">{translate(description)}</p>
</div>
<div className="row tutorial-cards">
{displayTutorials.map((tutorial) => (
<div key={tutorial.path} className="col-lg-4 col-md-6 mb-5">
{tutorial.github ? (
<ContributionCard tutorial={tutorial} translate={translate} />
) : (
<TutorialCard
tutorial={tutorial}
detectedLanguages={tutorialLanguages[tutorial.path]}
showFooter={showFooter}
translate={translate}
/>
)}
</div>
))}
</div>
{hasMore && (
<div className="explore-more-wrapper">
<button
className="explore-more-link"
onClick={handleToggle}
>
{expanded ? translate("Show less") : translate("Explore more")} {expanded ? "↑" : "→"}
</button>
</div>
)}
</section>
)
}
// Copyable URL component with click-to-copy functionality
function CopyableUrl({ url, translate }: { url: string; translate: (text: string) => string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(url)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error("Failed to copy:", err)
}
}
return (
<button
type="button"
className={`quick-ref-value-btn ${copied ? "copied" : ""}`}
onClick={handleCopy}
title={copied ? translate("Copied!") : translate("Click to copy")}
>
<code className="quick-ref-value">{url}</code>
<span className="copy-icon">{copied ? "✓" : ""}</span>
</button>
)
}
// Quick reference card showing public server URLs and faucet link
function QuickReferenceCard({ translate }: { translate: (text: string) => string }) {
return (
<div className="quick-ref-card">
<div className="quick-ref-section">
<span className="quick-ref-label">{translate("PUBLIC SERVERS")}</span>
<div className="quick-ref-group">
<span className="quick-ref-key"><strong>{translate("Mainnet")}</strong></span>
<div className="quick-ref-urls">
<span className="quick-ref-protocol">{translate("WebSocket")}</span>
<CopyableUrl url="wss://xrplcluster.com" translate={translate} />
<span className="quick-ref-protocol">{translate("JSON-RPC")}</span>
<CopyableUrl url="https://xrplcluster.com" translate={translate} />
</div>
</div>
<div className="quick-ref-group">
<span className="quick-ref-key"><strong>{translate("Testnet")}</strong></span>
<div className="quick-ref-urls">
<span className="quick-ref-protocol">{translate("WebSocket")}</span>
<CopyableUrl url="wss://s.altnet.rippletest.net:51233" translate={translate} />
<span className="quick-ref-protocol">{translate("JSON-RPC")}</span>
<CopyableUrl url="https://s.altnet.rippletest.net:51234" translate={translate} />
</div>
</div>
<Link to="/docs/tutorials/public-servers/" className="quick-ref-link">
{translate("View all servers")}
</Link>
</div>
<div className="quick-ref-divider"></div>
<div className="quick-ref-section">
<Link to="/resources/dev-tools/xrp-faucets/" className="quick-ref-faucet">
<span>{translate("Get Test XRP")}</span>
<span className="quick-ref-arrow"></span>
</Link>
</div>
</div>
)
}
// ── Page Component ──────────────────────────────────────────────────────────
export default function TutorialsIndex() {
const { useTranslate, usePageSharedData } = useThemeHooks()
const { translate } = useTranslate()
@@ -299,65 +399,160 @@ export default function TutorialsIndex() {
// Get auto-detected languages from the plugin (maps tutorial paths to language arrays).
const tutorialLanguages = usePageSharedData<TutorialLanguagesMap>("tutorial-languages") || {}
// Get tutorial metadata and sidebar categories from the tutorial-metadata plugin.
const tutorialMetadata = usePageSharedData<{
tutorials: TutorialMetadataItem[]
categories: { id: string; title: string }[]
}>("tutorial-metadata")
const allTutorials = tutorialMetadata?.tutorials || []
const sidebarCategories = tutorialMetadata?.categories || []
// What's New: most recently modified tutorials, excluding Get Started.
const whatsNewConfig = sectionConfig["whats-new"]!
const getStartedPaths = new Set(
(sectionConfig["get-started"]?.pinned || []).map(getPinnedPath)
)
const whatsNewTutorials: Tutorial[] = allTutorials
.filter((tutorial) => !getStartedPaths.has(tutorial.path))
.slice(0, MAX_WHATS_NEW)
.map((tutorial) => toTutorial(tutorial))
// Category sections (including Get Started): ordered by sectionConfig, then any new sidebar categories.
const sections = buildCategorySections(sidebarCategories, allTutorials)
return (
<main className="landing page-tutorials landing-builtin-bg">
<section className="container-new py-26">
<div className="col-lg-8 mx-auto text-lg-center">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">
{translate("Crypto Wallet and Blockchain Development Tutorials")}
</h1>
<h6 className="eyebrow mb-3">{translate("Tutorials")}</h6>
</div>
{/* Table of Contents */}
<nav className="mt-4">
<ul className="page-toc no-sideline d-flex flex-wrap justify-content-center gap-2 mb-0">
<li><a href="#get-started">{translate("Get Started with SDKs")}</a></li>
{sections.map((section) => (
<li key={section.id}><a href={`#${section.id}`}>{translate(section.title)}</a></li>
))}
</ul>
</nav>
</div>
</section>
{/* Get Started */}
<section className="container-new pt-10 pb-20" id="get-started">
<div className="col-12 col-xl-8 p-0">
<h3 className="h4 mb-3">{translate("Get Started with SDKs")}</h3>
<p className="mb-4">
{translate("These tutorials walk you through the basics of building a very simple XRP Ledger-connected application using your favorite programming language.")}
</p>
</div>
<div className="row tutorial-cards">
{getStartedTutorials.map((tutorial, idx) => (
<div key={idx} className="col-lg-4 col-md-6 mb-5">
<TutorialCard tutorial={tutorial} showFooter translate={translate} />
{/* Hero Section */}
<section className="container-new py-20">
<div className="row align-items-center">
<div className="col-lg-7">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">
{translate("Crypto Wallet and Blockchain Development Tutorials")}
</h1>
<h6 className="eyebrow mb-3">{translate("Tutorials")}</h6>
</div>
))}
<nav className="mt-4">
<ul className="page-toc no-sideline d-flex flex-wrap gap-2 mb-0">
{whatsNewTutorials.length > 0 && (
<li><Link to="#whats-new">{translate(whatsNewConfig.title)}</Link></li>
)}
{sections.map((section) => (
<li key={section.id}><Link to={`#${section.id}`}>{translate(section.title)}</Link></li>
))}
</ul>
</nav>
</div>
<div className="col-lg-5 mt-6 mt-lg-0">
<QuickReferenceCard translate={translate} />
</div>
</div>
</section>
{/* Other Tutorials */}
{/* What's New */}
{whatsNewTutorials.length > 0 && (
<TutorialSectionBlock
id="whats-new"
title={whatsNewConfig.title!}
description={whatsNewConfig.description!}
tutorials={whatsNewTutorials}
tutorialLanguages={tutorialLanguages}
showFooter
className="whats-new-section pb-20"
translate={translate}
/>
)}
{/* Tutorial Sections */}
{sections.map((section) => (
<section className="container-new pt-10 pb-10" key={section.id} id={section.id}>
<div className="col-12 col-xl-8 p-0">
<h3 className="h4 mb-3">{translate(section.title)}</h3>
<p className="mb-4">{translate(section.description)}</p>
</div>
<div className="row tutorial-cards">
{section.tutorials.slice(0, 6).map((tutorial, idx) => (
<div key={idx} className="col-lg-4 col-md-6 mb-5">
<TutorialCard
tutorial={tutorial}
detectedLanguages={tutorialLanguages[tutorial.path]}
translate={translate}
/>
</div>
))}
</div>
</section>
<TutorialSectionBlock
key={section.id}
id={section.id}
title={section.title}
description={section.description}
tutorials={section.tutorials}
tutorialLanguages={tutorialLanguages}
maxTutorials={section.showFooter ? undefined : MAX_TUTORIALS_PER_SECTION}
showFooter={section.showFooter}
className={section.showFooter ? "pb-20" : "category-section"}
translate={translate}
/>
))}
</main>
)
}
// ── Helpers ──────────────────────────────────────────────────────────────────
/** Type guard for external community contributions */
function isExternalContribution(entry: PinnedTutorial): entry is PinnedExternalTutorial {
return typeof entry !== "string" && "github" in entry
}
/** Get path from pinned tutorial entry*/
function getPinnedPath(entry: PinnedTutorial): string {
return typeof entry === "string" ? entry : isExternalContribution(entry) ? entry.github : entry.path
}
/** Convert tutorial metadata to the common Tutorial type */
function toTutorial(t: TutorialMetadataItem, descriptionOverride?: string): Tutorial {
return {
title: t.title,
description: descriptionOverride || t.description,
path: t.path,
}
}
/** Build Tutorial objects from pinned entries, resolving metadata for internal paths */
function buildPinnedTutorials(entries: PinnedTutorial[], allTutorials: TutorialMetadataItem[]): Tutorial[] {
return entries
.map((entry): Tutorial | null => {
if (isExternalContribution(entry)) {
return {
title: entry.title,
description: entry.description,
path: entry.url || entry.github,
author: entry.author,
github: entry.github,
externalUrl: entry.url,
}
}
const path = getPinnedPath(entry)
const descOverride = typeof entry === "string" ? undefined : entry.description
const metadata = allTutorials.find((t) => t.path === path)
return metadata ? toTutorial(metadata, descOverride) : null
})
.filter((t): t is Tutorial => t !== null)
}
/** Build category sections ordered by sectionConfig, with new sidebar categories appended */
function buildCategorySections(
sidebarCategories: { id: string; title: string }[],
allTutorials: TutorialMetadataItem[],
): TutorialSection[] {
const specialIds = new Set(["whats-new"])
const sidebarMap = new Map(sidebarCategories.map((category) => [category.id, category]))
const allPinnedPaths = new Set(
Object.values(sectionConfig).flatMap((config) => (config.pinned || []).map(getPinnedPath))
)
// Sections follow sectionConfig key order. New sidebar categories not in sectionConfig are appended at the end.
const configIds = Object.keys(sectionConfig).filter((id) => !specialIds.has(id))
const newIds = sidebarCategories
.filter((category) => !specialIds.has(category.id) && !sectionConfig[category.id])
.map((category) => category.id)
return [...configIds, ...newIds]
.filter((id) => sidebarMap.has(id))
.map((id) => {
const config = sectionConfig[id]
const title = config?.title || sidebarMap.get(id)!.title
const description = config?.description || ""
const pinned = buildPinnedTutorials(config?.pinned || [], allTutorials)
const remaining = allTutorials
.filter((t) => t.category === id && !allPinnedPaths.has(t.path))
.map((t) => toTutorial(t))
return { id, title, description, tutorials: [...pinned, ...remaining], showFooter: config?.showFooter }
})
.filter((section) => section.tutorials.length > 0)
}

View File

@@ -13,7 +13,6 @@ You may want to cancel an incoming Check if you do not want it. You might cancel
## Prerequisites
- You should be familiar with the basics of using the [xrpl.js client library](../get-started/get-started-javascript.md).
- You need an XRP Ledger account including its secret key. (You can get one on Testnet for free.) See also: [XRP Faucets](/resources/dev-tools/xrp-faucets).
- You need the ID of a Check ledger entry that you are either the sender or recipient of. See also: [Send a Check](./send-a-check.md).
## Source Code
@@ -34,21 +33,25 @@ Figure out the values of the [CheckCancel transaction][] fields. The following f
| `Account` | String (Address) | The address of the sender who is canceling the Check. (In other words, your address.) |
| `CheckID` | String | The ID of the Check entry to cancel. You can get this information when you [send a check](./send-a-check.md), or by [looking up checks](./look-up-checks.md). |
For example:
This example uses a preconfigured account and check from the `checks-setup.js` script, but you can replace `wallet` and `checkID` with your own values.
{% code-snippet file="/_code-samples/checks/js/cancel-check.js" from="// Prepare" before="// Submit" /%}
{% code-snippet file="/_code-samples/checks/js/cancel-check.js" language="js" from="// Load setup data" before="// Connect to Testnet" /%}
Then, use the loaded values to fill out the transaction:
{% code-snippet file="/_code-samples/checks/js/cancel-check.js" language="js" from="// Prepare the transaction" before="// Submit the transaction" /%}
### 2. Submit the CheckCancel transaction
Submit the CheckCancel transaction in the usual way and wait for it to be validated. If the result code is `tesSUCCESS` and the transaction is in a validated ledger, the transaction is successful. For example:
{% code-snippet file="/_code-samples/checks/js/cancel-check.js" from="// Submit" before="// Confirm" /%}
{% code-snippet file="/_code-samples/checks/js/cancel-check.js" language="js" from="// Submit the transaction" before="// Confirm transaction result" /%}
## 3. Confirm transaction result
### 3. Confirm transaction result
If the transaction succeeded, it should have a `"TransactionResult": "tesSUCCESS"` field in the metadata, and the field `"validated": true` in the result, indicating that this result is final. For example:
If the transaction succeeds, the code prints the cancelled check's details. For example:
{% code-snippet file="/_code-samples/checks/js/cancel-check.js" from="// Confirm" before="// Disconnect" /%}
{% code-snippet file="/_code-samples/checks/js/cancel-check.js" language="js" from="// Confirm transaction result" before="// Disconnect" /%}
{% admonition type="success" name="Tip" %}The `submitAndWait()` method in xrpl.js only returns when the transaction's result is final, so you can assume that the transaction is validated if it returns a result code of `tesSUCCESS`.{% /admonition %}

View File

@@ -8,13 +8,12 @@ labels:
This tutorial shows how to cash a [Check](/docs/concepts/payment-types/checks.md) for a flexible amount. As long as the Check is not expired, the specified recipient can cash it to receive the maximum amount available. You would cash a Check this way if you want to receive as much as possible. When doing this, you set a minimum amount to receive in case the sender does not have enough money to pay the full amount. If the check cannot deliver at least the minimum amount, cashing the check fails but you can try again later.
You can also [cash a check for an exact amount](cash-a-check-for-a-flexible-amount.md).
You can also [cash a check for an exact amount](cash-a-check-for-an-exact-amount.md).
## Prerequisites
- You should be familiar with the basics of using the [xrpl.js client library](../get-started/get-started-javascript.md).
- You need an XRP Ledger account including its secret key. (You can get one on Testnet for free.) See also: [XRP Faucets](/resources/dev-tools/xrp-faucets).
- You need the ID of a Check ledger entry that you are the recipient of. See also: [Send a Check](./send-a-check.md) and [Look Up Checks](./look-up-checks.md).
@@ -37,11 +36,11 @@ Figure out the values of the [CheckCash transaction][] fields. To cash a check f
| `CheckID` | String | The ID of the Check to cash. You can get this information from the person who sent you the Check, or by [looking up checks](./look-up-checks.md) where your account is the destination. |
| `DeliverMin` | [Currency Amount][] | A minimum amount to receive from the Check. If you cannot receive at least this much, cashing the Check fails, leaving the Check in the ledger so you can try again. For XRP, this must be a string specifying drops of XRP. For tokens, this is an object with `currency`, `issuer`, and `value` fields. The `currency` and `issuer` fields must match the corresponding fields in the Check object, and the `value` must be less than or equal to the amount in the Check object. For more information on specifying currency amounts, see [Specifying Currency Amounts][]. |
In the sample code, these values are hard-coded, so you should edit them to match your case:
This example uses a preconfigured account and check from the `checks-setup.js` script, but you can replace `wallet` and `checkID` with your own values.
{% code-snippet file="/_code-samples/checks/js/cash-check-flexible.js" language="js" from="// Define parameters" before="async function main()" /%}
{% code-snippet file="/_code-samples/checks/js/cash-check-flexible.js" language="js" from="// Load setup data" before="// Connect to Testnet" /%}
Then, you use these parameters to fill out the transaction. For example:
Then, use the loaded values to fill out the transaction:
{% code-snippet file="/_code-samples/checks/js/cash-check-flexible.js" language="js" from="// Prepare the transaction" before="// Submit the transaction" /%}
@@ -50,10 +49,10 @@ Then, you use these parameters to fill out the transaction. For example:
Send the transaction and wait for it to be validated by the consensus process, as normal:
{% code-snippet file="/_code-samples/checks/js/cash-check-flexible.js" from="// Submit" before="// Confirm" /%}
{% code-snippet file="/_code-samples/checks/js/cash-check-flexible.js" language="js" from="// Submit the transaction" before="// Confirm transaction result" /%}
## 3. Confirm final result
### 3. Confirm transaction result
If the transaction succeeded, it should have a `"TransactionResult": "tesSUCCESS"` field in the metadata, and the field `"validated": true` in the result, indicating that this result is final.
@@ -61,7 +60,7 @@ If the transaction succeeded, it should have a `"TransactionResult": "tesSUCCESS
If the transaction suceeded, you can assume that it delivered at least the `DeliverMin` amount of this transaction and at most the `SendMax` of the Check. To confirm the exact balance changes that occurred as a result of cashing the check, including how much was actually debited and credited, you must look at the transaction metadata. The `xrpl.getBalanceChanges()` function can help to summarize this. For example:
{% code-snippet file="/_code-samples/checks/js/cash-check-flexible.js" from="// Confirm transaction results" before="// Disconnect" /%}
{% code-snippet file="/_code-samples/checks/js/cash-check-flexible.js" language="js" from="// Confirm transaction result" before="// Disconnect" /%}
{% admonition type="info" name="Note" %}
The metadata shows the net balance changes as the result of all of the transactions effects, which may be surprising in some cases. If an account receives exactly the same amount of XRP as it burns, its balance stays the same so it does not even appear in the list of balance changes.
@@ -73,7 +72,7 @@ If you are not using `getBalanceChanges()`, the following guidelines should help
For example, the following `ModifiedNode` shows that the account `rGPnRH1EBpHeTF2QG8DCAgM7z5pb75LAis`, the Check's recipient and the sender of this CheckCash transaction, had its XRP balance change from `9999999970` drops to `10099999960` drops, meaning the recipient was credited a _net_ of 99.99999 XRP as a result of processing the transaction.
```
```json
{
"ModifiedNode": {
"FinalFields": {
@@ -97,7 +96,7 @@ If you are not using `getBalanceChanges()`, the following guidelines should help
The net amount of 99.99999 XRP includes deducting the transaction cost that is destroyed to pay for sending this CheckCash transaction. The following part of the transaction instructions shows that the transaction cost (the `Fee` field) was 10 drops of XRP. By adding this to the net balance change, we conclude that the recipient, `rGPnRH1EBpHeTF2QG8DCAgM7z5pb75LAis`, was credited a _gross_ amount of exactly 100 XRP for cashing the Check.
```
```json
"Account" : "rGPnRH1EBpHeTF2QG8DCAgM7z5pb75LAis",
"TransactionType" : "CheckCash",
"DeliverMin" : "95000000",

View File

@@ -13,7 +13,6 @@ You can also [cash a check for a flexible amount](./cash-a-check-for-a-flexible-
## Prerequisites
- You should be familiar with the basics of using the [xrpl.js client library](../get-started/get-started-javascript.md).
- You need an XRP Ledger account including its secret key. (You can get one on Testnet for free.) See also: [XRP Faucets](/resources/dev-tools/xrp-faucets).
- You need the ID of a Check ledger entry that you are the recipient of. See also: [Send a Check](./send-a-check.md) and [Look Up Checks](./look-up-checks.md).
## Source Code
@@ -23,9 +22,12 @@ The complete source code for this tutorial is available in the source repository
{% repo-link path="_code-samples/checks/js/" %}Checks sample code{% /repo-link %}
## Steps
Before running these scripts, run `checks-setup.js` once to generate the test wallets and checks used throughout the tutorials.
### 1. Prepare the CheckCash transaction
Figure out the values of the [CheckCash transaction][] fields. You also need to create a `Wallet` instance for your account's key pair. To cash a check for an exact amount, the following fields are the bare minimum; everything else is either optional or can be [auto-filled](../../references/protocol/transactions/common-fields.md#auto-fillable-fields) when signing:
Figure out the values of the [CheckCash transaction][] fields. To cash a check for an exact amount, the following fields are the bare minimum; everything else is either optional or can be [auto-filled](../../references/protocol/transactions/common-fields.md#auto-fillable-fields) when signing:
| Field | Value | Description |
|:------------------|:---------------------|:-----------------------------|
@@ -34,20 +36,19 @@ Figure out the values of the [CheckCash transaction][] fields. You also need to
| `CheckID` | String | The ID of the Check to cash. You can get this information from the person who sent you the Check, or by [looking up checks](./look-up-checks.md) where your account is the destination. |
| `Amount` | [Currency Amount][] | The amount to receive. The type of currency (token or XRP) must match the Check object. The quantity in the `value` field must be less than or equal to the amount in the Check object. (For currencies with transfer fees, you must cash the Check for less than its `SendMax` so the transfer fee can be paid by the `SendMax`.) For more information on specifying currency amounts, see [Specifying Currency Amounts][]. |
In the sample code, these values are hard-coded, so you should edit them to match your case:
This example uses a preconfigured account and check from the `checks-setup.js` script, but you can replace `wallet` and `checkID` with your own values.
{% code-snippet file="/_code-samples/checks/js/cash-check-exact.js" language="js" from="// Define parameters" before="async function main()" /%}
{% code-snippet file="/_code-samples/checks/js/cash-check-exact.js" language="js" from="// Load setup data" before="// Connect to Testnet" /%}
Then, you use these parameters to fill out the transaction. For example:
Then, use the loaded values to fill out the transaction:
{% code-snippet file="/_code-samples/checks/js/cash-check-exact.js" language="js" from="// Prepare the transaction" before="// Submit the transaction" /%}
### 2. Submit the transaction
Send the transaction and wait for it to be validated by the consensus process, as normal:
{% code-snippet file="/_code-samples/checks/js/cash-check-exact.js" from="// Submit" before="// Confirm" /%}
{% code-snippet file="/_code-samples/checks/js/cash-check-exact.js" language="js" from="// Submit the transaction" before="// Confirm transaction result" /%}
### 3. Confirm transaction result
@@ -58,7 +59,7 @@ If the transaction succeeded, it should have a `"TransactionResult": "tesSUCCESS
You can look at the transaction metadata to confirm the balance changes that occurred as a result of delivering the exact amount. The `xrpl.getBalanceChanges()` function can help to summarize this. For example:
{% code-snippet file="/_code-samples/checks/js/cash-check-exact.js" from="// Confirm transaction results" before="// Disconnect" /%}
{% code-snippet file="/_code-samples/checks/js/cash-check-exact.js" language="js" from="// Confirm transaction result" before="// Disconnect" /%}
Example balance changes output:

View File

@@ -25,14 +25,14 @@ The complete source code for this tutorial is available in the source repository
To get a list of all incoming and outgoing Checks for an account, use the `account_objects` command and set the `type` field of the request to `checks`. You may need to make multiple requests if the result is [paginated](../../references/http-websocket-apis/api-conventions/markers-and-pagination.md).
{% code-snippet file="/_code-samples/checks/js/get-checks.js" from="// Loop through account objects" before="// Filter results" /%}
{% code-snippet file="/_code-samples/checks/js/get-checks.js" language="js" from="// Loop through account objects until marker is undefined" before="// Filter results by recipient" /%}
### 2. Filter the responses by recipient
The response may include Checks where the account from the request is the sender or the recipient. Each member of the `account_objects` array of the response represents one Check. For each such Check object, the address in the `Destination` is address of that Check's recipient, such as in the following code:
{% code-snippet file="/_code-samples/checks/js/get-checks.js" from="// Filter results" before="// Disconnect" /%}
{% code-snippet file="/_code-samples/checks/js/get-checks.js" language="js" from="// Filter results by recipient" before="// Disconnect" /%}
To filter by sender, check the address in the `Account` field of the Check instead.

View File

@@ -49,16 +49,18 @@ For example, imagine you were asked to pay a company named Grand Payments for so
Send the transaction and wait for it to be validated by the consensus process, as normal:
{% code-snippet file="/_code-samples/checks/js/create-check.js" language="js" from="// Submit the transaction" before="// Get transaction result" /%}
{% code-snippet file="/_code-samples/checks/js/create-check.js" language="js" from="// Submit the transaction" before="// Confirm transaction result and get check ID" /%}
### 3. Confirm transaction result
If the transaction succeeded, it should have a `"TransactionResult": "tesSUCCESS"` field in the metadata, and the field `"validated": true` in the result, indicating that this result is final.
{% code-snippet file="/_code-samples/checks/js/create-check.js" language="js" from="// Confirm transaction result and get check ID" before="// Disconnect" /%}
{% admonition type="success" name="Tip" %}The `submitAndWait()` method in xrpl.js only returns when the transaction's result is final, so you can assume that the transaction is validated if it returns a result code of `tesSUCCESS`.{% /admonition %}
To cash or cancel the Check later, you'll need the Check ID. You can find this in the transaction's metadata by looking for a `CreatedNode` entry with a `LedgerEntryType` of `"Check"`. This indicates that the transaction created a [Check ledger entry](../../references/protocol/ledger-data/ledger-entry-types/check.md). The `LedgerIndex` of this object is the ID of the Check. This should be a [hash][] value such as `84C61BE9B39B2C4A2267F67504404F1EC76678806C1B901EA781D1E3B4CE0CD9`.
To cash or cancel the Check later, you'll need the Check ID printed above.
At this point, it is up to the recipient to cash the Check.

View File

@@ -0,0 +1,63 @@
---
seo:
description: Accelerate development on the XRPL with AI tools.
---
# AI Tools
Augment your AI with additional tools to accelerate development, automate integrations, and deploy secure solutions on the XRP Ledger.
## Model Context Protocol (MCP) Servers
AI models are limited by their training data, which will always be out-of-date with the most recent XRPL features and SDK improvements. MCP servers solve this by giving your AI real-time access to current documentation through a standardized interface. Instead of relying on potentially outdated training data, your AI can query an MCP server for accurate, up-to-date context. Below is a list of available MCP servers:
### Context7
[Context7](https://context7.com/) is a searchable database of user-submitted repos and websites. Official XRPL docs are maintained on the site and include:
- [xrpl.org](https://xrpl.org/docs): The official developer portal for up-to-date XRPL docs, including use cases, concepts, tutorials, references, and code samples.
- [opensource.ripple.com](https://opensource.ripple.com/): The technical doc site for all features in development by Ripple.
- [docs.xrplevm.org](https://docs.xrplevm.org/): The technical doc site for the XRPL EVM Sidechain.
- [XRPL JavaScript SDK](https://github.com/xrplf/xrpl.js)
- [XRPL Python SDK](https://github.com/xrplf/xrpl-py)
- [XRPL Go SDK](https://github.com/xrplf/xrpl-go)
To set up Context7, see: [Installation](https://github.com/upstash/context7?tab=readme-ov-file#installation).
### xrpl.org MCP Server
The xrpl.org site hosts a standalone MCP server, which only contains documentation hosted on the site. From any doc page, you can click the **Copy** dropdown by the page title and select either **Connect to Cursor** or **Connect to VS Code**. If you are using a different code editor, you can manually configure the MCP server using the `https://xrpl.org/mcp` endpoint.
## SKILL.md
A `SKILL.md` file provides behavioral instructions for AI models working with XRPL code. It defines specific steps and rules to produce more precise outcomes, such as forming transactions, implementing security best practices, or issuing tokens. By loading a skill in your coding agent, you reduce the need for verbose or repeated queries.
### XRPL Development Skill for Claude Code
A comprehensive Claude Code skill for modern XRP Ledger development, provided by XRPL Commons. This skill uses Claude Code's progressive disclosure pattern; the main `SKILL.md` provides core guidance, and Claude reads specialized markdown files only when needed for specific tasks. For a full list of available skills, as well as installation instructions, check the [GitHub repo](https://github.com/XRPL-Commons/xrpl-dev-skills?tab=readme-ov-file#xrpl-development-skill-for-claude-code).
### Generate Release Notes
The `generate-release-notes` skill generates a draft release notes to publish to the xrpl.org blog. This skill is intended for contributors to the XRPL docs. For additional details, check the [GitHub repo](https://github.com/XRPLF/xrpl-dev-portal/blob/master/.claude/skills/generate-release-notes/SKILL.md).
## Site Optimizations
The XRPL developer portal has been updated with AI optimizations to serve both human and AI audiences.
### AI Chatbot
An AI chatbot is hosted on [xrpl.org](https://xrpl.org/docs), [opensource.ripple.com](https://opensource.ripple.com/), and [docs.xrplevm.org](https://docs.xrplevm.org/) for users who prefer a more natural-language approach to documentation. You can access the chatbots from the **Ask AI** button at the bottom right of each website, or from the **Search with AI** button embedded in the search bar.
### llms.txt
The site hosts an [llms.txt](https://xrpl.org/llms.txt) file at the root directory, providing a curated index of content for AI crawlers and tools to find relevant information quickly. If you don't want to use the MCP servers, you can provide this file to your AI as context.
### Context Optimization
Markdown is an efficient format for providing documentation context to an AI. Every documentation page on the site hosts an `.md` version, which is accessible from the **Copy** dropdown at the top of the page. The dropdown includes options to:
- Copy the contents of the `.md` file.
- View the page as an `.md` file.
- Open **Claude** or **ChatGPT** with the contents of the page automatically loaded as context.
- Set up the xrpl.org MCP server in **VS Code** or **Cursor** with one click.

View File

@@ -297,7 +297,6 @@
expanded: false
items:
- page: docs/tutorials/compliance-features/require-destination-tags.md
- page: docs/tutorials/compliance-features/manage-credentials.md
- page: docs/tutorials/compliance-features/verify-credentials.md
- page: docs/tutorials/compliance-features/create-permissioned-domains-in-javascript.md
- group: Programmability
@@ -752,6 +751,7 @@
page: resources/dev-tools/index.page.tsx
expanded: false
items:
- page: resources/dev-tools/ai-tools.md
- label: RPC Tool
labelTranslationKey: sidebar.resources.dev-tools.rpc-tool
page: resources/dev-tools/rpc-tool.page.tsx

File diff suppressed because one or more lines are too long

View File

@@ -320,128 +320,3 @@ main article .card-grid {
margin-bottom: 0.25rem;
margin-top: 0.5rem;
}
/* Tutorial cards */
.page-tutorials .tutorial-cards {
> div {
display: flex;
}
.card {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
min-height: 280px;
transition: all 0.35s ease-out;
cursor: pointer;
&:hover {
transform: translateY(-16px);
}
.card-header {
border-bottom: none;
background: transparent;
padding: 1.5rem 1.5rem 0 2rem;
}
.circled-logo {
margin-left: -10px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.card-body {
padding: 1rem 1.5rem;
flex: 1;
}
.card-title {
font-weight: 700;
margin-bottom: 1rem;
}
.card-text {
font-size: 1rem;
margin-bottom: 1.5rem;
}
.card-footer {
background-color: transparent;
border-top: none;
padding: 0;
height: 40px;
background-size: cover;
background-position: bottom;
background-repeat: no-repeat;
margin-top: auto;
}
}
// Apply colored footers to each card
> div:nth-child(1) .card .card-footer {
background-image: url("../img/cards/3-col-pink.svg");
}
> div:nth-child(2) .card .card-footer {
background-image: url("../img/cards/3col-blue-light-blue.svg");
}
> div:nth-child(3) .card .card-footer {
background-image: url("../img/cards/3-col-light-blue.svg");
}
> div:nth-child(4) .card .card-footer {
background-image: url("../img/cards/3col-blue-green.svg");
}
> div:nth-child(5) .card .card-footer {
background-image: url("../img/cards/3col-magenta.svg");
}
> div:nth-child(6) .card .card-footer {
background-image: url("../img/cards/3-col-orange.svg");
}
}
.light .page-tutorials .tutorial-cards .circled-logo img[alt="HTTP / WebSocket"] {
filter: invert(1);
}
// TOC buttons for tutorials page
.page-tutorials .page-toc.no-sideline {
border-left: none;
gap: 0.75rem;
li {
a {
border-radius: 100px;
padding: 0.5rem 1rem;
margin: 0;
background-color: $gray-800;
color: $gray-200;
font-weight: 500;
transition: all 0.2s ease;
&:hover,
&:focus,
&:active {
background-color: $blue-purple-500;
color: $white;
border-left: none;
margin-left: 0;
}
}
}
}
.light .page-tutorials .page-toc.no-sideline {
li a {
background-color: $gray-200;
color: $gray-800;
&:hover,
&:focus,
&:active {
background-color: $blue-purple-500;
color: $white;
}
}
}

489
styles/_tutorials.scss Normal file
View File

@@ -0,0 +1,489 @@
// Tutorials landing page styles
// Card footer gradient images
$card-footers: (
"3-col-pink",
"3col-blue-light-blue",
"3-col-light-blue",
"3col-blue-green",
"3col-magenta",
"3-col-orange"
);
$whats-new-footers: (
"3col-green-purple",
"3col-purple-blue-green",
"3col-green-blue"
);
// Tutorial cards
.page-tutorials .tutorial-cards {
> div {
display: flex;
}
.card {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
min-height: 300px;
transition: all 0.35s ease-out;
cursor: pointer;
&:hover {
transform: translateY(-16px);
}
.card-header {
border-bottom: none;
background: transparent;
padding: 1.5rem 1.5rem 0 2rem;
}
.circled-logo {
margin-left: -10px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.card-body {
padding: 1rem 1.5rem;
flex: 1;
}
.card-title {
font-weight: 700;
margin-bottom: 1rem;
}
.card-text {
font-size: 1rem;
margin-bottom: 1.5rem;
}
.card-footer {
background-color: transparent;
border-top: none;
padding: 0;
height: 40px;
background-size: cover;
background-position: bottom;
background-repeat: no-repeat;
margin-top: auto;
}
}
// Apply colored footers to each card
@for $i from 1 through length($card-footers) {
> div:nth-child(#{$i}) .card .card-footer {
background-image: url("../img/cards/#{nth($card-footers, $i)}.svg");
}
}
}
// Light mode: invert HTTP/WebSocket icon
.light .page-tutorials .tutorial-cards .circled-logo img[alt="HTTP / WebSocket"] {
filter: invert(1);
}
// Contribution Card - community contribution with meta links
.page-tutorials .tutorial-cards .contribution-card {
.contribution-header {
display: flex;
align-items: center;
gap: 0.6rem;
}
.contribution-icon {
background: rgba($blue-purple-500, 0.15);
color: $blue-purple-300;
font-size: 26px;
}
.card-external-icon {
font-size: 0.85rem;
margin-left: 0.35rem;
color: $gray-500;
vertical-align: middle;
}
.card-meta-row {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.8rem;
flex-wrap: wrap;
margin-left: auto;
margin-top: -4px;
.meta-link {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: $gray-300;
text-decoration: none;
transition: color 0.15s ease;
.fa {
font-size: 0.8rem;
}
&:hover {
color: $blue-purple-300;
}
}
.meta-dot {
color: $gray-500;
font-weight: bold;
user-select: none;
}
}
}
// Light mode: Contribution Card
.light .page-tutorials .tutorial-cards .contribution-card {
.contribution-icon {
background: rgba($blue-purple-500, 0.1);
color: $blue-purple-600;
}
.card-meta-row {
.meta-link {
color: $gray-600;
&:hover {
color: $blue-purple-600;
}
}
.meta-dot {
color: $gray-500;
}
}
}
// Tutorial category section spacing
.page-tutorials .category-section + .category-section {
margin-top: 2rem;
}
// Explore more link
.page-tutorials .explore-more-wrapper {
margin-top: -1.5rem;
margin-bottom: 1.5rem;
.explore-more-link {
background: none;
border: none;
padding: 0;
cursor: pointer;
font-family: inherit;
font-size: 1.05rem;
color: $blue-purple-300;
&:hover {
text-decoration: underline;
}
}
}
.light .page-tutorials .explore-more-wrapper .explore-more-link {
color: $blue-purple-600;
}
// TOC navigation buttons
.page-tutorials .page-toc.no-sideline {
border-left: none;
gap: 0.75rem;
li {
a {
border-radius: 100px;
padding: 0.5rem 1rem;
margin: 0;
background-color: $gray-800;
color: $gray-200;
font-weight: 500;
transition: all 0.2s ease;
&:hover,
&:focus,
&:active {
background-color: $blue-purple-500;
color: $white;
border-left: none;
margin-left: 0;
}
}
}
}
// Light mode: TOC buttons
.light .page-tutorials .page-toc.no-sideline {
li a {
background-color: $gray-200;
color: $gray-800;
&:hover,
&:focus,
&:active {
background-color: $blue-purple-500;
color: $white;
}
}
}
// What's New section
.whats-new-section {
// Gradient underline on section title
h3 {
display: inline-block;
position: relative;
padding-bottom: 8px;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 60px;
height: 3px;
background: linear-gradient(90deg, $blue-purple-500, $green-400);
border-radius: 2px;
}
}
// Different footer colors for What's New cards
.tutorial-cards {
@for $i from 1 through length($whats-new-footers) {
> div:nth-child(#{$i}) .card .card-footer {
background-image: url("../img/cards/#{nth($whats-new-footers, $i)}.svg");
}
}
}
}
// Quick Reference Card
.page-tutorials .quick-ref-card {
background: rgba($gray-800, 0.7);
border: 1px solid $gray-700;
border-left: 3px solid $blue-purple-500;
border-radius: 12px;
padding: 1rem 1.5rem;
backdrop-filter: blur(8px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
margin-left: auto;
max-width: 480px;
@media (max-width: 991px) {
margin-left: 0;
max-width: 100%;
}
.quick-ref-section {
padding: 0.25rem 0;
}
.quick-ref-label {
display: block;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.08em;
color: $blue-purple-300;
margin-bottom: 0.75rem;
}
.quick-ref-group {
margin-bottom: 0.75rem;
&:last-of-type {
margin-bottom: 0.5rem;
}
}
.quick-ref-key {
display: block;
font-size: 0.85rem;
color: $gray-300;
margin-bottom: 0.35rem;
strong {
font-weight: 700;
color: $white;
}
}
.quick-ref-urls {
display: grid;
grid-template-columns: auto auto;
gap: 0.25rem 0.75rem;
align-items: center;
width: fit-content;
max-width: 100%;
@media (max-width: 576px) {
grid-template-columns: 1fr;
gap: 0.2rem;
}
}
.quick-ref-protocol {
font-size: 0.7rem;
font-weight: 500;
color: $gray-500;
letter-spacing: 0.02em;
}
.quick-ref-value {
font-size: 0.75rem;
color: $blue-purple-300;
background: rgba($gray-900, 0.5);
padding: 0.2rem 0.5rem;
border-radius: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.quick-ref-value-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: none;
border: none;
padding: 0;
cursor: pointer;
transition: opacity 0.2s ease;
max-width: 100%;
overflow: hidden;
&:hover {
opacity: 0.8;
.quick-ref-value {
background: rgba($gray-800, 0.8);
}
}
&.copied .quick-ref-value {
background: rgba($green-500, 0.2);
color: $green-400;
}
.copy-icon {
font-size: 0.7rem;
color: $green-400;
min-width: 0.8rem;
flex-shrink: 0;
}
@media (max-width: 576px) {
margin-bottom: 0.5rem;
}
}
.quick-ref-link {
display: inline-block;
font-size: 0.8rem;
color: $blue-purple-300;
margin-top: 0.35rem;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.quick-ref-divider {
height: 1px;
background: $gray-700;
margin: 0.5rem 0;
}
.quick-ref-faucet {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
font-weight: 500;
color: $white;
text-decoration: none;
padding: 0.25rem 0;
&:hover {
color: $blue-purple-300;
}
.quick-ref-arrow {
color: $blue-purple-400;
}
}
}
// Light mode: Quick Reference Card
.light .page-tutorials .quick-ref-card {
background: rgba($white, 0.95);
border-color: $gray-300;
border-left: 3px solid $blue-purple-500;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
.quick-ref-label {
color: $blue-purple-600;
}
.quick-ref-key {
color: $gray-700;
strong {
color: $gray-900;
}
}
.quick-ref-protocol {
color: $gray-500;
}
.quick-ref-value {
color: $blue-purple-600;
background: rgba($gray-300, 0.6);
}
.quick-ref-value-btn {
&:hover .quick-ref-value {
background: rgba($gray-400, 0.6);
}
&.copied .quick-ref-value {
background: rgba($green-500, 0.15);
color: $green-700;
}
.copy-icon {
color: $green-600;
}
}
.quick-ref-link {
color: $blue-purple-600;
}
.quick-ref-divider {
background: $gray-200;
}
.quick-ref-faucet {
color: $gray-900;
&:hover {
color: $blue-purple-600;
}
.quick-ref-arrow {
color: $blue-purple-500;
}
}
}

View File

@@ -69,6 +69,7 @@ $line-height-base: 1.5;
@import "_pages.scss";
@import "_rpc-tool.scss";
@import "_blog.scss";
@import "_tutorials.scss";
@import "_feedback.scss";
@import "_video.scss";
@import "_contribute.scss";