Compare commits

...

68 Commits

Author SHA1 Message Date
Oliver Eggert
601e79ed00 add send fungible token escrows tutorial 2026-04-03 16:51:50 -07:00
Oliver Eggert
e05aa8259b fix comments for code-snippet references 2026-04-03 16:03:05 -07:00
Oliver Eggert
b7be1f878e remove numbered steps from code comments 2026-04-03 13:31:12 -07:00
Oliver Eggert
ac60d7786b add go code sample and readme 2026-04-03 12:47:06 -07:00
Oliver Eggert
394eb4b5d4 add intro comment to code sample files 2026-04-03 12:45:18 -07:00
Oliver Eggert
cd4bb02ae2 update js and py readmes to explain what script does 2026-04-03 12:44:28 -07:00
Oliver Eggert
a309eb51c5 add send_fungible_token_escrow.py and update readme.md 2026-04-03 00:06:17 -07:00
Oliver Eggert
578e8cdefc clean up variable names 2026-04-02 22:48:35 -07:00
Oliver Eggert
c5b161c746 update cancelafter and finishafter to use same now() time 2026-04-02 22:23:40 -07:00
Oliver Eggert
f493ca49cf add token escrow js code sample 2026-04-02 21:15:10 -07:00
Rome Reginelli
201a250072 Merge pull request #3543 from XRPLF/redocly_131
Upgrade Redocly to 0.131.2
2026-03-31 09:58:31 -07:00
rachelflynn
c04c9042b8 Merge pull request #3547 from rachelflynn/fix-hot-topics-links
Fix: Make community icons and text clickable links
2026-03-31 10:12:17 -04:00
Maria Shodunke
7f7903a336 Merge pull request #3566 from XRPLF/fix-link-to-issue-mpt-tutorial
Fix link to Issue an MPT Tutorial
2026-03-27 16:48:53 +00:00
Maria Shodunke
78da4d6f3b Fix link to Issue an MPT Tutorial
Minor link fix. Must have been missed when we did the Tutorial revamp.
2026-03-27 11:48:59 +00:00
oeggert
17abd49d92 Merge pull request #3507 from XRPLF/go-code-samples
Add Go code samples/tutorials for Lending Protocol
2026-03-25 10:16:58 -07:00
Oliver Eggert
d0c622abf4 update xrpl-go version and lending-setup code 2026-03-24 13:44:16 -07:00
Amarantha Kulkarni
0ed490da0b Merge pull request #3553 from XRPLF/fix_ammclawback_xrp
Fix AMMClawback asset fields
2026-03-24 13:33:18 -07:00
Amarantha Kulkarni
5fe416654f Merge pull request #3552 from XRPLF/amarantha-k-patch-1
Revert change so Redocly doesn't ignore CODE-OF-CONDUCT.md
2026-03-24 12:21:28 -07:00
mDuo13
e0aeab7157 Remove problematic link from Code of Conduct 2026-03-24 11:44:25 -07:00
mDuo13
84987aa2df Fix AMMClawback asset fields (#3520) 2026-03-24 11:32:38 -07:00
Amarantha Kulkarni
175e0a96eb Revert change so Redocly doesn't ignore CODE-OF-CONDUCT.md 2026-03-24 11:29:39 -07:00
Rome Reginelli
d4be722dbc Merge pull request #3529 from XRPLF/new-delete-account-tutorial
Update deletion requirements & add new "Delete an Account" tutorial
2026-03-24 11:15:57 -07:00
mDuo13
839da237b6 Bump Redocly to Realm 0.131.2 & fix one more PluginInstance type 2026-03-24 11:12:30 -07:00
mDuo13
00e636c562 Bump Redocly to Realm 0.131.1 2026-03-24 10:43:20 -07:00
oeggert
3cb15342a7 Merge pull request #3548 from XRPLF/claim-context7
add json file to claim repo
2026-03-23 15:24:51 -07:00
Oliver Eggert
4346cf9a94 add json file to claim repo 2026-03-23 15:21:39 -07:00
rachelflynn
6638ea8f6c fix: make hot topics icons and text clickable links 2026-03-23 14:33:26 -04:00
Bart
476ab17cc5 Merge pull request #3546 from XRPLF/bthomee-patch-1
Create vulnerabilitydisclosurereport-bug-mar2026.md
2026-03-23 13:43:10 -04:00
Bart
7d0b02bae9 Update sidebars.yaml 2026-03-23 13:27:33 -04:00
Bart
7ab575804a Apply suggestions from code review
Co-authored-by: oeggert <117319296+oeggert@users.noreply.github.com>
2026-03-23 13:23:07 -04:00
Amarantha Kulkarni
2801055b14 Merge pull request #3532 from interc0der/add-honeycluster
chore: add honeycluster to the list of public servers
2026-03-23 09:57:30 -07:00
Bart
1d52bd8eaa Create vulnerabilitydisclosurereport-bug-mar2026.md
This blog post describes the two liveness bugs reported by Common Prefix last year.
2026-03-23 10:39:02 -04:00
Oliver Eggert
dbabe8171d add go sample to pay-off-a-loan tutorial 2026-03-20 18:31:46 -07:00
Oliver Eggert
66522b160e add go sample for manage-a-loan tutorial 2026-03-20 18:18:48 -07:00
Oliver Eggert
16b63f9c88 add go sample for deposit-and-withdraw-cover tutorial 2026-03-20 17:32:29 -07:00
Oliver Eggert
32b14d2a14 add go sample for create-a-loan 2026-03-20 17:17:14 -07:00
Oliver Eggert
72fb4710ec add go samples to create-a-loan-broker 2026-03-20 16:57:49 -07:00
Oliver Eggert
10431480f1 add Go samples to claw-back-cover tutorial 2026-03-20 16:32:31 -07:00
Oliver Eggert
c5f81cea25 update lending-setup script 2026-03-19 20:18:22 -07:00
Oliver Eggert
9393f40366 update loan flag check to use flag.Contains() 2026-03-19 19:26:37 -07:00
mDuo13
9f4fcc845f Update Redocly plugin syntax for 0.131+ 2026-03-19 12:26:23 -07:00
Rome Reginelli
d54684f45a Apply one more suggestion
Co-authored-by: oeggert <117319296+oeggert@users.noreply.github.com>
2026-03-19 10:44:00 -07:00
Oliver Eggert
c505b992e0 update create-loan and README entry 2026-03-18 22:03:37 -07:00
mDuo13
26c966fa51 Bump Redocly to Realm 0.131.0 2026-03-18 11:12:41 -07:00
Rome Reginelli
86618b900f Fix typo in Deleting Accounts intro 2026-03-17 16:05:16 -07:00
Rome Reginelli
b634b6902e Account deletion: Fix off-by-two errors & other suggestions
Co-authored-by: oeggert <117319296+oeggert@users.noreply.github.com>
2026-03-17 16:01:37 -07:00
Oliver Eggert
1540e36b8b update library to 0.1.16 2026-03-17 15:07:58 -07:00
Oliver Eggert
598a15eef5 add initial draft for create-loan and lending-setup 2026-03-17 14:59:52 -07:00
Oliver Eggert
7cd8e31a21 add reminder to update flag parsing in loan-manage 2026-03-17 14:59:52 -07:00
Oliver Eggert
e56324c57c add loan-pay go code samples and update readme 2026-03-17 14:59:52 -07:00
Oliver Eggert
5ce6218fd5 add go.sum to gitignore 2026-03-17 14:59:52 -07:00
Oliver Eggert
9934414492 add go.mod and go.sum 2026-03-17 14:59:52 -07:00
Oliver Eggert
ba7694f472 add loan-manage go code sample and update readme 2026-03-17 14:59:52 -07:00
Oliver Eggert
102a7cc8b9 add cover-deposit-and-withdraw go code and update readme 2026-03-17 14:59:52 -07:00
Oliver Eggert
0ac42f6acf add cover-clawback go code and update readme 2026-03-17 14:59:52 -07:00
Oliver Eggert
bc0bcfa89b clean up variable names in create-loan-broker 2026-03-17 14:59:52 -07:00
Oliver Eggert
f9b2bce755 add go code for create-loan-broker and update readme 2026-03-17 14:59:52 -07:00
Rome Reginelli
c53eddfbd8 Merge pull request #3541 from XRPLF/blog_rippled312
Add 3.1.2 release announcement & update link in 3.1.1 build instructions
2026-03-12 17:30:51 -07:00
mDuo13
1aac0d4fa2 Add 3.1.2 release announcement and update link in 3.1.1 build instructions 2026-03-12 17:25:10 -07:00
Rome Reginelli
a8e70dc49b Merge pull request #3540 from XRPLF/update_readme
Update README to remove outdated info & remove redundant translation files
2026-03-11 15:01:56 -07:00
mDuo13
bce839d6b3 Exclude code of conduct (false positive broken link) 2026-03-11 14:46:19 -07:00
mDuo13
295fbc8a4e Update package description 2026-03-11 14:02:27 -07:00
mDuo13
1a3b3d47ac Remove duplicate copies of Code of Conduct / Contributing translations 2026-03-11 13:58:55 -07:00
mDuo13
28c30fad41 Update README to remove outdated info 2026-03-11 13:56:13 -07:00
interc0der
b20b963c8c chore: add honeycluster to the list of public servers 2026-03-03 12:01:02 -08:00
mDuo13
91380d73e1 Update delete account tutorial with more deletion conditions 2026-03-02 13:09:25 -08:00
mDuo13
1ba708467b Update account deletion docs & code 2026-02-27 21:33:05 -08:00
mDuo13
d1adbd575a New 'Delete Account' tutorial & sample code
Delete account sample code: update JS & add Python

Add account deletion tutorial
2026-02-26 16:19:16 -08:00
56 changed files with 5990 additions and 747 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,14 +1,15 @@
// @ts-check
import { getInnerText } from '@redocly/realm/dist/server/plugins/markdown/markdoc/helpers/get-inner-text.js';
import { getInnerText } from '@redocly/realm/dist/markdoc/helpers/get-inner-text.js';
import { dirname, relative, join as joinPath } from 'path';
import markdoc from '@markdoc/markdoc';
import moment from "moment";
export function blogPosts() {
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
/** @type {import("@redocly/realm/dist/server/types").ExternalPlugin } */
const instance = {
id: 'blog-posts',
processContent: async (actions, { fs, cache }) => {
try {
const posts = [];

View File

@@ -1,12 +1,13 @@
// @ts-check
import { getInnerText } from '@redocly/realm/dist/server/plugins/markdown/markdoc/helpers/get-inner-text.js';
import { getInnerText } from '@redocly/realm/dist/markdoc/helpers/get-inner-text.js';
import { dirname, relative, join as joinPath } from 'path';
export function codeSamples() {
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
/** @type {import("@redocly/realm/dist/server/types").ExternalPlugin } */
const instance = {
id: 'code-samples',
processContent: async (actions, { fs, cache }) => {
try {
const samples = [];

View File

@@ -3,8 +3,9 @@ import { readSharedData } from '@redocly/realm/dist/server/utils/shared-data.js'
const INDEX_PAGE_INFO_DATA_KEY = 'index-page-items';
export function indexPages() {
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
/** @type {import("@redocly/realm/dist/server/types").ExternalPlugin } */
const instance = {
id: 'index-pages',
// hook that gets executed after all routes were created
async afterRoutesCreated(actions, { cache }) {
// get all the routes that are ind pages

View File

@@ -1,46 +0,0 @@
# コントリビューター行動規範
## 誓約
私たちコントリビューターとメンテナーは、オープンで友好的な環境を育むために、年齢、体格、身体障害、民族、性的特徴、性自認および性表現、経験の度合い、学歴、社会経済的地位、国籍、容姿、人種、宗教、性的同一性および性的指向などを問わず、誰もが私たちのプロジェクトとコミュニティーに不快な思いをすることなく参加できるよう努めることを誓います。
## 標準
前向きな環境を作り上げることに貢献する行動の例:
* 友好的で差別のない言葉の使用
* 異なる観点や経験の尊重
* 建設的な批判の素直な受け入れ
* コミュニティーにとっての最善への注力
* 他のコミュニティーメンバーへの共感の表示
前向きな環境を作り上げることに貢献しない行動の例:
* 性的な意味を含む言葉や画像の使用、望まない性的注目や誘いかけ
* あおり、侮辱的または軽蔑的なコメント、個人攻撃や政治攻撃
* 公的または私的な嫌がらせ
* 住所やメールアドレスなどの個人情報の、明確な許可なしでの公開
* 職場において不適切であると合理的に考えられる、その他の行為
## 責任
プロジェクトのメンテナーは、許容できる行動の基準を明確にする責任があります。また、許容できない行動がなされた場合に、適切かつ公平な是正処置を講じることが期待されます。
プロジェクトのメンテナーは、この行動規範に沿わないコメント、コミット、コード、wiki編集、issueなどの投稿を削除、編集、拒否する権利と義務を有します。また、他の不適切、脅迫的、攻撃的、嫌がらせと考えられる行動を取ったコントリビューターを一時的もしくは恒久的に追放する権利と義務を有します。
## 適用範囲
この行動規範はすべてのプロジェクトスペース内で適用されます。また、個人がパブリックスペースでプロジェクトやコミュニティーを代表する際にも適用されます。プロジェクトやコミュニティーを代表する際の例としては、プロジェクトの公式メールアドレスを使用すること、公式ソーシャルメディアアカウントで投稿すること、もしくはオンラインまたはオフラインのイベントで、任命された代表者を務めることが挙げられます。プロジェクトを代表する行為については、プロジェクトのメンテナーがさらに細かく定義して明確にすることができます。
## 執行
暴言、嫌がらせ、またはその他の許容できない行動は、プロジェクトチーム(<ripplex@ripple.com>)に連絡して報告することができます。すべての申し立ては確認、調査されたうえで、その状況に対して必要かつ適切と判断された対応が取られます。プロジェクトチームは、事象の報告者に関する秘密を保持する義務があります。特定の執行方針の詳細は、別途掲載される場合があります。
この行動規範を誠実に遵守または執行することができないプロジェクトのメンテナーは、プロジェクトを率いる他のメンバーの判断により、一時的または恒久的な措置が執られることがあります。
## 帰属
この行動規範は、[コントリビューター行動規範][ホームページ]バージョン1.4https://www.contributor-covenant.org/version/1/4/code-of-conduct.htmlから抜粋したものです。
[ホームページ]: https://www.contributor-covenant.org
この行動規範に関するよくある質問と回答については、https://www.contributor-covenant.org/faq をご覧ください。

View File

@@ -1,48 +0,0 @@
# Código de conducta del pacto de contribuidores
## Nuestro compromiso
Con el fin de fomentar un ambiente abierto y acogedor, nosotros, como contribuidores y mantenedores, nos comprometemos a hacer de la participación en nuestro proyecto y nuestra comunidad una experiencia libre de acoso para todos, independientemente de, entre otras, características como la edad, tamaño corporal, discapacidad, origen étnico, características sexuales, identidad y expresión de género, nivel de experiencia, educación, estatus socioeconómico, nacionalidad, apariencia personal, raza, religión o identidad y orientación sexual.
## Nuestros estándares
Ejemplos de comportamiento que contribuyen a crear un ambiente positivo incluyen:
* Utilizar lenguaje acogedor e inclusivo
* Ser respetuoso con los diferentes puntos de vista y experiencias
* Saber aceptar las críticas constructivas
* Centrarse en lo que es lo mejor para la comunidad
* Mostrar empatía hacia otros miembros de la comunidad
Ejemplos de comportamiento que no contribuyen a crear un ambiente positivo incluyen:
* Utilizar un lenguaje o imágenes sexualizadas y atención o insinuaciones sexuales no deseadas
* Trolear, comentario insultantes/peyorativos y ataques personales o políticos
* Acoso público o en privado
* Publicar información privada de otras personas, así cómo direcciones físicas o electrónicas, sin permiso explícito
* Cualquier otra conducta que pueda ser razonablemente considerada inapropiada en un sentido profesional
## Nuestras responsabilidades
Los mantenedores del proyecto son responsables de aclarar los estándares de comportamiento aceptable y se espera que tomen acciones correctivas justas y apropiadas en respuesta a cualquier caso de comportamiento inaceptable.
Los mantenedores del proyecto tienen el derecho y la responsaiblidad de eliminar, editar o rechazar comentarios, commits, código, ediciones de wiki, problemas y otras contribuciones que no estén alineadas con este Código de Conducta, o de expulsar temporal o definitivamente a cualquier colaborador por otros comportamientos que consideren inapropiados, amenazantes, ofensivos, dañinos o que viole de cualquier modo este Código de Conducta.
## Alcance
Este Código de Conducta aplica en todos los espacios del proyecto y también aplica cuando un individuo está representando el proyecto o su comunidad en espacios públicos. Ejemplos de representación de un proyecto o la comunidad incluye usar un correo electrónico oficial del proyecto, publicaciones a través de una cuenta oficial de redes sociales o actuar como representante asignado en un evento en línea o en la vida real. La representación de un proyecto debe ser definida y aclarada con más detalle por los mantenedores del proyecto.
## Aplicación
Los casos de comportamiento abusivo, acoso, o de cualquier otro modo inaceptable se pueden informar contactando con el equipo del proyecto al correo <ripplex@ripple.com>. Todas las quejas serán revisadas e investigadas y resultarán en una resupuesta que se considere adecuada y necesaria a las circunstancias. El equipo del proyecto está obligado a mantener la confidencialidad con respecto al informador del incidente. Podría darse el caso de publicar más detalles sobre políticas de comportamiento específicas.
Los mantenedores de proyecto que no sigan o hagan cumplir el Código de conducta de buena fe podrían enfrentarse a repercusiones temporales o definitivas según lo determinen otros miembros que lideren el proyecto.
## Atribución
Este Código de Conducta está adaptado de el [Pacto del Contribuidores][inicio], versión 1.4, disponible en https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[inicio]: https://www.contributor-covenant.org
Para respuestas a preguntas comunes sobre este código de conducta, visita
https://www.contributor-covenant.org/faq

View File

@@ -1,3 +0,0 @@
# Contribuir
Para obtener información sobre cómo contribuir a este repositorio, consulta [Contribute Documentation (XRPL.org)](https://xrpl.org/es_ES/contribute-documentation.html).

View File

@@ -1,3 +0,0 @@
# コントリビューション
コントリビューションの情報には[「ドキュメントへの貢献」](https://xrpl.org/ja/contribute-documentation.html)をご覧ください。

View File

@@ -1,12 +1,10 @@
# XRPL Dev Portal
The [XRP Ledger Dev Portal](https://xrpl.org) is the authoritative source for XRP Ledger documentation, including the `rippled` server, client libraries, and other open-source XRP Ledger software.
The [XRP Ledger Dev Portal](https://xrpl.org) is the authoritative source for XRP Ledger documentation, including the core server, client libraries, and other open-source XRP Ledger software.
The site is built and published using Redocly.
NOTE: The toolchain used to build and publish the site has recently been migrated from Dactyl to Redocly.
Before you proceed, make sure you have Node version >= 18 LTS.
Before you proceed, make sure you have Node.js and NPM installed. The site is tested with the current LTS release of each.
To build the site locally:
@@ -26,58 +24,18 @@ To build the site locally:
npm start
For more details, see the [contribution guidelines (EN)](CONTRIBUTING.md) ([日本語](CONTRIBUTING.ja.md)) and the [contributor Code of Conduct (EN)](CODE-OF-CONDUCT.md) ([日本語](CODE-OF-CONDUCT.ja.md)).
## Domain Verification Checker
If you make changes to the [Domain Verification Checker](https://xrpl.org/validator-domain-verifier.html) tool and edit the domain-verifier-checker.js file, you will need to do the following:
1. Install [webpack](https://webpack.js.org/) and required libraries via npm:
npm install webpack webpack-cli --save-dev
npm install ripple-binary-codec ripple-address-codec ripple-keypairs
2. From the project root directory (this step may be different depending on how you installed webpack)
cd assets/js
webpack-cli domain-verifier-checker.js --optimize-minimize -o domain-verifier-bundle.js
3. Build the site:
npm start
For more details, see the [contribution guidelines (EN)](CONTRIBUTING.md) ([日本語](@l10n/ja/CONTRIBUTING.md)) and the [contributor Code of Conduct (EN)](CODE-OF-CONDUCT.md) ([日本語](@l10n/ja/CODE-OF-CONDUCT.md)).
### Internationalization
### Localization / Translations
This repo includes English (en) and Japanese (ja) locales.
This is done by setting up the internationalization (@l10n) folders, adding the `i18n` configuration to your `redocly.yaml` file, and adding the translated content in the respective language directory under the @l10n directory.
To add support for a new language:
1. Create a new subdirectory in the @l10n directory of the portal. For example, to add support for Spanish, create a new subdirectory "es-ES".
2. Update the i18n configuration in your `redocly.yaml` file defining the display labels for the different languages you support.
l10n:
defaultLocale: en-US
locales:
- code: en-US
name: English
- code: ja
name: 日本語
- code: es-ES
name: Spanish
3. Add the translated content in the respective language directory under the @l10n directory.
The relative path from the language directory to the translated file must be the same as the relative path from the root of the portal to the file in the default language. For example, if you originally had a file with path `path/to/my/markdown.md`, the file translated to Spanish must be /`@l10n/es-ES/path/to/my/markdown.md`.
The documentation in this repository is created in English first, then translated into other languages by community contributors. Currently, only the Japanese translations are live on the site; Spanish translation efforts are incomplete and not actively used. For information on the process of adding and maintaining translated files, see [Translations](https://xrpl.org/resources/contribute-documentation/documentation-translations).
## Issues, Projects, and Project Boards
Use GitHub Issues under the [`xrpl-dev-portal`](https://github.com/XRPLF/xrpl-dev-portal) repository to report bugs, feature requests, and suggestions for the XRP Ledger Documentation or the `xrpl.org` website.
For issues related to `rippled` or client libraries (`xrpl.js`, `xrpl-py`, and others), use the respective source repository under [`https://github.com/XRPLF`](https://github.com/XRPLF).
For issues related to `xrpld`/`rippled`, Clio, or client libraries (`xrpl.js`, `xrpl-py`, and others), use the respective source repository under [`https://github.com/XRPLF`](https://github.com/XRPLF).
If you are a contributor, use GitHub Projects and Project Boards to plan and track updates to xrpl.org.

View File

@@ -0,0 +1,3 @@
# Delete Account
Delete an account from the XRP Ledger, removing its data and sending its XRP to another account.

View File

@@ -0,0 +1,4 @@
# Replace the seed with the seed of the account to delete.
ACCOUNT_SEED=s████████████████████████████
# Change to secp256k1 if you generated the seed with that algorithm
ACCOUNT_ALGORITHM=ed25519

View File

@@ -0,0 +1,45 @@
# Delete Account (JavaScript)
JavaScript sample code showing how to delete an account from the XRP Ledger.
## Setup
```sh
npm i
```
## Usage
If you run the script by default, it gets an account from the faucet and outputs the details to the console. Example:
```sh
$ node delete-account.js
Got new account from faucet:
Address: rsuTU7xBF1u8jxKMw5UHvbKkLmvix7zQoe
Seed: sEdTpxrbDhe6M4YeHanSbCySFCZYrCk
Edit the .env file to add this seed, then wait until the account can be deleted.
Account is too new to be deleted.
Account sequence + 255: 15226794
Validated ledger index: 15226538
(Sequence + 255 must be less than or equal to the ledger index)
Estimate: 15 minutes until account can be deleted
OK: Account owner count (0) is low enough.
OK: Account balance (100000000 drops) is high enough.
A total of 1 problem(s) prevent the account from being deleted.
```
Edit the `.env` file to add the seed of the account to delete. For example:
```ini
# Replace the seed with the seed of the account to delete.
ACCOUNT_SEED=sEdTpxrbDhe6M4YeHanSbCySFCZYrCk
# Change to secp256k1 if you generated the seed with that algorithm
ACCOUNT_ALGORITHM=ed25519
```
Then run the script again:
```sh
node delete-account.js
```

View File

@@ -0,0 +1,205 @@
import { Client, Wallet, getBalanceChanges, validate } from 'xrpl'
import 'dotenv/config'
const client = new Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
// Where to send the deleted account's remaining XRP:
const DESTINATION_ACCOUNT = 'rJjHYTCPpNA3qAM8ZpCDtip3a8xg7B8PFo' // Testnet faucet
// Load the account to delete from .env file -----------------------------------
// If the seed value is still the default, get a new account from the faucet.
// It won't be deletable immediately.
let wallet
if (!process.env.ACCOUNT_SEED || process.env.ACCOUNT_SEED === 's████████████████████████████') {
console.log("Couldn't load seed from .env; getting account from the faucet.")
wallet = (await client.fundWallet()).wallet
console.log(`Got new account from faucet:
Address: ${wallet.address}
Seed: ${wallet.seed}
`)
console.log('Edit the .env file to add this seed, then wait until the account can be deleted.')
} else {
wallet = Wallet.fromSeed(process.env.ACCOUNT_SEED, { algorithm: process.env.ACCOUNT_ALGORITHM })
console.log(`Loaded account: ${wallet.address}`)
}
// Check account info to see if account can be deleted -------------------------
let acctInfoResp
try {
acctInfoResp = await client.request({
command: 'account_info',
account: wallet.address,
ledger_index: 'validated'
})
} catch (err) {
console.error('account_info failed with error:', err)
client.disconnect()
process.exit(1)
}
let numProblems = 0
// Check if sequence number is too high
const acctSeq = acctInfoResp.result.account_data.Sequence
const lastValidatedLedgerIndex = acctInfoResp.result.ledger_index
if (acctSeq + 255 > lastValidatedLedgerIndex) {
console.error(`Account is too new to be deleted.
Account sequence + 255: ${acctSeq + 255}
Validated ledger index: ${lastValidatedLedgerIndex}
(Sequence + 255 must be less than or equal to the ledger index)`)
// Estimate time until deletability assuming ledgers close every ~3.5 seconds
const estWaitTimeS = (acctSeq + 255 - lastValidatedLedgerIndex) * 3.5
if (estWaitTimeS < 120) {
console.log(`Estimate: ${estWaitTimeS} seconds until account can be deleted`)
} else {
const estWaitTimeM = Math.round(estWaitTimeS / 60, 0)
console.log(`Estimate: ${estWaitTimeM} minutes until account can be deleted`)
}
numProblems += 1
} else {
console.log(`OK: Account sequence number (${acctSeq}) is low enough.`)
}
// Check if owner count is too high
const ownerCount = acctInfoResp.result.account_data.OwnerCount
if (ownerCount > 1000) {
console.error(`Account owns too many objects in the ledger.
Owner count: ${ownerCount}
(Must be 1000 or less)`)
numProblems += 1
} else {
console.log(`OK: Account owner count (${ownerCount}) is low enough.`)
}
// Check if XRP balance is high enough
// Look up current incremental owner reserve to compare vs account's XRP balance
// using server_state so that both are in drops
let serverStateResp
try {
serverStateResp = await client.request({
command: 'server_state'
})
} catch (err) {
console.error('server_state failed with error:', err)
client.disconnect()
process.exit(1)
}
const deletionCost = serverStateResp.result.state.validated_ledger?.reserve_inc
if (!deletionCost) {
console.error("Couldn't get reserve values from server. " +
"Maybe it's not synced to the network?")
client.disconnect()
process.exit(1)
}
const acctBalance = acctInfoResp.result.account_data.Balance
if (acctBalance < deletionCost) {
console.error(`Account does not have enough XRP to pay the cost of deletion.
Balance: ${acctBalance}
Cost of account deletion: ${deletionCost}`)
numProblems += 1
} else {
console.log(`OK: Account balance (${acctBalance} drops) is high enough.`)
}
// Check if FirstNFTSequence is too high
const firstNFTSeq = acctInfoResp.result.account_data.FirstNFTokenSequence || 0
const mintedNFTs = acctInfoResp.result.account_data.MintedNFTokens || 0
if (firstNFTSeq + mintedNFTs + 255 > lastValidatedLedgerIndex) {
console.error(`Account's FirstNFTokenSequence + MintedNFTokens + 255 is too high.
Current total: ${firstNFTSeq + mintedNFTs + 255}
Validated ledger index: ${lastValidatedLedgerIndex}
(FirstNFTokenSequence + MintedNFTokens + 255 must be less than or equal to the ledger index)`)
numProblems += 1
} else {
console.log('OK: FirstNFTokenSequence + MintedNFTokens is low enough.')
}
// Check that all issued NFTs have been burned
const burnedNFTs = acctInfoResp.result.account_data.BurnedNFTokens || 0
if (mintedNFTs > burnedNFTs) {
console.error(`Account has issued NFTs outstanding.
Number of NFTs minted: ${mintedNFTs}
Number of NFTs burned: ${burnedNFTs}`)
numProblems += 1
} else {
console.log('OK: No outstanding, un-burned NFTs')
}
// Stop if any problems were found
if (numProblems) {
console.error(`A total of ${numProblems} problem(s) prevent the account from being deleted.`)
client.disconnect()
process.exit(1)
}
// Check for deletion blockers -------------------------------------------------
const blockers = []
let marker
const ledger_index = 'validated'
while (true) {
let accountObjResp
try {
accountObjResp = await client.request({
command: 'account_objects',
account: wallet.address,
deletion_blockers_only: true,
ledger_index,
marker
})
} catch (err) {
console.error('account_objects failed with error:', err)
client.disconnect()
process.exit(1)
}
for (const obj of accountObjResp.result.account_objects) {
blockers.push(obj)
}
if (accountObjResp.result.marker) {
marker = accountObjResp.result.marker
} else {
break
}
}
if (!blockers.length) {
console.log('OK: Account has no deletion blockers.')
} else {
console.log(`Account cannot be deleted until ${blockers.length} blocker(s) are removed:`)
for (const blocker of blockers) {
console.log(JSON.stringify(blocker, null, 2))
}
client.disconnect()
process.exit(1)
}
// Delete the account ----------------------------------------------------------
const accountDeleteTx = {
TransactionType: 'AccountDelete',
Account: wallet.address,
Destination: DESTINATION_ACCOUNT
}
validate(accountDeleteTx)
console.log('Signing and submitting the AccountDelete transaction:',
JSON.stringify(accountDeleteTx, null, 2))
const deleteTxResponse = await client.submitAndWait(accountDeleteTx, { wallet, autofill: true, failHard: true })
// Check result of the AccountDelete transaction -------------------------------
console.log(JSON.stringify(deleteTxResponse.result, null, 2))
const resultCode = deleteTxResponse.result.meta.TransactionResult
if (resultCode !== 'tesSUCCESS') {
console.error(`AccountDelete failed with code ${resultCode}.`)
client.disconnect()
process.exit(1)
}
console.log('Account deleted successfully.')
const balanceChanges = getBalanceChanges(deleteTxResponse.result.meta)
console.log('Balance changes:', JSON.stringify(balanceChanges, null, 2))
client.disconnect()

View File

@@ -0,0 +1,10 @@
{
"name": "delete-account",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"dotenv": "^17.3.1",
"xrpl": "^4.6.0"
},
"type": "module"
}

View File

@@ -0,0 +1,4 @@
# Replace the seed with the seed of the account to delete.
ACCOUNT_SEED=s████████████████████████████
# Change to secp256k1 if you generated the seed with that algorithm
ACCOUNT_ALGORITHM=ed25519

View File

@@ -0,0 +1,47 @@
# Delete Account (Python)
Python sample code showing how to delete an account from the XRP Ledger.
## Setup
```sh
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
## Usage
If you run the script by default, it gets an account from the faucet and outputs the details to the console. Example:
```sh
$ python delete-account.py
Got new account from faucet:
Address: rNqLzC9pVbphwwpTBNPjpx14QSauHH3kzv
Seed: sEdTNEJgK3cVshBEakfVic4MMtWCETY
Edit the .env file to add this seed, then wait until the account can be deleted.
Account is too new to be deleted.
Account sequence + 255: 15226905
Validated ledger index: 15226649
(Sequence + 255 must be less than or equal to the ledger index)
Estimate: 15 minutes until account can be deleted
OK: Account owner count (0) is low enough.
OK: Account balance (100000000 drops) is high enough.
A total of 1 problem(s) prevent the account from being deleted.
```
Edit the `.env` file to add the seed of the account to delete. For example:
```ini
# Replace the seed with the seed of the account to delete.
ACCOUNT_SEED=sEdTNEJgK3cVshBEakfVic4MMtWCETY
# Change to secp256k1 if you generated the seed with that algorithm
ACCOUNT_ALGORITHM=ed25519
```
Then run the script again:
```sh
python delete-account.py
```

View File

@@ -0,0 +1,199 @@
import os
import json
from dotenv import load_dotenv
from xrpl.clients import JsonRpcClient
from xrpl.wallet import Wallet, generate_faucet_wallet
from xrpl.models.requests import AccountInfo, ServerState, AccountObjects
from xrpl.models.transactions import AccountDelete
from xrpl.transaction import submit_and_wait
from xrpl.utils import get_balance_changes
client = JsonRpcClient("https://s.altnet.rippletest.net:51234")
# Where to send the deleted account's remaining XRP:
DESTINATION_ACCOUNT = "rJjHYTCPpNA3qAM8ZpCDtip3a8xg7B8PFo" # Testnet faucet
# Load the account to delete from .env file -----------------------------------
# If the seed value is still the default, get a new account from the faucet.
# It won't be deletable immediately.
load_dotenv()
account_seed = os.getenv("ACCOUNT_SEED")
account_algorithm = os.getenv("ACCOUNT_ALGORITHM", "ed25519")
if account_seed == "s████████████████████████████" or not account_seed:
print("Couldn't load seed from .env; getting account from the faucet.")
wallet = generate_faucet_wallet(client)
print(
f"Got new account from faucet:\n"
f" Address: {wallet.address}\n"
f" Seed: {wallet.seed}\n"
)
print(
"Edit the .env file to add this seed, then wait until the account can be deleted."
)
else:
wallet = Wallet.from_seed(account_seed, algorithm=account_algorithm)
print(f"Loaded account: {wallet.address}")
# Check account info to see if account can be deleted -------------------------
try:
acct_info_resp = client.request(
AccountInfo(account=wallet.address, ledger_index="validated")
)
except Exception as err:
print(f"account_info failed with error: {err}")
exit(1)
acct_info_result = acct_info_resp.result
num_problems = 0
# Check if sequence number is too high
acct_seq = acct_info_result["account_data"]["Sequence"]
last_validated_ledger_index = acct_info_result["ledger_index"]
if acct_seq + 255 > last_validated_ledger_index:
print(
f"Account is too new to be deleted.\n"
f" Account sequence + 255: {acct_seq + 255}\n"
f" Validated ledger index: {last_validated_ledger_index}\n"
f" (Sequence + 255 must be less than or equal to the ledger index)"
)
# Estimate time until deletability assuming ledgers close every ~3.5 seconds
est_wait_time_s = (acct_seq + 255 - last_validated_ledger_index) * 3.5
if est_wait_time_s < 120:
print(f"Estimate: {est_wait_time_s} seconds until account can be deleted")
else:
est_wait_time_m = round(est_wait_time_s / 60)
print(f"Estimate: {est_wait_time_m} minutes until account can be deleted")
num_problems += 1
else:
print(f"OK: Account sequence number ({acct_seq}) is low enough.")
# Check if owner count is too high
owner_count = acct_info_result["account_data"]["OwnerCount"]
if owner_count > 1000:
print(
f"Account owns too many objects in the ledger.\n"
f" Owner count: {owner_count}\n"
f" (Must be 1000 or less)"
)
num_problems += 1
else:
print(f"OK: Account owner count ({owner_count}) is low enough.")
# Check if XRP balance is high enough
# Look up current incremental owner reserve to compare vs account's XRP balance
# using server_state so that both are in drops
try:
server_state_resp = client.request(ServerState())
except Exception as err:
print("server_state failed with error:", err)
exit(1)
validated_ledger = server_state_resp.result["state"].get("validated_ledger", {})
deletion_cost = validated_ledger.get("reserve_inc")
if not deletion_cost:
print(
"Couldn't get reserve values from server. Maybe it's not synced to the network?"
)
print(json.dumps(server_state_resp.result, indent=2))
exit(1)
acct_balance = int(acct_info_result["account_data"]["Balance"])
if acct_balance < deletion_cost:
print(
f"Account does not have enough XRP to pay the cost of deletion.\n"
f" Balance: {acct_balance}\n"
f" Cost of account deletion: {deletion_cost}"
)
num_problems += 1
else:
print(f"OK: Account balance ({acct_balance} drops) is high enough.")
# Check if FirstNFTSequence is too high
first_nfq_seq = acct_info_result["account_data"].get("FirstNFTokenSequence", 0)
minted_nfts = acct_info_result["account_data"].get("MintedNFTokens", 0)
if first_nfq_seq + minted_nfts + 255 > last_validated_ledger_index:
print(f"""Account's FirstNFTokenSequence + MintedNFTokens + 255 is too high.
Current total: {first_nfq_seq + minted_nfts + 255}
Validated ledger index: {last_validated_ledger_index}
(FirstNFTokenSequence + MintedNFTokens + 255 must be less than or equal to the the ledger index)""")
num_problems += 1
else:
print("OK: FirstNFTokenSequence + MintedNFTokens is low enough.")
# Check that all issued NFTs have been burned
burned_nfts = acct_info_result["account_data"].get("BurnedNFTokens", 0)
if minted_nfts > burned_nfts:
print(f"""Account has NFTs outstanding.
Number of NFTs minted: {minted_nfts}
Number of NFTs burned: {burned_nfts}""")
num_problems += 1
else:
print("OK: No outstanding, un-burned NFTs")
# Stop if any problems were found
if num_problems:
print(
f"A total of {num_problems} problem(s) prevent the account from being deleted."
)
exit(1)
# Check for deletion blockers -------------------------------------------------
blockers = []
marker = None
ledger_index = "validated"
while True:
try:
account_obj_resp = client.request(
AccountObjects(
account=wallet.address,
deletion_blockers_only=True,
ledger_index=ledger_index,
marker=marker,
)
)
except Exception as err:
print(f"account_objects failed with error: {err}")
exit(1)
blockers.extend(account_obj_resp.result["account_objects"])
marker = account_obj_resp.result.get("marker")
if not marker:
break
if not blockers:
print("OK: Account has no deletion blockers.")
else:
print(f"Account cannot be deleted until {len(blockers)} blocker(s) are removed:")
for blocker in blockers:
print(json.dumps(blocker, indent=2))
exit(1)
# Delete the account ----------------------------------------------------------
account_delete_tx = AccountDelete(
account=wallet.address, destination=DESTINATION_ACCOUNT
)
print("Signing and submitting the AccountDelete transaction:")
print(json.dumps(account_delete_tx.to_xrpl(), indent=2))
delete_tx_response = submit_and_wait(account_delete_tx, client, wallet, fail_hard=True)
# Check result of the AccountDelete transaction -------------------------------
print(json.dumps(delete_tx_response.result, indent=2))
result_code = delete_tx_response.result["meta"]["TransactionResult"]
if result_code != "tesSUCCESS":
print(f"AccountDelete failed with code {result_code}.")
exit(1)
print("Account deleted successfully.")
balance_changes = get_balance_changes(delete_tx_response.result["meta"])
print("Balance changes:", json.dumps(balance_changes, indent=2))

View File

@@ -0,0 +1,2 @@
xrpl-py==4.5.0
python-dotenv==1.2.1

View 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
```

View 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
)

View 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 \"IOU\" tokens.\n")
// 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)
}

View File

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

View 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 "IOU" 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()

View File

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

View 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('Trust line successfully created for "IOU" 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']}")

View File

@@ -0,0 +1,395 @@
# Lending Protocol Examples (Go)
This directory contains Go examples demonstrating how to create a loan broker, claw back first-loss capital, deposit and withdraw first-loss capital, create a loan, manage a loan, and repay a loan.
## Setup
All commands should be run from this `go/` directory.
Install dependencies before running any examples:
```sh
go mod tidy
```
---
## Create a Loan Broker
```sh
go run ./create-loan-broker
```
The script should output the LoanBrokerSet transaction, loan broker ID, and loan broker pseudo-account.
```sh
Loan broker/vault owner address: rLsTX2RjNTqwiwNpMn7mny3MyrXtmbhFQV
Vault ID: A300D6F7D43E1B143683F1917EE6456B0C3E84F0F763D9A1366FCD22138A11E9
=== Preparing LoanBrokerSet transaction ===
{
"Account": "rLsTX2RjNTqwiwNpMn7mny3MyrXtmbhFQV",
"ManagementFeeRate": 1000,
"TransactionType": "LoanBrokerSet",
"VaultID": "A300D6F7D43E1B143683F1917EE6456B0C3E84F0F763D9A1366FCD22138A11E9"
}
=== Submitting LoanBrokerSet transaction ===
Loan broker created successfully!
=== Loan Broker Information ===
LoanBroker ID: E4D9C485E101FAE449C8ACEC7FD039920CC02D2443687F2593DB397CC8EA670B
LoanBroker Pseudo-Account Address: rMDsnf9CVRLRJzrL12Ex7nhstbni78y8af
```
---
## Claw Back First-loss Capital
```sh
go run ./cover-clawback
```
The script should output the cover available, the LoanBrokerCoverDeposit transaction, cover available after the deposit, the LoanBrokerCoverClawback transaction, and the final cover available after the clawback.
```sh
Loan broker address: rLsTX2RjNTqwiwNpMn7mny3MyrXtmbhFQV
MPT issuer address: rfYxCEWxA9ACyvpciPZYbKujjLEVX5CCwW
LoanBrokerID: 373BEBB8A1EF735FCD330C2B0DDF2C37FD3B1589B084C94F2CA52A904FBED08D
MPT ID: 003A9D5247DC1C9997DB5500A84C3EC748F3F61D2BC56D51
=== Cover Available ===
0 TSTUSD
=== Preparing LoanBrokerCoverDeposit transaction ===
{
"Account": "rLsTX2RjNTqwiwNpMn7mny3MyrXtmbhFQV",
"Amount": {
"mpt_issuance_id": "003A9D5247DC1C9997DB5500A84C3EC748F3F61D2BC56D51",
"value": "1000"
},
"LoanBrokerID": "373BEBB8A1EF735FCD330C2B0DDF2C37FD3B1589B084C94F2CA52A904FBED08D",
"TransactionType": "LoanBrokerCoverDeposit"
}
=== Submitting LoanBrokerCoverDeposit transaction ===
Cover deposit successful!
=== Cover Available After Deposit ===
1000 TSTUSD
=== Verifying Asset Issuer ===
MPT issuer account verified: rfYxCEWxA9ACyvpciPZYbKujjLEVX5CCwW. Proceeding to clawback.
=== Preparing LoanBrokerCoverClawback transaction ===
{
"Account": "rfYxCEWxA9ACyvpciPZYbKujjLEVX5CCwW",
"Amount": {
"mpt_issuance_id": "003A9D5247DC1C9997DB5500A84C3EC748F3F61D2BC56D51",
"value": "1000"
},
"LoanBrokerID": "373BEBB8A1EF735FCD330C2B0DDF2C37FD3B1589B084C94F2CA52A904FBED08D",
"TransactionType": "LoanBrokerCoverClawback"
}
=== Submitting LoanBrokerCoverClawback transaction ===
Successfully clawed back 1000 TSTUSD!
=== Final Cover Available After Clawback ===
0 TSTUSD
```
---
## Deposit and Withdraw First-loss Capital
```sh
go run ./cover-deposit-and-withdraw
```
The script should output the LoanBrokerCoverDeposit, cover balance after the deposit, the LoanBrokerCoverWithdraw transaction, and the cover balance after the withdrawal.
```sh
Loan broker address: rU9ANkvdSCs7p59Guf2XzGrrCPSMM2tDQV
LoanBrokerID: 633375BCB82DB1440189D3E0721AF92B0888304717DA1B002C8824D631E97DC3
MPT ID: 003B6A9EE92357082A44FA2EAA26385E2F85071634BC3315
=== Preparing LoanBrokerCoverDeposit transaction ===
{
"Account": "rU9ANkvdSCs7p59Guf2XzGrrCPSMM2tDQV",
"Amount": {
"mpt_issuance_id": "003B6A9EE92357082A44FA2EAA26385E2F85071634BC3315",
"value": "2000"
},
"LoanBrokerID": "633375BCB82DB1440189D3E0721AF92B0888304717DA1B002C8824D631E97DC3",
"TransactionType": "LoanBrokerCoverDeposit"
}
=== Submitting LoanBrokerCoverDeposit transaction ===
Cover deposit successful!
=== Cover Balance ===
LoanBroker Pseudo-Account: rJoTTaGKQr8o475xKNZkEPRsmTbUkr6sbi
Cover balance after deposit: 2000 TSTUSD
=== Preparing LoanBrokerCoverWithdraw transaction ===
{
"Account": "rU9ANkvdSCs7p59Guf2XzGrrCPSMM2tDQV",
"Amount": {
"mpt_issuance_id": "003B6A9EE92357082A44FA2EAA26385E2F85071634BC3315",
"value": "1000"
},
"LoanBrokerID": "633375BCB82DB1440189D3E0721AF92B0888304717DA1B002C8824D631E97DC3",
"TransactionType": "LoanBrokerCoverWithdraw"
}
=== Submitting LoanBrokerCoverWithdraw transaction ===
Cover withdraw successful!
=== Updated Cover Balance ===
LoanBroker Pseudo-Account: rJoTTaGKQr8o475xKNZkEPRsmTbUkr6sbi
Cover balance after withdraw: 1000 TSTUSD
```
---
## Create a Loan
```sh
go run ./create-loan
```
The script should output the LoanSet transaction, the updated LoanSet transaction with the loan broker signature, the final LoanSet transaction with the borrower signature added, and then the loan information.
```sh
Loan broker address: rKHVvo9vMQwm2xz44qfhHyDC2VwYKfzgrX
Borrower address: rKU5hZEsT71BUgCnnPekEF3d2zn4AXsyqp
LoanBrokerID: 490DB29AD0CCFBDFE9A176F71AB7512497407CCA37E86F0C88CCDA1DF99A1F09
=== Preparing LoanSet transaction ===
{
"Account": "rKHVvo9vMQwm2xz44qfhHyDC2VwYKfzgrX",
"Counterparty": "rKU5hZEsT71BUgCnnPekEF3d2zn4AXsyqp",
"Fee": "2",
"GracePeriod": 604800,
"InterestRate": 500,
"LastLedgerSequence": 437349,
"LoanBrokerID": "490DB29AD0CCFBDFE9A176F71AB7512497407CCA37E86F0C88CCDA1DF99A1F09",
"LoanOriginationFee": "100",
"LoanServiceFee": "10",
"PaymentInterval": 2592000,
"PaymentTotal": 12,
"PrincipalRequested": "1000",
"Sequence": 6905,
"TransactionType": "LoanSet"
}
=== Adding loan broker signature ===
TxnSignature: 9984D6061F4B03734CDCC5A5367A928671FEE1486EFD335B6C875423FCB9FCEF2464F2A610B4DF31875567869696DC36D16F72AFB7D5F245B43C19415537F50F
SigningPubKey: ED378AB6DCCD0401D6DB3358A9135CE43455A57DAF0CBC459E8D7B6611193690B1
Signed loanSetTx for borrower to sign over:
{
"Account": "rKHVvo9vMQwm2xz44qfhHyDC2VwYKfzgrX",
"Counterparty": "rKU5hZEsT71BUgCnnPekEF3d2zn4AXsyqp",
"Fee": "2",
"GracePeriod": 604800,
"InterestRate": 500,
"LastLedgerSequence": 437349,
"LoanBrokerID": "490DB29AD0CCFBDFE9A176F71AB7512497407CCA37E86F0C88CCDA1DF99A1F09",
"LoanOriginationFee": "100",
"LoanServiceFee": "10",
"PaymentInterval": 2592000,
"PaymentTotal": 12,
"PrincipalRequested": "1000",
"Sequence": 6905,
"SigningPubKey": "ED378AB6DCCD0401D6DB3358A9135CE43455A57DAF0CBC459E8D7B6611193690B1",
"TransactionType": "LoanSet",
"TxnSignature": "9984D6061F4B03734CDCC5A5367A928671FEE1486EFD335B6C875423FCB9FCEF2464F2A610B4DF31875567869696DC36D16F72AFB7D5F245B43C19415537F50F"
}
=== Adding borrower signature ===
Borrower TxnSignature: 5578161BA5480216644D63428D4FAA6FC761BEA10D91FFB733636AB4EA7C6CC4E07E241BF5418D92FBE9F0133E97CC3E6A2CDC56C86C801438C1CBAC4497B005
Borrower SigningPubKey: ED2BF9FFE428F80E3E174476EA334E2109BAF6C7309BB08D56A6A97CE0432AD85E
Fully signed LoanSet transaction:
{
"Account": "rKHVvo9vMQwm2xz44qfhHyDC2VwYKfzgrX",
"Counterparty": "rKU5hZEsT71BUgCnnPekEF3d2zn4AXsyqp",
"CounterpartySignature": {
"SigningPubKey": "ED2BF9FFE428F80E3E174476EA334E2109BAF6C7309BB08D56A6A97CE0432AD85E",
"TxnSignature": "5578161BA5480216644D63428D4FAA6FC761BEA10D91FFB733636AB4EA7C6CC4E07E241BF5418D92FBE9F0133E97CC3E6A2CDC56C86C801438C1CBAC4497B005"
},
"Fee": "2",
"GracePeriod": 604800,
"InterestRate": 500,
"LastLedgerSequence": 437349,
"LoanBrokerID": "490DB29AD0CCFBDFE9A176F71AB7512497407CCA37E86F0C88CCDA1DF99A1F09",
"LoanOriginationFee": "100",
"LoanServiceFee": "10",
"PaymentInterval": 2592000,
"PaymentTotal": 12,
"PrincipalRequested": "1000",
"Sequence": 6905,
"SigningPubKey": "ED378AB6DCCD0401D6DB3358A9135CE43455A57DAF0CBC459E8D7B6611193690B1",
"TransactionType": "LoanSet",
"TxnSignature": "9984D6061F4B03734CDCC5A5367A928671FEE1486EFD335B6C875423FCB9FCEF2464F2A610B4DF31875567869696DC36D16F72AFB7D5F245B43C19415537F50F"
}
=== Submitting signed LoanSet transaction ===
Loan created successfully!
=== Loan Information ===
{
"Borrower": "rKU5hZEsT71BUgCnnPekEF3d2zn4AXsyqp",
"GracePeriod": 604800,
"InterestRate": 500,
"LoanBrokerID": "490DB29AD0CCFBDFE9A176F71AB7512497407CCA37E86F0C88CCDA1DF99A1F09",
"LoanOriginationFee": "100",
"LoanSequence": 4,
"LoanServiceFee": "10",
"NextPaymentDueDate": 829803181,
"PaymentInterval": 2592000,
"PaymentRemaining": 12,
"PeriodicPayment": "83.55610375293148956",
"PrincipalOutstanding": "1000",
"StartDate": 827211181,
"TotalValueOutstanding": "1003"
}
```
---
## Manage a Loan
```sh
go run ./loan-manage
```
The script should output the initial status of the loan, the LoanManage transaction, and the updated loan status and grace period after impairment. The script will countdown the grace period before outputting another LoanManage transaction, and then the final flags on the loan.
```sh
Loan broker address: rN7eCZhKHcq5LEC2W2RrrGcUPBYwZagEPX
LoanID: 2BD3F3F587D1BD4FB247B0935FB098E2DC6E3B571F493472CED914216990EC6C
=== Loan Status ===
Total Amount Owed: 1001 TSTUSD.
Payment Due Date: 2026-03-23 01:23:40
=== Preparing LoanManage transaction to impair loan ===
{
"Account": "rN7eCZhKHcq5LEC2W2RrrGcUPBYwZagEPX",
"Flags": 131072,
"LoanID": "2BD3F3F587D1BD4FB247B0935FB098E2DC6E3B571F493472CED914216990EC6C",
"TransactionType": "LoanManage"
}
=== Submitting LoanManage impairment transaction ===
Loan impaired successfully!
New Payment Due Date: 2026-02-21 00:24:10
Grace Period: 60 seconds
=== Countdown until loan can be defaulted ===
Grace period expired. Loan can now be defaulted.
=== Preparing LoanManage transaction to default loan ===
{
"Account": "rN7eCZhKHcq5LEC2W2RrrGcUPBYwZagEPX",
"Flags": 65536,
"LoanID": "2BD3F3F587D1BD4FB247B0935FB098E2DC6E3B571F493472CED914216990EC6C",
"TransactionType": "LoanManage"
}
=== Submitting LoanManage default transaction ===
Loan defaulted successfully!
=== Checking final loan status ===
Final loan flags: [tfLoanDefault tfLoanImpair]
```
---
## Pay a Loan
```sh
go run ./loan-pay
```
The script should output the amount required to totally pay off a loan, the LoanPay transaction, the amount due after the payment, the LoanDelete transaction, and then the status of the loan ledger entry.
```sh
Borrower address: rFx8s3P5J66MAvWkp5rMj5bBF76gQUCt2
LoanID: D0455CD5F9C2FEC62FC67F31BD97134FBA877D7FE1AE7130EE0006D10661325A
MPT ID: 003B8FC2F51C1BC4E0211E6370EC4FC78BB20D5C4069F07B
=== Loan Status ===
Amount Owed: 1001 TSTUSD
Loan Service Fee: 10 TSTUSD
Total Payment Due (including fees): 1011 TSTUSD
=== Preparing LoanPay transaction ===
{
"Account": "rFx8s3P5J66MAvWkp5rMj5bBF76gQUCt2",
"Amount": {
"mpt_issuance_id": "003B8FC2F51C1BC4E0211E6370EC4FC78BB20D5C4069F07B",
"value": "1011"
},
"LoanID": "D0455CD5F9C2FEC62FC67F31BD97134FBA877D7FE1AE7130EE0006D10661325A",
"TransactionType": "LoanPay"
}
=== Submitting LoanPay transaction ===
Loan paid successfully!
=== Loan Status After Payment ===
Outstanding Loan Balance: Loan fully paid off!
=== Preparing LoanDelete transaction ===
{
"Account": "rFx8s3P5J66MAvWkp5rMj5bBF76gQUCt2",
"LoanID": "D0455CD5F9C2FEC62FC67F31BD97134FBA877D7FE1AE7130EE0006D10661325A",
"TransactionType": "LoanDelete"
}
=== Submitting LoanDelete transaction ===
Loan deleted successfully!
=== Verifying Loan Deletion ===
Loan has been successfully removed from the XRP Ledger!
```

View File

@@ -0,0 +1,210 @@
// IMPORTANT: This example deposits and claws back first-loss capital from a
// preconfigured LoanBroker entry. The first-loss capital is an MPT
// with clawback enabled.
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"github.com/Peersyst/xrpl-go/xrpl/queries/common"
"github.com/Peersyst/xrpl-go/xrpl/queries/ledger"
"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"
)
// mptIssuanceEntryRequest looks up an MPTIssuance ledger entry by its MPT ID.
// The library's GetLedgerEntry() method only supports lookup by ledger entry ID,
// so this custom type is used with the generic Request() method.
type mptIssuanceEntryRequest struct {
common.BaseRequest
MPTIssuance string `json:"mpt_issuance"`
LedgerIndex common.LedgerSpecifier `json:"ledger_index,omitempty"`
}
func (*mptIssuanceEntryRequest) Method() string { return "ledger_entry" }
func (*mptIssuanceEntryRequest) Validate() error { return nil }
func (*mptIssuanceEntryRequest) APIVersion() int { return 2 }
func main() {
// Connect to the network ----------------------
client := websocket.NewClient(
websocket.NewClientConfig().
WithHost("wss://s.devnet.rippletest.net:51233"),
)
defer client.Disconnect()
if err := client.Connect(); err != nil {
panic(err)
}
// Check for setup data; run lending-setup if missing
if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) {
fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n")
cmd := exec.Command("go", "run", "./lending-setup")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
// Load preconfigured accounts and LoanBrokerID
data, err := os.ReadFile("lending-setup.json")
if err != nil {
panic(err)
}
var setup map[string]any
if err := json.Unmarshal(data, &setup); err != nil {
panic(err)
}
// You can replace these values with your own
loanBrokerWallet, err := wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
mptIssuerWallet, err := wallet.FromSecret(setup["depositor"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
loanBrokerID := setup["loanBrokerID"].(string)
mptID := setup["mptID"].(string)
fmt.Printf("\nLoan broker address: %s\n", loanBrokerWallet.ClassicAddress)
fmt.Printf("MPT issuer address: %s\n", mptIssuerWallet.ClassicAddress)
fmt.Printf("LoanBrokerID: %s\n", loanBrokerID)
fmt.Printf("MPT ID: %s\n", mptID)
// Check cover available ----------------------
fmt.Printf("\n=== Cover Available ===\n\n")
coverInfo, err := client.GetLedgerEntry(&ledger.EntryRequest{
Index: loanBrokerID,
LedgerIndex: common.Validated,
})
if err != nil {
panic(err)
}
currentCoverAvailable := "0"
if ca, ok := coverInfo.Node["CoverAvailable"].(string); ok {
currentCoverAvailable = ca
}
fmt.Printf("%s TSTUSD\n", currentCoverAvailable)
// Prepare LoanBrokerCoverDeposit transaction ----------------------
fmt.Printf("\n=== Preparing LoanBrokerCoverDeposit transaction ===\n\n")
coverDepositTx := transaction.LoanBrokerCoverDeposit{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
LoanBrokerID: loanBrokerID,
Amount: types.MPTCurrencyAmount{
MPTIssuanceID: mptID,
Value: "1000",
},
}
// Flatten() converts the struct to a map and adds the TransactionType field
flatCoverDepositTx := coverDepositTx.Flatten()
coverDepositTxJSON, _ := json.MarshalIndent(flatCoverDepositTx, "", " ")
fmt.Printf("%s\n", string(coverDepositTxJSON))
// Sign, submit, and wait for deposit validation ----------------------
fmt.Printf("\n=== Submitting LoanBrokerCoverDeposit transaction ===\n\n")
depositResponse, err := client.SubmitTxAndWait(flatCoverDepositTx, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &loanBrokerWallet,
})
if err != nil {
panic(err)
}
if depositResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to deposit cover: %s\n", depositResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Cover deposit successful!\n")
// Extract updated cover available after deposit ----------------------
fmt.Printf("\n=== Cover Available After Deposit ===\n\n")
for _, node := range depositResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "LoanBroker" {
currentCoverAvailable = node.ModifiedNode.FinalFields["CoverAvailable"].(string)
break
}
}
fmt.Printf("%s TSTUSD\n", currentCoverAvailable)
// Verify issuer of cover asset matches ----------------------
// Only the issuer of the asset can submit clawback transactions.
// The asset must also have clawback enabled.
fmt.Printf("\n=== Verifying Asset Issuer ===\n\n")
assetIssuerInfo, err := client.Request(&mptIssuanceEntryRequest{
MPTIssuance: mptID,
LedgerIndex: common.Validated,
})
if err != nil {
panic(err)
}
issuer := assetIssuerInfo.Result["node"].(map[string]any)["Issuer"].(string)
if issuer != string(mptIssuerWallet.ClassicAddress) {
fmt.Printf("Error: %s does not match account (%s) attempting clawback!\n", issuer, mptIssuerWallet.ClassicAddress)
os.Exit(1)
}
fmt.Printf("MPT issuer account verified: %s. Proceeding to clawback.\n", mptIssuerWallet.ClassicAddress)
// Prepare LoanBrokerCoverClawback transaction ----------------------
fmt.Printf("\n=== Preparing LoanBrokerCoverClawback transaction ===\n\n")
lbID := types.LoanBrokerID(loanBrokerID)
coverClawbackTx := transaction.LoanBrokerCoverClawback{
BaseTx: transaction.BaseTx{
Account: mptIssuerWallet.ClassicAddress,
},
LoanBrokerID: &lbID,
Amount: types.MPTCurrencyAmount{
MPTIssuanceID: mptID,
Value: currentCoverAvailable,
},
}
flatCoverClawbackTx := coverClawbackTx.Flatten()
coverClawbackTxJSON, _ := json.MarshalIndent(flatCoverClawbackTx, "", " ")
fmt.Printf("%s\n", string(coverClawbackTxJSON))
// Sign, submit, and wait for clawback validation ----------------------
fmt.Printf("\n=== Submitting LoanBrokerCoverClawback transaction ===\n\n")
clawbackResponse, err := client.SubmitTxAndWait(flatCoverClawbackTx, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &mptIssuerWallet,
})
if err != nil {
panic(err)
}
if clawbackResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to clawback cover: %s\n", clawbackResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Successfully clawed back %s TSTUSD!\n", currentCoverAvailable)
// Extract final cover available ----------------------
fmt.Printf("\n=== Final Cover Available After Clawback ===\n\n")
for _, node := range clawbackResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "LoanBroker" {
finalCover := "0"
if ca, ok := node.ModifiedNode.FinalFields["CoverAvailable"].(string); ok {
finalCover = ca
}
fmt.Printf("%s TSTUSD\n", finalCover)
break
}
}
}

View File

@@ -0,0 +1,151 @@
// IMPORTANT: This example deposits and withdraws first-loss capital from a
// preconfigured LoanBroker entry.
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"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"
)
func main() {
// Connect to the network ----------------------
client := websocket.NewClient(
websocket.NewClientConfig().
WithHost("wss://s.devnet.rippletest.net:51233"),
)
defer client.Disconnect()
if err := client.Connect(); err != nil {
panic(err)
}
// Check for setup data; run lending-setup if missing
if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) {
fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n")
cmd := exec.Command("go", "run", "./lending-setup")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
// Load preconfigured accounts and LoanBrokerID
data, err := os.ReadFile("lending-setup.json")
if err != nil {
panic(err)
}
var setup map[string]any
if err := json.Unmarshal(data, &setup); err != nil {
panic(err)
}
// You can replace these values with your own
loanBrokerWallet, err := wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
loanBrokerID := setup["loanBrokerID"].(string)
mptID := setup["mptID"].(string)
fmt.Printf("\nLoan broker address: %s\n", loanBrokerWallet.ClassicAddress)
fmt.Printf("LoanBrokerID: %s\n", loanBrokerID)
fmt.Printf("MPT ID: %s\n", mptID)
// Prepare LoanBrokerCoverDeposit transaction ----------------------
fmt.Printf("\n=== Preparing LoanBrokerCoverDeposit transaction ===\n\n")
coverDepositTx := transaction.LoanBrokerCoverDeposit{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
LoanBrokerID: loanBrokerID,
Amount: types.MPTCurrencyAmount{
MPTIssuanceID: mptID,
Value: "2000",
},
}
// Flatten() converts the struct to a map and adds the TransactionType field
flatCoverDepositTx := coverDepositTx.Flatten()
coverDepositTxJSON, _ := json.MarshalIndent(flatCoverDepositTx, "", " ")
fmt.Printf("%s\n", string(coverDepositTxJSON))
// Sign, submit, and wait for deposit validation ----------------------
fmt.Printf("\n=== Submitting LoanBrokerCoverDeposit transaction ===\n\n")
depositResponse, err := client.SubmitTxAndWait(flatCoverDepositTx, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &loanBrokerWallet,
})
if err != nil {
panic(err)
}
if depositResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to deposit cover: %s\n", depositResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Cover deposit successful!\n")
// Extract cover balance from the transaction result
fmt.Printf("\n=== Cover Balance ===\n\n")
for _, node := range depositResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "LoanBroker" {
// First-loss capital is stored in the LoanBroker's pseudo-account.
fmt.Printf("LoanBroker Pseudo-Account: %s\n", node.ModifiedNode.FinalFields["Account"])
fmt.Printf("Cover balance after deposit: %s TSTUSD\n", node.ModifiedNode.FinalFields["CoverAvailable"])
break
}
}
// Prepare LoanBrokerCoverWithdraw transaction ----------------------
fmt.Printf("\n=== Preparing LoanBrokerCoverWithdraw transaction ===\n\n")
coverWithdrawTx := transaction.LoanBrokerCoverWithdraw{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
LoanBrokerID: loanBrokerID,
Amount: types.MPTCurrencyAmount{
MPTIssuanceID: mptID,
Value: "1000",
},
}
flatCoverWithdrawTx := coverWithdrawTx.Flatten()
coverWithdrawTxJSON, _ := json.MarshalIndent(flatCoverWithdrawTx, "", " ")
fmt.Printf("%s\n", string(coverWithdrawTxJSON))
// Sign, submit, and wait for withdraw validation ----------------------
fmt.Printf("\n=== Submitting LoanBrokerCoverWithdraw transaction ===\n\n")
withdrawResponse, err := client.SubmitTxAndWait(flatCoverWithdrawTx, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &loanBrokerWallet,
})
if err != nil {
panic(err)
}
if withdrawResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to withdraw cover: %s\n", withdrawResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Cover withdraw successful!\n")
// Extract updated cover balance from the transaction result
fmt.Printf("\n=== Updated Cover Balance ===\n\n")
for _, node := range withdrawResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "LoanBroker" {
fmt.Printf("LoanBroker Pseudo-Account: %s\n", node.ModifiedNode.FinalFields["Account"])
fmt.Printf("Cover balance after withdraw: %s TSTUSD\n", node.ModifiedNode.FinalFields["CoverAvailable"])
break
}
}
}

View File

@@ -0,0 +1,105 @@
// IMPORTANT: This example creates a loan broker using an existing account
// that has already created a PRIVATE vault.
// If you want to create a loan broker for a PUBLIC vault, you can replace the vaultID
// and loanBroker values with your own.
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"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"
)
func main() {
// Connect to the network ----------------------
client := websocket.NewClient(
websocket.NewClientConfig().
WithHost("wss://s.devnet.rippletest.net:51233"),
)
defer client.Disconnect()
if err := client.Connect(); err != nil {
panic(err)
}
// Check for setup data; run lending-setup if missing
if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) {
fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n")
cmd := exec.Command("go", "run", "./lending-setup")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
// Load preconfigured accounts and VaultID
data, err := os.ReadFile("lending-setup.json")
if err != nil {
panic(err)
}
var setup map[string]any
if err := json.Unmarshal(data, &setup); err != nil {
panic(err)
}
// You can replace these values with your own
loanBrokerWallet, err := wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
vaultID := setup["vaultID"].(string)
fmt.Printf("\nLoan broker/vault owner address: %s\n", loanBrokerWallet.ClassicAddress)
fmt.Printf("Vault ID: %s\n", vaultID)
// Prepare LoanBrokerSet transaction ----------------------
fmt.Printf("\n=== Preparing LoanBrokerSet transaction ===\n\n")
mgmtFeeRate := types.InterestRate(1000)
loanBrokerSetTx := transaction.LoanBrokerSet{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
VaultID: vaultID,
ManagementFeeRate: &mgmtFeeRate,
}
// Flatten() converts the struct to a map and adds the TransactionType field
flatLoanBrokerSetTx := loanBrokerSetTx.Flatten()
loanBrokerSetTxJSON, _ := json.MarshalIndent(flatLoanBrokerSetTx, "", " ")
fmt.Printf("%s\n", string(loanBrokerSetTxJSON))
// Submit, sign, and wait for validation ----------------------
fmt.Printf("\n=== Submitting LoanBrokerSet transaction ===\n\n")
loanBrokerSetResponse, err := client.SubmitTxAndWait(flatLoanBrokerSetTx, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &loanBrokerWallet,
})
if err != nil {
panic(err)
}
if loanBrokerSetResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to create loan broker: %s\n", loanBrokerSetResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Loan broker created successfully!\n")
// Extract loan broker information from the transaction result
fmt.Printf("\n=== Loan Broker Information ===\n\n")
for _, node := range loanBrokerSetResponse.Meta.AffectedNodes {
if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "LoanBroker" {
fmt.Printf("LoanBroker ID: %s\n", node.CreatedNode.LedgerIndex)
fmt.Printf("LoanBroker Pseudo-Account Address: %s\n", node.CreatedNode.NewFields["Account"])
break
}
}
}

View File

@@ -0,0 +1,153 @@
// IMPORTANT: This example creates a loan using a preconfigured
// loan broker, borrower, and private vault.
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"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"
)
// ptr is a helper that returns a pointer to the given value,
// used for setting optional transaction fields in Go.
func ptr[T any](v T) *T { return &v }
func main() {
// Connect to the network ----------------------
client := websocket.NewClient(
websocket.NewClientConfig().
WithHost("wss://s.devnet.rippletest.net:51233"),
)
defer client.Disconnect()
if err := client.Connect(); err != nil {
panic(err)
}
// Check for setup data; run lending-setup if missing
if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) {
fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n")
cmd := exec.Command("go", "run", "./lending-setup")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
// Load preconfigured accounts and LoanBrokerID
data, err := os.ReadFile("lending-setup.json")
if err != nil {
panic(err)
}
var setup map[string]any
if err := json.Unmarshal(data, &setup); err != nil {
panic(err)
}
// You can replace these values with your own
loanBrokerWallet, err := wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
borrowerWallet, err := wallet.FromSecret(setup["borrower"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
loanBrokerID := setup["loanBrokerID"].(string)
fmt.Printf("\nLoan broker address: %s\n", loanBrokerWallet.ClassicAddress)
fmt.Printf("Borrower address: %s\n", borrowerWallet.ClassicAddress)
fmt.Printf("LoanBrokerID: %s\n", loanBrokerID)
// Prepare LoanSet transaction ----------------------
// Account and Counterparty accounts can be swapped, but determines signing order.
// Account signs first, Counterparty signs second.
fmt.Printf("\n=== Preparing LoanSet transaction ===\n\n")
counterparty := borrowerWallet.ClassicAddress
loanSetTx := transaction.LoanSet{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
LoanBrokerID: loanBrokerID,
PrincipalRequested: "1000",
Counterparty: &counterparty,
InterestRate: ptr(types.InterestRate(500)),
PaymentTotal: ptr(types.PaymentTotal(12)),
PaymentInterval: ptr(types.PaymentInterval(2592000)),
GracePeriod: ptr(types.GracePeriod(604800)),
LoanOriginationFee: ptr(types.XRPLNumber("100")),
LoanServiceFee: ptr(types.XRPLNumber("10")),
}
// Flatten() converts the struct to a map and adds the TransactionType field.
// The result is cast to FlatTransaction, which is required by Autofill and signing methods.
flatLoanSetTx := transaction.FlatTransaction(loanSetTx.Flatten())
// Autofill the transaction
if err := client.Autofill(&flatLoanSetTx); err != nil {
panic(err)
}
loanSetTxJSON, _ := json.MarshalIndent(flatLoanSetTx, "", " ")
fmt.Printf("%s\n", string(loanSetTxJSON))
// Loan broker signs first
fmt.Printf("\n=== Adding loan broker signature ===\n\n")
_, _, err = loanBrokerWallet.Sign(flatLoanSetTx)
if err != nil {
panic(err)
}
fmt.Printf("TxnSignature: %s\n", flatLoanSetTx["TxnSignature"])
fmt.Printf("SigningPubKey: %s\n\n", flatLoanSetTx["SigningPubKey"])
loanBrokerSignedJSON, _ := json.MarshalIndent(flatLoanSetTx, "", " ")
fmt.Printf("Signed loanSetTx for borrower to sign over:\n%s\n", string(loanBrokerSignedJSON))
// Borrower signs second
fmt.Printf("\n=== Adding borrower signature ===\n\n")
fullySignedBlob, _, err := wallet.SignLoanSetByCounterparty(borrowerWallet, &flatLoanSetTx, nil)
if err != nil {
panic(err)
}
borrowerSignatures := flatLoanSetTx["CounterpartySignature"].(map[string]any)
fmt.Printf("Borrower TxnSignature: %s\n", borrowerSignatures["TxnSignature"])
fmt.Printf("Borrower SigningPubKey: %s\n", borrowerSignatures["SigningPubKey"])
fullySignedJSON, _ := json.MarshalIndent(flatLoanSetTx, "", " ")
fmt.Printf("\nFully signed LoanSet transaction:\n%s\n", string(fullySignedJSON))
// Submit and wait for validation ----------------------
fmt.Printf("\n=== Submitting signed LoanSet transaction ===\n\n")
loanSetResponse, err := client.SubmitTxBlobAndWait(fullySignedBlob, false)
if err != nil {
panic(err)
}
if loanSetResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to create loan: %s\n", loanSetResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Loan created successfully!\n")
// Extract loan information from the transaction result
fmt.Printf("\n=== Loan Information ===\n\n")
for _, node := range loanSetResponse.Meta.AffectedNodes {
if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "Loan" {
loanJSON, _ := json.MarshalIndent(node.CreatedNode.NewFields, "", " ")
fmt.Printf("%s\n", string(loanJSON))
break
}
}
}

View File

@@ -0,0 +1,19 @@
module github.com/XRPLF
go 1.24.3
require github.com/Peersyst/xrpl-go v0.1.17
require (
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/json-iterator/go v1.1.12 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/crypto v0.44.0 // indirect
)

View File

@@ -0,0 +1,656 @@
// Setup script for lending protocol tutorials
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
"time"
"github.com/Peersyst/xrpl-go/pkg/crypto"
"github.com/Peersyst/xrpl-go/xrpl/faucet"
ledger "github.com/Peersyst/xrpl-go/xrpl/ledger-entry-types"
"github.com/Peersyst/xrpl-go/xrpl/queries/account"
"github.com/Peersyst/xrpl-go/xrpl/queries/common"
requests "github.com/Peersyst/xrpl-go/xrpl/queries/transactions"
"github.com/Peersyst/xrpl-go/xrpl/rpc"
rpctypes "github.com/Peersyst/xrpl-go/xrpl/rpc/types"
"github.com/Peersyst/xrpl-go/xrpl/transaction"
"github.com/Peersyst/xrpl-go/xrpl/transaction/types"
"github.com/Peersyst/xrpl-go/xrpl/wallet"
)
// ptr is a helper that returns a pointer to the given value,
// used for setting optional transaction fields in Go.
func ptr[T any](v T) *T { return &v }
func main() {
fmt.Print("Setting up tutorial: 0/7\r")
// Connect to devnet
cfg, err := rpc.NewClientConfig(
"https://s.devnet.rippletest.net:51234",
rpc.WithFaucetProvider(faucet.NewDevnetFaucetProvider()),
)
if err != nil {
panic(err)
}
client := rpc.NewClient(cfg)
submitOpts := func(w *wallet.Wallet) *rpctypes.SubmitOptions {
return &rpctypes.SubmitOptions{Autofill: true, Wallet: w}
}
// Create and fund wallets concurrently
createAndFund := func(ch chan<- wallet.Wallet) {
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())
}
ch <- w
}
lbWalletCh := make(chan wallet.Wallet, 1)
brWalletCh := make(chan wallet.Wallet, 1)
depWalletCh := make(chan wallet.Wallet, 1)
credWalletCh := make(chan wallet.Wallet, 1)
go createAndFund(lbWalletCh)
go createAndFund(brWalletCh)
go createAndFund(depWalletCh)
go createAndFund(credWalletCh)
loanBrokerWallet := <-lbWalletCh
borrowerWallet := <-brWalletCh
depositorWallet := <-depWalletCh
credIssuerWallet := <-credWalletCh
fmt.Print("Setting up tutorial: 1/7\r")
// Create tickets for parallel transactions
extractTickets := func(resp *requests.TxResponse) []uint32 {
var tickets []uint32
for _, node := range resp.Meta.AffectedNodes {
if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "Ticket" {
ticketSeq, _ := node.CreatedNode.NewFields["TicketSequence"].(json.Number).Int64()
tickets = append(tickets, uint32(ticketSeq))
}
}
return tickets
}
ciTicketCh := make(chan []uint32, 1)
lbTicketCh := make(chan []uint32, 1)
brTicketCh := make(chan []uint32, 1)
dpTicketCh := make(chan []uint32, 1)
go func() {
resp, err := client.SubmitTxAndWait((&transaction.TicketCreate{
BaseTx: transaction.BaseTx{
Account: credIssuerWallet.GetAddress(),
},
TicketCount: 4,
}).Flatten(), submitOpts(&credIssuerWallet))
if err != nil {
panic(err)
}
ciTicketCh <- extractTickets(resp)
}()
go func() {
resp, err := client.SubmitTxAndWait((&transaction.TicketCreate{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.GetAddress(),
},
TicketCount: 4,
}).Flatten(), submitOpts(&loanBrokerWallet))
if err != nil {
panic(err)
}
lbTicketCh <- extractTickets(resp)
}()
go func() {
resp, err := client.SubmitTxAndWait((&transaction.TicketCreate{
BaseTx: transaction.BaseTx{
Account: borrowerWallet.GetAddress(),
},
TicketCount: 2,
}).Flatten(), submitOpts(&borrowerWallet))
if err != nil {
panic(err)
}
brTicketCh <- extractTickets(resp)
}()
go func() {
resp, err := client.SubmitTxAndWait((&transaction.TicketCreate{
BaseTx: transaction.BaseTx{
Account: depositorWallet.GetAddress(),
},
TicketCount: 2,
}).Flatten(), submitOpts(&depositorWallet))
if err != nil {
panic(err)
}
dpTicketCh <- extractTickets(resp)
}()
ciTickets := <-ciTicketCh
lbTickets := <-lbTicketCh
brTickets := <-brTicketCh
dpTickets := <-dpTicketCh
fmt.Print("Setting up tutorial: 2/7\r")
// Issue MPT with depositor
// Set up credentials and domain with credentialIssuer
credentialType := hex.EncodeToString([]byte("KYC-Verified"))
mptData := types.ParsedMPTokenMetadata{
Ticker: "TSTUSD",
Name: "Test USD MPT",
Desc: ptr("A sample non-yield-bearing stablecoin backed by U.S. Treasuries."),
Icon: "https://example.org/tstusd-icon.png",
AssetClass: "rwa",
AssetSubclass: ptr("stablecoin"),
IssuerName: "Example Treasury Reserve Co.",
URIs: []types.ParsedMPTokenMetadataURI{
{
URI: "https://exampletreasury.com/tstusd",
Category: "website",
Title: "Product Page",
},
{
URI: "https://exampletreasury.com/tstusd/reserve",
Category: "docs",
Title: "Reserve Attestation",
},
},
AdditionalInfo: map[string]any{
"reserve_type": "U.S. Treasury Bills",
"custody_provider": "Example Custodian Bank",
"audit_frequency": "Monthly",
"last_audit_date": "2026-01-15",
"pegged_currency": "USD",
},
}
mptMetadataHex, err := types.EncodeMPTokenMetadata(mptData)
if err != nil {
panic(err)
}
mptCh := make(chan *requests.TxResponse, 1)
domainCh := make(chan *requests.TxResponse, 1)
credLbCh := make(chan struct{}, 1)
credBrCh := make(chan struct{}, 1)
credDpCh := make(chan struct{}, 1)
// MPT issuance
go func() {
resp, err := client.SubmitTxAndWait((&transaction.MPTokenIssuanceCreate{
BaseTx: transaction.BaseTx{
Account: depositorWallet.GetAddress(),
Flags: transaction.TfMPTCanTransfer | transaction.TfMPTCanClawback | transaction.TfMPTCanTrade,
},
MaximumAmount: ptr(types.XRPCurrencyAmount(100000000)),
TransferFee: ptr(uint16(0)),
MPTokenMetadata: &mptMetadataHex,
}).Flatten(), submitOpts(&depositorWallet))
if err != nil {
panic(err)
}
mptCh <- resp
}()
// PermissionedDomainSet
go func() {
resp, err := client.SubmitTxAndWait((&transaction.PermissionedDomainSet{
BaseTx: transaction.BaseTx{
Account: credIssuerWallet.GetAddress(),
Sequence: 0,
TicketSequence: ciTickets[0],
},
AcceptedCredentials: types.AuthorizeCredentialList{
{
Credential: types.Credential{
Issuer: credIssuerWallet.GetAddress(),
CredentialType: types.CredentialType(credentialType),
},
},
},
}).Flatten(), submitOpts(&credIssuerWallet))
if err != nil {
panic(err)
}
domainCh <- resp
}()
// CredentialCreate for loan broker
go func() {
_, err := client.SubmitTxAndWait((&transaction.CredentialCreate{
BaseTx: transaction.BaseTx{
Account: credIssuerWallet.GetAddress(),
Sequence: 0,
TicketSequence: ciTickets[1],
},
CredentialType: types.CredentialType(credentialType),
Subject: loanBrokerWallet.GetAddress(),
}).Flatten(), submitOpts(&credIssuerWallet))
if err != nil {
panic(err)
}
credLbCh <- struct{}{}
}()
// CredentialCreate for borrower
go func() {
_, err := client.SubmitTxAndWait((&transaction.CredentialCreate{
BaseTx: transaction.BaseTx{
Account: credIssuerWallet.GetAddress(),
Sequence: 0,
TicketSequence: ciTickets[2],
},
CredentialType: types.CredentialType(credentialType),
Subject: borrowerWallet.GetAddress(),
}).Flatten(), submitOpts(&credIssuerWallet))
if err != nil {
panic(err)
}
credBrCh <- struct{}{}
}()
// CredentialCreate for depositor
go func() {
_, err := client.SubmitTxAndWait((&transaction.CredentialCreate{
BaseTx: transaction.BaseTx{
Account: credIssuerWallet.GetAddress(),
Sequence: 0,
TicketSequence: ciTickets[3],
},
CredentialType: types.CredentialType(credentialType),
Subject: depositorWallet.GetAddress(),
}).Flatten(), submitOpts(&credIssuerWallet))
if err != nil {
panic(err)
}
credDpCh <- struct{}{}
}()
mptResp := <-mptCh
domainResp := <-domainCh
<-credLbCh
<-credBrCh
<-credDpCh
// Extract MPT issuance ID
mptID := string(*mptResp.Meta.MPTIssuanceID)
// Extract domain ID from transaction meta
var domainID string
for _, node := range domainResp.Meta.AffectedNodes {
if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "PermissionedDomain" {
domainID = node.CreatedNode.LedgerIndex
break
}
}
fmt.Print("Setting up tutorial: 3/7\r")
// Accept credentials and authorize MPT for each account
lbCredCh := make(chan struct{}, 1)
lbMptCh := make(chan struct{}, 1)
brCredCh := make(chan struct{}, 1)
brMptCh := make(chan struct{}, 1)
depCredCh := make(chan struct{}, 1)
// Loan broker: accept credential
go func() {
_, err := client.SubmitTxAndWait((&transaction.CredentialAccept{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.GetAddress(),
Sequence: 0,
TicketSequence: lbTickets[0],
},
CredentialType: types.CredentialType(credentialType),
Issuer: credIssuerWallet.GetAddress(),
}).Flatten(), submitOpts(&loanBrokerWallet))
if err != nil {
panic(err)
}
lbCredCh <- struct{}{}
}()
// Loan broker: authorize MPT
go func() {
_, err := client.SubmitTxAndWait((&transaction.MPTokenAuthorize{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.GetAddress(),
Sequence: 0,
TicketSequence: lbTickets[1],
},
MPTokenIssuanceID: mptID,
}).Flatten(), submitOpts(&loanBrokerWallet))
if err != nil {
panic(err)
}
lbMptCh <- struct{}{}
}()
// Borrower: accept credential
go func() {
_, err := client.SubmitTxAndWait((&transaction.CredentialAccept{
BaseTx: transaction.BaseTx{
Account: borrowerWallet.GetAddress(),
Sequence: 0,
TicketSequence: brTickets[0],
},
CredentialType: types.CredentialType(credentialType),
Issuer: credIssuerWallet.GetAddress(),
}).Flatten(), submitOpts(&borrowerWallet))
if err != nil {
panic(err)
}
brCredCh <- struct{}{}
}()
// Borrower: authorize MPT
go func() {
_, err := client.SubmitTxAndWait((&transaction.MPTokenAuthorize{
BaseTx: transaction.BaseTx{
Account: borrowerWallet.GetAddress(),
Sequence: 0,
TicketSequence: brTickets[1],
},
MPTokenIssuanceID: mptID,
}).Flatten(), submitOpts(&borrowerWallet))
if err != nil {
panic(err)
}
brMptCh <- struct{}{}
}()
// Depositor: accept credential only
go func() {
_, err := client.SubmitTxAndWait((&transaction.CredentialAccept{
BaseTx: transaction.BaseTx{
Account: depositorWallet.GetAddress(),
},
CredentialType: types.CredentialType(credentialType),
Issuer: credIssuerWallet.GetAddress(),
}).Flatten(), submitOpts(&depositorWallet))
if err != nil {
panic(err)
}
depCredCh <- struct{}{}
}()
<-lbCredCh
<-lbMptCh
<-brCredCh
<-brMptCh
<-depCredCh
fmt.Print("Setting up tutorial: 4/7\r")
// Create private vault and distribute MPT to accounts concurrently
vaultCh := make(chan *requests.TxResponse, 1)
distLbCh := make(chan struct{}, 1)
distBrCh := make(chan struct{}, 1)
go func() {
resp, err := client.SubmitTxAndWait((&transaction.VaultCreate{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.GetAddress(),
Flags: transaction.TfVaultPrivate,
},
Asset: ledger.Asset{MPTIssuanceID: mptID},
DomainID: &domainID,
}).Flatten(), submitOpts(&loanBrokerWallet))
if err != nil {
panic(err)
}
vaultCh <- resp
}()
go func() {
_, err := client.SubmitTxAndWait((&transaction.Payment{
BaseTx: transaction.BaseTx{
Account: depositorWallet.GetAddress(),
Sequence: 0,
TicketSequence: dpTickets[0],
},
Destination: loanBrokerWallet.GetAddress(),
Amount: types.MPTCurrencyAmount{
MPTIssuanceID: mptID,
Value: "5000",
},
}).Flatten(), submitOpts(&depositorWallet))
if err != nil {
panic(err)
}
distLbCh <- struct{}{}
}()
go func() {
_, err := client.SubmitTxAndWait((&transaction.Payment{
BaseTx: transaction.BaseTx{
Account: depositorWallet.GetAddress(),
Sequence: 0,
TicketSequence: dpTickets[1],
},
Destination: borrowerWallet.GetAddress(),
Amount: types.MPTCurrencyAmount{
MPTIssuanceID: mptID,
Value: "2500",
},
}).Flatten(), submitOpts(&depositorWallet))
if err != nil {
panic(err)
}
distBrCh <- struct{}{}
}()
vaultResp := <-vaultCh
<-distLbCh
<-distBrCh
var vaultID string
for _, node := range vaultResp.Meta.AffectedNodes {
if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "Vault" {
vaultID = node.CreatedNode.LedgerIndex
break
}
}
fmt.Print("Setting up tutorial: 5/7\r")
// Create LoanBroker and deposit MPT into vault
lbSetCh := make(chan *requests.TxResponse, 1)
vaultDepCh := make(chan struct{}, 1)
go func() {
resp, err := client.SubmitTxAndWait((&transaction.LoanBrokerSet{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.GetAddress(),
},
VaultID: vaultID,
}).Flatten(), submitOpts(&loanBrokerWallet))
if err != nil {
panic(err)
}
lbSetCh <- resp
}()
go func() {
_, err := client.SubmitTxAndWait((&transaction.VaultDeposit{
BaseTx: transaction.BaseTx{
Account: depositorWallet.GetAddress(),
},
VaultID: types.Hash256(vaultID),
Amount: types.MPTCurrencyAmount{
MPTIssuanceID: mptID,
Value: "50000000",
},
}).Flatten(), submitOpts(&depositorWallet))
if err != nil {
panic(err)
}
vaultDepCh <- struct{}{}
}()
lbSetResp := <-lbSetCh
<-vaultDepCh
var loanBrokerID string
for _, node := range lbSetResp.Meta.AffectedNodes {
if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "LoanBroker" {
loanBrokerID = node.CreatedNode.LedgerIndex
break
}
}
fmt.Print("Setting up tutorial: 6/7\r")
// Create 2 identical loans with complete repayment due in 30 days
// Helper function to create, sign, and submit a LoanSet transaction
createLoan := func(ticketSequence uint32) *requests.TxResponse {
counterparty := borrowerWallet.GetAddress()
loanSetTx := &transaction.LoanSet{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.GetAddress(),
Sequence: 0,
TicketSequence: ticketSequence,
},
LoanBrokerID: loanBrokerID,
PrincipalRequested: "1000",
Counterparty: &counterparty,
InterestRate: ptr(types.InterestRate(500)),
PaymentTotal: ptr(types.PaymentTotal(1)),
PaymentInterval: ptr(types.PaymentInterval(2592000)),
LoanOriginationFee: ptr(types.XRPLNumber("100")),
LoanServiceFee: ptr(types.XRPLNumber("10")),
}
flatTx := transaction.FlatTransaction(loanSetTx.Flatten())
if err := client.Autofill(&flatTx); err != nil {
panic(err)
}
// Loan broker signs first
_, _, err := loanBrokerWallet.Sign(flatTx)
if err != nil {
panic(err)
}
// Borrower signs second
blob, _, err := wallet.SignLoanSetByCounterparty(borrowerWallet, &flatTx, nil)
if err != nil {
panic(err)
}
// Submit and wait for validation
resp, err := client.SubmitTxBlobAndWait(blob, false)
if err != nil {
panic(err)
}
return resp
}
loan1Ch := make(chan *requests.TxResponse, 1)
loan2Ch := make(chan *requests.TxResponse, 1)
go func() { loan1Ch <- createLoan(lbTickets[2]) }()
go func() { loan2Ch <- createLoan(lbTickets[3]) }()
loan1Resp := <-loan1Ch
loan2Resp := <-loan2Ch
var loanID1, loanID2 string
for _, node := range loan1Resp.Meta.AffectedNodes {
if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "Loan" {
loanID1 = node.CreatedNode.LedgerIndex
break
}
}
for _, node := range loan2Resp.Meta.AffectedNodes {
if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "Loan" {
loanID2 = node.CreatedNode.LedgerIndex
break
}
}
fmt.Print("Setting up tutorial: 7/7\r")
// Write setup data to JSON file.
// Using a struct to preserve field order.
setupData := struct {
Description string `json:"description"`
LoanBroker any `json:"loanBroker"`
Borrower any `json:"borrower"`
Depositor any `json:"depositor"`
CredentialIssuer any `json:"credentialIssuer"`
DomainID string `json:"domainID"`
MptID string `json:"mptID"`
VaultID string `json:"vaultID"`
LoanBrokerID string `json:"loanBrokerID"`
LoanID1 string `json:"loanID1"`
LoanID2 string `json:"loanID2"`
}{
Description: "This file is auto-generated by lending-setup. It stores XRPL account info for use in lending protocol tutorials.",
LoanBroker: map[string]string{
"address": string(loanBrokerWallet.ClassicAddress),
"seed": loanBrokerWallet.Seed,
},
Borrower: map[string]string{
"address": string(borrowerWallet.ClassicAddress),
"seed": borrowerWallet.Seed,
},
Depositor: map[string]string{
"address": string(depositorWallet.ClassicAddress),
"seed": depositorWallet.Seed,
},
CredentialIssuer: map[string]string{
"address": string(credIssuerWallet.ClassicAddress),
"seed": credIssuerWallet.Seed,
},
DomainID: domainID,
MptID: mptID,
VaultID: vaultID,
LoanBrokerID: loanBrokerID,
LoanID1: loanID1,
LoanID2: loanID2,
}
jsonData, err := json.MarshalIndent(setupData, "", " ")
if err != nil {
panic(err)
}
if err := os.WriteFile("lending-setup.json", jsonData, 0644); err != nil {
panic(err)
}
fmt.Println("Setting up tutorial: Complete!")
}

View File

@@ -0,0 +1,195 @@
// IMPORTANT: This example impairs an existing loan, which has a 60 second grace period.
// After the 60 seconds pass, this example defaults the loan.
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"time"
"github.com/Peersyst/xrpl-go/xrpl/flag"
"github.com/Peersyst/xrpl-go/xrpl/queries/common"
"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/wallet"
"github.com/Peersyst/xrpl-go/xrpl/websocket"
wstypes "github.com/Peersyst/xrpl-go/xrpl/websocket/types"
)
func main() {
// Connect to the network ----------------------
client := websocket.NewClient(
websocket.NewClientConfig().
WithHost("wss://s.devnet.rippletest.net:51233"),
)
defer client.Disconnect()
if err := client.Connect(); err != nil {
panic(err)
}
// Check for setup data; run lending-setup if missing
if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) {
fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n")
cmd := exec.Command("go", "run", "./lending-setup")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
// Load preconfigured accounts and LoanID
data, err := os.ReadFile("lending-setup.json")
if err != nil {
panic(err)
}
var setup map[string]any
if err := json.Unmarshal(data, &setup); err != nil {
panic(err)
}
// You can replace these values with your own
loanBrokerWallet, err := wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
loanID := setup["loanID1"].(string)
fmt.Printf("\nLoan broker address: %s\n", loanBrokerWallet.ClassicAddress)
fmt.Printf("LoanID: %s\n", loanID)
// Check loan status before impairment ----------------------
fmt.Printf("\n=== Loan Status ===\n\n")
loanStatus, err := client.GetLedgerEntry(&ledger.EntryRequest{
Index: loanID,
LedgerIndex: common.Validated,
})
if err != nil {
panic(err)
}
fmt.Printf("Total Amount Owed: %s TSTUSD.\n", loanStatus.Node["TotalValueOutstanding"])
// Convert Ripple Epoch timestamp to local date and time
nextPaymentDueDate := int64(loanStatus.Node["NextPaymentDueDate"].(float64))
paymentDue := time.UnixMilli(xrpltime.RippleTimeToUnixTime(nextPaymentDueDate))
fmt.Printf("Payment Due Date: %s\n", paymentDue.Local().Format(time.DateTime))
// Prepare LoanManage transaction to impair the loan ----------------------
fmt.Printf("\n=== Preparing LoanManage transaction to impair loan ===\n\n")
loanManageImpair := transaction.LoanManage{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
LoanID: loanID,
}
loanManageImpair.SetLoanImpairFlag()
// Flatten() converts the struct to a map and adds the TransactionType field
flatLoanManageImpair := loanManageImpair.Flatten()
loanManageImpairJSON, _ := json.MarshalIndent(flatLoanManageImpair, "", " ")
fmt.Printf("%s\n", string(loanManageImpairJSON))
// Sign, submit, and wait for impairment validation ----------------------
fmt.Printf("\n=== Submitting LoanManage impairment transaction ===\n\n")
impairResponse, err := client.SubmitTxAndWait(flatLoanManageImpair, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &loanBrokerWallet,
})
if err != nil {
panic(err)
}
if impairResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to impair loan: %s\n", impairResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Loan impaired successfully!\n")
// Extract loan impairment info from transaction results ----------------------
var loanNode map[string]any
for _, node := range impairResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "Loan" {
loanNode = node.ModifiedNode.FinalFields
break
}
}
// Check grace period and next payment due date
gracePeriod := int64(loanNode["GracePeriod"].(float64))
nextPaymentDueDate = int64(loanNode["NextPaymentDueDate"].(float64))
defaultTime := nextPaymentDueDate + gracePeriod
paymentDue = time.UnixMilli(xrpltime.RippleTimeToUnixTime(nextPaymentDueDate))
fmt.Printf("New Payment Due Date: %s\n", paymentDue.Local().Format(time.DateTime))
fmt.Printf("Grace Period: %d seconds\n", gracePeriod)
// Convert current time to Ripple Epoch timestamp
currentTime := xrpltime.UnixTimeToRippleTime(time.Now().Unix())
// Add a small buffer (5 seconds) to account for ledger close time
secondsUntilDefault := defaultTime - currentTime + 5
// Countdown until loan can be defaulted ----------------------
fmt.Printf("\n=== Countdown until loan can be defaulted ===\n\n")
for secondsUntilDefault >= 0 {
fmt.Printf("\r%d seconds...", secondsUntilDefault)
time.Sleep(time.Second)
secondsUntilDefault--
}
fmt.Print("\rGrace period expired. Loan can now be defaulted.\n")
// Prepare LoanManage transaction to default the loan ----------------------
fmt.Printf("\n=== Preparing LoanManage transaction to default loan ===\n\n")
loanManageDefault := transaction.LoanManage{
BaseTx: transaction.BaseTx{
Account: loanBrokerWallet.ClassicAddress,
},
LoanID: loanID,
}
loanManageDefault.SetLoanDefaultFlag()
flatLoanManageDefault := loanManageDefault.Flatten()
loanManageDefaultJSON, _ := json.MarshalIndent(flatLoanManageDefault, "", " ")
fmt.Printf("%s\n", string(loanManageDefaultJSON))
// Sign, submit, and wait for default validation ----------------------
fmt.Printf("\n=== Submitting LoanManage default transaction ===\n\n")
defaultResponse, err := client.SubmitTxAndWait(flatLoanManageDefault, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &loanBrokerWallet,
})
if err != nil {
panic(err)
}
if defaultResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to default loan: %s\n", defaultResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Loan defaulted successfully!\n")
// Verify loan default status from transaction results ----------------------
fmt.Printf("\n=== Checking final loan status ===\n\n")
for _, node := range defaultResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "Loan" {
loanNode = node.ModifiedNode.FinalFields
break
}
}
loanFlags := uint32(loanNode["Flags"].(float64))
// Check which loan flags are set
activeFlags := []string{}
if flag.Contains(loanFlags, transaction.TfLoanDefault) {
activeFlags = append(activeFlags, "tfLoanDefault")
}
if flag.Contains(loanFlags, transaction.TfLoanImpair) {
activeFlags = append(activeFlags, "tfLoanImpair")
}
fmt.Printf("Final loan flags: %v\n", activeFlags)
}

View File

@@ -0,0 +1,179 @@
// IMPORTANT: This example pays off an existing loan and then deletes it.
package main
import (
"encoding/json"
"fmt"
"math/big"
"os"
"os/exec"
"github.com/Peersyst/xrpl-go/xrpl/queries/common"
"github.com/Peersyst/xrpl-go/xrpl/queries/ledger"
"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"
)
func main() {
// Connect to the network ----------------------
client := websocket.NewClient(
websocket.NewClientConfig().
WithHost("wss://s.devnet.rippletest.net:51233"),
)
defer client.Disconnect()
if err := client.Connect(); err != nil {
panic(err)
}
// Check for setup data; run lending-setup if missing
if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) {
fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n")
cmd := exec.Command("go", "run", "./lending-setup")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
// Load preconfigured accounts and LoanID
data, err := os.ReadFile("lending-setup.json")
if err != nil {
panic(err)
}
var setup map[string]any
if err := json.Unmarshal(data, &setup); err != nil {
panic(err)
}
// You can replace these values with your own
borrowerWallet, err := wallet.FromSecret(setup["borrower"].(map[string]any)["seed"].(string))
if err != nil {
panic(err)
}
loanID := setup["loanID2"].(string)
mptID := setup["mptID"].(string)
fmt.Printf("\nBorrower address: %s\n", borrowerWallet.ClassicAddress)
fmt.Printf("LoanID: %s\n", loanID)
fmt.Printf("MPT ID: %s\n", mptID)
// Check initial loan status ----------------------
fmt.Printf("\n=== Loan Status ===\n\n")
loanStatus, err := client.GetLedgerEntry(&ledger.EntryRequest{
Index: loanID,
LedgerIndex: common.Validated,
})
if err != nil {
panic(err)
}
totalValueOutstanding := loanStatus.Node["TotalValueOutstanding"].(string)
loanServiceFee := loanStatus.Node["LoanServiceFee"].(string)
outstanding, _ := new(big.Int).SetString(totalValueOutstanding, 10)
serviceFee, _ := new(big.Int).SetString(loanServiceFee, 10)
totalPayment := new(big.Int).Add(outstanding, serviceFee).String()
fmt.Printf("Amount Owed: %s TSTUSD\n", totalValueOutstanding)
fmt.Printf("Loan Service Fee: %s TSTUSD\n", loanServiceFee)
fmt.Printf("Total Payment Due (including fees): %s TSTUSD\n", totalPayment)
// Prepare LoanPay transaction ----------------------
fmt.Printf("\n=== Preparing LoanPay transaction ===\n\n")
loanPayTx := transaction.LoanPay{
BaseTx: transaction.BaseTx{
Account: borrowerWallet.ClassicAddress,
},
LoanID: loanID,
Amount: types.MPTCurrencyAmount{
MPTIssuanceID: mptID,
Value: totalPayment,
},
}
// Flatten() converts the struct to a map and adds the TransactionType field
flatLoanPayTx := loanPayTx.Flatten()
loanPayTxJSON, _ := json.MarshalIndent(flatLoanPayTx, "", " ")
fmt.Printf("%s\n", string(loanPayTxJSON))
// Sign, submit, and wait for payment validation ----------------------
fmt.Printf("\n=== Submitting LoanPay transaction ===\n\n")
payResponse, err := client.SubmitTxAndWait(flatLoanPayTx, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &borrowerWallet,
})
if err != nil {
panic(err)
}
if payResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to pay loan: %s\n", payResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Loan paid successfully!\n")
// Extract updated loan info from transaction results ----------------------
fmt.Printf("\n=== Loan Status After Payment ===\n\n")
for _, node := range payResponse.Meta.AffectedNodes {
if node.ModifiedNode != nil && node.ModifiedNode.LedgerEntryType == "Loan" {
if balance, ok := node.ModifiedNode.FinalFields["TotalValueOutstanding"].(string); ok {
fmt.Printf("Outstanding Loan Balance: %s TSTUSD\n", balance)
} else {
fmt.Printf("Outstanding Loan Balance: Loan fully paid off!\n")
}
break
}
}
// Prepare LoanDelete transaction ----------------------
// Either the loan broker or borrower can submit this transaction.
fmt.Printf("\n=== Preparing LoanDelete transaction ===\n\n")
loanDeleteTx := transaction.LoanDelete{
BaseTx: transaction.BaseTx{
Account: borrowerWallet.ClassicAddress,
},
LoanID: loanID,
}
flatLoanDeleteTx := loanDeleteTx.Flatten()
loanDeleteTxJSON, _ := json.MarshalIndent(flatLoanDeleteTx, "", " ")
fmt.Printf("%s\n", string(loanDeleteTxJSON))
// Sign, submit, and wait for deletion validation ----------------------
fmt.Printf("\n=== Submitting LoanDelete transaction ===\n\n")
deleteResponse, err := client.SubmitTxAndWait(flatLoanDeleteTx, &wstypes.SubmitOptions{
Autofill: true,
Wallet: &borrowerWallet,
})
if err != nil {
panic(err)
}
if deleteResponse.Meta.TransactionResult != "tesSUCCESS" {
fmt.Printf("Error: Unable to delete loan: %s\n", deleteResponse.Meta.TransactionResult)
os.Exit(1)
}
fmt.Printf("Loan deleted successfully!\n")
// Verify loan deletion ----------------------
fmt.Printf("\n=== Verifying Loan Deletion ===\n\n")
_, err = client.GetLedgerEntry(&ledger.EntryRequest{
Index: loanID,
LedgerIndex: common.Validated,
})
if err != nil {
if err.Error() == "entryNotFound" {
fmt.Printf("Loan has been successfully removed from the XRP Ledger!\n")
} else {
panic(err)
}
} else {
fmt.Printf("Warning: Loan still exists in the ledger.\n")
}
}

View File

@@ -92,7 +92,7 @@ On supported platforms, see the [instructions on installing or updating `rippled
| [RPM for Red Hat / CentOS (x86-64)](https://repos.ripple.com/repos/rippled-rpm/stable/rippled-3.1.1-1.el9.x86_64.rpm) | `c6d028db1e2a4da898df68e5a92a893bebf1d167a0539d15ae27435f2155ccb2` |
| [DEB for Ubuntu / Debian (x86-64)](https://repos.ripple.com/repos/rippled-deb/pool/stable/rippled_3.1.1-1_amd64.deb) | `cc30c33012bd83ed793b38738870cf931a96ae106fde60b71685c766da1d22e3` |
For other platforms, please [build from source](https://github.com/XRPLF/rippled/blob/master/BUILD.md). The most recent commit in the git log should be the change setting the version:
For other platforms, please [build from source](https://github.com/XRPLF/rippled/blob/release-3.1/BUILD.md). The most recent commit in the git log should be the change setting the version:
```text
commit c5988233d05bedddac28866ed37607f4869855f9

View File

@@ -0,0 +1,61 @@
---
category: 2026
date: "2026-03-12"
template: '../../@theme/templates/blogpost'
seo:
description: rippled version 3.1.2 is now available. This version contains important security updates.
labels:
- rippled Release Notes
markdown:
editPage:
hide: true
---
# Introducing XRP Ledger version 3.1.2
Version 3.1.2 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release contains fixes for security issues that, in the worst case scenario, could cause servers to crash or restart. There are no new features or amendments in this release.
## Action Required
**If you operate a `rippled` server**, then you should update to version 3.1.2 as soon as possible to ensure service continuity.
### Impact of Not Upgrading
If you do not upgrade, your server may experience restarts or outages.
### Install / Upgrade
On supported platforms, see the [instructions on installing or updating `rippled`](../../docs/infrastructure/installation/index.md).
| Package | SHA-256 |
|:--------|:--------|
| [RPM for Red Hat / CentOS (x86-64)](https://repos.ripple.com/repos/rippled-rpm/stable/rippled-3.1.2-1.el9.x86_64.rpm) | `a51905bbffe97e714d0c66566e705704dea2783913a54c0fd62253f422d94713` |
| [DEB for Ubuntu / Debian (x86-64)](https://repos.ripple.com/repos/rippled-deb/pool/stable/rippled_3.1.2-1_amd64.deb) | `0d162a2778f7e1bcd8611bbfd23b9cb6d466a7dd752494a6640a909145446494` |
For other platforms, please [build from source](https://github.com/XRPLF/rippled/blob/release-3.1/BUILD.md). The most recent commit in the git log should be the change setting the version:
```text
commit 3ba3fcff4c4898a96a57838bb7c67a44a3d2ec5c
Author: Mayukha Vadari <mvadari@gmail.com>
Date: Thu Mar 12 15:01:01 2026 -0400
release: Bump version to 3.1.2
```
## Full Changelog
- refactor: Improve exception handling ([#6540](https://github.com/XRPLF/rippled/pull/6540))
## Credits
Thanks to the members of XRPL Commons who found and responsibly reported the issue: Luc Bocahut, Romain Thépaut, and Thomas Hussenet.
The fix was developed in collaboration with the team at RippleX.
## Bug Bounties and Responsible Disclosures
We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find.
For more information, see:
- [Ripple's Bug Bounty Program](https://ripple.com/legal/bug-bounty/)
- [`rippled` Security Policy](https://github.com/XRPLF/rippled/blob/develop/SECURITY.md)

View File

@@ -0,0 +1,101 @@
---
category: 2026
date: "2026-03-23"
template: '../../@theme/templates/blogpost'
seo:
description: This vulnerability disclosure report contains technical details of the XRP Ledger bug reported on June 9, 2025.
labels:
- Advisories
markdown:
editPage:
hide: true
---
# Vulnerability Disclosure Report: Transaction Set Handling
This vulnerability disclosure report contains technical details of the XRP Ledger bug reported on June 9, 2025.
**Date Reported:** June 9, 2025
**Affected Version(s):** rippled up to 2.6.2
## 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.
## Impact
If a UNL validator had been compromised before these vulnerabilities were fixed, a modified version of rippled could have been deployed that exploited the bugs. In particular, by manipulating the transaction data in transaction sets, the compromised validator could then cause a crash in all other validators that directly received the modified message. Crashes could then be triggered repeatedly until removal of the compromised validator from the UNL.
## Technical Details
### 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.
### Root Cause
A consensus round between validators is an attempt to agree on a set of transactions so they can be processed and included in the ledger. Each validator proposes a set of unprocessed transactions that they know about, and exchanges messages with other validators to establish a final set of transactions that a supermajority of them can agree on. The two vulnerabilities arose from how the transaction sets disputes were handled in our code.
Both vulnerabilities required one of the ~35 UNL validators to have been compromised, which would open the door to sending a maliciously crafted message that in turn would trigger any receiving node to crash. Even though compromising a UNL validator is challenging since they typically hide behind proxy nodes and only communicate with those nodes, it is not impossible.
### Vulnerability 1: comparing transactions
When a validator receives a transaction set from another validator, it compares that transaction set with its own and identifies the transactions that are different ("disputed"). This is where the first vulnerability came into play, as a compromised validator was able to claim that a certain transaction could be found in a node in the SHAMap where it actually was not located. Any validator who received the malicious transaction set would then crash the moment it tried to look up the transaction ID using the invalid node ID.
### Vulnerability 2: relaying transactions
When a validator has come across disputed transactions, it will relay them to its peers. The second vulnerability leveraged this functionality, whereby a compromised validator sent a transaction set in which the transaction data was an arbitrary hash. Any validator who received the malicious transaction set would identify this as a disputed transaction and would want to relay it to its peers.
Before relaying a transaction, a validator first checks if it is a pseudo transaction, as those should not be relayed. In this case, when a validator would try to perform this inspection, the invalid data in the transaction set would cause it to crash.
## Steps to reproduce
### Vulnerability 1: comparing transactions
1. A compromised validator constructs a `TxSet` where the `TxId` is not in the correct node.
2. The validator sends an accompanying proposal to its peers.
3. Peer validators receive the position, acquire the `TxSet`, and attempt to create disputes. This results in a crash when the node ID is used to look up the transaction ID.
### Vulnerability 2: relaying transactions
1. A compromised validator constructs a `TxSet` containing a `SHAMapItem` with data that cannot be read by the `STTx` constructor.
2. The validator sends an accompanying proposal to its peers.
3. Peer validators receive the position, acquire the `TxSet`, create disputes and attempt to relay them. This results in a crash when the pseudo transaction check is performed.
## Remediation
- To protect against the first vulnerability we added an extra check to confirm that a transaction can really be found in the node where the proposal said it would be.
- To protect against the second vulnerability we added a try-catch to handle the exception that the code will throw when a malicious transaction is inspected.
- To replicate the findings of Common Prefix, we deployed a modified version of rippled in our testing platform to simulate a compromised UNL validator. Without the first fix, the first attack would indeed crash all nodes that received a malicious message sent by the compromised validator. The same thing happened without the second fix when the second attack was launched. After applying both fixes, nodes that received the manipulated messages would no longer crash.
## Security enhancements roadmap
We continue to strengthen XRPLs security posture through multiple initiatives:
- Expanded security audits to preemptively find issues in unreleased code.
- AI-assisted code reviews to identify security issues.
- Hackathons and increased bug bounty incentives.
## Fixes / Patches Available
The fixes were released as part of [**rippled 3.0.0**](../2025/rippled-3.0.0.md).
## Acknowledgements
We extend our deepest gratitude to **Common Prefix** for responsibly disclosing both vulnerabilities and for giving us time and assistance to fix them.
## Contact
For more information or to report further issues, please see the [Bug Bounty Program](https://ripple.com/legal/bug-bounty) page.
## Incident Response Timeline
| Key Action | Date | Description |
|---|---|---|
| Initial discovery | 9 June 2025 | Nikolaos Kamarinakis from Common Prefix submits a bug report. |
| Test bed deployed | 10 July 2025 | Testing set up in place. |
| Bug reproduction | 6 August 2025 | First vulnerability reproduced. |
| Bug reproduction | 11 August 2025 | Second vulnerability reproduced. |
| Fixes created | 19 August 2025 | Fixes created in a private repository and in review. |
| Fixes tested | 10 October 2025 | Common Prefix tests the fixes. |
| Fixes approved | 16 October 2025 | Common Prefix approves the fixes. |
| Fixes released | 9 December 2025 | Fixes included in 3.0.0 release. |
| Report published | 23 March 2026 | Public vulnerability disclosure report published. |

View File

@@ -10,6 +10,8 @@
- group: '2026'
expanded: false
items:
- page: 2026/vulnerabilitydisclosurereport-bug-mar2026.md
- page: 2026/rippled-3.1.2.md
- page: 2026/vulnerabilitydisclosurereport-bug-feb2026.md
- page: 2026/rippled-3.1.1.md
- page: 2026/gpg-key-rotation.md

View File

@@ -730,12 +730,22 @@ const CommunityPage: React.FC = () => {
<tbody>
<tr>
<td className="td-img">
<a
href="https://discord.gg/xrpl"
target="_blank"
>
<img className="discord-icon" alt="discord icon" />
</a>
</td>
<td>
{translate(
"Join the XRPL Discord to connect and network with builders, validators, and cryptocurrency enthusiasts."
)}
<a
href="https://discord.gg/xrpl"
target="_blank"
>
{translate(
"Join the XRPL Discord to connect and network with builders, validators, and cryptocurrency enthusiasts."
)}
</a>
</td>
<td>
<a
@@ -749,12 +759,22 @@ const CommunityPage: React.FC = () => {
</tr>
<tr>
<td className="td-img">
<img className="twitter-icon" alt="twitter icon" />
<a
href="https://x.com/RippleXDev"
target="_blank"
>
<img className="twitter-icon" alt="twitter icon" />
</a>
</td>
<td>
{translate(
"Follow @RippleXDev on X for the latest updates on the XRP Ledger ecosystem."
)}
<a
href="https://x.com/RippleXDev"
target="_blank"
>
{translate(
"Follow @RippleXDev on X for the latest updates on the XRP Ledger ecosystem."
)}
</a>
</td>
<td>
<a
@@ -768,12 +788,22 @@ const CommunityPage: React.FC = () => {
</tr>
<tr>
<td className="td-img">
<img className="youtube-icon" alt="youtube icon" />
<a
href="https://youtube.com/playlist?list=PLl-QsmXvjodqxEjtUqEv3u2o2Zd6zqkNA&feature=shared"
target="_blank"
>
<img className="youtube-icon" alt="youtube icon" />
</a>
</td>
<td>
{translate(
"APEX 2025: View keynote sessions from the Apex 2025 where developers, entrepreneurs, and industry leaders come together to learn, build, and celebrate all things XRP Ledger."
)}
<a
href="https://youtube.com/playlist?list=PLl-QsmXvjodqxEjtUqEv3u2o2Zd6zqkNA&feature=shared"
target="_blank"
>
{translate(
"APEX 2025: View keynote sessions from the Apex 2025 where developers, entrepreneurs, and industry leaders come together to learn, build, and celebrate all things XRP Ledger."
)}
</a>
</td>
<td>
<a
@@ -787,12 +817,22 @@ const CommunityPage: React.FC = () => {
</tr>
<tr className="final-tr">
<td className="td-img">
<a
href="https://learn.xrpl.org/react-3d-game/"
target="_blank"
>
<img className="xrpl-icon" alt="xrpl icon" />
</a>
</td>
<td>
<a
href="https://learn.xrpl.org/react-3d-game/"
target="_blank"
>
{translate(
"Explore DeFi-Island: A 3D open-source world on the XRPL testnet. Chat with residents, complete quests, and dive into this React.js-powered experience—all in your web browser."
)}
</a>
</td>
<td>
<a

4
context7.json Normal file
View File

@@ -0,0 +1,4 @@
{
"url": "https://context7.com/xrplf/xrpl-dev-portal",
"public_key": "pk_dOzui5DRlEAqAN0pGtYMi"
}

View File

@@ -273,6 +273,8 @@
[XChainAddClaimAttestation transaction]: /docs/references/protocol/transactions/types/xchainaddclaimattestation.md
[XChainAddClaimAttestation transactions]: /docs/references/protocol/transactions/types/xchainaddclaimattestation.md
[XChainBridge amendment]: /resources/known-amendments.md#xchainbridge
[XChainClaim transaction]: /docs/references/protocol/transactions/types/xchainclaim.md
[XChainClaim transactions]: /docs/references/protocol/transactions/types/xchainclaim.md
[XChainCreateBridge transaction]: /docs/references/protocol/transactions/types/xchaincreatebridge.md
[XChainCreateBridge transactions]: /docs/references/protocol/transactions/types/xchaincreatebridge.md
[XChainCreateBridge]: /docs/references/protocol/transactions/types/xchaincreatebridge.md
@@ -370,6 +372,7 @@
[gateway_balances method]: /docs/references/http-websocket-apis/public-api-methods/account-methods/gateway_balances.md
[get_counts command]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/get_counts.md
[get_counts method]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/get_counts.md
[Get Started Using Go]: /docs/tutorials/get-started/get-started-go.md
[Get Started Using JavaScript]: /docs/tutorials/get-started/get-started-javascript.md
[Get Started Using Python]: /docs/tutorials/get-started/get-started-python.md
[hexadecimal]: https://en.wikipedia.org/wiki/Hexadecimal
@@ -487,5 +490,6 @@
[vault_info method]: /docs/references/http-websocket-apis/public-api-methods/vault-methods/vault_info.md
[wallet_propose command]: /docs/references/http-websocket-apis/admin-api-methods/key-generation-methods/wallet_propose.md
[wallet_propose method]: /docs/references/http-websocket-apis/admin-api-methods/key-generation-methods/wallet_propose.md
[xrpl-go library]: https://github.com/XRPLF/xrpl-go
[xrpl.js library]: https://github.com/XRPLF/xrpl.js
[xrpl-py library]: https://github.com/XRPLF/xrpl-py

View File

@@ -6,7 +6,7 @@ labels:
---
# Deleting Accounts
The owner of an account can send an [AccountDelete transaction][] to deletes the account and related entries from the ledger, sending most of the account's remaining XRP balance to another account. To discourage wasteful creation and deletion of accounts, deleting an account requires burning a higher than usual amount of XRP as the [transaction cost](../transactions/transaction-cost.md).
The owner of an account can send an [AccountDelete transaction][] to delete the account and related entries from the ledger, sending most of the account's remaining XRP balance to another account. To discourage wasteful creation and deletion of accounts, deleting an account requires burning a higher than usual amount of XRP as the [transaction cost](../transactions/transaction-cost.md).
Some types of associated ledger entries block an account from being deleted. For example, the issuer of a fungible token can't be deleted while anyone holds a nonzero balance of that token.
@@ -16,22 +16,32 @@ After an account has been deleted, it can be re-created in the ledger through th
To be deleted, an account must meet the following requirements:
- The account's `Sequence` number plus 256 must be less than the current [Ledger Index][].
- The account must not have any "deletion blockers" in its owner directory. This includes cases where the account is a sender _or_ receiver of funds. See below for a full list of deletion blockers.
- The account must own fewer than 1000 objects in the ledger.
- The account's `Sequence` number plus 255 must be less than or equal to the current [Ledger Index][]. This is to protect against replaying old transactions.
- The account must not have any "deletion blockers" in its owner directory. Deletion blockers are generally ledger entries that represent assets, obligations, or transfers of funds. See below for a full list of deletion blockers.
- The account must own 1000 or fewer objects in the ledger.
- The transaction must pay a special [transaction cost][] equal to at least the [owner reserve](reserves.md) for one item (currently {% $env.PUBLIC_OWNER_RESERVE %}).
- If the account has issued any [NFTs](../tokens/nfts/index.md), they must all have been burned. Additionally, the account's `FirstNFTSequence` number plus `MintedNFTokens` number plus 255 must be less than or equal to the current ledger index. This is to protect against reusing `NFTokenID` values. {% amendment-disclaimer name="fixNFTokenRemint" /%}
### Deletion Blockers
## Deletion Blockers
The following [ledger entry types](../../references/protocol/ledger-data/ledger-entry-types/index.md) are deletion blockers:
The following table shows which [ledger entry types](../../references/protocol/ledger-data/ledger-entry-types/index.md) are deletion blockers. Any other types of ledger entries that an account owns are automatically deleted along with the account.
- `Escrow`
- `PayChannel`
- `RippleState` (trust line)
- `Check`
- `PermissionedDomain` {% amendment-disclaimer name="PermissionedDomains" /%}
Some deletion blockers cannot be removed unilaterally. For example, if you have issued a token to someone else, you cannot delete your account as long as they hold your token. In other cases, you can remove the deletion blocker by sending a transaction that causes the entry to be removed from the ledger.
Any other types of ledger entries that an account owns are automatically deleted along with the account.
| Entry Type | Related Amendment | How to remove |
|---|---|---|
| [Bridge][Bridge entry] | {% amendment-disclaimer name="XChainBridge" compact=true /%} | Cannot be removed. |
| [Check][Check entry] | {% amendment-disclaimer name="Checks" compact=true /%} | Send a [CheckCancel transaction][] to cancel the check. |
| [Escrow][Escrow entry] | (Core protocol) | Send an [EscrowCancel transaction][] to cancel the transaction if it has expired; send [EscrowFinish transaction][] to finish it if you can satisfy the timed and/or conditional requirements of the escrow. Otherwise, you can't remove the entry. |
| [PayChannel][PayChannel entry] | (Core protocol) | Send a [PaymentChannelClaim transaction][] with the `tfClose` flag to request closing the channel; after the settle delay has passed, send another [PaymentChannelClaim transaction][] to fully close and remove the channel. |
| [PermissionedDomain][PermissionedDomain entry] | {% amendment-disclaimer name="PermissionedDomains" compact=true /%} | Send a [PermissionedDomainDelete transaction][] to delete the domain. |
| [RippleState][RippleState entry] | (Core protocol) | You can only remove the entry if the counterparty's settings are entirely default. If they are, you can remove it by getting the balance to 0 and setting your own settings to the default with a [TrustSet transaction][]. In the common case, the holder can remove the entry but the issuer cannot. |
| [MPToken][MPToken entry] | {% amendment-disclaimer name="MPTokensV1" compact=true /%} | If you are the holder of the MPT, reduce your balance to 0 (for example, using a payment), then send an [MPTokenAuthorize transaction][] with the `tfMPTUnauthorize` flag. If you are the issuer of the MPT, you can't remove the entry. |
| [MPTokenIssuance][MPTokenIssuance entry] | {% amendment-disclaimer name="MPTokensV1" compact=true /%} | Send an [MPTokenIssuanceDestroy transaction][]. You can only do this if there are no holders of the MPT. |
| [NFTokenPage][NFTokenPage entry] | {% amendment-disclaimer name="NonFungibleTokensV1_1" compact=true /%} | Send [NFTokenBurn transactions][] to burn, or [NFTokenCreateOffer transactions][] to sell or transfer, each NFT you hold. |
| [Vault][Vault entry] | {% amendment-disclaimer name="SingleAssetVault" compact=true /%} | Send a [VaultDelete transaction][] to delete the vault. You can only do this if the vault is empty. |
| [XChainOwnedClaimID][XChainOwnedClaimID entry] | {% amendment-disclaimer name="XChainBridge" compact=true /%} | Send an [XChainClaim transaction][] to complete the cross-chain transfer. |
| [XChainOwned<br>CreateAccountClaimID][XChainOwnedCreateAccountClaimID entry] | {% amendment-disclaimer name="XChainBridge" compact=true /%} | Send enough attestations ([XChainAddAccountCreateAttestation transactions][]) to create the new account. |
## Cost of Deleting

View File

@@ -45,9 +45,8 @@ Clawback is disabled by default. To use clawback, you must send an [AccountSet t
| Field | JSON Type | [Internal Type][] | Required | Description |
|:----------|:---------------------|:------------------|:---------|:------------------|
| `Account` | String - [Address][] | AccountID | Yes | The issuer of the asset being clawed back. Only the issuer can submit this transaction. |
| `Asset` | Object | Issue | Yes | Specifies the asset that the issuer wants to claw back from the AMM pool. The asset can be XRP, a token, or an MPT (see: [Specifying Without Amounts][]). The `issuer` field must match with `Account`. |
| `Asset2` | Object | Issue | Yes | Specifies the other asset in the AMM's pool. The asset can be XRP, a token, or an MPT (see: [Specifying Without Amounts][]). |
| `Asset` | Object | Issue | Yes | The asset to claw back, which can be a trust line token or MPT (see: [Specifying Without Amounts][]). The issuer must be the sender of this transaction. |
| `Asset2` | Object | Issue | Yes | The other asset of the AMM pool to claw back from. The asset can be XRP, a trust line token, or an MPT (see: [Specifying Without Amounts][]). |
| `Amount` | [Currency Amount][] | Amount | No | The maximum amount to claw back from the AMM account. The `currency` and `issuer` subfields should match the `Asset` subfields. If this field isn't specified, or the `value` subfield exceeds the holder's available tokens in the AMM, all of the holder's tokens are clawed back. |
| `Holder` | String - [Address][] | AccountID | Yes | The account holding the asset to be clawed back. |

View File

@@ -0,0 +1,240 @@
---
seo:
description: Delete an account, sending its remaining XRP to another account.
labels:
- Accounts
---
# Delete an Account
This tutorial shows how to delete an [account](../../../concepts/accounts/index.md) from the XRP Ledger, including checking that it meets the [requirements for deletion](../../../concepts/accounts/deleting-accounts.md).
## Goals
By following this tutorial, you should learn how to:
- Check if an account can be deleted.
- Delete an account.
## Prerequisites
To complete this tutorial, you should:
- Have a basic understanding of the XRP Ledger.
- Have an XRP Ledger [client library](../../../references/client-libraries.md), such as **xrpl.js**, installed.
- Have an XRP Ledger Testnet account to delete. If you create a new account as part of the tutorial, you must wait about 15 minutes for it to become eligible for deletion.
- Know an address where you want to send the deleted account's remaining XRP. For this tutorial, you can use the address `rJjHYTCPpNA3qAM8ZpCDtip3a8xg7B8PFo` to return funds to the Testnet faucet.
## Steps
### 1. Install dependencies
{% tabs %}
{% tab label="JavaScript" %}
From the code sample folder, use `npm` to install dependencies:
```sh
npm i
```
{% /tab %}
{% tab label="Python" %}
From the code sample folder, set up a virtual environment and use `pip` to install dependencies:
```sh
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
{% /tab %}
{% /tabs %}
### 2. Connect and get accounts
To get started, import the client library and instantiate an API client. To delete an account, you need the address of an account to receive the deleted account's remaining XRP.
{% tabs %}
{% tab label="JavaScript" %}
The sample code also imports `dotenv` so that it can load environment variables from a `.env` file.
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" before="// Load the account to delete" /%}
{% /tab %}
{% tab label="Python" %}
The sample code also imports `python-dotenv` so that it can load environment variables from a `.env` file.
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" before="# Load the account to delete" /%}
{% /tab %}
{% /tabs %}
You need to instantiate a wallet instance for the account you want to delete. Since you can only delete an account that is at least ~15 minutes old, the sample code loads a seed value from a `.env` file. If you don't have an account seed defined in the `.env` file, you can get a new account from the faucet, but it won't be possible to delete it right away.
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Load the account to delete" before="// Check account info" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Load the account to delete" before="# Check account info" /%}
{% /tab %}
{% /tabs %}
### 3. Check to see if the account can be deleted
Before deleting an account, you should check that it meets the requirements for deletion.
#### 3.1. Get account info
The first step to checking if an account can be deleted is to get its account info as of the latest validated ledger.
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Check account info" before="// Check if sequence number is too high" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Check account info" before="# Check if sequence number is too high" /%}
{% /tab %}
{% /tabs %}
#### 3.2. Check sequence number
Check that the account's current sequence number, in the `Sequence` field of the account data, is low enough compared with the latest validated ledger index. For the account to be deletable, its sequence number plus 255 must be lower than or equal to the ledger index.
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Check if sequence number is too high" before="// Check if owner count is too high" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Check if sequence number is too high" before="# Check if owner count is too high" /%}
{% /tab %}
{% /tabs %}
#### 3.3. Check owner count
Check the `OwnerCount` field of the account data to see if the account owns too many other ledger entries. For an account to be deletable, it must own 1000 or fewer entries (of any type) in the ledger.
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Check if owner count is too high" before="// Check if XRP balance is high enough" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Check if owner count is too high" before="# Check if XRP balance is high enough" /%}
{% /tab %}
{% /tabs %}
#### 3.4. Check XRP balance
Deleting an account requires a special [transaction cost][] equal to the incremental owner reserve, so an account can't be deleted if its current XRP balance is less than that. To check if an account has enough XRP, use the [server_state method][] to look up the current incremental reserve and compare with the account's XRP balance in the `Balance` field of the account data.
{% admonition type="warning" name="Caution" %}The [server_info method][] returns reserve values as decimal XRP, whereas [server_state][server_state method] returns drops of XRP. An account's `Balance` field is in drops of XRP. Be sure to compare equivalent units! (1 XRP = 1 million drops){% /admonition %}
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Check if XRP balance is high enough" before="// Check if FirstNFTSequence is too high" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Check if XRP balance is high enough" before="# Check if FirstNFTSequence is too high" /%}
{% /tab %}
{% /tabs %}
#### 3.5. Check NFT sequence number
Check the `FirstNFTokenSequence` and `MintedNFTokens` fields of the account. (If either field is absent, you can treat its value as `0` for this purpose.) For the account to be deletable, the sum of these two numbers plus 255 must be lower than or equal to the current ledger index.
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Check if FirstNFTSequence is too high" before="// Check that all issued NFTs have been burned" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Check if FirstNFTSequence is too high" before="# Check that all issued NFTs have been burned" /%}
{% /tab %}
{% /tabs %}
#### 3.6. Check that all issued NFTs have been burned
If the account has issued any NFTs that are still present in the ledger, the account cannot be deleted. You can check to see if any exist by comparing the `MintedNFTokens` field and `BurnedNFTokens` fields of the account. (In both cases, if the field is omitted, treat its value as `0`.) If the `MintedNFTokens` value is larger, the account has issued at least one NFT that has not been burned yet.
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Check that all issued NFTs have been burned" before="// Stop if any problems were found" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Check that all issued NFTs have been burned" before="# Stop if any problems were found" /%}
{% /tab %}
{% /tabs %}
#### 3.7. Stop if the account can't be deleted
If any of the previous checks failed, you can't delete the account. Resolving the problems varies by type:
- If the account sequence or NFT sequence number is too low, wait for the ledger index to advance automatically and try again later. About 15 minutes should be enough.
- If the account owns too many objects, or has issued NFTs outstanding, remove the offending objects.
- If the account does not have enough XRP, you can send XRP to it so that it can pay the deletion cost, but of course in this case the account does not have enough XRP for you to recover any by deleting it. You can also wait and try again if the network [votes to lower the reserve requirements](../../../concepts/consensus-protocol/fee-voting.md), which would also lower the cost to delete an account.
The sample code does not try to handle these problems, and quits if any problems were found:
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Stop if any problems were found" before="// Check for deletion blockers" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Stop if any problems were found" before="# Check for deletion blockers" /%}
{% /tab %}
{% /tabs %}
### 4. Check for deletion blockers and remove them if possible
Some types of ledger entries can block an account from being deleted. You can check for these types of entries using the [account_objects method][] with the `"deletion_blockers_only": true` parameter.
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Check for deletion blockers" before="// Delete the account" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Check for deletion blockers" before="# Delete the account" /%}
{% /tab %}
{% /tabs %}
If the account has deletion blockers, you may or may not be able to remove them by sending other transactions, depending on the ledger entry. For example, if one of the blockers is a `RippleState` entry, you may be able to remove it by reducing your balance to zero through payments or offers and using a [TrustSet transaction][] to return your settings to the default state. Since there are many possibilities, the sample code does not show how to remove deletion blockers.
### 5. Delete the account
If all the checks passed, send an [AccountDelete transaction][] to delete the account. Since this transaction type requires a much higher [transaction cost][] than normal, it's a good idea to submit the transaction with the "fail hard" setting enabled. This can save you from paying the transaction cost if the transaction was going to fail with a [`tec` result code](../../../references/protocol/transactions/transaction-results/tec-codes.md). (Fail hard stops the server from relaying the transaction to the network if the transaction provisionally fails, which catches common errors before the transaction can achieve a consensus.)
{% tabs %}
{% tab label="JavaScript" %}
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Delete the account" before="// Check result of the AccountDelete transaction" /%}
If the transaction is successful, you can use [getBalanceChanges(...)](https://js.xrpl.org/functions/getBalanceChanges.html) to check the metadata and see how much XRP was delivered from the deleted account to the destination account.
{% code-snippet file="/_code-samples/delete-account/js/delete-account.js" language="js" from="// Check result of the AccountDelete transaction" /%}
{% /tab %}
{% tab label="Python" %}
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Delete the account" before="# Check result of the AccountDelete transaction" /%}
If the transaction is successful, you can use [get_balance_changes(...)](https://xrpl-py.readthedocs.io/en/stable/source/xrpl.utils.html#xrpl.utils.get_balance_changes) to check the metadata and see how much XRP was delivered from the deleted account to the destination account.
{% code-snippet file="/_code-samples/delete-account/py/delete-account.py" language="py" from="# Check result of the AccountDelete transaction" /%}
{% /tab %}
{% /tabs %}
## See Also
- **Concepts:**
- [Deleting Accounts](../../../concepts/accounts/deleting-accounts.md)
- [Transaction Cost](../../../concepts/transactions/transaction-cost.md)
- **References:**
- [AccountDelete transaction][]
- [account_info method][]
- [account_objects method][]
- [server_state method][]
{% raw-partial file="/docs/_snippets/common-links.md" /%}

View File

@@ -31,6 +31,7 @@ To complete this tutorial, you should:
- Have an XRP Ledger client library set up in your development environment. 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
@@ -57,6 +58,13 @@ 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 accounts
@@ -77,6 +85,13 @@ To get started, import the necessary libraries and instantiate a client to conne
{% code-snippet file="/_code-samples/lending-protocol/py/cover_clawback.py" language="py" before="# This step checks" /%}
{% /tab %}
{% tab label="Go" %}
- `xrpl-go`: Used for XRPL client connection, transaction submission, and wallet handling.
- `encoding/json` and `fmt`: Used for formatting and printing results to the console.
- `os` and `os/exec`: Used to run tutorial set up scripts.
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" before="// Check for setup data" /%}
{% /tab %}
{% /tabs %}
Next, load the loan broker account, MPT issuer account, loan broker ID, and MPT ID.
@@ -92,6 +107,11 @@ This example uses preconfigured accounts, MPTs, and loan broker data from the `l
This example uses preconfigured accounts, MPTs, and loan broker data from the `lending_setup.py` script, but you can replace `loan_broker`, `mpt_issuer`, `loan_broker_id`, and `mpt_id` with your own values.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Check for setup data" before="// Check cover available" /%}
This example uses preconfigured accounts, MPTs, and loan broker data from the `lending-setup` script, but you can replace `loanBrokerWallet`, `mptIssuerWallet`, `loanBrokerID`, and `mptID` with your own values.
{% /tab %}
{% /tabs %}
### 3. Check initial cover available
@@ -105,6 +125,9 @@ Check the initial cover (first-loss capital) available using the [ledger_entry m
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_clawback.py" language="py" from="# Check cover available" before="# Prepare LoanBrokerCoverDeposit" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Check cover available" before="// Prepare LoanBrokerCoverDeposit" /%}
{% /tab %}
{% /tabs %}
If the `CoverAvailable` field is missing, it means no first-loss capital has been deposited.
@@ -124,6 +147,11 @@ The `Amount` field specifies the MPT and amount to deposit as first-loss capital
The `amount` field specifies the MPT and amount to deposit as first-loss capital.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Prepare LoanBrokerCoverDeposit" before="// Sign, submit, and wait for deposit validation" /%}
The `Amount` field specifies the MPT and amount to deposit as first-loss capital.
{% /tab %}
{% /tabs %}
If the transaction succeeds, the amount is deposited and held in the pseudo-account associated with the `LoanBroker` entry.
@@ -139,6 +167,9 @@ Sign and submit the `LoanBrokerCoverDeposit` transaction to the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_clawback.py" language="py" from="# Sign, submit, and wait for deposit validation" before="# Extract updated cover available" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Sign, submit, and wait for deposit validation" before="// Extract updated cover available" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -154,6 +185,9 @@ Retrieve the cover available from the transaction result by checking the `LoanBr
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_clawback.py" language="py" from="# Extract updated cover available" before="# Verify issuer of cover asset" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Extract updated cover available" before="// Verify issuer of cover asset" /%}
{% /tab %}
{% /tabs %}
### 7. Verify the asset issuer
@@ -167,6 +201,9 @@ Before executing a clawback, verify that the account submitting the transaction
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_clawback.py" language="py" from="# Verify issuer of cover asset" before="# Prepare LoanBrokerCoverClawback" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Verify issuer of cover asset" before="// Prepare LoanBrokerCoverClawback" /%}
{% /tab %}
{% /tabs %}
Clawback functionality is disabled by default. In the case of MPTs, the `tfMPTCanClawback` flag must be enabled when the [MPTokenIssuanceCreate transaction][] is submitted. This tutorial uses an MPT that is already configured for clawback.
@@ -182,6 +219,9 @@ Create the [LoanBrokerCoverClawback transaction][] object.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_clawback.py" language="py" from="# Prepare LoanBrokerCoverClawback" before="# Sign, submit, and wait for clawback validation" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Prepare LoanBrokerCoverClawback" before="// Sign, submit, and wait for clawback validation" /%}
{% /tab %}
{% /tabs %}
In this example we claw back the entire amount, but you can specify any amount so long as it doesn't exceed the available cover or reduce the cover below the minimum required by the `LoanBroker`.
@@ -197,6 +237,9 @@ Sign and submit the `LoanBrokerCoverClawback` transaction to the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_clawback.py" language="py" from="# Sign, submit, and wait for clawback validation" before="# Extract final cover available" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Sign, submit, and wait for clawback validation" before="// Extract final cover available" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -212,6 +255,9 @@ Retrieve the final cover available from the transaction result.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_clawback.py" language="py" from="# Extract final cover available" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-clawback/main.go" language="go" from="// Extract final cover available" /%}
{% /tab %}
{% /tabs %}
## See Also

View File

@@ -30,6 +30,7 @@ To complete this tutorial, you should:
- Have an XRP Ledger client library set up in your development environment. 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
@@ -56,6 +57,13 @@ 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 accounts
@@ -76,6 +84,13 @@ To get started, import the necessary libraries and instantiate a client to conne
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan_broker.py" language="py" before="# This step checks" /%}
{% /tab %}
{% tab label="Go" %}
- `xrpl-go`: Used for XRPL client connection, transaction submission, and wallet handling.
- `encoding/json` and `fmt`: Used for formatting and printing results to the console.
- `os` and `os/exec`: Used to run tutorial set up scripts.
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan-broker/main.go" language="go" before="// Check for setup data" /%}
{% /tab %}
{% /tabs %}
Next, load the vault owner account and vault ID. The vault owner will also be the loan broker.
@@ -91,6 +106,11 @@ This example uses preconfigured accounts and vault data from the `lendingSetup.j
This example uses preconfigured accounts and vault data from the `lending_setup.py` script, but you can replace `loan_broker` and `vault_id` with your own values.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan-broker/main.go" language="go" from="// Check for setup data" before="// Prepare LoanBrokerSet" /%}
This example uses preconfigured accounts and vault data from the `lending-setup` script, but you can replace `loanBrokerWallet` and `vaultID` with your own values.
{% /tab %}
{% /tabs %}
### 3. Prepare LoanBrokerSet transaction
@@ -104,6 +124,9 @@ Create the [LoanBrokerSet transaction][] object.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan_broker.py" language="py" from="# Prepare LoanBrokerSet" before="# Submit, sign" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan-broker/main.go" language="go" from="// Prepare LoanBrokerSet" before="// Submit, sign" /%}
{% /tab %}
{% /tabs %}
The management fee rate is set in 1/10th basis points. A value of `1000` equals 1% (100 basis points).
@@ -119,6 +142,9 @@ Sign and submit the `LoanBrokerSet` transaction to the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan_broker.py" language="py" from="# Submit, sign" before="# Extract loan broker" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan-broker/main.go" language="go" from="// Submit, sign" before="// Extract loan broker" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -134,6 +160,9 @@ Retrieve the loan broker's information from the transaction result by checking f
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan_broker.py" language="py" from="# Extract loan broker" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan-broker/main.go" language="go" from="// Extract loan broker" /%}
{% /tab %}
{% /tabs %}
The loan broker pseudo-account is a special account that holds first-loss capital.

View File

@@ -33,6 +33,7 @@ To complete this tutorial, you should:
- Have an XRP Ledger client library set up in your development environment. 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
@@ -59,6 +60,13 @@ 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 accounts
@@ -79,6 +87,13 @@ To get started, import the necessary libraries and instantiate a client to conne
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan.py" language="py" before="# This step checks" /%}
{% /tab %}
{% tab label="Go" %}
- `xrpl-go`: Used for XRPL client connection, transaction submission, and wallet handling.
- `encoding/json` and `fmt`: Used for formatting and printing results to the console.
- `os` and `os/exec`: Used to run tutorial set up scripts.
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan/main.go" language="go" before="// Check for setup data" /%}
{% /tab %}
{% /tabs %}
Next, load the loan broker account, borrower account, and loan broker ID.
@@ -94,6 +109,11 @@ This example uses preconfigured accounts and loan broker data from the `lendingS
This example uses preconfigured accounts and loan broker data from the `lending_setup.py` script, but you can replace `loan_broker`, `borrower`, and `loan_broker_id` with your own values.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan/main.go" language="go" from="// Check for setup data" before="// Prepare LoanSet" /%}
This example uses preconfigured accounts and loan broker data from the `lending-setup` script, but you can replace `loanBrokerWallet`, `borrowerWallet`, and `loanBrokerID` with your own values.
{% /tab %}
{% /tabs %}
### 3. Prepare LoanSet transaction
@@ -129,6 +149,20 @@ The loan terms include:
- `loan_origination_fee`: A one-time fee charged when the loan is created, paid in the borrowed asset.
- `loan_service_fee`: A fee charged with every loan payment, paid in the borrowed asset.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan/main.go" language="go" from="// Prepare LoanSet" before="// Loan broker signs first" /%}
The `Account` field is the loan broker, and the `Counterparty` field is the borrower. These fields can be swapped, but determine the signing order: the `Account` signs first, and the `Counterparty` signs second.
The loan terms include:
- `PrincipalRequested`: The amount of an asset requested by the borrower. You don't have to specify the type of asset in this field.
- `InterestRate`: The annualized interest rate in 1/10th basis points (500 = 0.5%).
- `PaymentTotal`: The number of payments to be made.
- `PaymentInterval`: The number of seconds between payments (2592000 = 30 days).
- `GracePeriod`: The number of seconds after a missed payment before the loan can be defaulted (604800 = 7 days).
- `LoanOriginationFee`: A one-time fee charged when the loan is created, paid in the borrowed asset.
- `LoanServiceFee`: A fee charged with every loan payment, paid in the borrowed asset.
{% /tab %}
{% /tabs %}
### 4. Add loan broker signature
@@ -142,6 +176,9 @@ The loan broker (the `Account`) signs the transaction first, adding their `TxnSi
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan.py" language="py" from="# Loan broker signs first" before="# Borrower signs second" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan/main.go" language="go" from="// Loan broker signs first" before="// Borrower signs second" /%}
{% /tab %}
{% /tabs %}
### 5. Add borrower signature
@@ -155,6 +192,9 @@ The borrower (the `Counterparty`) signs the transaction second. Their `TxnSignat
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan.py" language="py" from="# Borrower signs second" before="# Submit and wait" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan/main.go" language="go" from="// Borrower signs second" before="// Submit and wait" /%}
{% /tab %}
{% /tabs %}
### 6. Submit LoanSet transaction
@@ -168,6 +208,9 @@ Submit the fully signed `LoanSet` transaction to the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan.py" language="py" from="# Submit and wait" before="# Extract loan information" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan/main.go" language="go" from="// Submit and wait" before="// Extract loan information" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -183,6 +226,9 @@ Retrieve the loan's information from the transaction result by checking for the
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/create_loan.py" language="py" from="# Extract loan information" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/create-loan/main.go" language="go" from="// Extract loan information" /%}
{% /tab %}
{% /tabs %}
## See Also

View File

@@ -32,6 +32,7 @@ To complete this tutorial, you should:
- Have an XRP Ledger client library set up in your development environment. 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
@@ -58,6 +59,13 @@ 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 accounts
@@ -78,6 +86,13 @@ To get started, import the necessary libraries and instantiate a client to conne
{% code-snippet file="/_code-samples/lending-protocol/py/cover_deposit_and_withdraw.py" language="py" before="# This step checks" /%}
{% /tab %}
{% tab label="Go" %}
- `xrpl-go`: Used for XRPL client connection, transaction submission, and wallet handling.
- `encoding/json` and `fmt`: Used for formatting and printing results to the console.
- `os` and `os/exec`: Used to run tutorial set up scripts.
{% code-snippet file="/_code-samples/lending-protocol/go/cover-deposit-and-withdraw/main.go" language="go" before="// Check for setup data" /%}
{% /tab %}
{% /tabs %}
Next, load the loan broker account, loan broker ID, and MPT issuance ID.
@@ -93,6 +108,11 @@ This example uses preconfigured accounts and loan broker data from the `lendingS
This example uses preconfigured accounts and loan broker data from the `lending_setup.py` script, but you can replace `loan_broker`, `loan_broker_id`, and `mpt_id` with your own values.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-deposit-and-withdraw/main.go" language="go" from="// Check for setup data" before="// Prepare LoanBrokerCoverDeposit" /%}
This example uses preconfigured accounts and loan broker data from the `lending-setup` script, but you can replace `loanBrokerWallet`, `loanBrokerID`, and `mptID` with your own values.
{% /tab %}
{% /tabs %}
### 3. Prepare LoanBrokerCoverDeposit transaction
@@ -110,6 +130,11 @@ The `Amount` field specifies the MPT and amount to deposit as first-loss capital
The `amount` field specifies the MPT and amount to deposit as first-loss capital.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-deposit-and-withdraw/main.go" language="go" from="// Prepare LoanBrokerCoverDeposit" before="// Sign, submit, and wait for deposit" /%}
The `Amount` field specifies the MPT and amount to deposit as first-loss capital.
{% /tab %}
{% /tabs %}
If the transaction succeeds, the amount is deposited and held in the pseudo-account associated with the `LoanBroker` entry.
@@ -125,6 +150,9 @@ Sign and submit the `LoanBrokerCoverDeposit` transaction to the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_deposit_and_withdraw.py" language="py" from="# Sign, submit, and wait for deposit" before="# Extract cover balance" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-deposit-and-withdraw/main.go" language="go" from="// Sign, submit, and wait for deposit" before="// Extract cover balance" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -140,6 +168,9 @@ Retrieve the cover balance from the transaction result by checking the `LoanBrok
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_deposit_and_withdraw.py" language="py" from="# Extract cover balance" before="# Prepare LoanBrokerCoverWithdraw" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-deposit-and-withdraw/main.go" language="go" from="// Extract cover balance" before="// Prepare LoanBrokerCoverWithdraw" /%}
{% /tab %}
{% /tabs %}
The `LoanBroker` pseudo-account address is the `Account` field, and `CoverAvailable` shows the cover balance.
@@ -155,6 +186,9 @@ Create the [LoanBrokerCoverWithdraw transaction][] object.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_deposit_and_withdraw.py" language="py" from="# Prepare LoanBrokerCoverWithdraw" before="# Sign, submit, and wait for withdraw" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-deposit-and-withdraw/main.go" language="go" from="// Prepare LoanBrokerCoverWithdraw" before="// Sign, submit, and wait for withdraw" /%}
{% /tab %}
{% /tabs %}
### 7. Submit LoanBrokerCoverWithdraw transaction
@@ -168,6 +202,9 @@ Sign and submit the `LoanBrokerCoverWithdraw` transaction to the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_deposit_and_withdraw.py" language="py" from="# Sign, submit, and wait for withdraw" before="# Extract updated cover balance" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-deposit-and-withdraw/main.go" language="go" from="// Sign, submit, and wait for withdraw" before="// Extract updated cover balance" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -183,6 +220,9 @@ Retrieve the updated cover balance from the transaction result.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/cover_deposit_and_withdraw.py" language="py" from="# Extract updated cover balance" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/cover-deposit-and-withdraw/main.go" language="go" from="// Extract updated cover balance" /%}
{% /tab %}
{% /tabs %}
The `CoverAvailable` field now shows the reduced balance after the withdrawal.

View File

@@ -33,6 +33,7 @@ To complete this tutorial, you should:
- Have an XRP Ledger client library set up in your development environment. 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
@@ -59,6 +60,13 @@ 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 accounts
@@ -80,6 +88,14 @@ To get started, import the necessary libraries and instantiate a client to conne
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" before="# This step checks" /%}
{% /tab %}
{% tab label="Go" %}
- `xrpl-go`: Used for XRPL client connection, transaction submission, and wallet handling.
- `encoding/json` and `fmt`: Used for formatting and printing results to the console.
- `os` and `os/exec`: Used to run tutorial set up scripts.
- `time`: Used for grace period countdown and date formatting.
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" before="// Check for setup data" /%}
{% /tab %}
{% /tabs %}
Next, load the loan broker account and loan ID.
@@ -95,6 +111,11 @@ This example uses preconfigured accounts and loan data from the `lendingSetup.js
This example uses preconfigured accounts and loan data from the `lending_setup.py` script, but you can replace `loan_broker` and `loan_id` with your own values.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Check for setup data" before="// Check loan status" /%}
This example uses preconfigured accounts and loan data from the `lending-setup` script, but you can replace `loanBrokerWallet` and `loanID` with your own values.
{% /tab %}
{% /tabs %}
### 3. Check loan status
@@ -108,6 +129,9 @@ Check the current status of the loan using the [ledger_entry method][].
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" from="# Check loan status" before="# Prepare LoanManage transaction to impair" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Check loan status" before="// Prepare LoanManage transaction to impair" /%}
{% /tab %}
{% /tabs %}
This shows the total amount owed and the next payment due date. The [Ripple Epoch][] timestamp is converted to a readable date format.
@@ -123,6 +147,9 @@ Create the [LoanManage transaction][] with the `tfLoanImpair` flag.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" from="# Prepare LoanManage transaction to impair" before="# Sign, submit, and wait for impairment" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Prepare LoanManage transaction to impair" before="// Sign, submit, and wait for impairment" /%}
{% /tab %}
{% /tabs %}
### 5. Submit LoanManage impairment transaction
@@ -136,6 +163,9 @@ Sign and submit the `LoanManage` transaction to impair the loan.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" from="# Sign, submit, and wait for impairment" before="# Extract loan impairment info" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Sign, submit, and wait for impairment" before="// Extract loan impairment info" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -151,6 +181,9 @@ Retrieve the loan's grace period and updated payment due date from the transacti
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" from="# Extract loan impairment info" before="# Countdown until loan can be defaulted" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Extract loan impairment info" before="// Countdown until loan can be defaulted" /%}
{% /tab %}
{% /tabs %}
The loan can only be defaulted after the grace period expires. The example calculates when the grace period ends and displays a countdown.
@@ -166,6 +199,9 @@ This countdown displays the remaining seconds in real-time. Once the grace perio
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" from="# Countdown until loan can be defaulted" before="# Prepare LoanManage transaction to default" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Countdown until loan can be defaulted" before="// Prepare LoanManage transaction to default" /%}
{% /tab %}
{% /tabs %}
### 8. Prepare LoanManage transaction to default the loan
@@ -179,6 +215,9 @@ After the grace period expires, create a `LoanManage` transaction with the `tfLo
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" from="# Prepare LoanManage transaction to default" before="# Sign, submit, and wait for default" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Prepare LoanManage transaction to default" before="// Sign, submit, and wait for default" /%}
{% /tab %}
{% /tabs %}
### 9. Submit LoanManage default transaction
@@ -192,6 +231,9 @@ Sign and submit the `LoanManage` transaction to default the loan.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" from="# Sign, submit, and wait for default" before="# Verify loan default status" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Sign, submit, and wait for default" before="// Verify loan default status" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -207,6 +249,9 @@ Confirm the loan has been defaulted by checking the loan flags.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_manage.py" language="py" from="# Verify loan default status" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-manage/main.go" language="go" from="// Verify loan default status" /%}
{% /tab %}
{% /tabs %}
The loan flags are parsed to confirm the `tfLoanDefault` flag is now set.

View File

@@ -33,6 +33,7 @@ To complete this tutorial, you should:
- Have an XRP Ledger client library set up in your development environment. 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
@@ -59,6 +60,13 @@ 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 accounts
@@ -79,6 +87,14 @@ To get started, import the necessary libraries and instantiate a client to conne
{% code-snippet file="/_code-samples/lending-protocol/py/loan_pay.py" language="py" before="# This step checks" /%}
{% /tab %}
{% tab label="Go" %}
- `xrpl-go`: Used for XRPL client connection, transaction submission, and wallet handling.
- `encoding/json` and `fmt`: Used for formatting and printing results to the console.
- `math/big`: Used for calculating the total payment amount.
- `os` and `os/exec`: Used to run tutorial set up scripts.
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" before="// Check for setup data" /%}
{% /tab %}
{% /tabs %}
Next, load the borrower account, loan ID, and MPT issuance ID.
@@ -94,6 +110,11 @@ This example uses preconfigured accounts and loan data from the `lendingSetup.js
This example uses preconfigured accounts and loan data from the `lending_setup.py` script, but you can replace `borrower`, `loan_id`, and `mpt_id` with your own values.
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" from="// Check for setup data" before="// Check initial loan status" /%}
This example uses preconfigured accounts and loan data from the `lending-setup` script, but you can replace `borrowerWallet`, `loanID`, and `mptID` with your own values.
{% /tab %}
{% /tabs %}
### 3. Check loan status
@@ -107,6 +128,9 @@ Check the current status of the loan using the [ledger_entry method][].
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_pay.py" language="py" from="# Check initial loan status" before="# Prepare LoanPay transaction" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" from="// Check initial loan status" before="// Prepare LoanPay transaction" /%}
{% /tab %}
{% /tabs %}
The `TotalValueOutstanding` field contains the remaining principal plus accrued interest; the `LoanServiceFee` is an additional fee charged per payment. Add these together to calculate the total payment.
@@ -126,6 +150,9 @@ Create the [LoanPay transaction][] with the total payment amount.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_pay.py" language="py" from="# Prepare LoanPay transaction" before="# Sign, submit, and wait for payment validation" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" from="// Prepare LoanPay transaction" before="// Sign, submit, and wait for payment validation" /%}
{% /tab %}
{% /tabs %}
### 5. Submit LoanPay transaction
@@ -139,6 +166,9 @@ Sign and submit the `LoanPay` transaction to the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_pay.py" language="py" from="# Sign, submit, and wait for payment validation" before="# Extract updated loan info" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" from="// Sign, submit, and wait for payment validation" before="// Extract updated loan info" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -154,6 +184,9 @@ Retrieve the loan balance from the transaction result by checking for the `Loan`
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_pay.py" language="py" from="# Extract updated loan info" before="# Prepare LoanDelete transaction" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" from="// Extract updated loan info" before="// Prepare LoanDelete transaction" /%}
{% /tab %}
{% /tabs %}
If `TotalValueOutstanding` is absent from the loan metadata, the loan has been fully paid off and is ready for deletion.
@@ -169,6 +202,9 @@ Create a [LoanDelete transaction][] to remove the paid loan from the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_pay.py" language="py" from="# Prepare LoanDelete transaction" before="# Sign, submit, and wait for deletion validation" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" from="// Prepare LoanDelete transaction" before="// Sign, submit, and wait for deletion validation" /%}
{% /tab %}
{% /tabs %}
Either the loan broker or the borrower can submit a `LoanDelete` transaction. In this example, the borrower deletes their own paid off loan.
@@ -184,6 +220,9 @@ Sign and submit the `LoanDelete` transaction.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_pay.py" language="py" from="# Sign, submit, and wait for deletion validation" before="# Verify loan deletion" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" from="// Sign, submit, and wait for deletion validation" before="// Verify loan deletion" /%}
{% /tab %}
{% /tabs %}
Verify that the transaction succeeded by checking for a `tesSUCCESS` result code.
@@ -199,6 +238,9 @@ Confirm that the loan has been removed from the XRP Ledger.
{% tab label="Python" %}
{% code-snippet file="/_code-samples/lending-protocol/py/loan_pay.py" language="py" from="# Verify loan deletion" /%}
{% /tab %}
{% tab label="Go" %}
{% code-snippet file="/_code-samples/lending-protocol/go/loan-pay/main.go" language="go" from="// Verify loan deletion" /%}
{% /tab %}
{% /tabs %}
If the `ledger_entry` method returns an `entryNotFound` error, the loan has been successfully deleted.

View 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" /%}

View File

@@ -12,6 +12,7 @@ If you don't [run your own `rippled` server](../infrastructure/installation/inde
## Non-Commercial
| Operator | [Network][] | JSON-RPC URL | WebSocket URL | Notes |
|:----------|:------------|:-------------|:--------------|:---------------------|
| Honeycluster | **Mainnet** | `https://honeycluster.io/` | `wss://honeycluster.io/` | Full history server cluster with Clio |
| InFTF | **Mainnet** | `https://xrplcluster.com/` <br> `https://xrpl.ws/` [²][] | `wss://xrplcluster.com/` <br> `wss://xrpl.ws/` [²][] | Full history server cluster with CORS support. |
| Ripple[¹][] | **Mainnet** | `https://s1.ripple.com:51234/` | `wss://s1.ripple.com/` | General purpose server cluster |
| Ripple[¹][] | **Mainnet** | `https://s2.ripple.com:51234/` | `wss://s2.ripple.com/` | [Full-history server](../concepts/networks-and-servers/ledger-history.md#full-history) cluster |
@@ -28,10 +29,12 @@ If you don't [run your own `rippled` server](../infrastructure/installation/inde
| Operator | [Network][] | JSON-RPC URL | WebSocket URL | Notes |
|:----------|:------------|:-------------|:--------------|:---------------------|
| Ripple[¹][] | Testnet | `https://s.altnet.rippletest.net:51234/` | `wss://s.altnet.rippletest.net:51233/` | Testnet public server |
| Honeycluster | Testnet | `https://testnet.honeycluster.io/` | `wss://testnet.honeycluster.io/` | Testnet public server |
| XRPL Labs | Testnet | `https://testnet.xrpl-labs.com/` | `wss://testnet.xrpl-labs.com/` | Testnet public server with CORS support |
| Ripple[¹][] | Testnet (Clio) | `https://clio.altnet.rippletest.net:51234/` | `wss://clio.altnet.rippletest.net:51233/` | Testnet public server with Clio |
| Ripple[¹][] | Devnet | `https://s.devnet.rippletest.net:51234/` | `wss://s.devnet.rippletest.net:51233/` | Devnet public server |
| Ripple[¹][] | Devnet (Clio) | `https://clio.devnet.rippletest.net:51234/` | `wss://clio.devnet.rippletest.net:51233/` | Devnet public server with Clio |
| Honeycluster | Devnet | `https://devnet.honeycluster.io/` | `wss://devnet.honeycluster.io/` | Devnet public server |
| XRPL Labs | Xahau Testnet | `https://xahau-test.net/` | `wss://xahau-test.net/` | [Hooks-enabled](https://hooks.xrpl.org/) Xahau Testnet |

View File

@@ -28,7 +28,7 @@ To learn about MPTs in general, go to the **Concept** page. For developer-focuse
{% card-grid %}
{% xrpl-card title="Concept: MultiPurpose Tokens" body="Read the concept documentation to learn more about Multi-Purpose Tokens." href="docs/concepts/tokens/fungible-tokens/multi-purpose-tokens/" /%}
{% xrpl-card title="Tutorial: Issue a MultiPurpose Token" body="Stepbystep, handson tutorial to issue an MPT using the XRP Ledger SDKs." href="docs/tutorials/issue-a-multi-purpose-token/" /%}
{% xrpl-card title="Tutorial: Issue a MultiPurpose Token" body="Stepbystep, handson tutorial to issue an MPT using the XRP Ledger SDKs." href="docs/tutorials/tokens/mpts/issue-a-multi-purpose-token" /%}
{% xrpl-card title="MPT Generator" body="Download the MPT Generator and learn how to create an asset-backed Treasury bill." href="#mpt-generator"/%}
{% /card-grid %}

1235
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"private": true,
"version": "1.0.0",
"type": "module",
"description": "The XRP Ledger Dev Portal is the authoritative source for XRP Ledger documentation, including the `rippled` server, client libraries, and other open-source XRP Ledger software.",
"description": "The XRP Ledger Dev Portal is the authoritative source for XRP Ledger documentation, including the core server, client libraries, and other open-source XRP Ledger software.",
"scripts": {
"build-css": "sass --load-path styles/scss styles/xrpl.scss ./static/css/devportal2024-v1.css --style compressed --no-source-map",
"build-css-watch": "sass --watch --load-path styles/scss styles/xrpl.scss ./static/css/devportal2024-v1.css --style compressed --no-source-map",
@@ -15,7 +15,7 @@
"@codemirror/state": "6.5.2",
"@codemirror/view": "^6.22.2",
"@lezer/highlight": "^1.2.0",
"@redocly/realm": "0.130.4",
"@redocly/realm": "0.131.2",
"@uiw/codemirror-themes": "4.21.21",
"@uiw/react-codemirror": "^4.21.21",
"@xrplf/isomorphic": "^1.0.0-beta.1",

View File

@@ -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
@@ -326,7 +327,8 @@
- page: docs/tutorials/best-practices/key-management/remove-a-regular-key-pair.md
- page: docs/tutorials/best-practices/key-management/offline-account-setup.md
- page: docs/tutorials/best-practices/key-management/set-up-multi-signing.md
- page: docs/tutorials/best-practices/key-management/send-a-multi-signed-transaction.md
- page: docs/tutorials/best-practices/key-management/send-a-multi-signed-transaction.md
- page: docs/tutorials/best-practices/key-management/delete-an-account.md
- group: Advanced Developer Topics
groupTranslationKey: sidebar.docs.tutorials.advancedDeveloperTopics
expanded: false