Compare commits

..

4 Commits

Author SHA1 Message Date
mDuo13
7e4400cf70 Faucet: improve handling of saved address, display of modes 2026-04-22 17:37:00 -07:00
mDuo13
b80897f2b1 Re-gen CSS 2026-04-21 17:56:54 -07:00
mDuo13
21acd78467 Improve XRP faucet w/ custom amount & address 2026-04-21 17:56:42 -07:00
Maria Shodunke
d945d6a5d6 Tutorials landing page v2 (#3572) 2026-04-17 11:52:23 +01:00
21 changed files with 1467 additions and 1099 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

@@ -0,0 +1,28 @@
import { useRef, useState } from "react"
// Copyable URL component with click-to-copy functionality
export default 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>
)
}

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
export interface XRPLoaderProps {
message?: string
show: boolean
show?: boolean
}
export default function XRPLoader(props: XRPLoaderProps) {

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,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

@@ -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,53 @@
import { useThemeHooks } from "@redocly/theme/core/hooks"
import { Link } from "@redocly/theme/components/Link/Link"
import { useRef, useState } from "react"
import CopyableUrl from "../../@theme/components/CopyableUrl"
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 +67,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 +163,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 +179,193 @@ 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>
)
}
// 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 +373,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

@@ -24,14 +24,6 @@
"shortName": "Xahau-Testnet",
"desc": "Hooks (L1 smart contracts) enabled Xahau testnet."
},
{
"id": "faucet-select-batch-devnet",
"wsUrl": "wss://batch.nerdnest.xyz",
"jsonRpcUrl": "https://batch.rpc.nerdnest.xyz",
"faucetHost": "batch.faucet.nerdnest.xyz",
"shortName": "Batch-Devnet",
"desc": "Preview of XLS-56d Batch transactions."
},
{
"id": "faucet-select-lending-devnet",
"wsUrl": "wss://lend.devnet.rippletest.net:51233/",
@@ -39,6 +31,14 @@
"faucetHost": "lend-faucet.devnet.rippletest.net",
"shortName": "Lending-Devnet",
"desc": "Preview of XLS-66d Lending Protocol."
},
{
"id": "faucet-select-wasm",
"wsUrl": "wss://wasm.devnet.rippletest.net:51233",
"jsonRpcUrl": "http://wasm.devnet.rippletest.net:51234",
"faucetHost": "wasmfaucet.devnet.rippletest.net",
"shortName": "WASM Devnet",
"desc": "Preview of XLS-100d Smart Escrows and other WASM-based programmability."
}
]
}

View File

@@ -1,9 +1,10 @@
import { Link } from "@redocly/theme/components/Link/Link";
import { useThemeHooks } from '@redocly/theme/core/hooks';
import * as React from 'react';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Client, dropsToXrp, Wallet } from 'xrpl';
import XRPLoader from '../../@theme/components/XRPLoader';
import CopyableUrl from "../../@theme/components/CopyableUrl"
import * as faucetData from './faucets.json';
export const frontmatter = {
@@ -47,25 +48,30 @@ function FaucetEndpoints({ faucet, givenKey } : { faucet: FaucetInfo, givenKey:
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
return (<div key={givenKey}>
<h4>{faucet.shortName} {translate(`Servers`)}</h4>
<pre>
<code>
// WebSocket<br/>
{faucet.wsUrl}<br/>
<br/>
// JSON-RPC<br/>
{faucet.jsonRpcUrl}
</code>
</pre>
</div>)
return (
<div className="quick-ref-group">
<span className="quick-ref-key"><strong>{faucet.shortName}</strong></span>
<div className="quick-ref-urls">
<span className="quick-ref-protocol">{translate("WebSocket")}</span>
<CopyableUrl url={faucet.wsUrl} translate={translate} />
<span className="quick-ref-protocol">{translate("JSON-RPC")}</span>
<CopyableUrl url={faucet.jsonRpcUrl} translate={translate} />
</div>
</div>
)
}
function FaucetSidebar({ faucets }: { faucets: FaucetInfo[] }): React.JSX.Element {
return (<aside className="right-sidebar col-lg-6 order-lg-4" role="complementary">
{faucets.map(
(faucet) => <FaucetEndpoints faucet={faucet} key={faucet.shortName + " Endpoints"} givenKey={faucet.shortName + " Endpoints"}/>
)}
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
return (<aside className="right-sidebar col-lg-6 order-4" role="complementary">
<div className="quick-ref-card">
<h4 className="quick-ref-label">{translate("Public Servers")}</h4>
{faucets.map(
(faucet) => <FaucetEndpoints faucet={faucet} key={faucet.shortName + " Endpoints"} givenKey={faucet.shortName + " Endpoints"}/>
)}
</div>
</aside>)
}
@@ -76,29 +82,26 @@ export default function XRPFaucets(): React.JSX.Element {
const faucets: FaucetInfo[] = faucetData.knownFaucets
const [selectedFaucet, setSelectedFaucet] = useState(faucets[0])
const [selectedMode, setSelectedMode] = useState(FaucetMode.generate)
const [requestedAmount, setRequestedAmount] = useState('10')
const [refillAddress, setRefillAddress] = useState('')
useEffect( () => {
setRefillAddress(localStorage.getItem('faucet-saved-address') || "")
}, [])
return (
<div className="container-fluid" role="document" id="main_content_wrapper">
<div className="container-fluid page-faucet" role="document" id="main_content_wrapper">
<div className="row">
<FaucetSidebar faucets={faucets}/>
<main className="main col-md-7 col-lg-6 order-md-3" role="main" id="main_content_body">
<main className="main col-md-7 col-lg-6 order-1" role="main" id="main_content_body">
<section className="container-fluid pt-3 p-md-3">
<h1>{translate("XRP Faucets")}</h1>
<div className="content">
<p>{translate("resources.dev-tool.faucet.content.part1", "These ")}<Link to="../../docs/concepts/networks-and-servers/parallel-networks">{translate("resources.dev-tool.faucet.content.part2", "parallel XRP Ledger test networks")}</Link> {translate("resources.dev-tool.faucet.content.part3", "provide platforms for testing changes to the XRP Ledger and software built on it, without using real funds.")}</p>
<p>{translate("resources.dev-tool.faucet.content.part4", "These funds are intended for")} <strong>{translate("resources.dev-tool.faucet.content.part5", "testing")}</strong> {translate("resources.dev-tool.faucet.content.part6", "only. Test networks' ledger history and balances are reset as necessary. Devnets may be reset without warning.")}</p>
<p>{translate("resources.dev-tool.faucet.content.part7", "All balances and XRP on these networks are separate from Mainnet. As a precaution, do not use the Testnet or Devnet credentials on the Mainnet.")}</p>
<p>
{translate("resources.dev-tool.faucet.content.part8", "The tool below will generate credentials for you and recharge it immediately; if you want to top up an already existing address, you can do it here:")}
{' '}
<a
className="external-link"
href="https://test.xrplexplorer.com/faucet"
target="_blank"
>test.xrplexplorer.com/faucet</a>
</p>
<p>{translate("resources.dev-tool.faucet.content.part7", "All balances and XRP on these networks are separate from Mainnet. As a precaution, do not use the same credentials on Mainnet.")}</p>
<h3>{translate("Choose Network:")}</h3>
<h3>{translate("Choose network:")}</h3>
{ faucets.map((net) => (
<div className="form-check" key={"network-" + net.shortName}>
<input onChange={() => setSelectedFaucet(net)} className="form-check-input" type="radio"
@@ -109,8 +112,39 @@ export default function XRPFaucets(): React.JSX.Element {
</div>
)) }
<h3>{translate("Request how much?")}</h3>
<div className="input-group my-2">
<input type="number" className="form-control" id="request-xrp-amount" value={requestedAmount} min="1" max="10000"
onChange={(e) => setRequestedAmount(e.target.value)} />
<label className="input-group-text" htmlFor="request-xrp-amount">{translate("XRP")}</label>
</div>
<h3>{translate("Fund wallet:")}</h3>
<div className="form-group">
<div className="input-group">
<div className="form-check form-check-inline">
<label className="input-group-text" htmlFor='mode-generate'>
<input onChange={() => setSelectedMode(FaucetMode.generate)} className="form-check-input" type="radio"
name="faucet-mode-selector" id='mode-generate' defaultChecked={true} />
{translate("Generate keys")}</label>
</div>
<div className="form-check form-check-inline mr-0 pr-0 col">
<label className="input-group-text" htmlFor="mode-refill">
<input onChange={() => setSelectedMode(FaucetMode.refill)} className="form-check-input" type="radio"
name="faucet-mode-selector" id='mode-refill' defaultChecked={false} />
{translate("Refill wallet:")}
</label>
<div className="input-group">
<input type="text" className="form-control form-control-inline" id="refill-wallet-id" onChange={(e) => setRefillAddress(e.target.value)}
placeholder="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" value={refillAddress} pattern="^r[A-HJ-NP-Za-km-z1-9]{24,34}$" />
</div>
</div>
</div>
</div>
<br/>
<TestCredentials selectedFaucet={selectedFaucet} translate={translate}/>
<TestCredentials selectedFaucet={selectedFaucet} selectedMode={selectedMode} requestedAmount={requestedAmount} refillAddress={refillAddress} setRefillAddress={setRefillAddress} translate={translate}/>
</div>
</section>
</main>
@@ -119,92 +153,119 @@ export default function XRPFaucets(): React.JSX.Element {
)
}
async function generateFaucetCredentialsAndUpdateUI(
selectedFaucet: FaucetInfo,
setButtonClicked: React.Dispatch<React.SetStateAction<boolean>>,
setGeneratedCredentialsFaucet: React.Dispatch<React.SetStateAction<string>>,
setAddress: React.Dispatch<React.SetStateAction<string>>,
setSecret: React.Dispatch<React.SetStateAction<string>>,
setBalance: React.Dispatch<React.SetStateAction<string>>,
setSequence: React.Dispatch<React.SetStateAction<string>>,
translate: (key: string, options?: string) => string): Promise<void> {
setButtonClicked(true)
// Clear existing credentials
setGeneratedCredentialsFaucet(selectedFaucet.shortName)
setAddress("")
setSecret("")
setBalance("")
setSequence("")
const wallet = Wallet.generate()
const client = new Client(selectedFaucet.wsUrl)
client.apiVersion = 1 // Workaround for networks that don't support APIv2
await client.connect()
try {
setAddress(wallet.address)
setSecret(wallet.seed)
await client.fundWallet(wallet, { faucetHost: selectedFaucet.faucetHost, usageContext: "xrpl.org-faucet" })
const response = await waitForSequence(client, wallet.address)
setSequence(response.sequence)
setBalance(response.balance)
} catch (e) {
alert(`${translate('resources.dev-tools.faucet.error.part1', 'There was an error with the ')}${selectedFaucet.shortName}${translate('resources.dev-tools.faucet.error.part2', ' faucet. Please try again.')}`)
}
setButtonClicked(false)
enum FaucetMode {
generate = "generate",
refill = "refill",
}
function TestCredentials({selectedFaucet, translate}) {
interface FaucetOptions {
selectedFaucet: FaucetInfo,
selectedMode: FaucetMode,
requestedAmount: string,
refillAddress: string,
setRefillAddress: React.Dispatch<React.SetStateAction<string>>,
translate: (key: string, options?: string) => string
}
function TestCredentials({ selectedFaucet, selectedMode, requestedAmount, refillAddress, setRefillAddress, translate }: FaucetOptions) {
const [generatedCredentialsFaucet, setGeneratedCredentialsFaucet] = useState("")
const [address, setAddress] = useState("")
const [secret, setSecret] = useState("")
const [balance, setBalance] = useState("")
const [sequence, setSequence] = useState("")
const [buttonClicked, setButtonClicked] = useState(false)
const [waitingForSeq, setWaitingForSeq] = useState(false)
return (<div>
{/* <XRPLGuard> TODO: Re-add this once we find a good way to avoid browser/server mismatch errors */}
<div className="btn-toolbar" role="toolbar" aria-label="Button">
<button id="generate-creds-button" onClick={
() => generateFaucetCredentialsAndUpdateUI(
selectedFaucet,
setButtonClicked,
setGeneratedCredentialsFaucet,
setAddress,
setSecret,
setBalance,
setSequence,
translate)
} className="btn btn-primary mr-2 mb-2">
{`${translate('resources.dev-tools.faucet.cred-btn.part1', 'Generate ')}${selectedFaucet.shortName}${translate('resources.dev-tools.faucet.cred-btn.part2', ' credentials')}`}
</button>
</div>
{/* </XRPLGuard> */}
const clickGetXrp = async () => {
setButtonClicked(true)
// Clear existing credentials
setGeneratedCredentialsFaucet(selectedFaucet.shortName)
setAddress("")
setSecret("")
setBalance("")
setSequence("")
if (selectedMode === FaucetMode.refill && !refillAddress) {
alert(translate("Please provide an address to refill or choose 'Generate keys'."))
setButtonClicked(false)
return
}
const fundingOpts = {
amount: requestedAmount,
faucetHost: selectedFaucet.faucetHost,
usageContext: "xrpl.org-faucet"
}
console.log(`Funding amount: ${fundingOpts.amount}`)//TODO:remove
if (selectedMode === FaucetMode.generate) {
const wallet = Wallet.generate()
try {
const client = new Client(selectedFaucet.wsUrl)
client.apiVersion = 1 // Workaround for networks that don't support APIv2
await client.connect()
setAddress(wallet.address)
setSecret(wallet.seed)
await client.fundWallet(wallet, fundingOpts)
localStorage.setItem("faucet-saved-address", wallet.address)
setRefillAddress(wallet.address)
setWaitingForSeq(true)
const response = await waitForSequence(client, wallet.address)
setSequence(response.sequence)
setBalance(response.balance)
setWaitingForSeq(false)
} catch (e) {
alert(`${translate('resources.dev-tools.faucet.error.part1', 'There was an error with the ')}${selectedFaucet.shortName}${translate('resources.dev-tools.faucet.error.part2', ' faucet. Please try again.')}`)
console.error(e)
}
} else {
const client = new Client(selectedFaucet.wsUrl)
client.apiVersion = 1 // Workaround for networks that don't support APIv2
await client.connect()
const walletToFund = {classicAddress: refillAddress} // fake Wallet class
try {
setAddress(refillAddress)
await client.fundWallet(walletToFund, fundingOpts)
const response = await waitForSequence(client, refillAddress)
setWaitingForSeq(true)
setSequence(response.sequence)
setBalance(response.balance)
setWaitingForSeq(false)
} catch (e) {
alert(`${translate('resources.dev-tools.faucet.error.part1', 'There was an error with the ')}${selectedFaucet.shortName}${translate('resources.dev-tools.faucet.error.part2', ' faucet. Please try again.')}`)
console.error(e)
}
}
setButtonClicked(false)
}
return (
<div>
<div className="btn-toolbar" role="toolbar" aria-label="Button">
<button id="generate-creds-button" onClick={clickGetXrp} className="btn btn-primary mr-2 mb-2">
{translate('Get Test XRP')}
</button>
</div>
{generatedCredentialsFaucet && <div id="your-credentials">
<h2>{`${translate('resources.dev-tools.faucet.your-cred.part1', 'Your ')}${generatedCredentialsFaucet}${translate('resources.dev-tools.faucet.your-cred.part2', ' Credentials')}`}</h2>
</div>}
{(buttonClicked && address === "") && <XRPLoader message={translate("Generating keys..")}/>}
{address && <div id="address"><h3>{translate("Address")}</h3>{address}</div>}
{secret && <div id="secret"><h3>{translate("Secret")}</h3>{secret}</div>}
{(address && !balance) && (<div>
<br/>
<XRPLoader message={translate("Funding account...")}/>
</div>)}
<XRPLoader message={translate("Waiting for faucet...")} show={buttonClicked} />
{balance && <div id="balance">
<h3>{translate("Balance")}</h3>
@@ -216,7 +277,7 @@ function TestCredentials({selectedFaucet, translate}) {
{sequence}
</div>}
{(secret && !sequence) && <XRPLoader message={translate("Waiting...")}/>}
<XRPLoader message={translate("Waiting for sequence number...")} show={waitingForSeq} />
</div>
)

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

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;
}
}
}

View File

@@ -86,3 +86,14 @@
.form-text a {
text-decoration: underline;
}
.page-faucet {
input:invalid {
box-shadow: inset 0 0 5px 5px $danger;
}
h3 {
font-size: 1.5rem;
line-height: 1;
}
}

492
styles/_tutorials.scss Normal file
View File

@@ -0,0 +1,492 @@
// 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,
.page-faucet .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;
text-transform: uppercase;
}
.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,
.light .page-faucet .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";