mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-04-16 16:52:27 +00:00
Compare commits
32 Commits
add-calcul
...
tutorials-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b872c0600f | ||
|
|
ecfea346c6 | ||
|
|
86c805c540 | ||
|
|
1a92be83b4 | ||
|
|
62507dad3c | ||
|
|
c7098194db | ||
|
|
06b92f44d6 | ||
|
|
31ae9f6a00 | ||
|
|
7f907dd168 | ||
|
|
e45149a6c7 | ||
|
|
1ca0c3371b | ||
|
|
6673e6e0fe | ||
|
|
4e1ea13709 | ||
|
|
61529895af | ||
|
|
f31b3e7ca4 | ||
|
|
6a5ce20028 | ||
|
|
7e6234d9cf | ||
|
|
601e79ed00 | ||
|
|
e05aa8259b | ||
|
|
b058513278 | ||
|
|
b7be1f878e | ||
|
|
ac60d7786b | ||
|
|
394eb4b5d4 | ||
|
|
cd4bb02ae2 | ||
|
|
a309eb51c5 | ||
|
|
578e8cdefc | ||
|
|
c5b161c746 | ||
|
|
f493ca49cf | ||
|
|
5f47643585 | ||
|
|
7036a75881 | ||
|
|
57fde744fd | ||
|
|
91b68bae6a |
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
207
@theme/plugins/tutorial-metadata.js
Normal file
207
@theme/plugins/tutorial-metadata.js
Normal 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 };
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
# Calculate Reserves
|
||||
|
||||
This code sample demonstrates how to look up and calculate an XRP Ledger account's reserve requirements.
|
||||
Look up and calculate an account's reserve requirements.
|
||||
|
||||
See the [Calculate Account Reserves tutorial](https://xrpl.org/docs/tutorials/best-practices/account-management/calculate-reserves) for a detailed walkthrough.
|
||||
|
||||
Samples are provided in JavaScript, Python, and Go.
|
||||
@@ -3,6 +3,7 @@
|
||||
This code sample uses [xrpl-go](https://github.com/Peersyst/xrpl-go) to look up and calculate an XRP Ledger account's reserve requirements.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
go run calculate_reserves.go
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
// Set up client ----------------------
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
// Set up client ----------------------
|
||||
|
||||
import xrpl from 'xrpl'
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
"dependencies": {
|
||||
"xrpl": "^4.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
xrpl-py>=3.0.0
|
||||
xrpl-py>=4.4.0
|
||||
|
||||
184
_code-samples/escrow/go/README.md
Normal file
184
_code-samples/escrow/go/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Escrow (Go)
|
||||
|
||||
This directory contains Go examples demonstrating how to create, finish, and cancel escrows on the XRP Ledger.
|
||||
|
||||
## Setup
|
||||
|
||||
All commands should be run from this `go/` directory.
|
||||
|
||||
Install dependencies before running any examples:
|
||||
|
||||
```sh
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Send Fungible Token Escrow
|
||||
|
||||
```sh
|
||||
go run ./send-fungible-token-escrow
|
||||
```
|
||||
|
||||
The script issues an MPT and Trust Line Token, setting up both to be escrowable. It then creates and finishes a conditional escrow with the MPT and a timed escrow with the Trust Line Token.
|
||||
|
||||
```sh
|
||||
=== Funding Accounts ===
|
||||
|
||||
Funding Issuer account...
|
||||
Funding Escrow Creator account...
|
||||
Issuer: rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD
|
||||
Escrow Creator: rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk
|
||||
|
||||
=== Creating MPT ===
|
||||
|
||||
{
|
||||
"Account": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"Flags": 8,
|
||||
"MaximumAmount": "1000000",
|
||||
"TransactionType": "MPTokenIssuanceCreate"
|
||||
}
|
||||
|
||||
Submitting MPTokenIssuanceCreate transaction...
|
||||
MPT created: 00F7A9BD191FD9BB1D11E217CA5643AED429859BDD40EF8B
|
||||
|
||||
=== Escrow Creator Authorizing MPT ===
|
||||
|
||||
{
|
||||
"Account": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"MPTokenIssuanceID": "00F7A9BD191FD9BB1D11E217CA5643AED429859BDD40EF8B",
|
||||
"TransactionType": "MPTokenAuthorize"
|
||||
}
|
||||
|
||||
Submitting MPTokenAuthorize transaction...
|
||||
Escrow Creator authorized for MPT.
|
||||
|
||||
=== Issuer Sending MPTs to Escrow Creator ===
|
||||
|
||||
{
|
||||
"Account": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00F7A9BD191FD9BB1D11E217CA5643AED429859BDD40EF8B",
|
||||
"value": "5000"
|
||||
},
|
||||
"Destination": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
|
||||
Submitting MPT Payment transaction...
|
||||
Successfully sent 5000 MPTs to Escrow Creator.
|
||||
|
||||
=== Creating Conditional MPT Escrow ===
|
||||
|
||||
Condition: A025802057FDC219A423C4F0DA150941EB529B1D927816FAB394617A0430D1DDB39A3EDB810120
|
||||
Fulfillment: A0228020F16E8A8697ABAE14C60A5D812A2D228F9E6F67B8CA4818DC80BBF539004490DB
|
||||
|
||||
{
|
||||
"Account": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00F7A9BD191FD9BB1D11E217CA5643AED429859BDD40EF8B",
|
||||
"value": "1000"
|
||||
},
|
||||
"CancelAfter": 828559916,
|
||||
"Condition": "A025802057FDC219A423C4F0DA150941EB529B1D927816FAB394617A0430D1DDB39A3EDB810120",
|
||||
"Destination": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"TransactionType": "EscrowCreate"
|
||||
}
|
||||
|
||||
Submitting MPT EscrowCreate transaction...
|
||||
Conditional MPT escrow created. Sequence: 16230848
|
||||
|
||||
=== Finishing Conditional MPT Escrow ===
|
||||
|
||||
{
|
||||
"Account": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"Condition": "A025802057FDC219A423C4F0DA150941EB529B1D927816FAB394617A0430D1DDB39A3EDB810120",
|
||||
"Fulfillment": "A0228020F16E8A8697ABAE14C60A5D812A2D228F9E6F67B8CA4818DC80BBF539004490DB",
|
||||
"OfferSequence": 16230848,
|
||||
"Owner": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"TransactionType": "EscrowFinish"
|
||||
}
|
||||
|
||||
Submitting EscrowFinish transaction...
|
||||
Conditional MPT escrow finished successfully: https://testnet.xrpl.org/transactions/37CD7FECDC71CE70C24927969AD0FDAD55F57F2905A6F62867CB4F5AB2EE27BB
|
||||
|
||||
=== Enabling Trust Line Token Escrows on Issuer ===
|
||||
|
||||
{
|
||||
"Account": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"SetFlag": 17,
|
||||
"TransactionType": "AccountSet"
|
||||
}
|
||||
|
||||
Submitting AccountSet transaction...
|
||||
Trust line token escrows enabled by issuer.
|
||||
|
||||
=== Setting Up Trust Line ===
|
||||
|
||||
{
|
||||
"Account": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"LimitAmount": {
|
||||
"currency": "IOU",
|
||||
"issuer": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"value": "10000000"
|
||||
},
|
||||
"TransactionType": "TrustSet"
|
||||
}
|
||||
|
||||
Submitting TrustSet transaction...
|
||||
Trust line successfully created for "IOU" tokens.
|
||||
|
||||
=== Issuer Sending IOU Tokens to Escrow Creator ===
|
||||
|
||||
{
|
||||
"Account": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"Amount": {
|
||||
"currency": "IOU",
|
||||
"issuer": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"value": "5000"
|
||||
},
|
||||
"Destination": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
|
||||
Submitting Trust Line Token payment transaction...
|
||||
Successfully sent 5000 IOU tokens.
|
||||
|
||||
=== Creating Timed Trust Line Token Escrow ===
|
||||
|
||||
Escrow will mature after: 04/03/2026, 12:27:38 PM
|
||||
|
||||
{
|
||||
"Account": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"Amount": {
|
||||
"currency": "IOU",
|
||||
"issuer": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"value": "1000"
|
||||
},
|
||||
"CancelAfter": 828559948,
|
||||
"Destination": "rsHiso1Qb4vb9GfuWSvmPeBuk6BT1479uD",
|
||||
"FinishAfter": 828559658,
|
||||
"TransactionType": "EscrowCreate"
|
||||
}
|
||||
|
||||
Submitting Trust Line Token EscrowCreate transaction...
|
||||
Trust Line Token escrow created. Sequence: 16230851
|
||||
|
||||
=== Waiting For Timed Trust Line Token Escrow to Mature ===
|
||||
|
||||
Waiting for escrow to mature... done.
|
||||
Latest validated ledger closed at: 04/03/2026, 12:27:41 PM
|
||||
Escrow confirmed ready to finish.
|
||||
|
||||
=== Finishing Timed Trust Line Token Escrow ===
|
||||
|
||||
{
|
||||
"Account": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"OfferSequence": 16230851,
|
||||
"Owner": "rfQVjJ9sRYcLqwxRV2rqSSFm9jusXXo9Sk",
|
||||
"TransactionType": "EscrowFinish"
|
||||
}
|
||||
|
||||
Submitting EscrowFinish transaction...
|
||||
Timed Trust Line Token escrow finished successfully: https://testnet.xrpl.org/transactions/9D1937BE3ADFC42078F222B1DBAE8571BBC096DDA7A47911C4715221C83EC22D
|
||||
```
|
||||
23
_code-samples/escrow/go/go.mod
Normal file
23
_code-samples/escrow/go/go.mod
Normal file
@@ -0,0 +1,23 @@
|
||||
module github.com/XRPLF
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/Peersyst/xrpl-go v0.1.17
|
||||
github.com/go-interledger/cryptoconditions v0.0.0-20180612102545-aba58e59cef1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PromonLogicalis/asn1 v0.0.0-20190312173541-d60463189a56 // indirect
|
||||
github.com/bsv-blockchain/go-sdk v1.2.9 // indirect
|
||||
github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/kalaspuffar/base64url v0.0.0-20171121144659-483af17b794c // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/stevenroose/asn1 v0.0.0-20170613173945-a0d410e3f79f // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
)
|
||||
461
_code-samples/escrow/go/send-fungible-token-escrow/main.go
Normal file
461
_code-samples/escrow/go/send-fungible-token-escrow/main.go
Normal file
@@ -0,0 +1,461 @@
|
||||
// This example demonstrates how to create escrows that hold fungible tokens.
|
||||
// It covers MPTs and Trust Line Tokens, and uses conditional and timed escrows.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Peersyst/xrpl-go/pkg/crypto"
|
||||
"github.com/Peersyst/xrpl-go/xrpl/faucet"
|
||||
"github.com/Peersyst/xrpl-go/xrpl/queries/account"
|
||||
"github.com/Peersyst/xrpl-go/xrpl/queries/common"
|
||||
ledgerreq "github.com/Peersyst/xrpl-go/xrpl/queries/ledger"
|
||||
xrpltime "github.com/Peersyst/xrpl-go/xrpl/time"
|
||||
"github.com/Peersyst/xrpl-go/xrpl/transaction"
|
||||
"github.com/Peersyst/xrpl-go/xrpl/transaction/types"
|
||||
"github.com/Peersyst/xrpl-go/xrpl/wallet"
|
||||
"github.com/Peersyst/xrpl-go/xrpl/websocket"
|
||||
wstypes "github.com/Peersyst/xrpl-go/xrpl/websocket/types"
|
||||
"github.com/go-interledger/cryptoconditions"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := websocket.NewClient(
|
||||
websocket.NewClientConfig().
|
||||
WithHost("wss://s.altnet.rippletest.net:51233").
|
||||
WithFaucetProvider(faucet.NewTestnetFaucetProvider()),
|
||||
)
|
||||
defer client.Disconnect()
|
||||
|
||||
if err := client.Connect(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Fund an issuer account and an escrow creator account ----------------------
|
||||
fmt.Printf("\n=== Funding Accounts ===\n\n")
|
||||
|
||||
createAndFund := func(label string) wallet.Wallet {
|
||||
fmt.Printf("Funding %s account...\n", label)
|
||||
w, err := wallet.New(crypto.ED25519())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := client.FundWallet(&w); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Poll until account is validated on ledger
|
||||
funded := false
|
||||
for range 20 {
|
||||
_, err := client.Request(&account.InfoRequest{
|
||||
Account: w.GetAddress(),
|
||||
LedgerIndex: common.Validated,
|
||||
})
|
||||
if err == nil {
|
||||
funded = true
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if !funded {
|
||||
panic("Issue funding account: " + w.GetAddress().String())
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
issuer := createAndFund("Issuer")
|
||||
creator := createAndFund("Escrow Creator")
|
||||
fmt.Printf("Issuer: %s\n", issuer.ClassicAddress)
|
||||
fmt.Printf("Escrow Creator: %s\n", creator.ClassicAddress)
|
||||
|
||||
// ====== Conditional MPT Escrow ======
|
||||
|
||||
// Issuer creates an MPT ----------------------
|
||||
fmt.Printf("\n=== Creating MPT ===\n\n")
|
||||
maxAmount := types.XRPCurrencyAmount(1000000)
|
||||
mptCreateTx := transaction.MPTokenIssuanceCreate{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: issuer.ClassicAddress,
|
||||
Flags: transaction.TfMPTCanEscrow,
|
||||
},
|
||||
MaximumAmount: &maxAmount,
|
||||
}
|
||||
|
||||
// Flatten() converts the struct to a map and adds the TransactionType field
|
||||
flatMptCreateTx := mptCreateTx.Flatten()
|
||||
mptCreateTxJSON, _ := json.MarshalIndent(flatMptCreateTx, "", " ")
|
||||
fmt.Printf("%s\n", string(mptCreateTxJSON))
|
||||
|
||||
// Submit, sign, and wait for validation
|
||||
fmt.Printf("\nSubmitting MPTokenIssuanceCreate transaction...\n")
|
||||
mptCreateResponse, err := client.SubmitTxAndWait(flatMptCreateTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &issuer,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if mptCreateResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("MPTokenIssuanceCreate failed: %s\n", mptCreateResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Extract the MPT issuance ID from the transaction result
|
||||
mptIssuanceID := string(*mptCreateResponse.Meta.MPTIssuanceID)
|
||||
fmt.Printf("MPT created: %s\n", mptIssuanceID)
|
||||
|
||||
// Escrow Creator authorizes the MPT ----------------------
|
||||
fmt.Printf("\n=== Escrow Creator Authorizing MPT ===\n\n")
|
||||
mptAuthTx := transaction.MPTokenAuthorize{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: creator.ClassicAddress,
|
||||
},
|
||||
MPTokenIssuanceID: mptIssuanceID,
|
||||
}
|
||||
|
||||
flatMptAuthTx := mptAuthTx.Flatten()
|
||||
mptAuthTxJSON, _ := json.MarshalIndent(flatMptAuthTx, "", " ")
|
||||
fmt.Printf("%s\n", string(mptAuthTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting MPTokenAuthorize transaction...\n")
|
||||
mptAuthResponse, err := client.SubmitTxAndWait(flatMptAuthTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &creator,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if mptAuthResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("MPTokenAuthorize failed: %s\n", mptAuthResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Escrow Creator authorized for MPT.\n")
|
||||
|
||||
// Issuer sends MPTs to escrow creator ----------------------
|
||||
fmt.Printf("\n=== Issuer Sending MPTs to Escrow Creator ===\n\n")
|
||||
mptPaymentTx := transaction.Payment{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: issuer.ClassicAddress,
|
||||
},
|
||||
Destination: creator.ClassicAddress,
|
||||
Amount: types.MPTCurrencyAmount{
|
||||
MPTIssuanceID: mptIssuanceID,
|
||||
Value: "5000",
|
||||
},
|
||||
}
|
||||
|
||||
flatMptPaymentTx := mptPaymentTx.Flatten()
|
||||
mptPaymentTxJSON, _ := json.MarshalIndent(flatMptPaymentTx, "", " ")
|
||||
fmt.Printf("%s\n", string(mptPaymentTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting MPT Payment transaction...\n")
|
||||
mptPaymentResponse, err := client.SubmitTxAndWait(flatMptPaymentTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &issuer,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if mptPaymentResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("MPT Payment failed: %s\n", mptPaymentResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Successfully sent 5000 MPTs to Escrow Creator.\n")
|
||||
|
||||
// Escrow Creator creates a conditional MPT escrow ----------------------
|
||||
fmt.Printf("\n=== Creating Conditional MPT Escrow ===\n\n")
|
||||
|
||||
// Generate crypto-condition
|
||||
preimage := make([]byte, 32)
|
||||
if _, err := rand.Read(preimage); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fulfillment := cryptoconditions.NewPreimageSha256(preimage)
|
||||
fulfillmentBinary, err := fulfillment.Encode()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conditionBinary, err := fulfillment.Condition().Encode()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fulfillmentHex := strings.ToUpper(hex.EncodeToString(fulfillmentBinary))
|
||||
conditionHex := strings.ToUpper(hex.EncodeToString(conditionBinary))
|
||||
fmt.Printf("Condition: %s\n", conditionHex)
|
||||
fmt.Printf("Fulfillment: %s\n\n", fulfillmentHex)
|
||||
|
||||
// Set expiration (300 seconds from now)
|
||||
cancelAfterRippleTime := xrpltime.UnixTimeToRippleTime(time.Now().Unix()) + 300
|
||||
|
||||
mptEscrowCreateTx := transaction.EscrowCreate{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: creator.ClassicAddress,
|
||||
},
|
||||
Destination: issuer.ClassicAddress,
|
||||
Amount: types.MPTCurrencyAmount{
|
||||
MPTIssuanceID: mptIssuanceID,
|
||||
Value: "1000",
|
||||
},
|
||||
Condition: conditionHex,
|
||||
CancelAfter: uint32(cancelAfterRippleTime), // Fungible token escrows require a CancelAfter time
|
||||
}
|
||||
|
||||
flatMptEscrowCreateTx := mptEscrowCreateTx.Flatten()
|
||||
mptEscrowCreateTxJSON, _ := json.MarshalIndent(flatMptEscrowCreateTx, "", " ")
|
||||
fmt.Printf("%s\n", string(mptEscrowCreateTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting MPT EscrowCreate transaction...\n")
|
||||
mptEscrowResponse, err := client.SubmitTxAndWait(flatMptEscrowCreateTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &creator,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if mptEscrowResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("MPT EscrowCreate failed: %s\n", mptEscrowResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Save the sequence number to identify the escrow later
|
||||
mptEscrowSeq := mptEscrowResponse.TxJSON.Sequence()
|
||||
fmt.Printf("Conditional MPT escrow created. Sequence: %d\n", mptEscrowSeq)
|
||||
|
||||
// Finish the conditional MPT escrow with the fulfillment ----------------------
|
||||
fmt.Printf("\n=== Finishing Conditional MPT Escrow ===\n\n")
|
||||
mptEscrowFinishTx := transaction.EscrowFinish{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: creator.ClassicAddress,
|
||||
},
|
||||
Owner: creator.ClassicAddress,
|
||||
OfferSequence: mptEscrowSeq,
|
||||
Condition: conditionHex,
|
||||
Fulfillment: fulfillmentHex,
|
||||
}
|
||||
|
||||
flatMptEscrowFinishTx := mptEscrowFinishTx.Flatten()
|
||||
mptEscrowFinishTxJSON, _ := json.MarshalIndent(flatMptEscrowFinishTx, "", " ")
|
||||
fmt.Printf("%s\n", string(mptEscrowFinishTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting EscrowFinish transaction...\n")
|
||||
mptFinishResponse, err := client.SubmitTxAndWait(flatMptEscrowFinishTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &creator,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if mptFinishResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("MPT EscrowFinish failed: %s\n", mptFinishResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Conditional MPT escrow finished successfully: https://testnet.xrpl.org/transactions/%s\n", mptFinishResponse.Hash)
|
||||
|
||||
// ====== Timed Trust Line Token Escrow ======
|
||||
|
||||
// Enable trust line token escrows on the issuer ----------------------
|
||||
fmt.Printf("\n=== Enabling Trust Line Token Escrows on Issuer ===\n\n")
|
||||
accountSetTx := transaction.AccountSet{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: issuer.ClassicAddress,
|
||||
},
|
||||
SetFlag: transaction.AsfAllowTrustLineLocking,
|
||||
}
|
||||
|
||||
flatAccountSetTx := accountSetTx.Flatten()
|
||||
accountSetTxJSON, _ := json.MarshalIndent(flatAccountSetTx, "", " ")
|
||||
fmt.Printf("%s\n", string(accountSetTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting AccountSet transaction...\n")
|
||||
accountSetResponse, err := client.SubmitTxAndWait(flatAccountSetTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &issuer,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if accountSetResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("AccountSet failed: %s\n", accountSetResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Trust line token escrows enabled by issuer.\n")
|
||||
|
||||
// Escrow Creator sets up a trust line to the issuer ----------------------
|
||||
fmt.Printf("\n=== Setting Up Trust Line ===\n\n")
|
||||
currencyCode := "IOU"
|
||||
|
||||
trustSetTx := transaction.TrustSet{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: creator.ClassicAddress,
|
||||
},
|
||||
LimitAmount: types.IssuedCurrencyAmount{
|
||||
Currency: currencyCode,
|
||||
Issuer: issuer.ClassicAddress,
|
||||
Value: "10000000",
|
||||
},
|
||||
}
|
||||
|
||||
flatTrustSetTx := trustSetTx.Flatten()
|
||||
trustSetTxJSON, _ := json.MarshalIndent(flatTrustSetTx, "", " ")
|
||||
fmt.Printf("%s\n", string(trustSetTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting TrustSet transaction...\n")
|
||||
trustResponse, err := client.SubmitTxAndWait(flatTrustSetTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &creator,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if trustResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("TrustSet failed: %s\n", trustResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Trust line successfully created for \"%s\" tokens.\n", currencyCode)
|
||||
|
||||
// Issuer sends IOU tokens to creator ----------------------
|
||||
fmt.Printf("\n=== Issuer Sending IOU Tokens to Escrow Creator ===\n\n")
|
||||
iouPaymentTx := transaction.Payment{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: issuer.ClassicAddress,
|
||||
},
|
||||
Destination: creator.ClassicAddress,
|
||||
Amount: types.IssuedCurrencyAmount{
|
||||
Currency: currencyCode,
|
||||
Value: "5000",
|
||||
Issuer: issuer.ClassicAddress,
|
||||
},
|
||||
}
|
||||
|
||||
flatIouPaymentTx := iouPaymentTx.Flatten()
|
||||
iouPaymentTxJSON, _ := json.MarshalIndent(flatIouPaymentTx, "", " ")
|
||||
fmt.Printf("%s\n", string(iouPaymentTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting Trust Line Token payment transaction...\n")
|
||||
iouPayResponse, err := client.SubmitTxAndWait(flatIouPaymentTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &issuer,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if iouPayResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("Trust Line Token payment failed: %s\n", iouPayResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Successfully sent 5000 %s tokens.\n", currencyCode)
|
||||
|
||||
// Escrow Creator creates a timed trust line token escrow ----------------------
|
||||
fmt.Printf("\n=== Creating Timed Trust Line Token Escrow ===\n\n")
|
||||
delay := 10 // seconds
|
||||
now := time.Now()
|
||||
finishAfterRippleTime := xrpltime.UnixTimeToRippleTime(now.Unix()) + int64(delay)
|
||||
matureTime := now.Add(time.Duration(delay) * time.Second).Format("01/02/2006, 03:04:05 PM")
|
||||
fmt.Printf("Escrow will mature after: %s\n\n", matureTime)
|
||||
|
||||
iouCancelAfterRippleTime := xrpltime.UnixTimeToRippleTime(now.Unix()) + 300
|
||||
|
||||
iouEscrowCreateTx := transaction.EscrowCreate{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: creator.ClassicAddress,
|
||||
},
|
||||
Destination: issuer.ClassicAddress,
|
||||
Amount: types.IssuedCurrencyAmount{
|
||||
Currency: currencyCode,
|
||||
Value: "1000",
|
||||
Issuer: issuer.ClassicAddress,
|
||||
},
|
||||
FinishAfter: uint32(finishAfterRippleTime),
|
||||
CancelAfter: uint32(iouCancelAfterRippleTime),
|
||||
}
|
||||
|
||||
flatIouEscrowCreateTx := iouEscrowCreateTx.Flatten()
|
||||
iouEscrowCreateTxJSON, _ := json.MarshalIndent(flatIouEscrowCreateTx, "", " ")
|
||||
fmt.Printf("%s\n", string(iouEscrowCreateTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting Trust Line Token EscrowCreate transaction...\n")
|
||||
iouEscrowResponse, err := client.SubmitTxAndWait(flatIouEscrowCreateTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &creator,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if iouEscrowResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("Trust Line Token EscrowCreate failed: %s\n", iouEscrowResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Save the sequence number to identify the escrow later
|
||||
iouEscrowSeq := iouEscrowResponse.TxJSON.Sequence()
|
||||
fmt.Printf("Trust Line Token escrow created. Sequence: %d\n", iouEscrowSeq)
|
||||
|
||||
// Wait for the escrow to mature, then finish it --------------------
|
||||
fmt.Printf("\n=== Waiting For Timed Trust Line Token Escrow to Mature ===\n\n")
|
||||
|
||||
// Countdown delay until escrow matures
|
||||
for i := delay; i >= 0; i-- {
|
||||
fmt.Printf("Waiting for escrow to mature... %ds remaining...\r", i)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
fmt.Printf("Waiting for escrow to mature... done. \n")
|
||||
|
||||
// Confirm latest validated ledger close time is after the FinishAfter time
|
||||
escrowReady := false
|
||||
for !escrowReady {
|
||||
ledgerResp, err := client.GetLedger(&ledgerreq.Request{
|
||||
LedgerIndex: common.Validated,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ledgerCloseTime := int64(ledgerResp.Ledger.CloseTime)
|
||||
ledgerCloseLocal := time.Unix(xrpltime.RippleTimeToUnixTime(ledgerCloseTime)/1000, 0).Format("01/02/2006, 03:04:05 PM")
|
||||
fmt.Printf("Latest validated ledger closed at: %s\n", ledgerCloseLocal)
|
||||
if ledgerCloseTime > finishAfterRippleTime {
|
||||
escrowReady = true
|
||||
fmt.Printf("Escrow confirmed ready to finish.\n")
|
||||
} else {
|
||||
timeDifference := finishAfterRippleTime - ledgerCloseTime
|
||||
if timeDifference == 0 {
|
||||
timeDifference = 1
|
||||
}
|
||||
fmt.Printf("Escrow needs to wait another %ds.\n", timeDifference)
|
||||
time.Sleep(time.Duration(timeDifference) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// Finish the timed trust line token escrow --------------------
|
||||
fmt.Printf("\n=== Finishing Timed Trust Line Token Escrow ===\n\n")
|
||||
iouEscrowFinishTx := transaction.EscrowFinish{
|
||||
BaseTx: transaction.BaseTx{
|
||||
Account: creator.ClassicAddress,
|
||||
},
|
||||
Owner: creator.ClassicAddress,
|
||||
OfferSequence: iouEscrowSeq,
|
||||
}
|
||||
|
||||
flatIouEscrowFinishTx := iouEscrowFinishTx.Flatten()
|
||||
iouEscrowFinishTxJSON, _ := json.MarshalIndent(flatIouEscrowFinishTx, "", " ")
|
||||
fmt.Printf("%s\n", string(iouEscrowFinishTxJSON))
|
||||
|
||||
fmt.Printf("\nSubmitting EscrowFinish transaction...\n")
|
||||
iouFinishResponse, err := client.SubmitTxAndWait(flatIouEscrowFinishTx, &wstypes.SubmitOptions{
|
||||
Autofill: true,
|
||||
Wallet: &creator,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if iouFinishResponse.Meta.TransactionResult != "tesSUCCESS" {
|
||||
fmt.Printf("Trust Line Token EscrowFinish failed: %s\n", iouFinishResponse.Meta.TransactionResult)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Timed Trust Line Token escrow finished successfully: https://testnet.xrpl.org/transactions/%s\n", iouFinishResponse.Hash)
|
||||
}
|
||||
@@ -20,6 +20,173 @@ node send-timed-escrow.js
|
||||
node send-conditional-escrow.js
|
||||
```
|
||||
|
||||
## Send Fungible Token Escrow
|
||||
|
||||
```sh
|
||||
node sendFungibleTokenEscrow.js
|
||||
```
|
||||
|
||||
The script issues an MPT and Trust Line Token, setting up both to be escrowable. It then creates and finishes a conditional escrow with the MPT and a timed escrow with the Trust Line Token.
|
||||
|
||||
```sh
|
||||
=== Funding Accounts ===
|
||||
|
||||
Issuer: rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP
|
||||
Escrow Creator: rGRvH4FanVixca934o3ui4MbcrU56x9Qj4
|
||||
|
||||
=== Creating MPT ===
|
||||
|
||||
{
|
||||
"TransactionType": "MPTokenIssuanceCreate",
|
||||
"Account": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP",
|
||||
"MaximumAmount": "1000000",
|
||||
"Flags": 8
|
||||
}
|
||||
|
||||
Submitting MPTokenIssuanceCreate transaction...
|
||||
MPT created: 00F763A2D998FA5E720228B31E1162AC55E6311C7D31F3FC
|
||||
|
||||
=== Escrow Creator Authorizing MPT ===
|
||||
|
||||
{
|
||||
"TransactionType": "MPTokenAuthorize",
|
||||
"Account": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"MPTokenIssuanceID": "00F763A2D998FA5E720228B31E1162AC55E6311C7D31F3FC"
|
||||
}
|
||||
|
||||
Submitting MPTokenAuthorize transaction...
|
||||
Escrow Creator authorized for MPT.
|
||||
|
||||
=== Issuer Sending MPTs to Escrow Creator ===
|
||||
|
||||
{
|
||||
"TransactionType": "Payment",
|
||||
"Account": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP",
|
||||
"Destination": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00F763A2D998FA5E720228B31E1162AC55E6311C7D31F3FC",
|
||||
"value": "5000"
|
||||
}
|
||||
}
|
||||
|
||||
Submitting MPT Payment transaction...
|
||||
Successfully sent 5000 MPTs to Escrow Creator.
|
||||
|
||||
=== Creating Conditional MPT Escrow ===
|
||||
|
||||
Condition: A0258020AA2B8450898500A9E6332B7AD107264982CB09C63E3D16D139D63E997597E6F6810120
|
||||
Fulfillment: A0228020CA07971CB0C63ED20C69931B41EEA7C4C8CC6F214183FDE031CDC7413856977F
|
||||
|
||||
{
|
||||
"TransactionType": "EscrowCreate",
|
||||
"Account": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"Destination": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00F763A2D998FA5E720228B31E1162AC55E6311C7D31F3FC",
|
||||
"value": "1000"
|
||||
},
|
||||
"Condition": "A0258020AA2B8450898500A9E6332B7AD107264982CB09C63E3D16D139D63E997597E6F6810120",
|
||||
"CancelAfter": 828504579
|
||||
}
|
||||
|
||||
Submitting MPT EscrowCreate transaction...
|
||||
Conditional MPT escrow created. Sequence: 16212899
|
||||
|
||||
=== Finishing Conditional MPT Escrow ===
|
||||
|
||||
{
|
||||
"TransactionType": "EscrowFinish",
|
||||
"Account": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"Owner": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"OfferSequence": 16212899,
|
||||
"Condition": "A0258020AA2B8450898500A9E6332B7AD107264982CB09C63E3D16D139D63E997597E6F6810120",
|
||||
"Fulfillment": "A0228020CA07971CB0C63ED20C69931B41EEA7C4C8CC6F214183FDE031CDC7413856977F"
|
||||
}
|
||||
|
||||
Submitting EscrowFinish transaction...
|
||||
Conditional MPT escrow finished successfully: https://testnet.xrpl.org/transactions/BB6E8BF8A7F28D15C12C24FFDB215180262ABFAEAD43FB020DCB39E826027078
|
||||
|
||||
=== Enabling Trust Line Token Escrows on Issuer ===
|
||||
|
||||
{
|
||||
"TransactionType": "AccountSet",
|
||||
"Account": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP",
|
||||
"SetFlag": 17
|
||||
}
|
||||
|
||||
Submitting AccountSet transaction...
|
||||
Trust line token escrows enabled by issuer.
|
||||
|
||||
=== Setting Up Trust Line ===
|
||||
|
||||
{
|
||||
"TransactionType": "TrustSet",
|
||||
"Account": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"LimitAmount": {
|
||||
"currency": "IOU",
|
||||
"issuer": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP",
|
||||
"value": "10000000"
|
||||
}
|
||||
}
|
||||
|
||||
Submitting TrustSet transaction...
|
||||
Trust line successfully created for "IOU" tokens.
|
||||
|
||||
=== Issuer Sending IOU Tokens to Escrow Creator ===
|
||||
|
||||
{
|
||||
"TransactionType": "Payment",
|
||||
"Account": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP",
|
||||
"Destination": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"Amount": {
|
||||
"currency": "IOU",
|
||||
"value": "5000",
|
||||
"issuer": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP"
|
||||
}
|
||||
}
|
||||
|
||||
Submitting Trust Line Token payment transaction...
|
||||
Successfully sent 5000 IOU tokens.
|
||||
|
||||
=== Creating Timed Trust Line Token Escrow ===
|
||||
|
||||
Escrow will mature after: 4/2/2026, 9:05:12 PM
|
||||
|
||||
{
|
||||
"TransactionType": "EscrowCreate",
|
||||
"Account": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"Destination": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP",
|
||||
"Amount": {
|
||||
"currency": "IOU",
|
||||
"value": "1000",
|
||||
"issuer": "rLqYtjhg56pVNJFKueVVKkiA8z5UtznxQP"
|
||||
},
|
||||
"FinishAfter": 828504312,
|
||||
"CancelAfter": 828504602
|
||||
}
|
||||
|
||||
Submitting Trust Line Token EscrowCreate transaction...
|
||||
Trust Line Token escrow created. Sequence: 16212902
|
||||
|
||||
=== Waiting For Timed Trust Line Token Escrow to Mature ===
|
||||
|
||||
Waiting for escrow to mature... done.
|
||||
Latest validated ledger closed at: 4/2/2026, 9:05:13 PM
|
||||
Escrow confirmed ready to finish.
|
||||
|
||||
=== Finishing Timed Trust Line Token Escrow ===
|
||||
|
||||
{
|
||||
"TransactionType": "EscrowFinish",
|
||||
"Account": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"Owner": "rGRvH4FanVixca934o3ui4MbcrU56x9Qj4",
|
||||
"OfferSequence": 16212902
|
||||
}
|
||||
|
||||
Submitting EscrowFinish transaction...
|
||||
Timed Trust Line Token escrow finished successfully: https://testnet.xrpl.org/transactions/136402974863BF553706B0A4A341F24DDA5385BB6F93B905038D8FD9863B6D91
|
||||
```
|
||||
|
||||
## List Escrows
|
||||
|
||||
```sh
|
||||
|
||||
358
_code-samples/escrow/js/sendFungibleTokenEscrow.js
Normal file
358
_code-samples/escrow/js/sendFungibleTokenEscrow.js
Normal file
@@ -0,0 +1,358 @@
|
||||
// This example demonstrates how to create escrows that hold fungible tokens.
|
||||
// It covers MPTs and Trust Line Tokens, and uses conditional and timed escrows.
|
||||
|
||||
import xrpl from 'xrpl'
|
||||
import { PreimageSha256 } from 'five-bells-condition'
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// Fund an issuer account and an escrow creator account ----------------------
|
||||
console.log(`\n=== Funding Accounts ===\n`)
|
||||
const [
|
||||
{ wallet: issuer },
|
||||
{ wallet: creator }
|
||||
] = await Promise.all([
|
||||
client.fundWallet(),
|
||||
client.fundWallet()
|
||||
])
|
||||
console.log(`Issuer: ${issuer.address}`)
|
||||
console.log(`Escrow Creator: ${creator.address}`)
|
||||
|
||||
// ====== Conditional MPT Escrow ======
|
||||
|
||||
// Issuer creates an MPT ----------------------
|
||||
console.log('\n=== Creating MPT ===\n')
|
||||
const mptCreateTx = {
|
||||
TransactionType: 'MPTokenIssuanceCreate',
|
||||
Account: issuer.address,
|
||||
MaximumAmount: '1000000',
|
||||
Flags: xrpl.MPTokenIssuanceCreateFlags.tfMPTCanEscrow
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(mptCreateTx)
|
||||
console.log(JSON.stringify(mptCreateTx, null, 2))
|
||||
|
||||
// Submit, sign, and wait for validation
|
||||
console.log(`\nSubmitting MPTokenIssuanceCreate transaction...`)
|
||||
const mptCreateResponse = await client.submitAndWait(mptCreateTx, {
|
||||
wallet: issuer,
|
||||
autofill: true
|
||||
})
|
||||
if (mptCreateResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`MPTokenIssuanceCreate failed: ${mptCreateResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Extract the MPT issuance ID from the transaction result
|
||||
const mptIssuanceId = mptCreateResponse.result.meta.mpt_issuance_id
|
||||
console.log(`MPT created: ${mptIssuanceId}`)
|
||||
|
||||
// Escrow Creator authorizes the MPT ----------------------
|
||||
console.log('\n=== Escrow Creator Authorizing MPT ===\n')
|
||||
const mptAuthTx = {
|
||||
TransactionType: 'MPTokenAuthorize',
|
||||
Account: creator.address,
|
||||
MPTokenIssuanceID: mptIssuanceId
|
||||
}
|
||||
|
||||
xrpl.validate(mptAuthTx)
|
||||
console.log(JSON.stringify(mptAuthTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting MPTokenAuthorize transaction...`)
|
||||
const mptAuthResponse = await client.submitAndWait(mptAuthTx, {
|
||||
wallet: creator,
|
||||
autofill: true
|
||||
})
|
||||
if (mptAuthResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`MPTokenAuthorize failed: ${mptAuthResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Escrow Creator authorized for MPT.')
|
||||
|
||||
// Issuer sends MPTs to escrow creator ----------------------
|
||||
console.log('\n=== Issuer Sending MPTs to Escrow Creator ===\n')
|
||||
const mptPaymentTx = {
|
||||
TransactionType: 'Payment',
|
||||
Account: issuer.address,
|
||||
Destination: creator.address,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptIssuanceId,
|
||||
value: '5000'
|
||||
}
|
||||
}
|
||||
|
||||
xrpl.validate(mptPaymentTx)
|
||||
console.log(JSON.stringify(mptPaymentTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting MPT Payment transaction...`)
|
||||
const mptPaymentResponse = await client.submitAndWait(mptPaymentTx, {
|
||||
wallet: issuer,
|
||||
autofill: true
|
||||
})
|
||||
if (mptPaymentResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`MPT Payment failed: ${mptPaymentResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Successfully sent 5000 MPTs to Escrow Creator.')
|
||||
|
||||
// Escrow Creator creates a conditional MPT escrow ----------------------
|
||||
console.log('\n=== Creating Conditional MPT Escrow ===\n')
|
||||
|
||||
// Generate crypto-condition
|
||||
const preimage = randomBytes(32)
|
||||
const fulfillment = new PreimageSha256()
|
||||
fulfillment.setPreimage(preimage)
|
||||
const fulfillmentHex = fulfillment.serializeBinary().toString('hex').toUpperCase()
|
||||
const conditionHex = fulfillment.getConditionBinary().toString('hex').toUpperCase()
|
||||
console.log(`Condition: ${conditionHex}`)
|
||||
console.log(`Fulfillment: ${fulfillmentHex}\n`)
|
||||
|
||||
// Set expiration (300 seconds from now)
|
||||
const cancelAfter = new Date()
|
||||
cancelAfter.setSeconds(cancelAfter.getSeconds() + 300)
|
||||
const cancelAfterRippleTime = xrpl.isoTimeToRippleTime(cancelAfter.toISOString())
|
||||
|
||||
const mptEscrowCreateTx = {
|
||||
TransactionType: 'EscrowCreate',
|
||||
Account: creator.address,
|
||||
Destination: issuer.address,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptIssuanceId,
|
||||
value: '1000'
|
||||
},
|
||||
Condition: conditionHex,
|
||||
CancelAfter: cancelAfterRippleTime // Fungible token escrows require a CancelAfter time
|
||||
}
|
||||
|
||||
xrpl.validate(mptEscrowCreateTx)
|
||||
console.log(JSON.stringify(mptEscrowCreateTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting MPT EscrowCreate transaction...`)
|
||||
const mptEscrowResponse = await client.submitAndWait(mptEscrowCreateTx, {
|
||||
wallet: creator,
|
||||
autofill: true
|
||||
})
|
||||
if (mptEscrowResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`MPT EscrowCreate failed: ${mptEscrowResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Save the sequence number to identify the escrow later
|
||||
const mptEscrowSeq = mptEscrowResponse.result.tx_json.Sequence
|
||||
console.log(`Conditional MPT escrow created. Sequence: ${mptEscrowSeq}`)
|
||||
|
||||
// Finish the conditional MPT escrow with the fulfillment ----------------------
|
||||
console.log('\n=== Finishing Conditional MPT Escrow ===\n')
|
||||
const mptEscrowFinishTx = {
|
||||
TransactionType: 'EscrowFinish',
|
||||
Account: creator.address,
|
||||
Owner: creator.address,
|
||||
OfferSequence: mptEscrowSeq,
|
||||
Condition: conditionHex,
|
||||
Fulfillment: fulfillmentHex
|
||||
}
|
||||
|
||||
xrpl.validate(mptEscrowFinishTx)
|
||||
console.log(JSON.stringify(mptEscrowFinishTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting EscrowFinish transaction...`)
|
||||
const mptFinishResponse = await client.submitAndWait(mptEscrowFinishTx, {
|
||||
wallet: creator,
|
||||
autofill: true
|
||||
})
|
||||
if (mptFinishResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`MPT EscrowFinish failed: ${mptFinishResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`Conditional MPT escrow finished successfully: https://testnet.xrpl.org/transactions/${mptFinishResponse.result.hash}`)
|
||||
|
||||
// ====== Timed Trust Line Token Escrow ======
|
||||
|
||||
// Enable trust line token escrows on the issuer ----------------------
|
||||
console.log('\n=== Enabling Trust Line Token Escrows on Issuer ===\n')
|
||||
const accountSetTx = {
|
||||
TransactionType: 'AccountSet',
|
||||
Account: issuer.address,
|
||||
SetFlag: xrpl.AccountSetAsfFlags.asfAllowTrustLineLocking
|
||||
}
|
||||
|
||||
xrpl.validate(accountSetTx)
|
||||
console.log(JSON.stringify(accountSetTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting AccountSet transaction...`)
|
||||
const accountSetResponse = await client.submitAndWait(accountSetTx, {
|
||||
wallet: issuer,
|
||||
autofill: true
|
||||
})
|
||||
if (accountSetResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`AccountSet failed: ${accountSetResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Trust line token escrows enabled by issuer.')
|
||||
|
||||
// Escrow Creator sets up a trust line to the issuer ----------------------
|
||||
console.log('\n=== Setting Up Trust Line ===\n')
|
||||
const currencyCode = 'IOU'
|
||||
|
||||
const trustSetTx = {
|
||||
TransactionType: 'TrustSet',
|
||||
Account: creator.address,
|
||||
LimitAmount: {
|
||||
currency: currencyCode,
|
||||
issuer: issuer.address,
|
||||
value: '10000000'
|
||||
}
|
||||
}
|
||||
|
||||
xrpl.validate(trustSetTx)
|
||||
console.log(JSON.stringify(trustSetTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting TrustSet transaction...`)
|
||||
const trustResponse = await client.submitAndWait(trustSetTx, {
|
||||
wallet: creator,
|
||||
autofill: true
|
||||
})
|
||||
if (trustResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`TrustSet failed: ${trustResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`Trust line successfully created for "${currencyCode}" tokens.`)
|
||||
|
||||
// Issuer sends IOU tokens to creator ----------------------
|
||||
console.log('\n=== Issuer Sending IOU Tokens to Escrow Creator ===\n')
|
||||
const iouPaymentTx = {
|
||||
TransactionType: 'Payment',
|
||||
Account: issuer.address,
|
||||
Destination: creator.address,
|
||||
Amount: {
|
||||
currency: currencyCode,
|
||||
value: '5000',
|
||||
issuer: issuer.address
|
||||
}
|
||||
}
|
||||
|
||||
xrpl.validate(iouPaymentTx)
|
||||
console.log(JSON.stringify(iouPaymentTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting Trust Line Token payment transaction...`)
|
||||
const iouPayResponse = await client.submitAndWait(iouPaymentTx, {
|
||||
wallet: issuer,
|
||||
autofill: true
|
||||
})
|
||||
if (iouPayResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`Trust Line Token payment failed: ${iouPayResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`Successfully sent 5000 ${currencyCode} tokens.`)
|
||||
|
||||
// Escrow Creator creates a timed trust line token escrow ----------------------
|
||||
console.log('\n=== Creating Timed Trust Line Token Escrow ===\n')
|
||||
const delay = 10 // seconds
|
||||
const now = new Date()
|
||||
const finishAfter = new Date(now.getTime() + delay * 1000)
|
||||
const finishAfterRippleTime = xrpl.isoTimeToRippleTime(finishAfter.toISOString())
|
||||
console.log(`Escrow will mature after: ${finishAfter.toLocaleString()}\n`)
|
||||
|
||||
const iouCancelAfter = new Date(now.getTime() + 300 * 1000)
|
||||
const iouCancelAfterRippleTime = xrpl.isoTimeToRippleTime(iouCancelAfter.toISOString())
|
||||
|
||||
const iouEscrowCreateTx = {
|
||||
TransactionType: 'EscrowCreate',
|
||||
Account: creator.address,
|
||||
Destination: issuer.address,
|
||||
Amount: {
|
||||
currency: currencyCode,
|
||||
value: '1000',
|
||||
issuer: issuer.address
|
||||
},
|
||||
FinishAfter: finishAfterRippleTime,
|
||||
CancelAfter: iouCancelAfterRippleTime
|
||||
}
|
||||
|
||||
xrpl.validate(iouEscrowCreateTx)
|
||||
console.log(JSON.stringify(iouEscrowCreateTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting Trust Line Token EscrowCreate transaction...`)
|
||||
const iouEscrowResponse = await client.submitAndWait(iouEscrowCreateTx, {
|
||||
wallet: creator,
|
||||
autofill: true
|
||||
})
|
||||
if (iouEscrowResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`Trust Line Token EscrowCreate failed: ${iouEscrowResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Save the sequence number to identify the escrow later
|
||||
const iouEscrowSeq = iouEscrowResponse.result.tx_json.Sequence
|
||||
console.log(`Trust Line Token escrow created. Sequence: ${iouEscrowSeq}`)
|
||||
|
||||
// Wait for the escrow to mature, then finish it --------------------
|
||||
console.log(`\n=== Waiting For Timed Trust Line Token Escrow to Mature ===\n`)
|
||||
|
||||
// Sleep function to countdown delay until escrow matures
|
||||
function sleep (delayInSeconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, delayInSeconds * 1000))
|
||||
}
|
||||
for (let i = delay; i >= 0; i--) {
|
||||
process.stdout.write(`\rWaiting for escrow to mature... ${i}s remaining...`)
|
||||
await sleep(1)
|
||||
}
|
||||
console.log('\rWaiting for escrow to mature... done. ')
|
||||
|
||||
// Confirm latest validated ledger close time is after the FinishAfter time
|
||||
let escrowReady = false
|
||||
while (!escrowReady) {
|
||||
const validatedLedger = await client.request({
|
||||
command: 'ledger',
|
||||
ledger_index: 'validated'
|
||||
})
|
||||
const ledgerCloseTime = validatedLedger.result.ledger.close_time
|
||||
console.log(`Latest validated ledger closed at: ${new Date(xrpl.rippleTimeToISOTime(ledgerCloseTime)).toLocaleString()}`)
|
||||
if (ledgerCloseTime > finishAfterRippleTime) {
|
||||
escrowReady = true
|
||||
console.log('Escrow confirmed ready to finish.')
|
||||
} else {
|
||||
let timeDifference = finishAfterRippleTime - ledgerCloseTime
|
||||
if (timeDifference === 0) { timeDifference = 1 }
|
||||
console.log(`Escrow needs to wait another ${timeDifference}s.`)
|
||||
await sleep(timeDifference)
|
||||
}
|
||||
}
|
||||
|
||||
// Finish the timed trust line token escrow --------------------
|
||||
console.log('\n=== Finishing Timed Trust Line Token Escrow ===\n')
|
||||
const iouEscrowFinishTx = {
|
||||
TransactionType: 'EscrowFinish',
|
||||
Account: creator.address,
|
||||
Owner: creator.address,
|
||||
OfferSequence: iouEscrowSeq
|
||||
}
|
||||
|
||||
xrpl.validate(iouEscrowFinishTx)
|
||||
console.log(JSON.stringify(iouEscrowFinishTx, null, 2))
|
||||
|
||||
console.log(`\nSubmitting EscrowFinish transaction...`)
|
||||
const iouFinishResponse = await client.submitAndWait(iouEscrowFinishTx, {
|
||||
wallet: creator,
|
||||
autofill: true
|
||||
})
|
||||
if (iouFinishResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error(`Trust Line Token EscrowFinish failed: ${iouFinishResponse.result.meta.TransactionResult}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`Timed Trust Line Token escrow finished successfully: https://testnet.xrpl.org/transactions/${iouFinishResponse.result.hash}`)
|
||||
|
||||
await client.disconnect()
|
||||
@@ -22,6 +22,187 @@ python send_timed_escrow.py
|
||||
python send_conditional_escrow.py
|
||||
```
|
||||
|
||||
## Send Fungible Token Escrow
|
||||
|
||||
```sh
|
||||
python send_fungible_token_escrow.py
|
||||
```
|
||||
|
||||
The script issues an MPT and Trust Line Token, setting up both to be escrowable. It then creates and finishes a conditional escrow with the MPT and a timed escrow with the Trust Line Token.
|
||||
|
||||
```sh
|
||||
=== Funding Accounts ===
|
||||
|
||||
Attempting to fund address rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2
|
||||
Faucet fund successful.
|
||||
Attempting to fund address rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU
|
||||
Faucet fund successful.
|
||||
Issuer: rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2
|
||||
Escrow Creator: rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU
|
||||
|
||||
=== Creating MPT ===
|
||||
|
||||
{
|
||||
"Account": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"TransactionType": "MPTokenIssuanceCreate",
|
||||
"Flags": 8,
|
||||
"SigningPubKey": "",
|
||||
"MaximumAmount": "1000000"
|
||||
}
|
||||
|
||||
Submitting MPTokenIssuanceCreate transaction...
|
||||
MPT created: 00F7705DFE38372A760229755F9E4F5EADE06F2CE36BDA18
|
||||
|
||||
=== Escrow Creator Authorizing MPT ===
|
||||
|
||||
{
|
||||
"Account": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU",
|
||||
"TransactionType": "MPTokenAuthorize",
|
||||
"SigningPubKey": "",
|
||||
"MPTokenIssuanceID": "00F7705DFE38372A760229755F9E4F5EADE06F2CE36BDA18"
|
||||
}
|
||||
|
||||
Submitting MPTokenAuthorize transaction...
|
||||
Escrow Creator authorized for MPT.
|
||||
|
||||
=== Issuer Sending MPTs to Escrow Creator ===
|
||||
|
||||
{
|
||||
"Account": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"TransactionType": "Payment",
|
||||
"SigningPubKey": "",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00F7705DFE38372A760229755F9E4F5EADE06F2CE36BDA18",
|
||||
"value": "5000"
|
||||
},
|
||||
"Destination": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU"
|
||||
}
|
||||
|
||||
Submitting MPT Payment transaction...
|
||||
Successfully sent 5000 MPTs to Escrow Creator.
|
||||
|
||||
=== Creating Conditional MPT Escrow ===
|
||||
|
||||
Condition: A02580202959C2DFA17829F23F8A7F2F3A81FE73F9E964A56810A250CB836DE1AB851E47810120
|
||||
Fulfillment: A022802079204EBCCCF4816441F0D4F7B15E7003A757675FC90691107AB770044B07697B
|
||||
|
||||
{
|
||||
"Account": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU",
|
||||
"TransactionType": "EscrowCreate",
|
||||
"SigningPubKey": "",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00F7705DFE38372A760229755F9E4F5EADE06F2CE36BDA18",
|
||||
"value": "1000"
|
||||
},
|
||||
"Destination": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"CancelAfter": 828514495,
|
||||
"Condition": "A02580202959C2DFA17829F23F8A7F2F3A81FE73F9E964A56810A250CB836DE1AB851E47810120"
|
||||
}
|
||||
|
||||
Submitting MPT EscrowCreate transaction...
|
||||
Conditional MPT escrow created. Sequence: 16216160
|
||||
|
||||
=== Finishing Conditional MPT Escrow ===
|
||||
|
||||
{
|
||||
"Account": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU",
|
||||
"TransactionType": "EscrowFinish",
|
||||
"SigningPubKey": "",
|
||||
"Owner": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU",
|
||||
"OfferSequence": 16216160,
|
||||
"Condition": "A02580202959C2DFA17829F23F8A7F2F3A81FE73F9E964A56810A250CB836DE1AB851E47810120",
|
||||
"Fulfillment": "A022802079204EBCCCF4816441F0D4F7B15E7003A757675FC90691107AB770044B07697B"
|
||||
}
|
||||
|
||||
Submitting EscrowFinish transaction...
|
||||
Conditional MPT escrow finished successfully: https://testnet.xrpl.org/transactions/4FE4B0AD6FD9D8CD968F5AFD43C3E1F2180C40C3A20DE7416B1E16069D2340DD
|
||||
|
||||
=== Enabling Trust Line Token Escrows on Issuer ===
|
||||
|
||||
{
|
||||
"Account": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"TransactionType": "AccountSet",
|
||||
"SigningPubKey": "",
|
||||
"SetFlag": 17
|
||||
}
|
||||
|
||||
Submitting AccountSet transaction...
|
||||
Trust line token escrows enabled by issuer.
|
||||
|
||||
=== Setting Up Trust Line ===
|
||||
|
||||
{
|
||||
"Account": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU",
|
||||
"TransactionType": "TrustSet",
|
||||
"SigningPubKey": "",
|
||||
"LimitAmount": {
|
||||
"currency": "IOU",
|
||||
"issuer": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"value": "10000000"
|
||||
}
|
||||
}
|
||||
|
||||
Submitting TrustSet transaction...
|
||||
Trust line successfully created for "IOU" tokens.
|
||||
|
||||
=== Issuer Sending IOU Tokens to Escrow Creator ===
|
||||
|
||||
{
|
||||
"Account": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"TransactionType": "Payment",
|
||||
"SigningPubKey": "",
|
||||
"Amount": {
|
||||
"currency": "IOU",
|
||||
"issuer": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"value": "5000"
|
||||
},
|
||||
"Destination": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU"
|
||||
}
|
||||
|
||||
Submitting Trust Line Token payment transaction...
|
||||
Successfully sent 5000 IOU tokens.
|
||||
|
||||
=== Creating Timed Trust Line Token Escrow ===
|
||||
|
||||
Escrow will mature after: 04/02/2026, 11:50:39 PM
|
||||
|
||||
{
|
||||
"Account": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU",
|
||||
"TransactionType": "EscrowCreate",
|
||||
"SigningPubKey": "",
|
||||
"Amount": {
|
||||
"currency": "IOU",
|
||||
"issuer": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"value": "1000"
|
||||
},
|
||||
"Destination": "rQBByeTLvRVQy9GLsqvsnczMB3QZ7b7gs2",
|
||||
"CancelAfter": 828514529,
|
||||
"FinishAfter": 828514239
|
||||
}
|
||||
|
||||
Submitting Trust Line Token EscrowCreate transaction...
|
||||
Trust Line Token escrow created. Sequence: 16216163
|
||||
|
||||
=== Waiting For Timed Trust Line Token Escrow to Mature ===
|
||||
|
||||
Waiting for escrow to mature... done.
|
||||
Latest validated ledger closed at: 04/02/2026, 11:50:42 PM
|
||||
Escrow confirmed ready to finish.
|
||||
|
||||
=== Finishing Timed Trust Line Token Escrow ===
|
||||
|
||||
{
|
||||
"Account": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU",
|
||||
"TransactionType": "EscrowFinish",
|
||||
"SigningPubKey": "",
|
||||
"Owner": "rBkkT1Q3E3bm5tM5D5YazgX8cpq3a2V6zU",
|
||||
"OfferSequence": 16216163
|
||||
}
|
||||
|
||||
Submitting EscrowFinish transaction...
|
||||
Timed Trust Line Token escrow finished successfully: https://testnet.xrpl.org/transactions/F3FCFE9D0E9A0A7CC6FA804526A509532833AEAD4C7A7BF1978194F4F1CCDED4
|
||||
```
|
||||
|
||||
## List Escrows
|
||||
|
||||
```sh
|
||||
|
||||
301
_code-samples/escrow/py/send_fungible_token_escrow.py
Normal file
301
_code-samples/escrow/py/send_fungible_token_escrow.py
Normal file
@@ -0,0 +1,301 @@
|
||||
# This example demonstrates how to create escrows that hold fungible tokens.
|
||||
# It covers MPTs and Trust Line Tokens, and uses conditional and timed escrows.
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta, UTC
|
||||
from os import urandom
|
||||
from time import sleep
|
||||
|
||||
from cryptoconditions import PreimageSha256
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import (
|
||||
AccountSet,
|
||||
EscrowCreate,
|
||||
EscrowFinish,
|
||||
MPTokenAuthorize,
|
||||
MPTokenIssuanceCreate,
|
||||
Payment,
|
||||
TrustSet,
|
||||
)
|
||||
from xrpl.models.amounts import IssuedCurrencyAmount, MPTAmount
|
||||
from xrpl.models.requests import Ledger
|
||||
from xrpl.models.transactions.account_set import AccountSetAsfFlag
|
||||
from xrpl.models.transactions.mptoken_issuance_create import MPTokenIssuanceCreateFlag
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.utils import datetime_to_ripple_time, ripple_time_to_datetime
|
||||
from xrpl.wallet import generate_faucet_wallet
|
||||
|
||||
client = JsonRpcClient("https://s.altnet.rippletest.net:51234")
|
||||
|
||||
# Fund an issuer account and an escrow creator account ----------------------
|
||||
print("\n=== Funding Accounts ===\n")
|
||||
issuer = generate_faucet_wallet(client, debug=True)
|
||||
creator = generate_faucet_wallet(client, debug=True)
|
||||
print(f"Issuer: {issuer.address}")
|
||||
print(f"Escrow Creator: {creator.address}")
|
||||
|
||||
# ====== Conditional MPT Escrow ======
|
||||
|
||||
# Issuer creates an MPT ----------------------
|
||||
print("\n=== Creating MPT ===\n")
|
||||
mpt_create_tx = MPTokenIssuanceCreate(
|
||||
account=issuer.address,
|
||||
maximum_amount="1000000",
|
||||
flags=MPTokenIssuanceCreateFlag.TF_MPT_CAN_ESCROW,
|
||||
)
|
||||
|
||||
print(json.dumps(mpt_create_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Submit, sign, and wait for validation
|
||||
print("\nSubmitting MPTokenIssuanceCreate transaction...")
|
||||
mpt_create_response = submit_and_wait(mpt_create_tx, client, issuer)
|
||||
|
||||
if mpt_create_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"MPTokenIssuanceCreate failed: {mpt_create_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
|
||||
# Extract the MPT issuance ID from the transaction result
|
||||
mpt_issuance_id = mpt_create_response.result["meta"]["mpt_issuance_id"]
|
||||
print(f"MPT created: {mpt_issuance_id}")
|
||||
|
||||
# Escrow Creator authorizes the MPT ----------------------
|
||||
print("\n=== Escrow Creator Authorizing MPT ===\n")
|
||||
mpt_auth_tx = MPTokenAuthorize(
|
||||
account=creator.address,
|
||||
mptoken_issuance_id=mpt_issuance_id,
|
||||
)
|
||||
|
||||
print(json.dumps(mpt_auth_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting MPTokenAuthorize transaction...")
|
||||
mpt_auth_response = submit_and_wait(mpt_auth_tx, client, creator)
|
||||
|
||||
if mpt_auth_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"MPTokenAuthorize failed: {mpt_auth_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
print("Escrow Creator authorized for MPT.")
|
||||
|
||||
# Issuer sends MPTs to escrow creator ----------------------
|
||||
print("\n=== Issuer Sending MPTs to Escrow Creator ===\n")
|
||||
mpt_payment_tx = Payment(
|
||||
account=issuer.address,
|
||||
destination=creator.address,
|
||||
amount=MPTAmount(
|
||||
mpt_issuance_id=mpt_issuance_id,
|
||||
value="5000",
|
||||
),
|
||||
)
|
||||
|
||||
print(json.dumps(mpt_payment_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting MPT Payment transaction...")
|
||||
mpt_payment_response = submit_and_wait(mpt_payment_tx, client, issuer)
|
||||
|
||||
if mpt_payment_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"MPT Payment failed: {mpt_payment_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
print("Successfully sent 5000 MPTs to Escrow Creator.")
|
||||
|
||||
# Escrow Creator creates a conditional MPT escrow ----------------------
|
||||
print("\n=== Creating Conditional MPT Escrow ===\n")
|
||||
|
||||
# Generate crypto-condition
|
||||
preimage = urandom(32)
|
||||
fulfillment = PreimageSha256(preimage=preimage)
|
||||
fulfillment_hex = fulfillment.serialize_binary().hex().upper()
|
||||
condition_hex = fulfillment.condition_binary.hex().upper()
|
||||
print(f"Condition: {condition_hex}")
|
||||
print(f"Fulfillment: {fulfillment_hex}\n")
|
||||
|
||||
# Set expiration (300 seconds from now)
|
||||
cancel_after = datetime.now(tz=UTC) + timedelta(seconds=300)
|
||||
cancel_after_ripple_time = datetime_to_ripple_time(cancel_after)
|
||||
|
||||
mpt_escrow_create_tx = EscrowCreate(
|
||||
account=creator.address,
|
||||
destination=issuer.address,
|
||||
amount=MPTAmount(
|
||||
mpt_issuance_id=mpt_issuance_id,
|
||||
value="1000",
|
||||
),
|
||||
condition=condition_hex,
|
||||
cancel_after=cancel_after_ripple_time, # Fungible token escrows require a cancel_after time
|
||||
)
|
||||
|
||||
print(json.dumps(mpt_escrow_create_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting MPT EscrowCreate transaction...")
|
||||
mpt_escrow_response = submit_and_wait(mpt_escrow_create_tx, client, creator)
|
||||
|
||||
if mpt_escrow_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"MPT EscrowCreate failed: {mpt_escrow_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
|
||||
# Save the sequence number to identify the escrow later
|
||||
mpt_escrow_seq = mpt_escrow_response.result["tx_json"]["Sequence"]
|
||||
print(f"Conditional MPT escrow created. Sequence: {mpt_escrow_seq}")
|
||||
|
||||
# Finish the conditional MPT escrow with the fulfillment ----------------------
|
||||
print("\n=== Finishing Conditional MPT Escrow ===\n")
|
||||
mpt_escrow_finish_tx = EscrowFinish(
|
||||
account=creator.address,
|
||||
owner=creator.address,
|
||||
offer_sequence=mpt_escrow_seq,
|
||||
condition=condition_hex,
|
||||
fulfillment=fulfillment_hex,
|
||||
)
|
||||
|
||||
print(json.dumps(mpt_escrow_finish_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting EscrowFinish transaction...")
|
||||
mpt_finish_response = submit_and_wait(mpt_escrow_finish_tx, client, creator)
|
||||
|
||||
if mpt_finish_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"MPT EscrowFinish failed: {mpt_finish_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
print(f"Conditional MPT escrow finished successfully: https://testnet.xrpl.org/transactions/{mpt_finish_response.result['hash']}")
|
||||
|
||||
# ====== Timed Trust Line Token Escrow ======
|
||||
|
||||
# Enable trust line token escrows on the issuer ----------------------
|
||||
print("\n=== Enabling Trust Line Token Escrows on Issuer ===\n")
|
||||
account_set_tx = AccountSet(
|
||||
account=issuer.address,
|
||||
set_flag=AccountSetAsfFlag.ASF_ALLOW_TRUSTLINE_LOCKING,
|
||||
)
|
||||
|
||||
print(json.dumps(account_set_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting AccountSet transaction...")
|
||||
account_set_response = submit_and_wait(account_set_tx, client, issuer)
|
||||
|
||||
if account_set_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"AccountSet failed: {account_set_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
print("Trust line token escrows enabled by issuer.")
|
||||
|
||||
# Escrow Creator sets up a trust line to the issuer ----------------------
|
||||
print("\n=== Setting Up Trust Line ===\n")
|
||||
currency_code = "IOU"
|
||||
|
||||
trust_set_tx = TrustSet(
|
||||
account=creator.address,
|
||||
limit_amount=IssuedCurrencyAmount(
|
||||
currency=currency_code,
|
||||
issuer=issuer.address,
|
||||
value="10000000",
|
||||
),
|
||||
)
|
||||
|
||||
print(json.dumps(trust_set_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting TrustSet transaction...")
|
||||
trust_response = submit_and_wait(trust_set_tx, client, creator)
|
||||
|
||||
if trust_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"TrustSet failed: {trust_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
print(f'Trust line successfully created for "{currency_code}" tokens.')
|
||||
|
||||
# Issuer sends IOU tokens to creator ----------------------
|
||||
print("\n=== Issuer Sending IOU Tokens to Escrow Creator ===\n")
|
||||
iou_payment_tx = Payment(
|
||||
account=issuer.address,
|
||||
destination=creator.address,
|
||||
amount=IssuedCurrencyAmount(
|
||||
currency=currency_code,
|
||||
value="5000",
|
||||
issuer=issuer.address,
|
||||
),
|
||||
)
|
||||
|
||||
print(json.dumps(iou_payment_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting Trust Line Token payment transaction...")
|
||||
iou_pay_response = submit_and_wait(iou_payment_tx, client, issuer)
|
||||
|
||||
if iou_pay_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"Trust Line Token payment failed: {iou_pay_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
print(f"Successfully sent 5000 {currency_code} tokens.")
|
||||
|
||||
# Escrow Creator creates a timed trust line token escrow ----------------------
|
||||
print("\n=== Creating Timed Trust Line Token Escrow ===\n")
|
||||
delay = 10 # seconds
|
||||
now = datetime.now(tz=UTC)
|
||||
finish_after = now + timedelta(seconds=delay)
|
||||
finish_after_ripple_time = datetime_to_ripple_time(finish_after)
|
||||
mature_time = finish_after.astimezone().strftime("%m/%d/%Y, %I:%M:%S %p")
|
||||
print(f"Escrow will mature after: {mature_time}\n")
|
||||
|
||||
iou_cancel_after = now + timedelta(seconds=300)
|
||||
iou_cancel_after_ripple_time = datetime_to_ripple_time(iou_cancel_after)
|
||||
|
||||
iou_escrow_create_tx = EscrowCreate(
|
||||
account=creator.address,
|
||||
destination=issuer.address,
|
||||
amount=IssuedCurrencyAmount(
|
||||
currency=currency_code,
|
||||
value="1000",
|
||||
issuer=issuer.address,
|
||||
),
|
||||
finish_after=finish_after_ripple_time,
|
||||
cancel_after=iou_cancel_after_ripple_time,
|
||||
)
|
||||
|
||||
print(json.dumps(iou_escrow_create_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting Trust Line Token EscrowCreate transaction...")
|
||||
iou_escrow_response = submit_and_wait(iou_escrow_create_tx, client, creator)
|
||||
|
||||
if iou_escrow_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"Trust Line Token EscrowCreate failed: {iou_escrow_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
|
||||
# Save the sequence number to identify the escrow later
|
||||
iou_escrow_seq = iou_escrow_response.result["tx_json"]["Sequence"]
|
||||
print(f"Trust Line Token escrow created. Sequence: {iou_escrow_seq}")
|
||||
|
||||
# Wait for the escrow to mature, then finish it --------------------
|
||||
print("\n=== Waiting For Timed Trust Line Token Escrow to Mature ===\n")
|
||||
|
||||
# Countdown delay until escrow matures
|
||||
for i in range(delay, -1, -1):
|
||||
print(f"Waiting for escrow to mature... {i}s remaining...", end="\r", flush=True)
|
||||
sleep(1)
|
||||
print("Waiting for escrow to mature... done. ")
|
||||
|
||||
# Confirm latest validated ledger close time is after the finish_after time
|
||||
escrow_ready = False
|
||||
while not escrow_ready:
|
||||
validated_ledger = client.request(Ledger(ledger_index="validated"))
|
||||
ledger_close_time = validated_ledger.result["ledger"]["close_time"]
|
||||
ledger_close_local = ripple_time_to_datetime(ledger_close_time).astimezone().strftime("%m/%d/%Y, %I:%M:%S %p")
|
||||
print(f"Latest validated ledger closed at: {ledger_close_local}")
|
||||
if ledger_close_time > finish_after_ripple_time:
|
||||
escrow_ready = True
|
||||
print("Escrow confirmed ready to finish.")
|
||||
else:
|
||||
time_difference = finish_after_ripple_time - ledger_close_time
|
||||
if time_difference == 0:
|
||||
time_difference = 1
|
||||
print(f"Escrow needs to wait another {time_difference}s.")
|
||||
sleep(time_difference)
|
||||
|
||||
# Finish the timed trust line token escrow --------------------
|
||||
print("\n=== Finishing Timed Trust Line Token Escrow ===\n")
|
||||
iou_escrow_finish_tx = EscrowFinish(
|
||||
account=creator.address,
|
||||
owner=creator.address,
|
||||
offer_sequence=iou_escrow_seq,
|
||||
)
|
||||
|
||||
print(json.dumps(iou_escrow_finish_tx.to_xrpl(), indent=2))
|
||||
|
||||
print("\nSubmitting EscrowFinish transaction...")
|
||||
iou_finish_response = submit_and_wait(iou_escrow_finish_tx, client, creator)
|
||||
|
||||
if iou_finish_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"Trust Line Token EscrowFinish failed: {iou_finish_response.result['meta']['TransactionResult']}")
|
||||
exit(1)
|
||||
print(f"Timed Trust Line Token escrow finished successfully: https://testnet.xrpl.org/transactions/{iou_finish_response.result['hash']}")
|
||||
@@ -20,7 +20,7 @@ This vulnerability disclosure report contains technical details of the XRP Ledge
|
||||
|
||||
## Summary of Vulnerability
|
||||
|
||||
Two vulnerabilities that affected XRPL's liveness were discovered by **Common Prefix**, which could have prevented the network from making forward progress. A UNL validator would need to have been compromised in order to exploit the bugs. Fixes for both vulnerabilities were released as part of version 3.0.0 of rippled.
|
||||
Two vulnerabilities that affected XRPL's liveness were discovered by **[Common Prefix](https://www.commonprefix.com)**, which could have prevented the network from making forward progress. A UNL validator would need to have been compromised in order to exploit the bugs. Fixes for both vulnerabilities were released as part of version 3.0.0 of rippled.
|
||||
|
||||
## Impact
|
||||
|
||||
@@ -30,7 +30,7 @@ If a UNL validator had been compromised before these vulnerabilities were fixed,
|
||||
|
||||
### Discovery
|
||||
|
||||
**Nikolaos Kamarinakis** from **Common Prefix** reported the vulnerabilities via a responsible disclosure report. Ripple engineering teams validated the report with an independent proof-of-concept that reproduced both bugs in a separate network.
|
||||
**Nikolaos Kamarinakis**, **Dejan Cabrilo**, **Dr. Dimitris Karakostas**, and **Prof. Zeta Avarikioti** from **Common Prefix** reported the vulnerabilities via a responsible disclosure report. Ripple engineering teams validated the report with an independent proof-of-concept that reproduced both bugs in a separate network.
|
||||
|
||||
### Root Cause
|
||||
|
||||
|
||||
@@ -1372,6 +1372,61 @@ const events = [
|
||||
image: require("../static/img/events/meetup-london.png"),
|
||||
end_date: "February 18, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRP Ledger Meetup Poland: The Builder's Foundation",
|
||||
description:
|
||||
"XRPL Commons brings its ecosystem to Warsaw with a first pilot event alongside Neti. Join us at EXPO XXI during Polish Blockchain Week for a high-impact session with developers and founders shaping the next wave of the Agentic Web.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/boucntsh",
|
||||
location: "Warsaw, Poland",
|
||||
date: "March 24, 2026",
|
||||
image: require("../static/img/events/xrp-ledger-meetup-poland.jpg"),
|
||||
end_date: "March 24, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRPL Aquarium Demo Day #8 Social Impact",
|
||||
description:
|
||||
"The Aquarium Residency is a 12-week program for entrepreneurs & developers building on the XRP Ledger blockchain. Join us at our Paris HQ to connect with our residents, discover their projects focused on Social Impact, and engage with the XRPL community.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/2feub5uj",
|
||||
location: "Paris, France",
|
||||
date: "March 25, 2026",
|
||||
image: require("../static/img/events/aquarium-demo-day-8.jpg"),
|
||||
end_date: "March 25, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRPL & GDF Stablecoins Round table",
|
||||
description:
|
||||
"Invite-only Executive Roundtable on Stablecoins in Paris, hosted by XRPL Commons and Global Digital Finance. Senior leaders from finance, policy, and tech will explore institutional use cases, regulation, and real-world deployments shaping the future of digital money.",
|
||||
type: "conference",
|
||||
link: "https://luma.com/tgg0id1d",
|
||||
location: "Paris, France",
|
||||
date: "April 7, 2026",
|
||||
image: require("../static/img/events/gdf-stablecoins-roundtable.jpg"),
|
||||
end_date: "April 7, 2026",
|
||||
},
|
||||
{
|
||||
name: "HACK THE BLOCK 2026 Paris Blockchain Week XRPL Hackathon",
|
||||
description:
|
||||
"The flagship hackathon of Paris Blockchain Week, a 36-hour sprint where developers from around the world turn ideas into real on-chain solutions, backed by leading ecosystems and judged by Europe's top tech entrepreneurs and investors. Teams don't just compete, they get discovered.",
|
||||
type: "hackathon",
|
||||
link: "https://luma.com/Hacktheblock2026-PBW-XRPL",
|
||||
location: "Paris, France",
|
||||
date: "April 11, 2026",
|
||||
image: require("../static/img/events/hack-the-block-2026.jpg"),
|
||||
end_date: "April 12, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRPL Zone Paris",
|
||||
description:
|
||||
"Connecting XRPL Builders at Paris Blockchain Week 2026. XRPL Commons invites core ecosystem projects & builders to gather at our headquarters in the heart of Paris for a special edition of XRPL Zone Paris.",
|
||||
type: "zone",
|
||||
link: "https://luma.com/780xhfr7",
|
||||
location: "Paris, France",
|
||||
date: "April 14, 2026",
|
||||
image: require("../static/img/events/xrpl-zone-paris.jpg"),
|
||||
end_date: "April 14, 2026",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1453,33 +1508,33 @@ export default function Events() {
|
||||
<div className="pr-2 col">
|
||||
<img
|
||||
alt="xrp ledger events hero"
|
||||
src={require("../static/img/events/xrpl-hero.png")}
|
||||
src={require("../static/img/events/xrp-community-night-paris.png")}
|
||||
className="w-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-5 pr-2 col">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="mb-8 h4 h2-sm">
|
||||
{translate("XRP Community Night Denver")}
|
||||
{translate("XRP Community Night Paris")}
|
||||
</h2>
|
||||
<h6 className="mb-3 eyebrow">{translate("Save the Date")}</h6>
|
||||
</div>
|
||||
<p className="mb-4">
|
||||
{translate(
|
||||
"Attending ETHDenver? Join us for an evening with the XRP community in Denver. Connect with the users, builders and projects innovating with and utilizing XRP."
|
||||
"Attending Paris Blockchain Week? Join us for an evening with the XRP community in Paris. Connect with the users, builders and projects innovating with and utilizing XRP."
|
||||
)}
|
||||
</p>
|
||||
<div className=" my-3 event-small-gray">
|
||||
{translate("Location: Denver, CO")}
|
||||
{translate("Location: Paris, France")}
|
||||
</div>
|
||||
<div className="py-2 my-3 event-small-gray">
|
||||
{translate("February 18, 2026")}
|
||||
{translate("April 15, 2026")}
|
||||
</div>
|
||||
<div className="d-lg-block">
|
||||
<a
|
||||
className="btn btn-primary btn-arrow-out"
|
||||
target="_blank"
|
||||
href="https://luma.com/chz145tf?utm_source=xprlorg"
|
||||
href="https://luma.com/wnkqmmqy?utm_source=xprlorg"
|
||||
>
|
||||
{translate("Register Now")}
|
||||
</a>
|
||||
|
||||
@@ -25,8 +25,6 @@ Provisionally issue a [credential](../../../../concepts/decentralized-storage/cr
|
||||
|
||||
{% raw-partial file="/docs/_snippets/tx-fields-intro.md" /%}
|
||||
|
||||
In addition to the [common fields][], CredentialCreate transactions use the following fields:
|
||||
|
||||
| Field | JSON Type | [Internal Type][] | Required? | Description |
|
||||
|:-----------------|:---------------------|:------------------|:----------|:------------|
|
||||
| `Subject` | String - [Address][] | AccountID | Yes | The subject of the credential. |
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
301
docs/tutorials/payments/send-fungible-token-escrows.md
Normal file
301
docs/tutorials/payments/send-fungible-token-escrows.md
Normal file
@@ -0,0 +1,301 @@
|
||||
---
|
||||
seo:
|
||||
description: Create and finish escrows that hold fungible tokens (MPTs and trust line tokens) on the XRP Ledger.
|
||||
metadata:
|
||||
indexPage: true
|
||||
labels:
|
||||
- Escrow
|
||||
---
|
||||
|
||||
# Send Fungible Token Escrows
|
||||
|
||||
This tutorial shows you how to create and finish escrows that hold fungible tokens on the XRP Ledger. It covers two types of fungible token escrows:
|
||||
|
||||
- **Conditional MPT escrow**: An escrow holding [Multi-Purpose Tokens](../../concepts/tokens/fungible-tokens/multi-purpose-tokens.md) that is released when a crypto-condition is fulfilled.
|
||||
- **Timed trust line token escrow**: An escrow holding [trust line tokens](../../concepts/tokens/fungible-tokens/trust-line-tokens.md) that is released after a specified time.
|
||||
|
||||
{% admonition type="info" name="Note" %}
|
||||
Though this tutorial covers these two specific scenarios, both fungible token types can be used in either conditional or timed escrows.
|
||||
{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="TokenEscrow" /%}
|
||||
|
||||
|
||||
## Goals
|
||||
|
||||
By the end of this tutorial, you will be able to:
|
||||
|
||||
- Issue an MPT with escrow support enabled.
|
||||
- Create and finish a conditional escrow that holds MPTs.
|
||||
- Enable trust line token escrows on an issuer account.
|
||||
- Create and finish a timed escrow that holds trust line tokens.
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To complete this tutorial, you should:
|
||||
|
||||
- Have a basic understanding of the XRP Ledger.
|
||||
- Have an XRP Ledger client library installed. This page provides examples for the following:
|
||||
- **JavaScript** with the [xrpl.js library][]. See [Get Started Using JavaScript][] for setup steps.
|
||||
- **Python** with the [xrpl-py library][]. See [Get Started Using Python][] for setup steps.
|
||||
- **Go** with the [xrpl-go library][]. See [Get Started Using Go][] for setup steps.
|
||||
|
||||
|
||||
## Source Code
|
||||
|
||||
You can find the complete source code for this tutorial's examples in the {% repo-link path="_code-samples/escrow/" %}code samples section of this website's repository{% /repo-link %}.
|
||||
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Install dependencies
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
From the code sample folder, use `npm` to install dependencies.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
From the code sample folder, set up a virtual environment and use `pip` to install dependencies.
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
From the code sample folder, use `go` to install dependencies.
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 2. Set up client and fund accounts
|
||||
|
||||
Import the necessary libraries, instantiate a client to connect to the XRPL, and fund two new accounts (**Issuer** and **Escrow Creator**). This example imports:
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
- `xrpl`: Used for XRPL client connection, transaction submission, and wallet handling.
|
||||
- `five-bells-condition` and `crypto`: Used to generate a crypto-condition.
|
||||
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" before="// ====== Conditional MPT Escrow ======" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
- `xrpl`: Used for XRPL client connection, transaction submission, and wallet handling.
|
||||
- `json`: Used for loading and formatting JSON data.
|
||||
- `os` and `cryptoconditions`: Used to generate a crypto-condition.
|
||||
- `datetime` and `time`: Used for time calculations.
|
||||
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" before="# ====== Conditional MPT Escrow ======" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
- `xrpl-go`: Used for XRPL client connection, transaction submission, and wallet handling.
|
||||
- `encoding/json`, `strings`, and `fmt`: Used for formatting and printing results to the console.
|
||||
- `cryptoconditions`, `crypto/rand` and `encoding/hex`: Used to generate a crypto-condition.
|
||||
- `time`: Used for time calculations.
|
||||
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" before="// ====== Conditional MPT Escrow ======" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 3. Issue an MPT with escrow support
|
||||
|
||||
Construct an [MPTokenIssuanceCreate transaction][] with the `tfMPTCanEscrow` flag, which enables the token to be held in escrows. Then, retrieve the MPT issuance ID from the transaction result. This example creates an escrow that sends MPTs back to the original issuer. If you wanted to create an escrow for another account, the issuer would also have to set the `tfMPTCanTransfer` flag.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// ====== Conditional MPT Escrow ======" before="// Escrow Creator authorizes the MPT" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# ====== Conditional MPT Escrow ======" before="# Escrow Creator authorizes the MPT" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// ====== Conditional MPT Escrow ======" before="// Escrow Creator authorizes the MPT" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 4. Authorize the MPT
|
||||
|
||||
Before the escrow creator can hold the MPT, they must indicate their willingness to hold it with the [MPTokenAuthorize transaction][].
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Escrow Creator authorizes the MPT" before="// Issuer sends MPTs to escrow creator" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Escrow Creator authorizes the MPT" before="# Issuer sends MPTs to escrow creator" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Escrow Creator authorizes the MPT" before="// Issuer sends MPTs to escrow creator" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 5. Send MPTs to the escrow creator
|
||||
|
||||
Send MPTs from the issuer to the escrow creator using a [Payment transaction][].
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Issuer sends MPTs to escrow creator" before="// Escrow Creator creates a conditional MPT escrow" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Issuer sends MPTs to escrow creator" before="# Escrow Creator creates a conditional MPT escrow" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Issuer sends MPTs to escrow creator" before="// Escrow Creator creates a conditional MPT escrow" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 6. Create a condition and fulfillment
|
||||
|
||||
Conditional escrows require a fulfillment and its corresponding condition in the format of a PREIMAGE-SHA-256 _crypto-condition_, represented as hexadecimal. To calculate these in the correct format, use a crypto-conditions library. Generally, you want to generate the fulfillment using at least 32 random bytes from a cryptographically secure source of randomness.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Escrow Creator creates a conditional MPT escrow" before="// Set expiration" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Escrow Creator creates a conditional MPT escrow" before="# Set expiration" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Escrow Creator creates a conditional MPT escrow" before="// Set expiration" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 7. Create the conditional MPT escrow
|
||||
|
||||
Create a conditional escrow using the generated crypto-condition. Fungible token escrows require an expiration date. This example sets an expiration time of five minutes. After creating the escrow, save the sequence number to reference it later.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Set expiration" before="// Finish the conditional MPT escrow" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Set expiration" before="# Finish the conditional MPT escrow" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Set expiration" before="// Finish the conditional MPT escrow" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 8. Finish the conditional MPT escrow
|
||||
|
||||
Finish the escrow by providing the original condition and its matching fulfillment.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Finish the conditional MPT escrow" before="// ====== Timed Trust Line Token Escrow ======" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Finish the conditional MPT escrow" before="# ====== Timed Trust Line Token Escrow ======" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Finish the conditional MPT escrow" before="// ====== Timed Trust Line Token Escrow ======" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 9. Enable trust line token escrows
|
||||
|
||||
Token issuers enable trust line token escrows differently from MPTs. Unlike MPTs, which are escrowable at the token level, trust line tokens are escrowable at the account level. When an issuer enables the `asfAllowTrustLineLocking` flag on their account, _all_ trust line tokens issued from that account are escrowable.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// ====== Timed Trust Line Token Escrow ======" before="// Escrow Creator sets up a trust line" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# ====== Timed Trust Line Token Escrow ======" before="# Escrow Creator sets up a trust line" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// ====== Timed Trust Line Token Escrow ======" before="// Escrow Creator sets up a trust line" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 10. Set up a trust line
|
||||
|
||||
Establish a trust line between the escrow creator and issuer using the [TrustSet transaction][]. The escrow creator submits this transaction to indicate their willingness to receive the token, defining the currency and maximum amount they're willing to hold.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Escrow Creator sets up a trust line" before="// Issuer sends IOU tokens to creator" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Escrow Creator sets up a trust line" before="# Issuer sends IOU tokens to creator" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Escrow Creator sets up a trust line" before="// Issuer sends IOU tokens to creator" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 11. Send IOU tokens to the escrow creator
|
||||
|
||||
Send IOU tokens from the issuer to the escrow creator using a [Payment transaction][].
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Issuer sends IOU tokens to creator" before="// Escrow Creator creates a timed trust line token escrow" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Issuer sends IOU tokens to creator" before="# Escrow Creator creates a timed trust line token escrow" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Issuer sends IOU tokens to creator" before="// Escrow Creator creates a timed trust line token escrow" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 12. Create a timed trust line token escrow
|
||||
|
||||
To make a timed escrow, set the maturity time of the escrow, which is a timestamp formatted as [seconds since the Ripple Epoch][]. This example sets a maturity time of ten seconds from the time the code executes. Since it is a fungible token escrow, it also sets an expiration time of five minutes. After submitting the [EscrowCreate transaction][], save the sequence number from the transaction result.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Escrow Creator creates a timed trust line token escrow" before="// Wait for the escrow to mature" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Escrow Creator creates a timed trust line token escrow" before="# Wait for the escrow to mature" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Escrow Creator creates a timed trust line token escrow" before="// Wait for the escrow to mature" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 13. Wait for escrow to mature and finish
|
||||
|
||||
Wait for the escrow to mature. Before submitting the [EscrowFinish][] transaction, the code checks the current validated ledger to confirm the close time is after the escrow maturation time. This check ensures the escrow is matured on a validated ledger before trying to finish it.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="JavaScript" %}
|
||||
{% code-snippet file="/_code-samples/escrow/js/sendFungibleTokenEscrow.js" language="js" from="// Wait for the escrow to mature" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Python" %}
|
||||
{% code-snippet file="/_code-samples/escrow/py/send_fungible_token_escrow.py" language="py" from="# Wait for the escrow to mature" /%}
|
||||
{% /tab %}
|
||||
{% tab label="Go" %}
|
||||
{% code-snippet file="/_code-samples/escrow/go/send-fungible-token-escrow/main.go" language="go" from="// Wait for the escrow to mature" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
|
||||
## See Also
|
||||
|
||||
**Concepts**:
|
||||
- [Escrow](../../concepts/payment-types/escrow.md)
|
||||
- [Multi-Purpose Tokens](../../concepts/tokens/fungible-tokens/multi-purpose-tokens.md)
|
||||
- [Trust Line Tokens](../../concepts/tokens/fungible-tokens/trust-line-tokens.md)
|
||||
|
||||
**Tutorials**:
|
||||
- [Look up Escrows](./look-up-escrows.md)
|
||||
- [Cancel an Expired Escrow](./cancel-an-expired-escrow.md)
|
||||
|
||||
**References**:
|
||||
- [EscrowCreate transaction][]
|
||||
- [EscrowFinish transaction][]
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
276
package-lock.json
generated
276
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.22.2",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@redocly/realm": "0.131.2",
|
||||
@@ -458,15 +458,6 @@
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands/node_modules/@codemirror/state": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
|
||||
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
@@ -575,9 +566,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
|
||||
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
|
||||
"version": "6.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
|
||||
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
@@ -620,9 +611,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
|
||||
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
@@ -641,9 +632,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz",
|
||||
"integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==",
|
||||
"version": "6.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz",
|
||||
"integrity": "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.6.0",
|
||||
@@ -652,15 +643,6 @@
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view/node_modules/@codemirror/state": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
|
||||
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -1534,9 +1516,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@libsql/darwin-arm64": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.22.tgz",
|
||||
"integrity": "sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz",
|
||||
"integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1547,9 +1529,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/darwin-x64": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.22.tgz",
|
||||
"integrity": "sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz",
|
||||
"integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1591,9 +1573,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@libsql/linux-arm-gnueabihf": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.22.tgz",
|
||||
"integrity": "sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz",
|
||||
"integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1604,9 +1586,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-arm-musleabihf": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.22.tgz",
|
||||
"integrity": "sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz",
|
||||
"integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1617,9 +1599,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-arm64-gnu": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.22.tgz",
|
||||
"integrity": "sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz",
|
||||
"integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1630,9 +1612,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-arm64-musl": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.22.tgz",
|
||||
"integrity": "sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz",
|
||||
"integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1643,9 +1625,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-x64-gnu": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.22.tgz",
|
||||
"integrity": "sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz",
|
||||
"integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1656,9 +1638,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-x64-musl": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.22.tgz",
|
||||
"integrity": "sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz",
|
||||
"integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1669,9 +1651,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/win32-x64-msvc": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.22.tgz",
|
||||
"integrity": "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz",
|
||||
"integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3361,9 +3343,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||
"version": "4.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.8.tgz",
|
||||
"integrity": "sha512-9Rr+liiBmK4xzZHszL+twNRJApthqmITBwDP3emNTtTrkBFN4gHlqfp+nodKmoVt1+bUH1qQCtyqt+7dbDTHiw==",
|
||||
"version": "4.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz",
|
||||
"integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
@@ -3388,21 +3370,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-theme-material": {
|
||||
"version": "4.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-material/-/codemirror-theme-material-4.25.8.tgz",
|
||||
"integrity": "sha512-VlSkd2ZgQ9YXkW0x3fPssyngrPVhy6XAx84wWO55yU/VQHGIHxTr1/0gF2eEcKOy4M/7aQ4EtYzAVVJlSvrUoA==",
|
||||
"version": "4.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-material/-/codemirror-theme-material-4.25.9.tgz",
|
||||
"integrity": "sha512-6f2x+gmj2hHagqy6VkpnPbK7SWyP6kKruGgqpyIy09/f9pAUCqkW8mRY5ZEr28tA+YEGQaSY0Z2IBCHl8OKJog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@uiw/codemirror-themes": "4.25.8"
|
||||
"@uiw/codemirror-themes": "4.25.9"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-theme-material/node_modules/@uiw/codemirror-themes": {
|
||||
"version": "4.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.8.tgz",
|
||||
"integrity": "sha512-U6ZSO9A+nsN8zvNddtwhxxpi33J9okb4Li9HdhAItApKjYM22IgC8XSpGfs+ABGfsp1u6NhDSfBR9vAh3oTWXg==",
|
||||
"version": "4.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.9.tgz",
|
||||
"integrity": "sha512-DAHKb/L9ELwjY4nCf/MP/mIllHOn4GQe7RR4x8AMJuNeh9nGRRoo1uPxrxMmUL/bKqe6kDmDbIZ2AlhlqyIJuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
@@ -3438,16 +3420,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/react-codemirror": {
|
||||
"version": "4.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.8.tgz",
|
||||
"integrity": "sha512-A0aLOuJZm2yJ+U9GlMFwxwFciztjd5LhcAG4SMqFxdD58wH+sCQXuY4UU5J2hqgS390qAlShtUgREvJPUonbuQ==",
|
||||
"version": "4.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz",
|
||||
"integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"@codemirror/commands": "^6.1.0",
|
||||
"@codemirror/state": "^6.1.1",
|
||||
"@codemirror/theme-one-dark": "^6.0.0",
|
||||
"@uiw/codemirror-extensions-basic-setup": "4.25.8",
|
||||
"@uiw/codemirror-extensions-basic-setup": "4.25.9",
|
||||
"codemirror": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3527,6 +3509,7 @@
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||
"deprecated": "this version has critical issues, please update to the latest version",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -3557,12 +3540,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz",
|
||||
"integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==",
|
||||
"version": "12.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz",
|
||||
"integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.75",
|
||||
"@xyflow/system": "0.0.76",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
@@ -3572,9 +3555,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.75",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.75.tgz",
|
||||
"integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==",
|
||||
"version": "0.0.76",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz",
|
||||
"integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
@@ -3799,14 +3782,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
|
||||
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
@@ -3850,9 +3833,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.8",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
|
||||
"integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
|
||||
"version": "2.10.13",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
|
||||
"integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
@@ -3940,9 +3923,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
@@ -3964,9 +3947,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
||||
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3983,11 +3966,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
"electron-to-chromium": "^1.5.328",
|
||||
"node-releases": "^2.0.36",
|
||||
"update-browserslist-db": "^1.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -4083,9 +4066,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001780",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
|
||||
"integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
|
||||
"version": "1.0.30001784",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz",
|
||||
"integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -4837,9 +4820,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.321",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz",
|
||||
"integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==",
|
||||
"version": "1.5.331",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
||||
"integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -6118,9 +6101,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/libsql": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.22.tgz",
|
||||
"integrity": "sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA==",
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz",
|
||||
"integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64",
|
||||
@@ -6138,15 +6121,15 @@
|
||||
"detect-libc": "2.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@libsql/darwin-arm64": "0.5.22",
|
||||
"@libsql/darwin-x64": "0.5.22",
|
||||
"@libsql/linux-arm-gnueabihf": "0.5.22",
|
||||
"@libsql/linux-arm-musleabihf": "0.5.22",
|
||||
"@libsql/linux-arm64-gnu": "0.5.22",
|
||||
"@libsql/linux-arm64-musl": "0.5.22",
|
||||
"@libsql/linux-x64-gnu": "0.5.22",
|
||||
"@libsql/linux-x64-musl": "0.5.22",
|
||||
"@libsql/win32-x64-msvc": "0.5.22"
|
||||
"@libsql/darwin-arm64": "0.5.29",
|
||||
"@libsql/darwin-x64": "0.5.29",
|
||||
"@libsql/linux-arm-gnueabihf": "0.5.29",
|
||||
"@libsql/linux-arm-musleabihf": "0.5.29",
|
||||
"@libsql/linux-arm64-gnu": "0.5.29",
|
||||
"@libsql/linux-arm64-musl": "0.5.29",
|
||||
"@libsql/linux-x64-gnu": "0.5.29",
|
||||
"@libsql/linux-x64-musl": "0.5.29",
|
||||
"@libsql/win32-x64-msvc": "0.5.29"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
@@ -6165,9 +6148,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
@@ -6599,9 +6582,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.36",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
|
||||
"integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
|
||||
"version": "2.0.37",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
|
||||
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
@@ -6727,9 +6710,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/openapi-sampler/node_modules/fast-xml-parser": {
|
||||
"version": "5.5.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz",
|
||||
"integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==",
|
||||
"version": "5.5.9",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz",
|
||||
"integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6739,8 +6722,8 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.1.3",
|
||||
"strnum": "^2.1.2"
|
||||
"path-expression-matcher": "^1.2.0",
|
||||
"strnum": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
@@ -6838,9 +6821,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
|
||||
"integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
|
||||
"integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6986,10 +6969,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.0",
|
||||
@@ -7453,9 +7439,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react18-json-view": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/react18-json-view/-/react18-json-view-0.2.9.tgz",
|
||||
"integrity": "sha512-z3JQgCwZRKbmWh54U94loCU6vE0ZoDBK7C8ZpcMYQB8jYMi+mR/fcgMI9jKgATeF0I6+OAF025PD+UKkXIqueQ==",
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/react18-json-view/-/react18-json-view-0.2.10.tgz",
|
||||
"integrity": "sha512-rYEbaCG/U4THY1qp1xY14/Kbnp9yY3W6Qm3Rmu+jlCIdxzMS5EcD+wI97kCKRoN3CuJyJU8hqkax5xWfl8A4EA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-to-clipboard": "^3.3.3"
|
||||
@@ -7907,18 +7893,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/slugify": {
|
||||
"version": "1.6.8",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.8.tgz",
|
||||
"integrity": "sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA==",
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz",
|
||||
"integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz",
|
||||
"integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
|
||||
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@@ -8060,9 +8046,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
|
||||
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
|
||||
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -8746,9 +8732,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.22.2",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@redocly/realm": "0.131.2",
|
||||
|
||||
@@ -251,6 +251,7 @@
|
||||
- page: docs/tutorials/payments/create-trust-line-send-currency-in-python.md
|
||||
- page: docs/tutorials/payments/send-a-conditional-escrow.md
|
||||
- page: docs/tutorials/payments/send-a-timed-escrow.md
|
||||
- page: docs/tutorials/payments/send-fungible-token-escrows.md
|
||||
- page: docs/tutorials/payments/look-up-escrows.md
|
||||
- page: docs/tutorials/payments/cancel-an-expired-escrow.md
|
||||
- page: docs/tutorials/payments/send-a-check.md
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
static/img/events/aquarium-demo-day-8.jpg
Normal file
BIN
static/img/events/aquarium-demo-day-8.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
static/img/events/gdf-stablecoins-roundtable.jpg
Normal file
BIN
static/img/events/gdf-stablecoins-roundtable.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
static/img/events/hack-the-block-2026.jpg
Normal file
BIN
static/img/events/hack-the-block-2026.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
static/img/events/xrp-community-night-paris.png
Normal file
BIN
static/img/events/xrp-community-night-paris.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
BIN
static/img/events/xrp-ledger-meetup-poland.jpg
Normal file
BIN
static/img/events/xrp-ledger-meetup-poland.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
static/img/events/xrpl-zone-paris.jpg
Normal file
BIN
static/img/events/xrpl-zone-paris.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -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
489
styles/_tutorials.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user