mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-03-24 21:42:30 +00:00
Compare commits
1 Commits
qa/pagegri
...
section/ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aa2092bb0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,7 +9,6 @@ yarn-error.log
|
||||
.venv/
|
||||
_code-samples/*/js/package-lock.json
|
||||
*.css.map
|
||||
_code-samples/*/*/*[Ss]etup.json
|
||||
|
||||
# PHP
|
||||
composer.lock
|
||||
|
||||
@@ -2,7 +2,6 @@ navbar.about: Acerca de
|
||||
navbar.docs: Docs
|
||||
navbar.resources: Recursos
|
||||
navbar.community: Comunidad
|
||||
navbar.showcase: Escaparate
|
||||
footer.about: Acerca de
|
||||
footer.docs: Docs
|
||||
footer.resources: Recursos
|
||||
|
||||
@@ -124,9 +124,9 @@ XRP Ledgerは、スパム対策として、需要に基づいて[トランザク
|
||||
|
||||
XRP Ledgerネットワークはオープンネットワークであり、すべての取引はオープンに公開されています。
|
||||
|
||||
Rippleは XRP Ledgerネットワーク全体のAMLフラグを監視・報告し、該当する疑わしい活動をFinCENに報告することにコミットしています。
|
||||
Rippleは Ledgerネットワーク全体のAMLフラグを監視・報告し、該当する疑わしい活動をFinCENに報告することにコミットしています。
|
||||
|
||||
[XRP Forensics / xrplorer](https://xrplorer.com/)は、XRP Ledgerのマネーロンダリング、詐欺、不正使用を追跡し、最小限に抑えるための勧告リストを維持しています。取引所やその他のサービス・プロバイダは、金融犯罪を防止し対応するためにこのサービスを利用することができます。
|
||||
[XRP Forensics / xrplorer](https://xrplorer.com/)は、XRP Ledgerのマネーロンダリング、詐欺、詐欺、不正使用を追跡し、最小限に抑えるための勧告リストを維持しています。取引所やその他のサービス・プロバイダは、金融犯罪を防止し対応するためにこのサービスを利用することができます。
|
||||
|
||||
|
||||
## セキュリティ上の懸念
|
||||
|
||||
@@ -26,14 +26,14 @@ labels:
|
||||
- `RippleState`
|
||||
- `Check`
|
||||
- アカウントがレジャー内に所有するオブジェクトが1000個未満であること。
|
||||
- トランザクションの送信時、少なくとも1つ分の[所有者準備金](reserves.md)(現在{% $env.PUBLIC_OWNER_RESERVE %})に相当する特別な[トランザクションコスト][]を支払う必要があります。
|
||||
- トランザクションの送信時、少なくとも1つ分の[所有者準備金](reserves.md)(現在2XRP)に相当する特別な[トランザクションコスト][]を支払う必要があります。
|
||||
|
||||
|
||||
## 削除コスト
|
||||
|
||||
{% admonition type="warning" name="注意" %}アカウントの削除要件を満たしていないためにトランザクションが失敗した場合でも、[AccountDeleteトランザクション][]のトランザクションコストは、トランザクションが検証済みレジャーに含まれる場合常に発生します。アカウントを削除できなかった場合に高いトランザクションコストを支払う可能性を減らすには、AccountDeleteトランザクションを送信するときに`fail_hard`オプションを使用してください。{% /admonition %}
|
||||
|
||||
ビットコインや他の多くの暗号通貨とは異なり、XRP Ledgerの公開レジャーチェーンのそれぞれの新しいレジャーバージョンは、レジャーの完全な状態を含んでおり、新しいアカウントが増えるごとにサイズが増加します。そのため、必要な場合を除き、新しいXRP Ledgerアカウントを作成すべきではありません。アカウントを削除することで、アカウントの{% $env.PUBLIC_BASE_RESERVE %}の[準備金](reserves.md)の一部を回復することができますが、そのためには少なくとも{% $env.PUBLIC_OWNER_RESERVE %}を破棄する必要があります。
|
||||
ビットコインや他の多くの暗号通貨とは異なり、XRP Ledgerの公開レジャーチェーンのそれぞれの新しいレジャーバージョンは、レジャーの完全な状態を含んでおり、新しいアカウントが増えるごとにサイズが増加します。そのため、必要な場合を除き、新しいXRP Ledgerアカウントを作成すべきではありません。アカウントを削除することで、アカウントの10XRPの[準備金](reserves.md)の一部を回復することができますが、そのためには少なくとも2XRPを破棄する必要があります。
|
||||
|
||||
取引所など、多くのユーザのために価値の送受信を行う組織は、[**送信元タグ**と**宛先タグ**](../transactions/source-and-destination-tags.md)を使用することで、XRP Ledgerのアカウントを1つだけ(または少数)使用するだけで、ユーザの支払いを区別することができます。
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ Deposit Authorizationが有効化されているアカウントの特徴は次
|
||||
|
||||
- [Paymentトランザクション][]の送信先には**できません**。ただし**以下の例外**は除きます。
|
||||
- 送金先により、支払の送金元が[事前承認](#事前承認)されている場合。{% amendment-disclaimer name="DepositPreauth" /%}
|
||||
- アカウントのXRP残高がアカウントの最低[必要準備金](reserves.md)以下で、XRP PaymentのAmountがアカウントの最低準備金(現時点では{% $env.PUBLIC_BASE_RESERVE %})以下である場合は、このアカウントを送金先に指定できます。これにより、アカウントがトランザクションを送信することも、XRPを受領することもできずに操作不可能な状態になるのを防ぎます。この場合、アカウントの所有者の準備金は関係ありません。
|
||||
- アカウントのXRP残高がアカウントの最低[必要準備金](reserves.md)以下で、XRP PaymentのAmountがアカウントの最低準備金(現時点では10XRP)以下である場合は、このアカウントを送金先に指定できます。これにより、アカウントがトランザクションを送信することも、XRPを受領することもできずに操作不可能な状態になるのを防ぎます。この場合、アカウントの所有者の準備金は関係ありません。
|
||||
- **以下に該当する場合にのみ**[PaymentChannelClaimトランザクション][]からXRPを受領できます。
|
||||
- PaymentChannelClaimトランザクションの送金元がPayment Channelの送金先である場合。
|
||||
- PaymentChannelClaimトランザクションの送金先がPaymentChannelClaimの送金元を[事前承認している](#事前承認)場合。{% amendment-disclaimer name="DepositPreauth" /%}
|
||||
|
||||
@@ -46,7 +46,7 @@ XRP Ledgerでアカウントを取得する一般的な方法は次のとおり
|
||||
|
||||
- 例えば、一般的な取引所でXRPを購入し、その取引所から、指定したアドレスにXRPを出金することができます。
|
||||
|
||||
{% admonition type="warning" name="注意" %}自身のXRP Ledgerアドレスで初めてXRPを受け取る場合は[アカウントの準備金](reserves.md)(現在は{% $env.PUBLIC_BASE_RESERVE %})を支払う必要があります。この金額のXRPは無期限に使用できなくなります。一方で、一般的な取引所では通常、顧客のXRPはすべて、共有されたいくつかのXRP Ledgerアカウントに保有されているため、顧客はその取引所で個々のアカウントの準備金を支払う必要はありません。引き出す前に、XRP Ledgerに直接アカウントを保有することが、金額に見合う価値があるかどうかを検討してください。{% /admonition %}
|
||||
{% admonition type="warning" name="注意" %}自身のXRP Ledgerアドレスで初めてXRPを受け取る場合は[アカウントの準備金](reserves.md)(現在は10XRP)を支払う必要があります。この金額のXRPは無期限に使用できなくなります。一方で、一般的な取引所では通常、顧客のXRPはすべて、共有されたいくつかのXRP Ledgerアカウントに保有されているため、顧客はその取引所で個々のアカウントの準備金を支払う必要はありません。引き出す前に、XRP Ledgerに直接アカウントを保有することが、金額に見合う価値があるかどうかを検討してください。{% /admonition %}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ XRP Ledgerのチケットは、取引をすぐに送信せずに、その取引
|
||||
|
||||
上記の例では、シーケンス番号105または作成した3つのチケットのいずれかを使用してトランザクションを送信できます。チケット103を使ってトランザクションを送信すると、それによってチケット103は元帳から削除されます。その後の次のトランザクションでは、シーケンス番号105、チケット102、またはチケット104を使用できます。
|
||||
|
||||
{% admonition type="warning" name="注意" %}チケットは1枚ごとに[所有者準備金](reserves.md#所有者準備金)としてカウントされますので、チケット1枚につき{% $env.PUBLIC_OWNER_RESERVE %}を確保する必要があります。 (このXRPは、チケットを使用した後に再び使用可能になります)一度に多くのチケットを作成すると、このコストはすぐに膨れ上がります。{% /admonition %}
|
||||
{% admonition type="warning" name="注意" %}チケットは1枚ごとに[所有者準備金](reserves.md#所有者準備金)としてカウントされますので、チケット1枚につき2XRPを確保する必要があります。 (このXRPは、チケットを使用した後に再び使用可能になります)一度に多くのチケットを作成すると、このコストはすぐに膨れ上がります。{% /admonition %}
|
||||
|
||||
シーケンス番号と同様に、トランザクションの送信は、そのトランザクションが[コンセンサス](../consensus-protocol/index.md)によって確認された場合にのみ、チケットを使用します。しかし、意図した通りにならなかった取引でも、[`tec`クラスの結果コード](../../references/protocol/transactions/transaction-results/tec-codes.md)を用いてコンセンサスで確認することができます。
|
||||
|
||||
@@ -51,7 +51,7 @@ XRP Ledgerのチケットは、取引をすぐに送信せずに、その取引
|
||||
- 各チケットは一度しか使用できません。同じチケットシーケンスを使用する複数の異なるトランザクション候補があることは可能ですが、コンセンサスで検証できるのはそのうちの1つだけです。
|
||||
- 各アカウントでは、一度に250枚以上のチケットをレジャーに登録することはできません。また、一度に250枚以上のチケットを作成することもできません。
|
||||
- チケットを使って別のチケットを作ることは _できます_。その場合、使用したチケットは、一度に所持できるチケットの合計数にはカウントされません。
|
||||
- 各チケットは[所有者準備金](reserves.md#所有者準備金)にカウントされるため、まだ使用していないチケット1枚につき{% $env.PUBLIC_OWNER_RESERVE %}を確保する必要があります。このXRPは、チケットを使用した後、再び使用することができます。
|
||||
- 各チケットは[所有者準備金](reserves.md#所有者準備金)にカウントされるため、まだ使用していないチケット1枚につき2XRPを確保する必要があります。このXRPは、チケットを使用した後、再び使用することができます。
|
||||
- 個々の元帳の中では、チケットを使用した取引は、同じ送信者からの他の取引の後に実行されます。1つのアカウントが同じ元帳のバージョンでTicketを使用する複数のトランザクションを持つ場合、それらのTicketは最も低いTicket Sequenceから最も高いTicket Sequenceの順に実行されます。 (詳細については、コンセンサスの[正規順序](../consensus-protocol/consensus-structure.md#xrp-ledgerプロトコル-コンセンサスと検証)に関するドキュメントをご覧ください)。
|
||||
- 個々の元帳の中では、チケットを使用した取引は、同じ送信者からの他の取引の後に実行されます。1つのアカウントが同じ元帳のバージョンでチケットを使用する複数のトランザクションを持つ場合、それらのチケットは最も低いチケット シーケンス番号から最も高いチケット シーケンス番号の順に実行されます。 (詳細については、コンセンサスの[正規順序](../consensus-protocol/consensus-structure.md#xrp-ledgerプロトコル-コンセンサスと検証)に関するドキュメントをご覧ください)。
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
html: consensus-protections.html
|
||||
parent: consensus.html
|
||||
seo:
|
||||
description: 分散型金融システムで発生する可能性のあるさまざまな問題や攻撃から、XRP Ledgerコンセンサスプロトコルがどのように保護されているかを学びます。
|
||||
description: Learn how the XRP Ledger Consensus Protocol is protected against various problems and attacks that may occur in a decentralized financial system. #TODO: translate
|
||||
labels:
|
||||
- ブロックチェーン
|
||||
---
|
||||
|
||||
@@ -12,11 +12,11 @@ NFTをミントし、保有し、販売するためには、XRPを準備金と
|
||||
|
||||
## 基本準備金
|
||||
|
||||
アカウントでは、基本準備金(現在{% $env.PUBLIC_BASE_RESERVE %})を用意する必要があります。基本準備金のXRPの金額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
|
||||
アカウントでは、基本準備金(現在10XRP)を用意する必要があります。基本準備金のXRPの金額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
|
||||
|
||||
## 所有者準備金
|
||||
|
||||
XRP Ledgerで所有する各オブジェクトには、現在{% $env.PUBLIC_OWNER_RESERVE %}の所有者準備金が必要とされています。これは、ユーザが不必要なデータで台帳にスパムをかけることを抑制し、不要になったデータを削除することを促すためのものです。所有者準備金の額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
|
||||
XRP Ledgerで所有する各オブジェクトには、現在2XRPの所有者準備金が必要とされています。これは、ユーザが不必要なデータで台帳にスパムをかけることを抑制し、不要になったデータを削除することを促すためのものです。所有者準備金の額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
|
||||
|
||||
NFTの場合、 _オブジェクト_ はそれぞれのNFTを指すのではなく、アカウントが所有する`NFTokenPage`オブジェクトを指します。`NFTokenPage`オブジェクトは最大32個のNFTを格納することができます。
|
||||
|
||||
@@ -38,7 +38,7 @@ NFTの保有枚数や保有ページ数によって、所有者準備金の総
|
||||
|
||||
## `NFTokenOffer`の準備金
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、オファーを出すアカウントに対して準備金の1つの増加を必要とします。この記事の執筆時点では、準備金の増分は{% $env.PUBLIC_OWNER_RESERVE %}です。準備金は、オファーをキャンセルすることで取り戻すことができます。また、オファーが受け入れられると、XRP Ledgerからオファーが削除され、準備金は取り戻されます。
|
||||
各`NFTokenOffer`オブジェクトは、オファーを出すアカウントに対して準備金の1つの増加を必要とします。この記事の執筆時点では、準備金の増分は2XRPです。準備金は、オファーをキャンセルすることで取り戻すことができます。また、オファーが受け入れられると、XRP Ledgerからオファーが削除され、準備金は取り戻されます。
|
||||
|
||||
## Practical Considerations
|
||||
|
||||
@@ -55,7 +55,7 @@ NFTをミントし、保有し、売買のオファーをする場合、必要
|
||||
|
||||
{% admonition type="info" name="注記" %}準備金要件ではありませんが、ミントと売却のプロセスにおけるトランザクションの些細な手数料(通常12drops、または.000012XRP)を負担するために、少なくとも必要準備金より1XRPより多く用意しておきくべきです。{% /admonition %}
|
||||
|
||||
仮に200個のNFTをミントし、それぞれに「NFTokenSellOffer」を作成すると、43.6XRPもの準備金が必要になります。
|
||||
仮に200個のNFTをミントし、それぞれに「NFTokenSellOffer」を作成すると、436XRPもの準備金が必要になります。
|
||||
|
||||
| 準備金の種類 | 準備金の額 |
|
||||
|:--------------------|--------:|
|
||||
|
||||
@@ -75,10 +75,10 @@ Clioをインストールする前に、以下の条件を満たしている必
|
||||
|
||||
```
|
||||
gpg: WARNING: no command supplied. Trying to guess what you mean ...
|
||||
pub ed25519 2026-02-16 [SC] [expires: 2033-02-14]
|
||||
E057C1CF72B0DF1A4559E8577DEE9236AB06FAA6
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub ed25519 2026-02-16 [S] [expires: 2029-02-15]
|
||||
pub rsa3072 2019-02-14 [SC] [expires: 2026-02-17]
|
||||
C0010EC205B35A3310DC90DE395F97FFCCAFD9A2
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub rsa3072 2019-02-14 [E] [expires: 2026-02-17]
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ labels:
|
||||
|
||||
```
|
||||
gpg: WARNING: no command supplied. Trying to guess what you mean ...
|
||||
pub ed25519 2026-02-16 [SC] [expires: 2033-02-14]
|
||||
E057C1CF72B0DF1A4559E8577DEE9236AB06FAA6
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub ed25519 2026-02-16 [S] [expires: 2029-02-15]
|
||||
pub rsa3072 2019-02-14 [SC] [expires: 2026-02-17]
|
||||
C0010EC205B35A3310DC90DE395F97FFCCAFD9A2
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub rsa3072 2019-02-14 [E] [expires: 2026-02-17]
|
||||
```
|
||||
|
||||
特に、フィンガープリントが一致することを確認してください。(上記の例では、フィンガープリントは三行目の`C001`で始まる部分です。)
|
||||
|
||||
@@ -72,7 +72,7 @@ labels:
|
||||
|
||||
### `NFTokenOffer`の準備金
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、オファーを出すアカウントに1つ分の準備金の増額を要求します。執筆時点では、準備金の増分は{% $env.PUBLIC_OWNER_RESERVE %}です。この準備金は、オファーをキャンセルすることで取り戻すことができます。
|
||||
各`NFTokenOffer`オブジェクトは、オファーを出すアカウントに1つ分の準備金の増額を要求します。執筆時点では、準備金の増分は2XRPです。この準備金は、オファーをキャンセルすることで取り戻すことができます。
|
||||
|
||||
|
||||
### `NFTokenOfferID`のフォーマット
|
||||
|
||||
@@ -48,7 +48,7 @@ AMMを表す[AMMエントリ][]と[特殊なAccountRootエントリ](../../ledge
|
||||
|
||||
## 特殊なトランザクションコスト
|
||||
|
||||
各AMMインスタンスはAccountRootレジャーエントリ、AMMレジャーエントリ、プール内の各トークンのトラストラインを含むため、AMMCreateトランザクションは台帳スパムを抑止するために通常よりもはるかに高い[トランザクションコスト][]を必要とします。標準的な最低0.00001XRPの代わりに、AMMCreateは少なくとも所有者準備金の増分(現在は{% $env.PUBLIC_OWNER_RESERVE %})を破棄しなければなりません。これは[AccountDeleteトランザクション][]と同じ特別なトランザクションコストです。
|
||||
各AMMインスタンスはAccountRootレジャーエントリ、AMMレジャーエントリ、プール内の各トークンのトラストラインを含むため、AMMCreateトランザクションは台帳スパムを抑止するために通常よりもはるかに高い[トランザクションコスト][]を必要とします。標準的な最低0.00001XRPの代わりに、AMMCreateは少なくとも所有者準備金の増分(現在は2XRP)を破棄しなければなりません。これは[AccountDeleteトランザクション][]と同じ特別なトランザクションコストです。
|
||||
|
||||
## エラーケース
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ labels:
|
||||
|
||||
- トランザクションを送信するための十分なXRPが供給されていて、新しい署名者リストの[必要準備金](../../../concepts/accounts/reserves.md)を満たしている資金供給のあるXRP Ledger[アドレス](../../../concepts/accounts/index.md)が必要です。
|
||||
|
||||
- [MultiSignReserve Amendment][]が有効な場合、マルチシグを使用するには、使用する署名と署名者の数に関わらず、アカウントの準備金として{% $env.PUBLIC_OWNER_RESERVE %}が必要です。(MultiSignReserve Amendmentは**2019年4月7日**以降、本番環境のXRP Ledgerで有効になっています。)
|
||||
- [MultiSignReserve Amendment][]が有効な場合、マルチシグを使用するには、使用する署名と署名者の数に関わらず、アカウントの準備金として2 XRPが必要です。(MultiSignReserve Amendmentは**2019年4月7日**以降、本番環境のXRP Ledgerで有効になっています。)
|
||||
|
||||
- [MultiSignReserve Amendment][]が有効ではないテストネットワークでは、マルチシグを使用するには[アカウント準備金](../../../concepts/accounts/reserves.md)に通常よりも多くのXRPが必要となります。必要額は、リストの署名者の数に応じて増加します。
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ XRP Ledgerの分散型取引所(DEX)には、「アルゴリズムトレード
|
||||
|
||||
裁定取引を行うには、XRP Ledgerの内部と関連する部分の両方で、多くの方法があります。以下の例は潜在的な戦略を説明するためのものですが、他の方法も可能です。
|
||||
|
||||
**循環支払い**を利用して、マルチアセットトレードを完了し利益を得ることができます。XRP Ledgerは、XRPが真ん中のアセットである3つのアセットセットと同様に、アセットペア間の重複した取引を自動的に接続します。しかし、XRP Ledgerのプロトコルは、他のより長い、あるいはより複雑な経路のトレードを自動的に見つけて競うことはしません。(可能な限り最善の経路を見つけることは、計算集約型な問題のカテゴリとして知られています。)したがって、自分で経路探索(PathFinding)を行えば、このような有益な裁定取引の機会を見つけることが可能です。その場合は、[Paymentトランザクション](../../references/protocol/transactions/types/payment.md)でそれらの[経路(Paths)](../../concepts/tokens/fungible-tokens/paths.md)を明示的に指定できます。1つのFOOを使って2つのBARを買い、その2つのBARを使って3つのTSTを買い、最後に3つのTSTを使って1.1 FOOを買えば、0.1 FOOから取引に関わるトークンの[送金手数料](../../concepts/tokens/fungible-tokens/transfer-fees.md)などのコストを差し引いた利益を得ることができます。
|
||||
**循環支払い**を利用して、マルチアセットトレードを完了し利益を得ることができます。XRP Ledgerは、XRPが真ん中のアセットである3つのアセットセットセットと同様に、アセットペア間の重複した取引を自動的に接続します。しかし、XRP Ledgerのプロトコルは、他のより長い、あるいはより複雑な経路のトレードを自動的に見つけて競うことはしません。(可能な限り最善の経路を見つけることは、計算集約型な問題のカテゴリとして知られています。)したがって、XRP Ledgerが独自の経路を見つける場合、XRP Ledgerのプロトコルは自動的に他の、より長い、あるいはより複雑な経路の取引を見つけ、競争させることはありません。したがって、自分で経路探索(PathFinding)を行えば、このような有益な裁定取引の機会を見つけることが可能です。その場合は、[Paymentトランザクション](../../references/protocol/transactions/types/payment.md)でそれらの[経路(Paths)](../../concepts/tokens/fungible-tokens/paths.md)を明示的に指定できます。1つのFOOを使って2つのBARを買い、その2つのBARを使って3つのTSTを買い、最後に3つのTSTを使って1.1 FOOを買えば、0.1 FOOから取引に関わるトークンの[送金手数料](../../concepts/tokens/fungible-tokens/transfer-fees.md)などのコストを差し引いた利益を得ることができます。
|
||||
|
||||
資産の価格が異なる複数の取引所(CEX)に口座を持っている場合、**取引所間の裁定取引**を行うことができます。例えば、ACME取引所でXRPを1XRPあたり0.45ドルで購入し、そのXRPをWayGate取引所に移動して1XRPあたり0.50ドルで売却した場合、XRPあたり0.05ドルの利益を得ることができます。より複雑な例として、ACME取引所でBTC:ETHの価格が変動し、BTCに対してETHが安くなった場合、ある取引所でETH→XRPを売却し、そのXRPをACME取引所に移動し、XRP→BTC→ETHを取引して利益を得ることで、この価格変動を利用できる可能性があります。XRP Ledgerの取引は数秒で決済されますが、イーサリアムの取引は数分、ビットコインの取引は数時間かかることがあるため、XRPをブリッジ通貨として使用することで、ACME取引所でETH→BTC→BTC→ETHと取引するよりも早くこの機会を利用できる可能性があります。(これはもちろん、XRPへの交換が利益以上のコストにならないだけの十分な流動性と狭いスプレッドがある場合にのみ機能します)
|
||||
|
||||
@@ -48,7 +48,7 @@ XRP Ledgerの分散型取引所(DEX)には、「アルゴリズムトレード
|
||||
|
||||
## テストとよくある間違い
|
||||
|
||||
どのような取引でもそうですが、アルゴリズムトレードは確実に儲かる方法ではありません。手作業によるトレードと比べると、アルゴリズムトレードはエラーの余地が非常に少なくなります。小さなミスを犯しても、そのミスを大量のトレードで倍増させようとすれば、問題を修正する前に損失があっという間に膨らんでしまいます。したがって、自分のトレード戦略が実際に利益を上げるかどうかを確認するために、さまざまなテストを行うのが賢明です。戦略やその実際の実装(よく _ボット_ と呼ばれます)をテストするために、次のようなことを行うことができます。
|
||||
どのような取引でもそうですが、アルゴリズムトレー ドは確実に儲かる方法ではありません。手作業によるトレードと比べると、アルゴリズムトレードはエラーの余地が非常に少なくなります。小さなミスを犯しても、そのミスを大量のトレードで倍増させようとすれば、問題を修正する前に損失があっという間に膨らんでしまいます。したがって、自分のトレード戦略が実際に利益を上げるかどうかを確認するために、さまざまなテストを行うのが賢明です。戦略やその実際の実装(よく _ボット_ と呼ばれます)をテストするために、次のようなことを行うことができます。
|
||||
|
||||
- 現在のレジャーの状態または過去のトレードに基づいて、潜在的な利益を手動で計算します。
|
||||
- 過去のデータを記録してボットに送り、ボットがどのようなアクションを取ったかを記録し、実際の過去の値動きと結果を比較します。
|
||||
@@ -61,7 +61,7 @@ XRP Ledgerの分散型取引所(DEX)には、「アルゴリズムトレード
|
||||
- 通常、四捨五入の違いや、計算時と約定時の値動きの違いを考慮し、金額を調整する必要があります。この金額は「スリッページ」と呼ばれ、適切な金額を設定することが重要です。スリッページが低すぎると、トレードがまったく約定しない可能性があります。一方、スリッページが高すぎると、フロントランニングの影響を受けやすくなり、スリッページが高ければ高いほど、値動きによって利益が削られる可能性が高くなります。
|
||||
- **余分なコストと遅延を考慮しないこと**: 例えば、2つのステーブルコインの裏付けが米ドルであるにもかかわらず、ある発行者が0.5%の送金手数料を請求し、別の発行者が0.25%の[送金手数料](../../concepts/tokens/fungible-tokens/transfer-fees.md)を請求した場合、そのステーブルコインの取引価格には約0.25%の差が生じます。トランザクションを送信するためのコストは、通常は少額ですが、その他の潜在的な遅延の影響も忘れないでください。例えば、オフレジャーの取引所が現時点で有利な価格を示していたとしても、その取引所の入金処理に数時間から数日かかる場合、その取引所で事前に流動性を持っていない限り、その価格を利用することはできません。
|
||||
- **稀な事象を考慮していないこと**: 前例のない出来事(「ブラック・スワン」)はさておき、個々の異常値によって計算結果がゆがむことがあります。一例として(これは実話ですが)、あるトレーダーが、ある戦略の潜在的な利益を特定の時間帯で計算したところ、利益の80%以上が、他のユーザが誤って価格にゼロを追加してしまった1つの「入力ミス」の取引によるものであったと報じました。同じ戦略を、これらの異常値の取引を含まない時間範囲に対して計算すると、利益ははるかに少なくなりました。
|
||||
- **トランザクションのフラグを確認しないこと**: XRP Ledgerのトランザクションのフラグは、そのトランザクションの処理方法や、プロトコルがそれを「成功」とマークするタイミングに大きな影響を与える可能性があります。例えば、"Offer"トランザクションのフラグは、全額がすぐに得られる場合にのみトレードされる"Fill or Kill"注文にすることができます。"Payment"トランザクションのフラグは、意図した宛先に全額を届けることができなくても成功する[partial payments](../../concepts/payment-types/partial-payments.md)にすることができます。トランザクションの`Flags`フィールドを解析するためにビット演算をする必要がありますが、それをスキップしてしまうと、予想と結果が全く異なったものとなってしまう可能性があります。
|
||||
- **トランザクションのフラグを確認しないこ**と: XRP Ledgerのトランザクションのフラグは、そのトランザクションの処理方法や、プロトコルがそれを「成功」とマークするタイミングに大きな影響を与える可能性があります。例えば、"Offer"トランザクションのフラグは、全額がすぐに得られる場合にのみトレードされる"Fill or Kill"注文にすることができます。"Payment"トランザクションのフラグは、意図した宛先に全額を届けることができなくても成功する[partial payments](../../concepts/payment-types/partial-payments.md)にすることができます。トランザクションの`Flags`フィールドを解析するためにビット演算をする必要がありますが、それをスキップしてしまうと、予想と結果が全く異なったものとなってしまう可能性があります。
|
||||
|
||||
## 税金とライセンス
|
||||
|
||||
@@ -72,7 +72,7 @@ XRP Ledgerの分散型取引所(DEX)には、「アルゴリズムトレード
|
||||
|
||||
### トレードの発注
|
||||
|
||||
XRP Ledgerの分散型取引所で _代替可能_ トークンとXRPを売買するには、通常[OfferCreateトランザクション](../../references/protocol/transactions/types/offercreate.md)を送信します。この方法でトレードを行うためのコードと技術的ステップの詳細なウォークスルーについては、[分散型取引所でのトレード](../../tutorials/how-tos/use-tokens/trade-in-the-decentralized-exchange.md)をご覧ください。[Paymentトランザクション](../../references/protocol/transactions/types/payment.md)を使用して通貨を両替することも可能です。[クロスカレンシー支払い](../../concepts/payment-types/cross-currency-payments.md)を他のユーザに送ったり、長い[パス](../../concepts/tokens/fungible-tokens/paths.md)を使って裁定取引の機会を1つの操作にまとめることで、自分自身に送り返すこともできます。
|
||||
XRP Ledgerの分散型取引所で _代替可能_ トークンとXRPを売買するには、通常[OfferCreateトランザクション](../../references/protocol/transactions/types/offercreate.md)を送信します。この方法でトレードを行うためのコードと技術的ステップの詳細なウォークスルーについては、[分散型取引所でのトレード](../../tutorials/how-tos/use-tokens/trade-in-the-decentralized-exchange.md)をご覧ください。[Paymentトランザクション](../../references/protocol/transactions/types/payment.md)を使用して通貨を両替することも可能です。[クロスカレンしー支払い](../../concepts/payment-types/cross-currency-payments.md)を他のユーザに送ったり、長い[パス](../../concepts/tokens/fungible-tokens/paths.md)を使って裁定取引の機会を1つの操作にまとめることで、自分自身に送り返すこともできます。
|
||||
|
||||
NFTをトレードするためのコードと技術的な手順については、[JavaScriptを使用したNFTokenの送信](../../tutorials/javascript/nfts/transfer-nfts.md)をご覧ください。
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ NFTをオークション形式で販売することができます。[NFTオー
|
||||
|
||||
### 準備金要件
|
||||
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
|
||||
各`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
|
||||
|
||||
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料(およそ6000ドロップ、または0.006 XRP)が発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ NFTokenのURLは、NFTのコンテンツが保存されている場所へのリ
|
||||
|
||||
[認可Minter](../../concepts/tokens/nfts/authorizing-another-minter.md)をご覧ください。
|
||||
|
||||
ミント済みのNFTは、`NFTokenPage`に記録されます。アカウント上の`NFTokenPage`1つにつき{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。[NFT準備金](../../concepts/tokens/nfts/reserve-requirements.md)をご覧ください。
|
||||
ミント済みのNFTは、`NFTokenPage`に記録されます。アカウント上の`NFTokenPage`1つにつき2XRPの準備金が必要です。[NFT準備金](../../concepts/tokens/nfts/reserve-requirements.md)をご覧ください。
|
||||
|
||||
各「NFTokenPage」は16~32個のNFTを保持します。大量のNFTをミントすると、あなたのXRPを大量に準備金としてロックすることになります。オンデマンドミント(または _遅延ミント_ )を行うことで、XRPを柔軟に維持することができます。[遅延ミント](../../concepts/tokens/nfts/batch-minting.md#mint-on-demand-lazy-minting)と[スクリプトミント](../../concepts/tokens/nfts/batch-minting.md#scripted-minting)をご覧下さい。
|
||||
|
||||
@@ -89,9 +89,9 @@ NFTをオークション形式で販売することができます。[NFTオー
|
||||
|
||||
#### 準備金要件
|
||||
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
|
||||
各`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
|
||||
|
||||
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料(およそ6000ドロップ、または0.006 XRP)が発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ NFTをオークション形式で販売することができます。[NFTオー
|
||||
|
||||
### 準備金要件
|
||||
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
|
||||
各`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
|
||||
|
||||
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料(およそ6000ドロップ、または0.006 XRP)が発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
---
|
||||
# リソース
|
||||
|
||||
XRP Ledgerの理解や開発ためのリソース。
|
||||
XRP Ledgerの理解や開発ためのリソース。Other resources to help understand the XRPL and develop on it.
|
||||
|
||||
|
||||
{% child-pages /%}
|
||||
|
||||
@@ -16,7 +16,6 @@ navbar.about: 概要
|
||||
navbar.docs: ドキュメント
|
||||
navbar.resources: リソース
|
||||
navbar.community: コミュニティ
|
||||
navbar.showcase: ショーケース
|
||||
footer.about: 概要
|
||||
footer.docs: ドキュメント
|
||||
footer.resources: リソース
|
||||
@@ -156,8 +155,6 @@ amendment.table.status: ステータス
|
||||
amendment.status.enabled: 有効
|
||||
amendment.status.eta: 予定
|
||||
amendment.status.openForVoting: 投票中
|
||||
amendment.status.inactive: 無効
|
||||
amendment.status.inactiveButton: 詳細を取得する
|
||||
|
||||
# index.page.tsx
|
||||
home.hero.h1part1: ビジネスのための
|
||||
|
||||
@@ -23,7 +23,6 @@ type AmendmentsCachePayload = {
|
||||
|
||||
// API data caching
|
||||
const amendmentsEndpoint = 'https://vhs.prod.ripplex.io/v1/network/amendments/vote/main/'
|
||||
const amendmentsInfoEndpoint = 'https://vhs.prod.ripplex.io/v1/network/amendments/info/main/'
|
||||
const amendmentsCacheKey = 'xrpl.amendments.mainnet.cache'
|
||||
const amendmentsTTL = 15 * 60 * 1000 // 15 minutes in milliseconds
|
||||
|
||||
@@ -181,8 +180,6 @@ function AmendmentBadge(props: { amendment: Amendment }) {
|
||||
const enabledLabel = translate("amendment.status.enabled", "Enabled")
|
||||
const votingLabel = translate("amendment.status.openForVoting", "Open for Voting")
|
||||
const etaLabel = translate("amendment.status.eta", "Expected")
|
||||
const inactiveLabel = translate("amendment.status.inactive", "Inactive")
|
||||
const inactiveButton = translate("amendment.status.inactiveButton", "Get details")
|
||||
|
||||
React.useEffect(() => {
|
||||
const amendment = props.amendment
|
||||
@@ -205,16 +202,10 @@ function AmendmentBadge(props: { amendment: Amendment }) {
|
||||
else if (amendment.consensus) {
|
||||
setStatus(`${votingLabel}: ${amendment.consensus}`)
|
||||
setColor('80d0e0')
|
||||
setHref(undefined)
|
||||
setHref(undefined) // No link for voting amendments
|
||||
}
|
||||
// Fallback: amendment is inactive
|
||||
else {
|
||||
setStatus(`${inactiveLabel}: ${inactiveButton}`)
|
||||
setColor('lightgrey')
|
||||
setHref(`/resources/known-amendments#${amendment.name.toLowerCase()}`)
|
||||
}
|
||||
}, [props.amendment, enabledLabel, etaLabel, votingLabel, inactiveLabel])
|
||||
|
||||
}, [props.amendment, enabledLabel, etaLabel, votingLabel])
|
||||
|
||||
// Split the status at the colon to create two-color badge
|
||||
const parts = status.split(':')
|
||||
const label = shieldsIoEscape(parts[0])
|
||||
@@ -266,32 +257,15 @@ export function AmendmentDisclaimer(props: {
|
||||
const response = await fetch(amendmentsEndpoint)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`)
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data: AmendmentsResponse = await response.json()
|
||||
writeAmendmentsCache(data.amendments)
|
||||
|
||||
const found = data.amendments.find(a => a.name === props.name)
|
||||
|
||||
// 3. If not found in live data, try the info endpoint.
|
||||
if (!found) {
|
||||
|
||||
const infoResponse = await fetch(amendmentsInfoEndpoint)
|
||||
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error(`HTTP error from info endpoint! Status: ${infoResponse.status}`)
|
||||
}
|
||||
|
||||
const infoData: AmendmentsResponse = await infoResponse.json()
|
||||
const foundInInfo = infoData.amendments.find(a => a.name === props.name)
|
||||
|
||||
if (!foundInInfo) {
|
||||
throw new Error(`Couldn't find ${props.name} amendment in status tables.`)
|
||||
}
|
||||
|
||||
setStatus(foundInInfo)
|
||||
return
|
||||
throw new Error(`Couldn't find ${props.name} amendment in status table.`)
|
||||
}
|
||||
|
||||
setStatus(found)
|
||||
@@ -415,8 +389,6 @@ export function Badge(props: {
|
||||
"更新": "blue", // ja: updated in
|
||||
"in development": "lightgrey",
|
||||
"開発中": "lightgrey", // ja: in development
|
||||
"inactive": "lightgrey",
|
||||
"無効": "lightgrey" // ja: inactive
|
||||
}
|
||||
|
||||
let childstrings = ""
|
||||
|
||||
@@ -11,11 +11,45 @@ export { default as chevronDown } from "../../../../static/img/navbar/chevron-do
|
||||
export { default as hamburgerIcon } from "../../../../static/img/navbar/hamburger-icon.svg";
|
||||
export { default as arrowUpRight } from "../../../../static/img/icons/arrow-up-right-custom.svg";
|
||||
|
||||
// Network submenu pattern images (used for both light and dark mode)
|
||||
export { default as resourcesIconPattern } from "../../../../static/img/navbar/resources-icon.svg";
|
||||
export { default as insightsIconPattern } from "../../../../static/img/navbar/insights-icon.svg";
|
||||
// Wallet icons for submenu
|
||||
export { default as greenWallet } from "../../../../static/img/navbar/green-wallet.svg";
|
||||
export { default as lilacWallet } from "../../../../static/img/navbar/lilac-wallet.svg";
|
||||
export { default as yellowWallet } from "../../../../static/img/navbar/yellow-wallet.svg";
|
||||
export { default as pinkWallet } from "../../../../static/img/navbar/pink-wallet.svg";
|
||||
export { default as blueWallet } from "../../../../static/img/navbar/blue-wallet.svg";
|
||||
|
||||
// Submenu icons - imported once, exported individually and used in navIcons mapping
|
||||
// Develop submenu icons
|
||||
export { default as devHomeIcon } from "../../../../static/img/navbar/dev_home.svg";
|
||||
export { default as learnIcon } from "../../../../static/img/navbar/learn.svg";
|
||||
export { default as codeSamplesIcon } from "../../../../static/img/navbar/code_samples.svg";
|
||||
export { default as docsIcon } from "../../../../static/img/navbar/docs.svg";
|
||||
export { default as clientLibIcon } from "../../../../static/img/navbar/client_lib.svg";
|
||||
|
||||
// Use Cases submenu icons
|
||||
export { default as paymentsIcon } from "../../../../static/img/navbar/payments.svg";
|
||||
export { default as tokenizationIcon } from "../../../../static/img/navbar/tokenization.svg";
|
||||
export { default as creditIcon } from "../../../../static/img/navbar/credit.svg";
|
||||
export { default as tradingIcon } from "../../../../static/img/navbar/trading.svg";
|
||||
|
||||
// Community submenu icons
|
||||
export { default as communityIcon } from "../../../../static/img/navbar/community.svg";
|
||||
|
||||
// Network submenu icons
|
||||
export { default as insightsIcon } from "../../../../static/img/navbar/insights.svg";
|
||||
export { default as resourcesIcon } from "../../../../static/img/navbar/resources.svg";
|
||||
|
||||
// Network submenu pattern images
|
||||
export { default as resourcesPurplePattern } from "../../../../static/img/navbar/resources-purple.svg";
|
||||
export { default as insightsGreenPattern } from "../../../../static/img/navbar/insights-green.svg";
|
||||
export { default as darkInsightsGreenPattern } from "../../../../static/img/navbar/dark-insights-green.svg";
|
||||
export { default as darkLilacPattern } from "../../../../static/img/navbar/dark-lilac.svg";
|
||||
|
||||
// Wallet icon mapping for dynamic icon lookup
|
||||
import greenWallet from "../../../../static/img/navbar/green-wallet.svg";
|
||||
import lilacWallet from "../../../../static/img/navbar/lilac-wallet.svg";
|
||||
import yellowWallet from "../../../../static/img/navbar/yellow-wallet.svg";
|
||||
import pinkWallet from "../../../../static/img/navbar/pink-wallet.svg";
|
||||
import blueWallet from "../../../../static/img/navbar/blue-wallet.svg";
|
||||
import devHomeIcon from "../../../../static/img/navbar/dev_home.svg";
|
||||
import learnIcon from "../../../../static/img/navbar/learn.svg";
|
||||
import codeSamplesIcon from "../../../../static/img/navbar/code_samples.svg";
|
||||
@@ -26,33 +60,15 @@ import tokenizationIcon from "../../../../static/img/navbar/tokenization.svg";
|
||||
import creditIcon from "../../../../static/img/navbar/credit.svg";
|
||||
import tradingIcon from "../../../../static/img/navbar/trading.svg";
|
||||
import communityIcon from "../../../../static/img/navbar/community.svg";
|
||||
import fundingIcon from "../../../../static/img/navbar/funding.svg";
|
||||
import contributeIcon from "../../../../static/img/navbar/contribute.svg";
|
||||
import ecosystemIcon from "../../../../static/img/navbar/ecosystem.svg";
|
||||
import insightsIcon from "../../../../static/img/navbar/insights.svg";
|
||||
import resourcesIcon from "../../../../static/img/navbar/resources.svg";
|
||||
|
||||
// Re-export submenu icons for direct imports
|
||||
export default {
|
||||
devHomeIcon,
|
||||
learnIcon,
|
||||
codeSamplesIcon,
|
||||
docsIcon,
|
||||
clientLibIcon,
|
||||
paymentsIcon,
|
||||
tokenizationIcon,
|
||||
creditIcon,
|
||||
tradingIcon,
|
||||
communityIcon,
|
||||
fundingIcon,
|
||||
contributeIcon,
|
||||
ecosystemIcon,
|
||||
insightsIcon,
|
||||
resourcesIcon,
|
||||
};
|
||||
|
||||
// Dynamic icon lookup for navbar submenus
|
||||
export const navIcons: Record<string, string> = {
|
||||
export const walletIcons: Record<string, string> = {
|
||||
green: greenWallet,
|
||||
lilac: lilacWallet,
|
||||
yellow: yellowWallet,
|
||||
pink: pinkWallet,
|
||||
blue: blueWallet,
|
||||
dev_home: devHomeIcon,
|
||||
learn: learnIcon,
|
||||
code_samples: codeSamplesIcon,
|
||||
@@ -63,9 +79,7 @@ export const navIcons: Record<string, string> = {
|
||||
credit: creditIcon,
|
||||
trading: tradingIcon,
|
||||
community: communityIcon,
|
||||
contribute: contributeIcon,
|
||||
insights: insightsIcon,
|
||||
resources: resourcesIcon,
|
||||
ecosystem: ecosystemIcon,
|
||||
funding: fundingIcon,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,11 +10,10 @@ export const alertBanner = {
|
||||
|
||||
// Main navigation items
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: "Develop", labelTranslationKey: "navbar.develop", href: "/develop", hasSubmenu: true },
|
||||
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/use-cases", hasSubmenu: true },
|
||||
{ label: "Develop", labelTranslationKey: "navbar.develop", href: "/docs", hasSubmenu: true },
|
||||
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/about/uses", hasSubmenu: true },
|
||||
{ label: "Community", labelTranslationKey: "navbar.community", href: "/community", hasSubmenu: true },
|
||||
{ label: "Network", labelTranslationKey: "navbar.network", href: "/resources", hasSubmenu: true },
|
||||
{ label: "Showcase (Temporary)", labelTranslationKey: "navbar.showcase", href: "/showcase", hasSubmenu: false },
|
||||
{ label: "Network", labelTranslationKey: "navbar.network", href: "/docs/concepts/networks-and-servers", hasSubmenu: true },
|
||||
];
|
||||
|
||||
// Develop submenu data structure
|
||||
@@ -23,9 +22,9 @@ export const developSubmenuData: {
|
||||
right: SubmenuItemWithChildren[];
|
||||
} = {
|
||||
left: [
|
||||
{ label: "Developer's Home", href: "/develop", icon: "dev_home" },
|
||||
{ label: "Learn", href: "https://learn.xrpl.org", icon: "learn" },
|
||||
{ label: "Code Samples", href: "/resources/code-samples", icon: "code_samples" },
|
||||
{ label: "Developer's Home", href: "/docs", icon: "dev_home" },
|
||||
{ label: "Learn", href: "/docs/tutorials", icon: "learn" },
|
||||
{ label: "Code Samples", href: "/_code-samples", icon: "code_samples" },
|
||||
],
|
||||
right: [
|
||||
{
|
||||
@@ -33,21 +32,21 @@ export const developSubmenuData: {
|
||||
href: "/docs",
|
||||
icon: "docs",
|
||||
children: [
|
||||
{ label: "API Reference", href: "/references" },
|
||||
{ label: "API Reference", href: "/docs/references" },
|
||||
{ label: "Tutorials", href: "/docs/tutorials" },
|
||||
{ label: "Concepts", href: "/concepts" },
|
||||
{ label: "Concepts", href: "/docs/concepts" },
|
||||
{ label: "Infrastructure", href: "/docs/infrastructure" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Client Libraries",
|
||||
href: "#",
|
||||
href: "/docs/references/client-libraries",
|
||||
icon: "client_lib",
|
||||
children: [
|
||||
{ label: "JavaScript", href: "#" },
|
||||
{ label: "Python", href: "#" },
|
||||
{ label: "PHP", href: "#" },
|
||||
{ label: "Go", href: "#" },
|
||||
{ label: "JavaScript", href: "/docs/references/xrpljs" },
|
||||
{ label: "Python", href: "/docs/references/xrpl-py" },
|
||||
{ label: "PHP", href: "/docs/references/xrpl-php" },
|
||||
{ label: "Go", href: "/docs/references/xrpl-go" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -61,44 +60,44 @@ export const useCasesSubmenuData: {
|
||||
left: [
|
||||
{
|
||||
label: "Payments",
|
||||
href: "/use-cases/payments",
|
||||
href: "/about/uses/payments",
|
||||
icon: "payments",
|
||||
children: [
|
||||
{ label: "Direct XRP Payments", href: "/use-cases/payments/direct-xrp-payments" },
|
||||
{ label: "Cross-currency Payments", href: "/use-cases/payments/cross-currency-payments" },
|
||||
{ label: "Escrow", href: "/use-cases/payments/escrow" },
|
||||
{ label: "Checks", href: "/use-cases/payments/checks" },
|
||||
{ label: "Direct XRP Payments", href: "/about/uses/direct-xrp-payments" },
|
||||
{ label: "Cross-currency Payments", href: "/about/uses/cross-currency-payments" },
|
||||
{ label: "Escrow", href: "/about/uses/escrow" },
|
||||
{ label: "Checks", href: "/about/uses/checks" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Tokenization",
|
||||
href: "/use-cases/tokenization",
|
||||
href: "/about/uses/tokenization",
|
||||
icon: "tokenization",
|
||||
children: [
|
||||
{ label: "Stablecoin", href: "/use-cases/tokenization/stablecoin" },
|
||||
{ label: "NFT", href: "/use-cases/tokenization/nft" },
|
||||
{ label: "Stablecoin", href: "/about/uses/stablecoin" },
|
||||
{ label: "NFT", href: "/about/uses/nft" },
|
||||
],
|
||||
},
|
||||
],
|
||||
right: [
|
||||
{
|
||||
label: "Credit",
|
||||
href: "/use-cases/credit",
|
||||
href: "/about/uses/credit",
|
||||
icon: "credit",
|
||||
children: [
|
||||
{ label: "Lending", href: "/use-cases/credit/lending" },
|
||||
{ label: "Collateralization", href: "/use-cases/credit/collateralization" },
|
||||
{ label: "Sustainability", href: "/use-cases/credit/sustainability" },
|
||||
{ label: "Lending", href: "/about/uses/lending" },
|
||||
{ label: "Collateralization", href: "/about/uses/collateralization" },
|
||||
{ label: "Sustainability", href: "/about/uses/sustainability" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Trading",
|
||||
href: "/use-cases/trading",
|
||||
href: "/about/uses/trading",
|
||||
icon: "trading",
|
||||
children: [
|
||||
{ label: "DEX", href: "/use-cases/trading/dex" },
|
||||
{ label: "Permissioned Trading", href: "/use-cases/trading/permissioned-trading" },
|
||||
{ label: "AMM", href: "/use-cases/trading/amm" },
|
||||
{ label: "DEX", href: "/about/uses/dex" },
|
||||
{ label: "Permissioned Trading", href: "/about/uses/permissioned-trading" },
|
||||
{ label: "AMM", href: "/about/uses/amm" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -116,22 +115,26 @@ export const communitySubmenuData: {
|
||||
icon: "community",
|
||||
children: [
|
||||
{ label: "Events", href: "/community/events" },
|
||||
{ label: "News", href: "/blog", active: true },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Marketplace", href: "/community/marketplace" },
|
||||
{ label: "Partner Connect", href: "/community/partner-connect" },
|
||||
],
|
||||
},
|
||||
{ label: "Funding", href: "/community/developer-funding", icon: "funding" },
|
||||
{ label: "Funding", href: "/community/developer-funding", icon: "code_samples" },
|
||||
],
|
||||
right: [
|
||||
{
|
||||
label: "Contribute",
|
||||
href: "/community/contribute",
|
||||
icon: "contribute",
|
||||
href: "/resources/contribute-documentation",
|
||||
icon: "client_lib",
|
||||
children: [
|
||||
{ label: "Bug Bounty", href: "/blog/2020/rippled-1.5.0#bug-bounties-and-responsible-disclosures" },
|
||||
{ label: "Research", href: "https://xls.xrpl.org/" },
|
||||
{ label: "Ecosystem Map", href: "/community/ecosystem-map" },
|
||||
{ label: "Bug Bounty", href: "/community/bug-bounty" },
|
||||
{ label: "Research", href: "/community/research" },
|
||||
],
|
||||
},
|
||||
{ label: "Ecosystem Map", href: "/about/uses", icon: "ecosystem" },
|
||||
{ label: "Creators", href: "/community/ambassadors", icon: "learn" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -139,21 +142,23 @@ export const communitySubmenuData: {
|
||||
export const networkSubmenuData: NetworkSubmenuSection[] = [
|
||||
{
|
||||
label: "Resources",
|
||||
href: "/resources",
|
||||
href: "/docs/concepts/networks-and-servers",
|
||||
icon: "resources",
|
||||
children: [
|
||||
{ label: "About", href: "/about/history" },
|
||||
{ label: "XRPL Brand Kit", href: "/community/brand-kit" },
|
||||
{ label: "Validators", href: "/docs/concepts/networks-and-servers/validators" },
|
||||
{ label: "Governance", href: "/docs/concepts/networks-and-servers/governance", active: true },
|
||||
{ label: "XRPL Roadmap", href: "/docs/concepts/networks-and-servers/xrpl-roadmap" },
|
||||
],
|
||||
patternColor: 'lilac',
|
||||
},
|
||||
{
|
||||
label: "Insights",
|
||||
href: "/insights",
|
||||
href: "/docs/concepts/networks-and-servers/insights",
|
||||
icon: "insights",
|
||||
children: [
|
||||
{ label: "Explorer", href: "https://livenet.xrpl.org" },
|
||||
{ label: "Amendment Voting Status", href: "https://xrpl.org/resources/known-amendments" },
|
||||
{ label: "Data Dashboard", href: "/docs/concepts/networks-and-servers/data-dashboard" },
|
||||
{ label: "Amendment Voting Status", href: "/docs/concepts/networks-and-servers/amendments" },
|
||||
],
|
||||
patternColor: 'green',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { SubmenuArrow, SubmenuChildArrow } from "../icons";
|
||||
import { navIcons } from "../constants/icons";
|
||||
import { walletIcons } from "../constants/icons";
|
||||
import { hasChildren, type SubmenuItem } from "../types";
|
||||
|
||||
interface MobileMenuSectionProps {
|
||||
@@ -21,7 +21,7 @@ export function MobileMenuSection({ item }: MobileMenuSectionProps) {
|
||||
<React.Fragment>
|
||||
<a href={item.href} className="bds-mobile-menu__tier1 bds-mobile-menu__parent-link">
|
||||
<span className="bds-mobile-menu__icon">
|
||||
<img src={navIcons[item.icon]} alt={translate(item.label)} />
|
||||
<img src={walletIcons[item.icon]} alt="" />
|
||||
</span>
|
||||
<span className="bds-mobile-menu__link bds-mobile-menu__link--bold">
|
||||
{translate(item.label)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { SubmenuSection } from "./SubmenuSection";
|
||||
import { ArrowIcon } from "../icons";
|
||||
import { navIcons, resourcesIconPattern, insightsIconPattern } from "../constants/icons";
|
||||
import { walletIcons, resourcesPurplePattern, insightsGreenPattern, darkInsightsGreenPattern, darkLilacPattern } from "../constants/icons";
|
||||
import { developSubmenuData, useCasesSubmenuData, communitySubmenuData, networkSubmenuData } from "../constants/navigation";
|
||||
import type { SubmenuItem, SubmenuItemWithChildren, NetworkSubmenuSection } from "../types";
|
||||
|
||||
@@ -161,10 +161,13 @@ export function Submenu({ variant, isActive, isClosing, onClose }: SubmenuProps)
|
||||
);
|
||||
}
|
||||
|
||||
/** Network submenu with pattern images (same for light and dark mode) */
|
||||
/** Network submenu with theme-aware pattern images */
|
||||
function NetworkSubmenuContent({ isActive, isClosing, onClose }: { isActive: boolean; isClosing: boolean; onClose?: () => void }) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
// Start with null to indicate "not yet determined" - avoids hydration mismatch
|
||||
// by ensuring server and client both render the same initial state
|
||||
const [isDarkMode, setIsDarkMode] = React.useState<boolean | null>(null);
|
||||
|
||||
// Handle keyboard events for accessibility
|
||||
const handleKeyDown = React.useCallback((event: KeyboardEvent) => {
|
||||
@@ -220,11 +223,21 @@ function NetworkSubmenuContent({ isActive, isClosing, onClose }: { isActive: boo
|
||||
}
|
||||
}, [isActive, handleKeyDown]);
|
||||
|
||||
// Use same pattern images for both light and dark mode
|
||||
const patternImages = {
|
||||
lilac: resourcesIconPattern,
|
||||
green: insightsIconPattern,
|
||||
};
|
||||
React.useEffect(() => {
|
||||
const checkTheme = () => {
|
||||
setIsDarkMode(document.documentElement.classList.contains('dark'));
|
||||
};
|
||||
checkTheme();
|
||||
const observer = new MutationObserver(checkTheme);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Default to light mode patterns until client-side detection runs
|
||||
const patternImages = React.useMemo(() => ({
|
||||
lilac: isDarkMode === true ? darkLilacPattern : resourcesPurplePattern,
|
||||
green: isDarkMode === true ? darkInsightsGreenPattern : insightsGreenPattern,
|
||||
}), [isDarkMode]);
|
||||
|
||||
const classNames = [
|
||||
'bds-submenu',
|
||||
@@ -239,7 +252,7 @@ function NetworkSubmenuContent({ isActive, isClosing, onClose }: { isActive: boo
|
||||
<div key={section.label} className="bds-submenu__section">
|
||||
<a href={section.href} className="bds-submenu__tier1 bds-submenu__parent-link">
|
||||
<span className="bds-submenu__icon">
|
||||
<img src={navIcons[section.icon]} alt={translate(section.label)} />
|
||||
<img src={walletIcons[section.icon]} alt="" />
|
||||
</span>
|
||||
<span className="bds-submenu__link bds-submenu__link--bold">
|
||||
{translate(section.label)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { ArrowIcon } from "../icons";
|
||||
import { navIcons } from "../constants/icons";
|
||||
import { walletIcons } from "../constants/icons";
|
||||
import { hasChildren, type SubmenuItem, type SubmenuItemWithChildren, type SubmenuItemBase } from "../types";
|
||||
|
||||
interface SubmenuSectionProps {
|
||||
@@ -28,7 +28,7 @@ export function SubmenuSection({ item, showChildren = true }: SubmenuSectionProp
|
||||
<div className="bds-submenu__section">
|
||||
<a href={item.href} className="bds-submenu__tier1 bds-submenu__parent-link">
|
||||
<span className="bds-submenu__icon">
|
||||
<img src={navIcons[item.icon]} alt={translate(item.label)} />
|
||||
<img src={walletIcons[item.icon]} alt="" />
|
||||
</span>
|
||||
<span className="bds-submenu__link bds-submenu__link--bold">
|
||||
{translate(item.label)}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Lending Protocol Examples
|
||||
|
||||
Code samples showing 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.
|
||||
@@ -1,394 +0,0 @@
|
||||
# Lending Protocol Examples (JavaScript)
|
||||
|
||||
This directory contains JavaScript 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
|
||||
|
||||
Install dependencies before running any examples:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create a Loan Broker
|
||||
|
||||
```sh
|
||||
node createLoanBroker.js
|
||||
```
|
||||
|
||||
The script should output the LoanBrokerSet transaction, loan broker ID, and loan broker pseudo-account:
|
||||
|
||||
```sh
|
||||
Loan broker/vault owner address: rKL3u76wNGdF2Th4EvCuHV5885T6h2iFTY
|
||||
Vault ID: 33E51DD0333775E37F2CC1EB0DA788F9C663AF919DC23ED595A8D69330E5CD68
|
||||
|
||||
=== Preparing LoanBrokerSet transaction ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanBrokerSet",
|
||||
"Account": "rKL3u76wNGdF2Th4EvCuHV5885T6h2iFTY",
|
||||
"VaultID": "33E51DD0333775E37F2CC1EB0DA788F9C663AF919DC23ED595A8D69330E5CD68",
|
||||
"ManagementFeeRate": 1000
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerSet transaction ===
|
||||
|
||||
Loan broker created successfully!
|
||||
|
||||
=== Loan Broker Information ===
|
||||
|
||||
LoanBroker ID: 0AA13C8A8E95D8F2D9EF1FA1B15EF4668EF779A678D1D24D099C532E126E8BBF
|
||||
LoanBroker Psuedo-Account Address: rfhftuQGpqUVRcERZbY9htJshijKur7dS4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Claw Back First-loss Capital
|
||||
|
||||
```sh
|
||||
node coverClawback.js
|
||||
```
|
||||
|
||||
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: r9tQSk5rQdjjVGn1brt8K5XNYFvNSLv3xU
|
||||
MPT issuer address: rJ7DiJdcThwLD5rZjC7D1neXmvLFAGk9t3
|
||||
LoanBrokerID: 655C32ADFCA0712F3CB32CA034C29FE3DE9DE876A86141F0902FB1E05DA0E442
|
||||
MPT ID: 00349F41BFA01892C83AC779E4BBB80C8CE3B92D401E4B6E
|
||||
|
||||
=== Cover Available ===
|
||||
|
||||
0 TSTUSD
|
||||
|
||||
=== Preparing LoanBrokerCoverDeposit transaction ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanBrokerCoverDeposit",
|
||||
"Account": "r9tQSk5rQdjjVGn1brt8K5XNYFvNSLv3xU",
|
||||
"LoanBrokerID": "655C32ADFCA0712F3CB32CA034C29FE3DE9DE876A86141F0902FB1E05DA0E442",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00349F41BFA01892C83AC779E4BBB80C8CE3B92D401E4B6E",
|
||||
"value": "1000"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerCoverDeposit transaction ===
|
||||
|
||||
Cover deposit successful!
|
||||
|
||||
=== Cover Available After Deposit ===
|
||||
|
||||
1000 TSTUSD
|
||||
|
||||
=== Verifying Asset Issuer ===
|
||||
|
||||
MPT issuer account verified: rJ7DiJdcThwLD5rZjC7D1neXmvLFAGk9t3. Proceeding to clawback.
|
||||
|
||||
=== Preparing LoanBrokerCoverClawback transaction ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanBrokerCoverClawback",
|
||||
"Account": "rJ7DiJdcThwLD5rZjC7D1neXmvLFAGk9t3",
|
||||
"LoanBrokerID": "655C32ADFCA0712F3CB32CA034C29FE3DE9DE876A86141F0902FB1E05DA0E442",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00349F41BFA01892C83AC779E4BBB80C8CE3B92D401E4B6E",
|
||||
"value": "1000"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerCoverClawback transaction ===
|
||||
|
||||
Successfully clawed back 1000 TSTUSD!
|
||||
|
||||
=== Final Cover Available After Clawback ===
|
||||
|
||||
0 TSTUSD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deposit and Withdraw First-loss Capital
|
||||
|
||||
```sh
|
||||
node coverDepositAndWithdraw.js
|
||||
```
|
||||
|
||||
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: rKL3u76wNGdF2Th4EvCuHV5885T6h2iFTY
|
||||
LoanBrokerID: F133118D55342F7F78188BDC9259E8593853010878C9F6CEA0E2F56D829C6B15
|
||||
MPT ID: 0031034FF84EB2E8348A34F0A8889A54F45F180E80F12341
|
||||
|
||||
=== Preparing LoanBrokerCoverDeposit transaction ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanBrokerCoverDeposit",
|
||||
"Account": "rKL3u76wNGdF2Th4EvCuHV5885T6h2iFTY",
|
||||
"LoanBrokerID": "F133118D55342F7F78188BDC9259E8593853010878C9F6CEA0E2F56D829C6B15",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0031034FF84EB2E8348A34F0A8889A54F45F180E80F12341",
|
||||
"value": "2000"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerCoverDeposit transaction ===
|
||||
|
||||
Cover deposit successful!
|
||||
|
||||
=== Cover Balance ===
|
||||
|
||||
LoanBroker Pseudo-Account: rf5FREUsutDyDAaVPPvZnNmoEETr21sPDd
|
||||
Cover balance after deposit: 2000 TSTUSD
|
||||
|
||||
=== Preparing LoanBrokerCoverWithdraw transaction ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanBrokerCoverWithdraw",
|
||||
"Account": "rKL3u76wNGdF2Th4EvCuHV5885T6h2iFTY",
|
||||
"LoanBrokerID": "F133118D55342F7F78188BDC9259E8593853010878C9F6CEA0E2F56D829C6B15",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0031034FF84EB2E8348A34F0A8889A54F45F180E80F12341",
|
||||
"value": "1000"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerCoverWithdraw transaction ===
|
||||
|
||||
Cover withdraw successful!
|
||||
|
||||
=== Updated Cover Balance ===
|
||||
|
||||
LoanBroker Pseudo-Account: rf5FREUsutDyDAaVPPvZnNmoEETr21sPDd
|
||||
Cover balance after withdraw: 1000 TSTUSD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create a Loan
|
||||
|
||||
```sh
|
||||
node createLoan.js
|
||||
```
|
||||
|
||||
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: rn6CD8i3Yc3UGagSosZfegG7hpXxwgVAgz
|
||||
Borrower address: rN2PMxegkEMZHin78o7wSs1JeYjxAvAfvt
|
||||
LoanBrokerID: 3CDEA7CEB9F2ECDD76CD41A864F4E3B5DB9C91AEDBD0906EE466FDD21CCF49B5
|
||||
|
||||
=== Preparing LoanSet transaction ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanSet",
|
||||
"Account": "rn6CD8i3Yc3UGagSosZfegG7hpXxwgVAgz",
|
||||
"Counterparty": "rN2PMxegkEMZHin78o7wSs1JeYjxAvAfvt",
|
||||
"LoanBrokerID": "3CDEA7CEB9F2ECDD76CD41A864F4E3B5DB9C91AEDBD0906EE466FDD21CCF49B5",
|
||||
"PrincipalRequested": "1000",
|
||||
"InterestRate": 500,
|
||||
"PaymentTotal": 12,
|
||||
"PaymentInterval": 2592000,
|
||||
"GracePeriod": 604800,
|
||||
"LoanOriginationFee": "100",
|
||||
"LoanServiceFee": "10",
|
||||
"Flags": 0,
|
||||
"Sequence": 3670743,
|
||||
"LastLedgerSequence": 3673248,
|
||||
"Fee": "2"
|
||||
}
|
||||
|
||||
=== Adding loan broker signature ===
|
||||
|
||||
TxnSignature: F8B2F2AB960191991FC48120A48A089B479018A6469466E43E6F974E1345B32688D59D381E6BC18B6CA383235B708FE4FB44527C51E5B29BCDCC4A08C340A00A
|
||||
SigningPubKey: EDDABC72936FF734FA56D6C60C064D48C5DA9911C8B7C26C4AEAC06534B5D7C530
|
||||
|
||||
Signed loanSetTx for borrower to sign over:
|
||||
{
|
||||
"TransactionType": "LoanSet",
|
||||
"Flags": 0,
|
||||
"Sequence": 3670743,
|
||||
"LastLedgerSequence": 3673248,
|
||||
"PaymentInterval": 2592000,
|
||||
"GracePeriod": 604800,
|
||||
"PaymentTotal": 12,
|
||||
"InterestRate": 500,
|
||||
"LoanBrokerID": "3CDEA7CEB9F2ECDD76CD41A864F4E3B5DB9C91AEDBD0906EE466FDD21CCF49B5",
|
||||
"Fee": "2",
|
||||
"SigningPubKey": "EDDABC72936FF734FA56D6C60C064D48C5DA9911C8B7C26C4AEAC06534B5D7C530",
|
||||
"TxnSignature": "F8B2F2AB960191991FC48120A48A089B479018A6469466E43E6F974E1345B32688D59D381E6BC18B6CA383235B708FE4FB44527C51E5B29BCDCC4A08C340A00A",
|
||||
"Account": "rn6CD8i3Yc3UGagSosZfegG7hpXxwgVAgz",
|
||||
"Counterparty": "rN2PMxegkEMZHin78o7wSs1JeYjxAvAfvt",
|
||||
"LoanOriginationFee": "100",
|
||||
"LoanServiceFee": "10",
|
||||
"PrincipalRequested": "1000"
|
||||
}
|
||||
|
||||
=== Adding borrower signature ===
|
||||
|
||||
Borrower TxnSignature: 52E16B88F5640F637A05E59AB2BE0DBFE4FBE7F1D7580C2A39D4981F6066A7C42047A401B953CDAB4993954A85D73DE35F69317EE8279D23ECB4958AA10C0800
|
||||
Borrower SigningPubKey: EDE624A07899AEF826DF2A3E2A325F69BC1F169D23F08091E9042644D6B06D3D62
|
||||
|
||||
Fully signed LoanSet transaction:
|
||||
{
|
||||
"TransactionType": "LoanSet",
|
||||
"Flags": 0,
|
||||
"Sequence": 3670743,
|
||||
"LastLedgerSequence": 3673248,
|
||||
"PaymentInterval": 2592000,
|
||||
"GracePeriod": 604800,
|
||||
"PaymentTotal": 12,
|
||||
"InterestRate": 500,
|
||||
"LoanBrokerID": "3CDEA7CEB9F2ECDD76CD41A864F4E3B5DB9C91AEDBD0906EE466FDD21CCF49B5",
|
||||
"Fee": "2",
|
||||
"SigningPubKey": "EDDABC72936FF734FA56D6C60C064D48C5DA9911C8B7C26C4AEAC06534B5D7C530",
|
||||
"TxnSignature": "F8B2F2AB960191991FC48120A48A089B479018A6469466E43E6F974E1345B32688D59D381E6BC18B6CA383235B708FE4FB44527C51E5B29BCDCC4A08C340A00A",
|
||||
"Account": "rn6CD8i3Yc3UGagSosZfegG7hpXxwgVAgz",
|
||||
"Counterparty": "rN2PMxegkEMZHin78o7wSs1JeYjxAvAfvt",
|
||||
"LoanOriginationFee": "100",
|
||||
"LoanServiceFee": "10",
|
||||
"PrincipalRequested": "1000",
|
||||
"CounterpartySignature": {
|
||||
"SigningPubKey": "EDE624A07899AEF826DF2A3E2A325F69BC1F169D23F08091E9042644D6B06D3D62",
|
||||
"TxnSignature": "52E16B88F5640F637A05E59AB2BE0DBFE4FBE7F1D7580C2A39D4981F6066A7C42047A401B953CDAB4993954A85D73DE35F69317EE8279D23ECB4958AA10C0800"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting signed LoanSet transaction ===
|
||||
|
||||
Loan created successfully!
|
||||
|
||||
=== Loan Information ===
|
||||
|
||||
{
|
||||
"Borrower": "rN2PMxegkEMZHin78o7wSs1JeYjxAvAfvt",
|
||||
"GracePeriod": 604800,
|
||||
"InterestRate": 500,
|
||||
"LoanBrokerID": "3CDEA7CEB9F2ECDD76CD41A864F4E3B5DB9C91AEDBD0906EE466FDD21CCF49B5",
|
||||
"LoanOriginationFee": "100",
|
||||
"LoanSequence": 6,
|
||||
"LoanServiceFee": "10",
|
||||
"NextPaymentDueDate": 826862960,
|
||||
"PaymentInterval": 2592000,
|
||||
"PaymentRemaining": 12,
|
||||
"PeriodicPayment": "83.55610375293148956",
|
||||
"PrincipalOutstanding": "1000",
|
||||
"StartDate": 824270960,
|
||||
"TotalValueOutstanding": "1003"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manage a Loan
|
||||
|
||||
```sh
|
||||
node loanManage.js
|
||||
```
|
||||
|
||||
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: rKL3u76wNGdF2Th4EvCuHV5885T6h2iFTY
|
||||
LoanID: D28764B238CF3F7D7BF4AFD07394838EDD5F278B838F97A55BEAEC1E5152719C
|
||||
|
||||
=== Loan Status ===
|
||||
|
||||
Total Amount Owed: 1001 TSTUSD.
|
||||
Payment Due Date: 2/25/2026, 11:58:20 PM
|
||||
|
||||
=== Preparing LoanManage transaction to impair loan ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanManage",
|
||||
"Account": "rKL3u76wNGdF2Th4EvCuHV5885T6h2iFTY",
|
||||
"LoanID": "D28764B238CF3F7D7BF4AFD07394838EDD5F278B838F97A55BEAEC1E5152719C",
|
||||
"Flags": 131072
|
||||
}
|
||||
|
||||
=== Submitting LoanManage impairment transaction ===
|
||||
|
||||
Loan impaired successfully!
|
||||
New Payment Due Date: 1/27/2026, 12:05:02 AM
|
||||
Grace Period: 60 seconds
|
||||
|
||||
=== Countdown until loan can be defaulted ===
|
||||
|
||||
Grace period expired. Loan can now be defaulted.
|
||||
|
||||
=== Preparing LoanManage transaction to default loan ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanManage",
|
||||
"Account": "rKL3u76wNGdF2Th4EvCuHV5885T6h2iFTY",
|
||||
"LoanID": "D28764B238CF3F7D7BF4AFD07394838EDD5F278B838F97A55BEAEC1E5152719C",
|
||||
"Flags": 65536
|
||||
}
|
||||
|
||||
=== Submitting LoanManage default transaction ===
|
||||
|
||||
Loan defaulted successfully!
|
||||
|
||||
=== Checking final loan status ===
|
||||
|
||||
Final loan flags (parsed): {"tfLoanDefault":true,"tfLoanImpair":true}
|
||||
```
|
||||
|
||||
## Pay a Loan
|
||||
|
||||
```sh
|
||||
node loanPay.js
|
||||
```
|
||||
|
||||
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: r46Ef5jjnaY7CDP7g22sQgSJJPQEBSmbWA
|
||||
LoanID: 8AC2B4425E604E7BB1082DD2BF2CA902B5087143B7775BE0A4DA954D3F52D06E
|
||||
MPT ID: 0031034FF84EB2E8348A34F0A8889A54F45F180E80F12341
|
||||
|
||||
=== Loan Status ===
|
||||
|
||||
Amount Owed: 1001 TSTUSD
|
||||
Loan Service Fee: 10 TSTUSD
|
||||
Total Payment Due (including fees): 1011 TSTUSD
|
||||
|
||||
=== Preparing LoanPay transaction ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanPay",
|
||||
"Account": "r46Ef5jjnaY7CDP7g22sQgSJJPQEBSmbWA",
|
||||
"LoanID": "8AC2B4425E604E7BB1082DD2BF2CA902B5087143B7775BE0A4DA954D3F52D06E",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0031034FF84EB2E8348A34F0A8889A54F45F180E80F12341",
|
||||
"value": "1011"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanPay transaction ===
|
||||
|
||||
Loan paid successfully!
|
||||
|
||||
=== Loan Status After Payment ===
|
||||
|
||||
Outstanding Loan Balance: Loan fully paid off!
|
||||
|
||||
=== Preparing LoanDelete transaction ===
|
||||
|
||||
{
|
||||
"TransactionType": "LoanDelete",
|
||||
"Account": "r46Ef5jjnaY7CDP7g22sQgSJJPQEBSmbWA",
|
||||
"LoanID": "8AC2B4425E604E7BB1082DD2BF2CA902B5087143B7775BE0A4DA954D3F52D06E"
|
||||
}
|
||||
|
||||
=== Submitting LoanDelete transaction ===
|
||||
|
||||
Loan deleted successfully!
|
||||
|
||||
=== Verifying Loan Deletion ===
|
||||
|
||||
Loan has been successfully removed from the XRP Ledger!
|
||||
```
|
||||
@@ -1,143 +0,0 @@
|
||||
// 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.
|
||||
|
||||
import fs from 'fs'
|
||||
import { execSync } from 'child_process'
|
||||
import xrpl from 'xrpl'
|
||||
|
||||
// Connect to the network ----------------------
|
||||
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
// If missing, lendingSetup.js will generate the data.
|
||||
if (!fs.existsSync('lendingSetup.json')) {
|
||||
console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`)
|
||||
execSync('node lendingSetup.js', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
// Load preconfigured accounts and LoanBrokerID.
|
||||
const setupData = JSON.parse(fs.readFileSync('lendingSetup.json', 'utf8'))
|
||||
|
||||
// You can replace these values with your own
|
||||
const loanBroker = xrpl.Wallet.fromSeed(setupData.loanBroker.seed)
|
||||
const mptIssuer = xrpl.Wallet.fromSeed(setupData.depositor.seed)
|
||||
const loanBrokerID = setupData.loanBrokerID
|
||||
const mptID = setupData.mptID
|
||||
|
||||
console.log(`\nLoan broker address: ${loanBroker.address}`)
|
||||
console.log(`MPT issuer address: ${mptIssuer.address}`)
|
||||
console.log(`LoanBrokerID: ${loanBrokerID}`)
|
||||
console.log(`MPT ID: ${mptID}`)
|
||||
|
||||
// Check cover available ----------------------
|
||||
console.log(`\n=== Cover Available ===\n`)
|
||||
const coverInfo = await client.request({
|
||||
command: 'ledger_entry',
|
||||
index: loanBrokerID,
|
||||
ledger_index: 'validated'
|
||||
})
|
||||
|
||||
let currentCoverAvailable = coverInfo.result.node.CoverAvailable || '0'
|
||||
console.log(`${currentCoverAvailable} TSTUSD`)
|
||||
|
||||
// Prepare LoanBrokerCoverDeposit transaction ----------------------
|
||||
console.log(`\n=== Preparing LoanBrokerCoverDeposit transaction ===\n`)
|
||||
const coverDepositTx = {
|
||||
TransactionType: 'LoanBrokerCoverDeposit',
|
||||
Account: loanBroker.address,
|
||||
LoanBrokerID: loanBrokerID,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptID,
|
||||
value: '1000'
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(coverDepositTx)
|
||||
console.log(JSON.stringify(coverDepositTx, null, 2))
|
||||
|
||||
// Sign, submit, and wait for deposit validation ----------------------
|
||||
console.log(`\n=== Submitting LoanBrokerCoverDeposit transaction ===\n`)
|
||||
const depositResponse = await client.submitAndWait(coverDepositTx, {
|
||||
wallet: loanBroker,
|
||||
autofill: true
|
||||
})
|
||||
|
||||
if (depositResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = depositResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to deposit cover:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Cover deposit successful!')
|
||||
|
||||
// Extract updated cover available after deposit ----------------------
|
||||
console.log(`\n=== Cover Available After Deposit ===\n`)
|
||||
let loanBrokerNode = depositResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.ModifiedNode?.LedgerEntryType === 'LoanBroker'
|
||||
)
|
||||
|
||||
currentCoverAvailable = loanBrokerNode.ModifiedNode.FinalFields.CoverAvailable
|
||||
console.log(`${currentCoverAvailable} TSTUSD`)
|
||||
|
||||
// Verify issuer of cover asset matches ----------------------
|
||||
// Only the issuer of the asset can submit clawback transactions.
|
||||
// The asset must also have clawback enabled.
|
||||
console.log(`\n=== Verifying Asset Issuer ===\n`)
|
||||
const assetIssuerInfo = await client.request({
|
||||
command: 'ledger_entry',
|
||||
mpt_issuance: mptID,
|
||||
ledger_index: 'validated'
|
||||
})
|
||||
|
||||
if (assetIssuerInfo.result.node.Issuer !== mptIssuer.address) {
|
||||
console.error(`Error: ${assetIssuerInfo.result.node.Issuer} does not match account (${mptIssuer.address}) attempting clawback!`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`MPT issuer account verified: ${mptIssuer.address}. Proceeding to clawback.`)
|
||||
|
||||
// Prepare LoanBrokerCoverClawback transaction ----------------------
|
||||
console.log(`\n=== Preparing LoanBrokerCoverClawback transaction ===\n`)
|
||||
const coverClawbackTx = {
|
||||
TransactionType: 'LoanBrokerCoverClawback',
|
||||
Account: mptIssuer.address,
|
||||
LoanBrokerID: loanBrokerID,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptID,
|
||||
value: currentCoverAvailable
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(coverClawbackTx)
|
||||
console.log(JSON.stringify(coverClawbackTx, null, 2))
|
||||
|
||||
// Sign, submit, and wait for clawback validation ----------------------
|
||||
console.log(`\n=== Submitting LoanBrokerCoverClawback transaction ===\n`)
|
||||
const clawbackResponse = await client.submitAndWait(coverClawbackTx, {
|
||||
wallet: mptIssuer,
|
||||
autofill: true
|
||||
})
|
||||
|
||||
if (clawbackResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = clawbackResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to clawback cover:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Successfully clawed back ${currentCoverAvailable} TSTUSD!`)
|
||||
|
||||
// Extract final cover available ----------------------
|
||||
console.log(`\n=== Final Cover Available After Clawback ===\n`)
|
||||
loanBrokerNode = clawbackResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.ModifiedNode?.LedgerEntryType === 'LoanBroker'
|
||||
)
|
||||
|
||||
console.log(`${loanBrokerNode.ModifiedNode.FinalFields.CoverAvailable || '0'} TSTUSD`)
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,108 +0,0 @@
|
||||
// IMPORTANT: This example deposits and withdraws first-loss capital from a
|
||||
// preconfigured LoanBroker entry.
|
||||
|
||||
import fs from 'fs'
|
||||
import { execSync } from 'child_process'
|
||||
import xrpl from 'xrpl'
|
||||
|
||||
// Connect to the network ----------------------
|
||||
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
// If missing, lendingSetup.js will generate the data.
|
||||
if (!fs.existsSync('lendingSetup.json')) {
|
||||
console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`)
|
||||
execSync('node lendingSetup.js', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
// Load preconfigured accounts and LoanBrokerID.
|
||||
const setupData = JSON.parse(fs.readFileSync('lendingSetup.json', 'utf8'))
|
||||
|
||||
// You can replace these values with your own
|
||||
const loanBroker = xrpl.Wallet.fromSeed(setupData.loanBroker.seed)
|
||||
const loanBrokerID = setupData.loanBrokerID
|
||||
const mptID = setupData.mptID
|
||||
|
||||
console.log(`\nLoan broker address: ${loanBroker.address}`)
|
||||
console.log(`LoanBrokerID: ${loanBrokerID}`)
|
||||
console.log(`MPT ID: ${mptID}`)
|
||||
|
||||
// Prepare LoanBrokerCoverDeposit transaction ----------------------
|
||||
console.log(`\n=== Preparing LoanBrokerCoverDeposit transaction ===\n`)
|
||||
const coverDepositTx = {
|
||||
TransactionType: 'LoanBrokerCoverDeposit',
|
||||
Account: loanBroker.address,
|
||||
LoanBrokerID: loanBrokerID,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptID,
|
||||
value: '2000'
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(coverDepositTx)
|
||||
console.log(JSON.stringify(coverDepositTx, null, 2))
|
||||
|
||||
// Sign, submit, and wait for deposit validation ----------------------
|
||||
console.log(`\n=== Submitting LoanBrokerCoverDeposit transaction ===\n`)
|
||||
const depositResponse = await client.submitAndWait(coverDepositTx, {
|
||||
wallet: loanBroker,
|
||||
autofill: true
|
||||
})
|
||||
if (depositResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = depositResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to deposit cover:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Cover deposit successful!')
|
||||
|
||||
// Extract cover balance from the transaction result
|
||||
console.log(`\n=== Cover Balance ===\n`)
|
||||
let loanBrokerNode = depositResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.ModifiedNode?.LedgerEntryType === 'LoanBroker'
|
||||
)
|
||||
// First-loss capital is stored in the LoanBroker's pseudo-account.
|
||||
console.log(`LoanBroker Pseudo-Account: ${loanBrokerNode.ModifiedNode.FinalFields.Account}`)
|
||||
console.log(`Cover balance after deposit: ${loanBrokerNode.ModifiedNode.FinalFields.CoverAvailable} TSTUSD`)
|
||||
|
||||
// Prepare LoanBrokerCoverWithdraw transaction ----------------------
|
||||
console.log(`\n=== Preparing LoanBrokerCoverWithdraw transaction ===\n`)
|
||||
const coverWithdrawTx = {
|
||||
TransactionType: 'LoanBrokerCoverWithdraw',
|
||||
Account: loanBroker.address,
|
||||
LoanBrokerID: loanBrokerID,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptID,
|
||||
value: '1000'
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(coverWithdrawTx)
|
||||
console.log(JSON.stringify(coverWithdrawTx, null, 2))
|
||||
|
||||
// Sign, submit, and wait for withdraw validation ----------------------
|
||||
console.log(`\n=== Submitting LoanBrokerCoverWithdraw transaction ===\n`)
|
||||
const withdrawResponse = await client.submitAndWait(coverWithdrawTx, {
|
||||
wallet: loanBroker,
|
||||
autofill: true
|
||||
})
|
||||
if (withdrawResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = withdrawResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to withdraw cover:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Cover withdraw successful!')
|
||||
|
||||
// Extract updated cover balance from the transaction result
|
||||
console.log(`\n=== Updated Cover Balance ===\n`)
|
||||
loanBrokerNode = withdrawResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.ModifiedNode?.LedgerEntryType === 'LoanBroker'
|
||||
)
|
||||
console.log(`LoanBroker Pseudo-Account: ${loanBrokerNode.ModifiedNode.FinalFields.Account}`)
|
||||
console.log(`Cover balance after withdraw: ${loanBrokerNode.ModifiedNode.FinalFields.CoverAvailable} TSTUSD`)
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,95 +0,0 @@
|
||||
// IMPORTANT: This example creates a loan using a preconfigured
|
||||
// loan broker, borrower, and private vault.
|
||||
|
||||
import fs from 'fs'
|
||||
import { execSync } from 'child_process'
|
||||
import xrpl from 'xrpl'
|
||||
|
||||
// Connect to the network ----------------------
|
||||
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
// If missing, lendingSetup.js will generate the data.
|
||||
if (!fs.existsSync('lendingSetup.json')) {
|
||||
console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`)
|
||||
execSync('node lendingSetup.js', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
// Load preconfigured accounts and LoanBrokerID.
|
||||
const setupData = JSON.parse(fs.readFileSync('lendingSetup.json', 'utf8'))
|
||||
|
||||
// You can replace these values with your own
|
||||
const loanBroker = xrpl.Wallet.fromSeed(setupData.loanBroker.seed)
|
||||
const borrower = xrpl.Wallet.fromSeed(setupData.borrower.seed)
|
||||
const loanBrokerID = setupData.loanBrokerID
|
||||
|
||||
console.log(`\nLoan broker address: ${loanBroker.address}`)
|
||||
console.log(`Borrower address: ${borrower.address}`)
|
||||
console.log(`LoanBrokerID: ${loanBrokerID}`)
|
||||
|
||||
// Prepare LoanSet transaction ----------------------
|
||||
// Account and Counterparty accounts can be swapped, but determines signing order.
|
||||
// Account signs first, Counterparty signs second.
|
||||
console.log(`\n=== Preparing LoanSet transaction ===\n`)
|
||||
|
||||
// Suppress unnecessary console warning from autofilling LoanSet.
|
||||
console.warn = () => {}
|
||||
|
||||
const loanSetTx = await client.autofill({
|
||||
TransactionType: 'LoanSet',
|
||||
Account: loanBroker.address,
|
||||
Counterparty: borrower.address,
|
||||
LoanBrokerID: loanBrokerID,
|
||||
PrincipalRequested: '1000',
|
||||
InterestRate: 500,
|
||||
PaymentTotal: 12,
|
||||
PaymentInterval: 2592000,
|
||||
GracePeriod: 604800,
|
||||
LoanOriginationFee: '100',
|
||||
LoanServiceFee: '10'
|
||||
})
|
||||
|
||||
console.log(JSON.stringify(loanSetTx, null, 2))
|
||||
|
||||
// Loan broker signs first
|
||||
console.log(`\n=== Adding loan broker signature ===\n`)
|
||||
const loanBrokerSigned = loanBroker.sign(loanSetTx)
|
||||
const loanBrokerSignedTx = xrpl.decode(loanBrokerSigned.tx_blob)
|
||||
|
||||
console.log(`TxnSignature: ${loanBrokerSignedTx.TxnSignature}`)
|
||||
console.log(`SigningPubKey: ${loanBrokerSignedTx.SigningPubKey}\n`)
|
||||
console.log(`Signed loanSetTx for borrower to sign over:\n${JSON.stringify(loanBrokerSignedTx, null, 2)}`)
|
||||
|
||||
// Borrower signs second
|
||||
console.log(`\n=== Adding borrower signature ===\n`)
|
||||
const fullySigned = xrpl.signLoanSetByCounterparty(borrower, loanBrokerSignedTx)
|
||||
|
||||
console.log(`Borrower TxnSignature: ${fullySigned.tx.CounterpartySignature.TxnSignature}`)
|
||||
console.log(`Borrower SigningPubKey: ${fullySigned.tx.CounterpartySignature.SigningPubKey}`)
|
||||
|
||||
// Validate the transaction structure before submitting.
|
||||
xrpl.validate(fullySigned.tx)
|
||||
console.log(`\nFully signed LoanSet transaction:\n${JSON.stringify(fullySigned.tx, null, 2)}`)
|
||||
|
||||
// Submit and wait for validation ----------------------
|
||||
console.log(`\n=== Submitting signed LoanSet transaction ===\n`)
|
||||
|
||||
const submitResponse = await client.submitAndWait(fullySigned.tx)
|
||||
|
||||
if (submitResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = submitResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to create loan:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Loan created successfully!')
|
||||
|
||||
// Extract loan information from the transaction result.
|
||||
console.log(`\n=== Loan Information ===\n`)
|
||||
const loanNode = submitResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.CreatedNode?.LedgerEntryType === 'Loan'
|
||||
)
|
||||
console.log(JSON.stringify(loanNode.CreatedNode.NewFields, null, 2))
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,66 +0,0 @@
|
||||
// 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.
|
||||
|
||||
import fs from 'fs'
|
||||
import { execSync } from 'child_process'
|
||||
import xrpl from 'xrpl'
|
||||
|
||||
// Connect to the network ----------------------
|
||||
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
// If missing, lendingSetup.js will generate the data.
|
||||
if (!fs.existsSync('lendingSetup.json')) {
|
||||
console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`)
|
||||
execSync('node lendingSetup.js', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
// Load preconfigured accounts and VaultID.
|
||||
const setupData = JSON.parse(fs.readFileSync('lendingSetup.json', 'utf8'))
|
||||
|
||||
// You can replace these values with your own
|
||||
const loanBroker = xrpl.Wallet.fromSeed(setupData.loanBroker.seed)
|
||||
const vaultID = setupData.vaultID
|
||||
|
||||
console.log(`\nLoan broker/vault owner address: ${loanBroker.address}`)
|
||||
console.log(`Vault ID: ${vaultID}`)
|
||||
|
||||
// Prepare LoanBrokerSet transaction ----------------------
|
||||
console.log(`\n=== Preparing LoanBrokerSet transaction ===\n`)
|
||||
const loanBrokerSetTx = {
|
||||
TransactionType: 'LoanBrokerSet',
|
||||
Account: loanBroker.address,
|
||||
VaultID: vaultID,
|
||||
ManagementFeeRate: 1000
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(loanBrokerSetTx)
|
||||
console.log(JSON.stringify(loanBrokerSetTx, null, 2))
|
||||
|
||||
// Submit, sign, and wait for validation ----------------------
|
||||
console.log(`\n=== Submitting LoanBrokerSet transaction ===\n`)
|
||||
const submitResponse = await client.submitAndWait(loanBrokerSetTx, {
|
||||
wallet: loanBroker,
|
||||
autofill: true
|
||||
})
|
||||
if (submitResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = submitResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to create loan broker:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Loan broker created successfully!')
|
||||
|
||||
// Extract loan broker information from the transaction result
|
||||
console.log(`\n=== Loan Broker Information ===\n`)
|
||||
const loanBrokerNode = submitResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.CreatedNode?.LedgerEntryType === 'LoanBroker'
|
||||
)
|
||||
console.log(`LoanBroker ID: ${loanBrokerNode.CreatedNode.LedgerIndex}`)
|
||||
console.log(`LoanBroker Psuedo-Account Address: ${loanBrokerNode.CreatedNode.NewFields.Account}`)
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,337 +0,0 @@
|
||||
// Setup script for lending protocol tutorials
|
||||
|
||||
import xrpl from 'xrpl'
|
||||
import fs from 'fs'
|
||||
|
||||
process.stdout.write('Setting up tutorial: 0/6\r')
|
||||
|
||||
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// Create and fund wallets
|
||||
const [
|
||||
{ wallet: loanBroker },
|
||||
{ wallet: borrower },
|
||||
{ wallet: depositor },
|
||||
{ wallet: credentialIssuer }
|
||||
] = await Promise.all([
|
||||
client.fundWallet(),
|
||||
client.fundWallet(),
|
||||
client.fundWallet(),
|
||||
client.fundWallet()
|
||||
])
|
||||
|
||||
process.stdout.write('Setting up tutorial: 1/6\r')
|
||||
|
||||
// Issue MPT with depositor
|
||||
// Create tickets for later use with loanBroker
|
||||
// Set up credentials and domain with credentialIssuer
|
||||
const credentialType = xrpl.convertStringToHex('KYC-Verified')
|
||||
const mptData = {
|
||||
ticker: 'TSTUSD',
|
||||
name: 'Test USD MPT',
|
||||
desc: 'A sample non-yield-bearing stablecoin backed by U.S. Treasuries.',
|
||||
icon: 'https://example.org/tstusd-icon.png',
|
||||
asset_class: 'rwa',
|
||||
asset_subclass: 'stablecoin',
|
||||
issuer_name: 'Example Treasury Reserve Co.',
|
||||
uris: [
|
||||
{
|
||||
uri: 'https://exampletreasury.com/tstusd',
|
||||
category: 'website',
|
||||
title: 'Product Page'
|
||||
},
|
||||
{
|
||||
uri: 'https://exampletreasury.com/tstusd/reserve',
|
||||
category: 'docs',
|
||||
title: 'Reserve Attestation'
|
||||
}
|
||||
],
|
||||
additional_info: {
|
||||
reserve_type: 'U.S. Treasury Bills',
|
||||
custody_provider: 'Example Custodian Bank',
|
||||
audit_frequency: 'Monthly',
|
||||
last_audit_date: '2026-01-15',
|
||||
pegged_currency: 'USD'
|
||||
}
|
||||
}
|
||||
|
||||
const [ticketCreateResponse, mptIssuanceResponse] = await Promise.all([
|
||||
client.submitAndWait({
|
||||
TransactionType: 'TicketCreate',
|
||||
Account: loanBroker.address,
|
||||
TicketCount: 2
|
||||
}, { wallet: loanBroker, autofill: true }),
|
||||
client.submitAndWait({
|
||||
TransactionType: 'MPTokenIssuanceCreate',
|
||||
Account: depositor.address,
|
||||
MaximumAmount: '100000000',
|
||||
TransferFee: 0,
|
||||
Flags:
|
||||
xrpl.MPTokenIssuanceCreateFlags.tfMPTCanTransfer |
|
||||
xrpl.MPTokenIssuanceCreateFlags.tfMPTCanClawback |
|
||||
xrpl.MPTokenIssuanceCreateFlags.tfMPTCanTrade,
|
||||
MPTokenMetadata: xrpl.encodeMPTokenMetadata(mptData)
|
||||
}, { wallet: depositor, autofill: true }),
|
||||
client.submitAndWait({
|
||||
TransactionType: 'Batch',
|
||||
Account: credentialIssuer.address,
|
||||
Flags: xrpl.BatchFlags.tfAllOrNothing,
|
||||
RawTransactions: [
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'CredentialCreate',
|
||||
Account: credentialIssuer.address,
|
||||
Subject: loanBroker.address,
|
||||
CredentialType: credentialType,
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
},
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'CredentialCreate',
|
||||
Account: credentialIssuer.address,
|
||||
Subject: borrower.address,
|
||||
CredentialType: credentialType,
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
},
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'CredentialCreate',
|
||||
Account: credentialIssuer.address,
|
||||
Subject: depositor.address,
|
||||
CredentialType: credentialType,
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
},
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'PermissionedDomainSet',
|
||||
Account: credentialIssuer.address,
|
||||
AcceptedCredentials: [
|
||||
{
|
||||
Credential: {
|
||||
Issuer: credentialIssuer.address,
|
||||
CredentialType: credentialType
|
||||
}
|
||||
}
|
||||
],
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
}
|
||||
]
|
||||
}, { wallet: credentialIssuer, autofill: true })
|
||||
])
|
||||
|
||||
// Extract ticket sequence numbers
|
||||
const tickets = ticketCreateResponse.result.meta.AffectedNodes
|
||||
.filter(node => node.CreatedNode?.LedgerEntryType === 'Ticket')
|
||||
.map(node => node.CreatedNode.NewFields.TicketSequence)
|
||||
|
||||
// Extract MPT issuance ID
|
||||
const mptID = mptIssuanceResponse.result.meta.mpt_issuance_id
|
||||
|
||||
// Get domain ID
|
||||
const credentialIssuerObjects = await client.request({
|
||||
command: 'account_objects',
|
||||
account: credentialIssuer.address,
|
||||
ledger_index: 'validated'
|
||||
})
|
||||
const domainID = credentialIssuerObjects.result.account_objects.find(node =>
|
||||
node.LedgerEntryType === 'PermissionedDomain'
|
||||
).index
|
||||
|
||||
process.stdout.write('Setting up tutorial: 2/6\r')
|
||||
|
||||
// Accept credentials and authorize MPT for each account
|
||||
await Promise.all([
|
||||
...([loanBroker, borrower].map(wallet =>
|
||||
client.submitAndWait({
|
||||
TransactionType: 'Batch',
|
||||
Account: wallet.address,
|
||||
Flags: xrpl.BatchFlags.tfAllOrNothing,
|
||||
RawTransactions: [
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'CredentialAccept',
|
||||
Account: wallet.address,
|
||||
Issuer: credentialIssuer.address,
|
||||
CredentialType: credentialType,
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
},
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'MPTokenAuthorize',
|
||||
Account: wallet.address,
|
||||
MPTokenIssuanceID: mptID,
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
}
|
||||
]
|
||||
}, { wallet, autofill: true })
|
||||
)),
|
||||
// Depositor only needs to accept credentials
|
||||
client.submitAndWait({
|
||||
TransactionType: 'CredentialAccept',
|
||||
Account: depositor.address,
|
||||
Issuer: credentialIssuer.address,
|
||||
CredentialType: credentialType
|
||||
}, { wallet: depositor, autofill: true })
|
||||
])
|
||||
|
||||
process.stdout.write('Setting up tutorial: 3/6\r')
|
||||
|
||||
// Create private vault and distribute MPT to accounts
|
||||
const [vaultCreateResponse] = await Promise.all([
|
||||
client.submitAndWait({
|
||||
TransactionType: 'VaultCreate',
|
||||
Account: loanBroker.address,
|
||||
Asset: {
|
||||
mpt_issuance_id: mptID
|
||||
},
|
||||
Flags: xrpl.VaultCreateFlags.tfVaultPrivate,
|
||||
DomainID: domainID
|
||||
}, { wallet: loanBroker, autofill: true }),
|
||||
client.submitAndWait({
|
||||
TransactionType: 'Batch',
|
||||
Account: depositor.address,
|
||||
Flags: xrpl.BatchFlags.tfAllOrNothing,
|
||||
RawTransactions: [
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'Payment',
|
||||
Account: depositor.address,
|
||||
Destination: loanBroker.address,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptID,
|
||||
value: '5000'
|
||||
},
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
},
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'Payment',
|
||||
Account: depositor.address,
|
||||
Destination: borrower.address,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptID,
|
||||
value: '2500'
|
||||
},
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
}
|
||||
]
|
||||
}, { wallet: depositor, autofill: true })
|
||||
])
|
||||
|
||||
const vaultID = vaultCreateResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.CreatedNode?.LedgerEntryType === 'Vault'
|
||||
).CreatedNode.LedgerIndex
|
||||
|
||||
process.stdout.write('Setting up tutorial: 4/6\r')
|
||||
|
||||
// Create LoanBroker and deposit MPT into vault
|
||||
const [loanBrokerSetResponse] = await Promise.all([
|
||||
client.submitAndWait({
|
||||
TransactionType: 'LoanBrokerSet',
|
||||
Account: loanBroker.address,
|
||||
VaultID: vaultID
|
||||
}, { wallet: loanBroker, autofill: true }),
|
||||
client.submitAndWait({
|
||||
TransactionType: 'VaultDeposit',
|
||||
Account: depositor.address,
|
||||
VaultID: vaultID,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptID,
|
||||
value: '50000000'
|
||||
}
|
||||
}, { wallet: depositor, autofill: true })
|
||||
])
|
||||
|
||||
const loanBrokerID = loanBrokerSetResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.CreatedNode?.LedgerEntryType === 'LoanBroker'
|
||||
).CreatedNode.LedgerIndex
|
||||
|
||||
process.stdout.write('Setting up tutorial: 5/6\r')
|
||||
|
||||
// Create 2 identical loans with complete repayment due in 30 days
|
||||
|
||||
// Suppress unnecessary console warning from autofilling LoanSet.
|
||||
console.warn = () => {}
|
||||
|
||||
// Helper function to create, sign, and submit a LoanSet transaction
|
||||
async function createLoan (ticketSequence) {
|
||||
const loanSetTx = await client.autofill({
|
||||
TransactionType: 'LoanSet',
|
||||
Account: loanBroker.address,
|
||||
Counterparty: borrower.address,
|
||||
LoanBrokerID: loanBrokerID,
|
||||
PrincipalRequested: '1000',
|
||||
InterestRate: 500,
|
||||
PaymentTotal: 1,
|
||||
PaymentInterval: 2592000,
|
||||
LoanOriginationFee: '100',
|
||||
LoanServiceFee: '10',
|
||||
Sequence: 0,
|
||||
TicketSequence: ticketSequence
|
||||
})
|
||||
|
||||
const loanBrokerSigned = loanBroker.sign(loanSetTx)
|
||||
const loanBrokerSignedTx = xrpl.decode(loanBrokerSigned.tx_blob)
|
||||
|
||||
const fullySigned = xrpl.signLoanSetByCounterparty(borrower, loanBrokerSignedTx)
|
||||
const submitResponse = await client.submitAndWait(fullySigned.tx)
|
||||
|
||||
return submitResponse
|
||||
}
|
||||
|
||||
const [submitResponse1, submitResponse2] = await Promise.all([
|
||||
createLoan(tickets[0]),
|
||||
createLoan(tickets[1])
|
||||
])
|
||||
|
||||
const loanID1 = submitResponse1.result.meta.AffectedNodes.find(node =>
|
||||
node.CreatedNode?.LedgerEntryType === 'Loan'
|
||||
).CreatedNode.LedgerIndex
|
||||
|
||||
const loanID2 = submitResponse2.result.meta.AffectedNodes.find(node =>
|
||||
node.CreatedNode?.LedgerEntryType === 'Loan'
|
||||
).CreatedNode.LedgerIndex
|
||||
|
||||
process.stdout.write('Setting up tutorial: 6/6\r')
|
||||
|
||||
// Write setup data to JSON file
|
||||
const setupData = {
|
||||
description: 'This file is auto-generated by lendingSetup.js. It stores XRPL account info for use in lending protocol tutorials.',
|
||||
loanBroker: {
|
||||
address: loanBroker.address,
|
||||
seed: loanBroker.seed
|
||||
},
|
||||
borrower: {
|
||||
address: borrower.address,
|
||||
seed: borrower.seed
|
||||
},
|
||||
depositor: {
|
||||
address: depositor.address,
|
||||
seed: depositor.seed
|
||||
},
|
||||
credentialIssuer: {
|
||||
address: credentialIssuer.address,
|
||||
seed: credentialIssuer.seed
|
||||
},
|
||||
domainID,
|
||||
mptID,
|
||||
vaultID,
|
||||
loanBrokerID,
|
||||
loanID1,
|
||||
loanID2
|
||||
}
|
||||
|
||||
fs.writeFileSync('lendingSetup.json', JSON.stringify(setupData, null, 2))
|
||||
|
||||
process.stdout.write('Setting up tutorial: Complete!\n')
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,144 +0,0 @@
|
||||
// IMPORTANT: This example impairs an existing loan, which has a 60 second grace period.
|
||||
// After the 60 seconds pass, this example defaults the loan.
|
||||
|
||||
import fs from 'fs'
|
||||
import { execSync } from 'child_process'
|
||||
import xrpl from 'xrpl'
|
||||
|
||||
// Connect to the network ----------------------
|
||||
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
// If missing, lendingSetup.js will generate the data.
|
||||
if (!fs.existsSync('lendingSetup.json')) {
|
||||
console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`)
|
||||
execSync('node lendingSetup.js', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
// Load preconfigured accounts and LoanID.
|
||||
const setupData = JSON.parse(fs.readFileSync('lendingSetup.json', 'utf8'))
|
||||
|
||||
// You can replace these values with your own
|
||||
const loanBroker = xrpl.Wallet.fromSeed(setupData.loanBroker.seed)
|
||||
const loanID = setupData.loanID1
|
||||
|
||||
console.log(`\nLoan broker address: ${loanBroker.address}`)
|
||||
console.log(`LoanID: ${loanID}`)
|
||||
|
||||
// Check loan status before impairment ----------------------
|
||||
console.log(`\n=== Loan Status ===\n`)
|
||||
const loanStatus = await client.request({
|
||||
command: 'ledger_entry',
|
||||
index: loanID,
|
||||
ledger_index: 'validated'
|
||||
})
|
||||
|
||||
console.log(`Total Amount Owed: ${loanStatus.result.node.TotalValueOutstanding} TSTUSD.`)
|
||||
// Convert Ripple Epoch timestamp to local date and time
|
||||
let nextPaymentDueDate = loanStatus.result.node.NextPaymentDueDate
|
||||
let paymentDue = new Date((nextPaymentDueDate + 946684800) * 1000)
|
||||
console.log(`Payment Due Date: ${paymentDue.toLocaleString()}`)
|
||||
|
||||
// Prepare LoanManage transaction to impair the loan ----------------------
|
||||
console.log(`\n=== Preparing LoanManage transaction to impair loan ===\n`)
|
||||
const loanManageImpair = {
|
||||
TransactionType: 'LoanManage',
|
||||
Account: loanBroker.address,
|
||||
LoanID: loanID,
|
||||
Flags: xrpl.LoanManageFlags.tfLoanImpair
|
||||
}
|
||||
|
||||
// Validate the impairment transaction before submitting
|
||||
xrpl.validate(loanManageImpair)
|
||||
console.log(JSON.stringify(loanManageImpair, null, 2))
|
||||
|
||||
// Sign, submit, and wait for impairment validation ----------------------
|
||||
console.log(`\n=== Submitting LoanManage impairment transaction ===\n`)
|
||||
const impairResponse = await client.submitAndWait(loanManageImpair, {
|
||||
wallet: loanBroker,
|
||||
autofill: true
|
||||
})
|
||||
|
||||
if (impairResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = impairResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to impair loan:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Loan impaired successfully!')
|
||||
|
||||
// Extract loan impairment info from transaction results ----------------------
|
||||
let loanNode = impairResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.ModifiedNode?.LedgerEntryType === 'Loan'
|
||||
)
|
||||
|
||||
// Check grace period and next payment due date
|
||||
const gracePeriod = loanNode.ModifiedNode.FinalFields.GracePeriod
|
||||
nextPaymentDueDate = loanNode.ModifiedNode.FinalFields.NextPaymentDueDate
|
||||
const defaultTime = nextPaymentDueDate + gracePeriod
|
||||
paymentDue = new Date((nextPaymentDueDate + 946684800) * 1000)
|
||||
|
||||
console.log(`New Payment Due Date: ${paymentDue.toLocaleString()}`)
|
||||
console.log(`Grace Period: ${gracePeriod} seconds`)
|
||||
|
||||
// Convert current time to Ripple Epoch timestamp
|
||||
const currentTime = Math.floor(Date.now() / 1000) - 946684800
|
||||
let secondsUntilDefault = defaultTime - currentTime
|
||||
|
||||
// Countdown until loan can be defaulted ----------------------
|
||||
console.log(`\n=== Countdown until loan can be defaulted ===\n`)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const countdown = setInterval(() => {
|
||||
if (secondsUntilDefault <= 0) {
|
||||
clearInterval(countdown)
|
||||
process.stdout.write('\rGrace period expired. Loan can now be defaulted.\n')
|
||||
resolve()
|
||||
} else {
|
||||
process.stdout.write(`\r${secondsUntilDefault} seconds...`)
|
||||
secondsUntilDefault--
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
// Prepare LoanManage transaction to default the loan ----------------------
|
||||
console.log(`\n=== Preparing LoanManage transaction to default loan ===\n`)
|
||||
const loanManageDefault = {
|
||||
TransactionType: 'LoanManage',
|
||||
Account: loanBroker.address,
|
||||
LoanID: loanID,
|
||||
Flags: xrpl.LoanManageFlags.tfLoanDefault
|
||||
}
|
||||
|
||||
// Validate the default transaction before submitting
|
||||
xrpl.validate(loanManageDefault)
|
||||
console.log(JSON.stringify(loanManageDefault, null, 2))
|
||||
|
||||
// Sign, submit, and wait for default validation ----------------------
|
||||
console.log(`\n=== Submitting LoanManage default transaction ===\n`)
|
||||
const defaultResponse = await client.submitAndWait(loanManageDefault, {
|
||||
wallet: loanBroker,
|
||||
autofill: true
|
||||
})
|
||||
|
||||
if (defaultResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = defaultResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to default loan:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Loan defaulted successfully!')
|
||||
|
||||
// Verify loan default status from transaction results ----------------------
|
||||
console.log(`\n=== Checking final loan status ===\n`)
|
||||
loanNode = defaultResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.ModifiedNode?.LedgerEntryType === 'Loan'
|
||||
)
|
||||
const loanFlags = loanNode.ModifiedNode.FinalFields.Flags
|
||||
console.log(`Final loan flags (parsed): ${JSON.stringify(xrpl.parseTransactionFlags({
|
||||
TransactionType: 'LoanManage',
|
||||
Flags: loanFlags
|
||||
}))}`)
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,134 +0,0 @@
|
||||
// IMPORTANT: This example pays off an existing loan and then deletes it.
|
||||
|
||||
import fs from 'fs'
|
||||
import { execSync } from 'child_process'
|
||||
import xrpl from 'xrpl'
|
||||
|
||||
// Connect to the network ----------------------
|
||||
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
// If missing, lendingSetup.js will generate the data.
|
||||
if (!fs.existsSync('lendingSetup.json')) {
|
||||
console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`)
|
||||
execSync('node lendingSetup.js', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
// Load preconfigured accounts and LoanID.
|
||||
const setupData = JSON.parse(fs.readFileSync('lendingSetup.json', 'utf8'))
|
||||
|
||||
// You can replace these values with your own
|
||||
const borrower = xrpl.Wallet.fromSeed(setupData.borrower.seed)
|
||||
const loanID = setupData.loanID2
|
||||
const mptID = setupData.mptID
|
||||
|
||||
console.log(`\nBorrower address: ${borrower.address}`)
|
||||
console.log(`LoanID: ${loanID}`)
|
||||
console.log(`MPT ID: ${mptID}`)
|
||||
|
||||
// Check initial loan status ----------------------
|
||||
console.log(`\n=== Loan Status ===\n`)
|
||||
const loanStatus = await client.request({
|
||||
command: 'ledger_entry',
|
||||
index: loanID,
|
||||
ledger_index: 'validated'
|
||||
})
|
||||
|
||||
const totalValueOutstanding = loanStatus.result.node.TotalValueOutstanding
|
||||
const loanServiceFee = loanStatus.result.node.LoanServiceFee
|
||||
const totalPayment = (BigInt(totalValueOutstanding) + BigInt(loanServiceFee)).toString()
|
||||
|
||||
console.log(`Amount Owed: ${totalValueOutstanding} TSTUSD`)
|
||||
console.log(`Loan Service Fee: ${loanServiceFee} TSTUSD`)
|
||||
console.log(`Total Payment Due (including fees): ${totalPayment} TSTUSD`)
|
||||
|
||||
// Prepare LoanPay transaction ----------------------
|
||||
console.log(`\n=== Preparing LoanPay transaction ===\n`)
|
||||
|
||||
const loanPayTx = {
|
||||
TransactionType: 'LoanPay',
|
||||
Account: borrower.address,
|
||||
LoanID: loanID,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptID,
|
||||
value: totalPayment
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(loanPayTx)
|
||||
console.log(JSON.stringify(loanPayTx, null, 2))
|
||||
|
||||
// Sign, submit, and wait for payment validation ----------------------
|
||||
console.log(`\n=== Submitting LoanPay transaction ===\n`)
|
||||
const payResponse = await client.submitAndWait(loanPayTx, {
|
||||
wallet: borrower,
|
||||
autofill: true
|
||||
})
|
||||
|
||||
if (payResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = payResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to pay loan:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Loan paid successfully!')
|
||||
|
||||
// Extract updated loan info from transaction results ----------------------
|
||||
console.log(`\n=== Loan Status After Payment ===\n`)
|
||||
const loanNode = payResponse.result.meta.AffectedNodes.find(node =>
|
||||
node.ModifiedNode?.LedgerEntryType === 'Loan'
|
||||
)
|
||||
|
||||
const finalBalance = loanNode.ModifiedNode.FinalFields.TotalValueOutstanding
|
||||
? `${loanNode.ModifiedNode.FinalFields.TotalValueOutstanding} TSTUSD`
|
||||
: 'Loan fully paid off!'
|
||||
console.log(`Outstanding Loan Balance: ${finalBalance}`)
|
||||
|
||||
// Prepare LoanDelete transaction ----------------------
|
||||
// Either the loan broker or borrower can submit this transaction.
|
||||
console.log(`\n=== Preparing LoanDelete transaction ===\n`)
|
||||
const loanDeleteTx = {
|
||||
TransactionType: 'LoanDelete',
|
||||
Account: borrower.address,
|
||||
LoanID: loanID
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(loanDeleteTx)
|
||||
console.log(JSON.stringify(loanDeleteTx, null, 2))
|
||||
|
||||
// Sign, submit, and wait for deletion validation ----------------------
|
||||
console.log(`\n=== Submitting LoanDelete transaction ===\n`)
|
||||
const deleteResponse = await client.submitAndWait(loanDeleteTx, {
|
||||
wallet: borrower,
|
||||
autofill: true
|
||||
})
|
||||
|
||||
if (deleteResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
const resultCode = deleteResponse.result.meta.TransactionResult
|
||||
console.error('Error: Unable to delete loan:', resultCode)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Loan deleted successfully!')
|
||||
|
||||
// Verify loan deletion ----------------------
|
||||
console.log(`\n=== Verifying Loan Deletion ===\n`)
|
||||
try {
|
||||
await client.request({
|
||||
command: 'ledger_entry',
|
||||
index: loanID,
|
||||
ledger_index: 'validated'
|
||||
})
|
||||
console.log('Warning: Loan still exists in the ledger.')
|
||||
} catch (error) {
|
||||
if (error.data.error === 'entryNotFound') {
|
||||
console.log('Loan has been successfully removed from the XRP Ledger!')
|
||||
} else {
|
||||
console.error('Error checking loan status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "lending-protocol-examples",
|
||||
"description": "Example code for creating and managing loans.",
|
||||
"dependencies": {
|
||||
"xrpl": "^4.6.0"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
# Lending Protocol Examples (Python)
|
||||
|
||||
This directory contains Python 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
|
||||
|
||||
Install dependencies before running any examples:
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create a Loan Broker
|
||||
|
||||
```sh
|
||||
python3 create_loan_broker.py
|
||||
```
|
||||
|
||||
The script should output the LoanBrokerSet transaction, loan broker ID, and loan broker pseudo-account:
|
||||
|
||||
```sh
|
||||
Loan broker/vault owner address: rBeEX3qQzP3UL5WMwZAzdPPpzckH73YvBn
|
||||
Vault ID: 2B71E8E1323BFC8F2AC27F8C217870B63921EFA0C02DF7BA8B099C7DC6A1D00F
|
||||
|
||||
=== Preparing LoanBrokerSet transaction ===
|
||||
|
||||
{
|
||||
"Account": "rBeEX3qQzP3UL5WMwZAzdPPpzckH73YvBn",
|
||||
"TransactionType": "LoanBrokerSet",
|
||||
"SigningPubKey": "",
|
||||
"VaultID": "2B71E8E1323BFC8F2AC27F8C217870B63921EFA0C02DF7BA8B099C7DC6A1D00F",
|
||||
"ManagementFeeRate": 1000
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerSet transaction ===
|
||||
|
||||
Loan broker created successfully!
|
||||
|
||||
=== Loan Broker Information ===
|
||||
|
||||
LoanBroker ID: 86911896026EA9DEAEFC1A7959BC05D8B1A1EC25B9960E8C54424B7DC41F8DA8
|
||||
LoanBroker Psuedo-Account Address: rPhpC2XGz7v5g2rPom7JSWJcic1cnkoBh9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Claw Back First-loss Capital
|
||||
|
||||
```sh
|
||||
python3 cover_clawback.py
|
||||
```
|
||||
|
||||
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: rBeEX3qQzP3UL5WMwZAzdPPpzckH73YvBn
|
||||
MPT issuer address: rNzJg2EVwo56eAoBxz5WnTfmgoLbfaAT8d
|
||||
LoanBrokerID: 041E256F124841FF81DF105C62A72676BFD746975F86786166B689F304BE96E0
|
||||
MPT ID: 0037A8ED99701AFEC4BCC3A39299252CA41838059572E7F2
|
||||
|
||||
=== Cover Available ===
|
||||
|
||||
1000 TSTUSD
|
||||
|
||||
=== Preparing LoanBrokerCoverDeposit transaction ===
|
||||
|
||||
{
|
||||
"Account": "rBeEX3qQzP3UL5WMwZAzdPPpzckH73YvBn",
|
||||
"TransactionType": "LoanBrokerCoverDeposit",
|
||||
"SigningPubKey": "",
|
||||
"LoanBrokerID": "041E256F124841FF81DF105C62A72676BFD746975F86786166B689F304BE96E0",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0037A8ED99701AFEC4BCC3A39299252CA41838059572E7F2",
|
||||
"value": "1000"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerCoverDeposit transaction ===
|
||||
|
||||
Cover deposit successful!
|
||||
|
||||
=== Cover Available After Deposit ===
|
||||
|
||||
2000 TSTUSD
|
||||
|
||||
=== Verifying Asset Issuer ===
|
||||
|
||||
MPT issuer account verified: rNzJg2EVwo56eAoBxz5WnTfmgoLbfaAT8d. Proceeding to clawback.
|
||||
|
||||
=== Preparing LoanBrokerCoverClawback transaction ===
|
||||
|
||||
{
|
||||
"Account": "rNzJg2EVwo56eAoBxz5WnTfmgoLbfaAT8d",
|
||||
"TransactionType": "LoanBrokerCoverClawback",
|
||||
"SigningPubKey": "",
|
||||
"LoanBrokerID": "041E256F124841FF81DF105C62A72676BFD746975F86786166B689F304BE96E0",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0037A8ED99701AFEC4BCC3A39299252CA41838059572E7F2",
|
||||
"value": "2000"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerCoverClawback transaction ===
|
||||
|
||||
Successfully clawed back 2000 TSTUSD!
|
||||
|
||||
=== Final Cover Available After Clawback ===
|
||||
|
||||
0 TSTUSD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deposit and Withdraw First-loss Capital
|
||||
|
||||
```sh
|
||||
python3 cover_deposit_and_withdraw.py
|
||||
```
|
||||
|
||||
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: rBeEX3qQzP3UL5WMwZAzdPPpzckH73YvBn
|
||||
LoanBrokerID: 041E256F124841FF81DF105C62A72676BFD746975F86786166B689F304BE96E0
|
||||
MPT ID: 0037A8ED99701AFEC4BCC3A39299252CA41838059572E7F2
|
||||
|
||||
=== Preparing LoanBrokerCoverDeposit transaction ===
|
||||
|
||||
{
|
||||
"Account": "rBeEX3qQzP3UL5WMwZAzdPPpzckH73YvBn",
|
||||
"TransactionType": "LoanBrokerCoverDeposit",
|
||||
"SigningPubKey": "",
|
||||
"LoanBrokerID": "041E256F124841FF81DF105C62A72676BFD746975F86786166B689F304BE96E0",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0037A8ED99701AFEC4BCC3A39299252CA41838059572E7F2",
|
||||
"value": "2000"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerCoverDeposit transaction ===
|
||||
|
||||
Cover deposit successful!
|
||||
|
||||
=== Cover Balance ===
|
||||
|
||||
LoanBroker Pseudo-Account: rUrs1bkhQyh1nxE7u99H92U2Tg8Pogw1bZ
|
||||
Cover balance after deposit: 2000 TSTUSD
|
||||
|
||||
=== Preparing LoanBrokerCoverWithdraw transaction ===
|
||||
|
||||
{
|
||||
"Account": "rBeEX3qQzP3UL5WMwZAzdPPpzckH73YvBn",
|
||||
"TransactionType": "LoanBrokerCoverWithdraw",
|
||||
"SigningPubKey": "",
|
||||
"LoanBrokerID": "041E256F124841FF81DF105C62A72676BFD746975F86786166B689F304BE96E0",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0037A8ED99701AFEC4BCC3A39299252CA41838059572E7F2",
|
||||
"value": "1000"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanBrokerCoverWithdraw transaction ===
|
||||
|
||||
Cover withdraw successful!
|
||||
|
||||
=== Updated Cover Balance ===
|
||||
|
||||
LoanBroker Pseudo-Account: rUrs1bkhQyh1nxE7u99H92U2Tg8Pogw1bZ
|
||||
Cover balance after withdraw: 1000 TSTUSD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create a Loan
|
||||
|
||||
```sh
|
||||
python3 create_loan.py
|
||||
```
|
||||
|
||||
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: ra3aoaincCNBQ7uxvHDgFbtCbVw1VNQkZy
|
||||
Borrower address: raXnMyDFQWVhvVuyb2oK3oCLGZhemkLqKL
|
||||
LoanBrokerID: 61A1D6B0F019C5D5BD039AC3DBE2E31813471567854D07D278564E4E2463ABD2
|
||||
|
||||
=== Preparing LoanSet transaction ===
|
||||
|
||||
{
|
||||
"Account": "ra3aoaincCNBQ7uxvHDgFbtCbVw1VNQkZy",
|
||||
"TransactionType": "LoanSet",
|
||||
"Fee": "2",
|
||||
"Sequence": 3652181,
|
||||
"LastLedgerSequence": 3674792,
|
||||
"SigningPubKey": "",
|
||||
"LoanBrokerID": "61A1D6B0F019C5D5BD039AC3DBE2E31813471567854D07D278564E4E2463ABD2",
|
||||
"Counterparty": "raXnMyDFQWVhvVuyb2oK3oCLGZhemkLqKL",
|
||||
"LoanOriginationFee": "100",
|
||||
"LoanServiceFee": "10",
|
||||
"InterestRate": 500,
|
||||
"PrincipalRequested": "1000",
|
||||
"PaymentTotal": 12,
|
||||
"PaymentInterval": 2592000,
|
||||
"GracePeriod": 604800
|
||||
}
|
||||
|
||||
=== Adding loan broker signature ===
|
||||
|
||||
TxnSignature: 9756E70F33B359FAEA789D732E752401DE41CAB1A3711517B576DBFF4D89B6A01C234A379391C48B3D88CB031BD679A7EDE4F4BB67AA7297EEE25EA29FF6BD0D
|
||||
SigningPubKey: ED0DC8C222C4BB86CE07165CD0486C598B8146C3150EE40AF48921983DED98FA47
|
||||
|
||||
Signed loan_set_tx for borrower to sign over:
|
||||
{
|
||||
"Account": "ra3aoaincCNBQ7uxvHDgFbtCbVw1VNQkZy",
|
||||
"TransactionType": "LoanSet",
|
||||
"Fee": "2",
|
||||
"Sequence": 3652181,
|
||||
"LastLedgerSequence": 3674792,
|
||||
"SigningPubKey": "ED0DC8C222C4BB86CE07165CD0486C598B8146C3150EE40AF48921983DED98FA47",
|
||||
"TxnSignature": "9756E70F33B359FAEA789D732E752401DE41CAB1A3711517B576DBFF4D89B6A01C234A379391C48B3D88CB031BD679A7EDE4F4BB67AA7297EEE25EA29FF6BD0D",
|
||||
"LoanBrokerID": "61A1D6B0F019C5D5BD039AC3DBE2E31813471567854D07D278564E4E2463ABD2",
|
||||
"Counterparty": "raXnMyDFQWVhvVuyb2oK3oCLGZhemkLqKL",
|
||||
"LoanOriginationFee": "100",
|
||||
"LoanServiceFee": "10",
|
||||
"InterestRate": 500,
|
||||
"PrincipalRequested": "1000",
|
||||
"PaymentTotal": 12,
|
||||
"PaymentInterval": 2592000,
|
||||
"GracePeriod": 604800
|
||||
}
|
||||
|
||||
=== Adding borrower signature ===
|
||||
|
||||
Borrower TxnSignature: A0A515BFB131EDC7A8B74F7A66F9DA1DEE25B099373F581BDA340C95F918CEA91E3F4D2019A8DBAFEC53012038839FEA48436D61970B0834F6DDEA64B1776207
|
||||
Borrower SigningPubKey: ED36B94636EC0F98BB5F6EC58039E23A8C8F1521D2EC1B32C0422A86718C9B95DC
|
||||
|
||||
Fully signed LoanSet transaction:
|
||||
{
|
||||
"Account": "ra3aoaincCNBQ7uxvHDgFbtCbVw1VNQkZy",
|
||||
"TransactionType": "LoanSet",
|
||||
"Fee": "2",
|
||||
"Sequence": 3652181,
|
||||
"LastLedgerSequence": 3674792,
|
||||
"SigningPubKey": "ED0DC8C222C4BB86CE07165CD0486C598B8146C3150EE40AF48921983DED98FA47",
|
||||
"TxnSignature": "9756E70F33B359FAEA789D732E752401DE41CAB1A3711517B576DBFF4D89B6A01C234A379391C48B3D88CB031BD679A7EDE4F4BB67AA7297EEE25EA29FF6BD0D",
|
||||
"LoanBrokerID": "61A1D6B0F019C5D5BD039AC3DBE2E31813471567854D07D278564E4E2463ABD2",
|
||||
"Counterparty": "raXnMyDFQWVhvVuyb2oK3oCLGZhemkLqKL",
|
||||
"CounterpartySignature": {
|
||||
"SigningPubKey": "ED36B94636EC0F98BB5F6EC58039E23A8C8F1521D2EC1B32C0422A86718C9B95DC",
|
||||
"TxnSignature": "A0A515BFB131EDC7A8B74F7A66F9DA1DEE25B099373F581BDA340C95F918CEA91E3F4D2019A8DBAFEC53012038839FEA48436D61970B0834F6DDEA64B1776207"
|
||||
},
|
||||
"LoanOriginationFee": "100",
|
||||
"LoanServiceFee": "10",
|
||||
"InterestRate": 500,
|
||||
"PrincipalRequested": "1000",
|
||||
"PaymentTotal": 12,
|
||||
"PaymentInterval": 2592000,
|
||||
"GracePeriod": 604800
|
||||
}
|
||||
|
||||
=== Submitting signed LoanSet transaction ===
|
||||
|
||||
Loan created successfully!
|
||||
|
||||
=== Loan Information ===
|
||||
|
||||
{
|
||||
"Borrower": "raXnMyDFQWVhvVuyb2oK3oCLGZhemkLqKL",
|
||||
"GracePeriod": 604800,
|
||||
"InterestRate": 500,
|
||||
"LoanBrokerID": "61A1D6B0F019C5D5BD039AC3DBE2E31813471567854D07D278564E4E2463ABD2",
|
||||
"LoanOriginationFee": "100",
|
||||
"LoanSequence": 4,
|
||||
"LoanServiceFee": "10",
|
||||
"NextPaymentDueDate": 826867870,
|
||||
"PaymentInterval": 2592000,
|
||||
"PaymentRemaining": 12,
|
||||
"PeriodicPayment": "83.55610375293148956",
|
||||
"PrincipalOutstanding": "1000",
|
||||
"StartDate": 824275870,
|
||||
"TotalValueOutstanding": "1003"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manage a Loan
|
||||
|
||||
```sh
|
||||
python3 loan_manage.py
|
||||
```
|
||||
|
||||
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: r9x3etrs2GZSF73vQ8endi9CWpKr5N2Rjn
|
||||
LoanID: E86DB385401D361A33DD74C8E1B44D7F996E9BA02724BCD44127F60BE057A322
|
||||
|
||||
=== Loan Status ===
|
||||
|
||||
Total Amount Owed: 1001 TSTUSD.
|
||||
Payment Due Date: 2026-03-14 02:01:51
|
||||
|
||||
=== Preparing LoanManage transaction to impair loan ===
|
||||
|
||||
{
|
||||
"Account": "r9x3etrs2GZSF73vQ8endi9CWpKr5N2Rjn",
|
||||
"TransactionType": "LoanManage",
|
||||
"Flags": 131072,
|
||||
"SigningPubKey": "",
|
||||
"LoanID": "E86DB385401D361A33DD74C8E1B44D7F996E9BA02724BCD44127F60BE057A322"
|
||||
}
|
||||
|
||||
=== Submitting LoanManage impairment transaction ===
|
||||
|
||||
Loan impaired successfully!
|
||||
New Payment Due Date: 2026-02-12 01:01:50
|
||||
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": "r9x3etrs2GZSF73vQ8endi9CWpKr5N2Rjn",
|
||||
"TransactionType": "LoanManage",
|
||||
"Flags": 65536,
|
||||
"SigningPubKey": "",
|
||||
"LoanID": "E86DB385401D361A33DD74C8E1B44D7F996E9BA02724BCD44127F60BE057A322"
|
||||
}
|
||||
|
||||
=== Submitting LoanManage default transaction ===
|
||||
|
||||
Loan defaulted successfully!
|
||||
|
||||
=== Checking final loan status ===
|
||||
|
||||
Final loan flags: ['TF_LOAN_DEFAULT', 'TF_LOAN_IMPAIR']
|
||||
```
|
||||
|
||||
## Pay a Loan
|
||||
|
||||
```sh
|
||||
python3 loan_pay.py
|
||||
```
|
||||
|
||||
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: raXnMyDFQWVhvVuyb2oK3oCLGZhemkLqKL
|
||||
LoanID: A9CC92540995E49B39E79883A22FF10A374BF2CB32763E89AA986B613E16D5FD
|
||||
MPT ID: 0037BA4C909352D28BF9580F1D536AF4F7E07649B5B6E116
|
||||
|
||||
=== Loan Status ===
|
||||
|
||||
Amount Owed: 1001 TSTUSD
|
||||
Loan Service Fee: 10 TSTUSD
|
||||
Total Payment Due (including fees): 1011 TSTUSD
|
||||
|
||||
=== Preparing LoanPay transaction ===
|
||||
|
||||
{
|
||||
"Account": "raXnMyDFQWVhvVuyb2oK3oCLGZhemkLqKL",
|
||||
"TransactionType": "LoanPay",
|
||||
"SigningPubKey": "",
|
||||
"LoanID": "A9CC92540995E49B39E79883A22FF10A374BF2CB32763E89AA986B613E16D5FD",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0037BA4C909352D28BF9580F1D536AF4F7E07649B5B6E116",
|
||||
"value": "1011"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting LoanPay transaction ===
|
||||
|
||||
Loan paid successfully!
|
||||
|
||||
=== Loan Status After Payment ===
|
||||
|
||||
Outstanding Loan Balance: Loan fully paid off!
|
||||
|
||||
=== Preparing LoanDelete transaction ===
|
||||
|
||||
{
|
||||
"Account": "raXnMyDFQWVhvVuyb2oK3oCLGZhemkLqKL",
|
||||
"TransactionType": "LoanDelete",
|
||||
"SigningPubKey": "",
|
||||
"LoanID": "A9CC92540995E49B39E79883A22FF10A374BF2CB32763E89AA986B613E16D5FD"
|
||||
}
|
||||
|
||||
=== Submitting LoanDelete transaction ===
|
||||
|
||||
Loan deleted successfully!
|
||||
|
||||
=== Verifying Loan Deletion ===
|
||||
|
||||
Loan has been successfully removed from the XRP Ledger!
|
||||
```
|
||||
@@ -1,124 +0,0 @@
|
||||
# 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.
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import LedgerEntry, LoanBrokerCoverClawback, LoanBrokerCoverDeposit, MPTAmount
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.wallet import Wallet
|
||||
|
||||
# Set up client ----------------------
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
# If missing, lending_setup.py will generate the data.
|
||||
if not os.path.exists("lending_setup.json"):
|
||||
print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "lending_setup.py"], check=True)
|
||||
|
||||
# Load preconfigured accounts, loan_broker_id, and mpt_id.
|
||||
with open("lending_setup.json") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# You can replace these values with your own.
|
||||
loan_broker = Wallet.from_seed(setup_data["loan_broker"]["seed"])
|
||||
mpt_issuer = Wallet.from_seed(setup_data["depositor"]["seed"])
|
||||
loan_broker_id = setup_data["loan_broker_id"]
|
||||
mpt_id = setup_data["mpt_id"]
|
||||
|
||||
print(f"\nLoan broker address: {loan_broker.address}")
|
||||
print(f"MPT issuer address: {mpt_issuer.address}")
|
||||
print(f"LoanBrokerID: {loan_broker_id}")
|
||||
print(f"MPT ID: {mpt_id}")
|
||||
|
||||
# Check cover available ----------------------
|
||||
print("\n=== Cover Available ===\n")
|
||||
cover_info = client.request(LedgerEntry(
|
||||
index=loan_broker_id,
|
||||
ledger_index="validated",
|
||||
))
|
||||
|
||||
current_cover_available = cover_info.result["node"].get("CoverAvailable", "0")
|
||||
print(f"{current_cover_available} TSTUSD")
|
||||
|
||||
# Prepare LoanBrokerCoverDeposit transaction ----------------------
|
||||
print("\n=== Preparing LoanBrokerCoverDeposit transaction ===\n")
|
||||
cover_deposit_tx = LoanBrokerCoverDeposit(
|
||||
account=loan_broker.address,
|
||||
loan_broker_id=loan_broker_id,
|
||||
amount=MPTAmount(mpt_issuance_id=mpt_id, value="1000"),
|
||||
)
|
||||
|
||||
print(json.dumps(cover_deposit_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Sign, submit, and wait for deposit validation ----------------------
|
||||
print("\n=== Submitting LoanBrokerCoverDeposit transaction ===\n")
|
||||
deposit_response = submit_and_wait(cover_deposit_tx, client, loan_broker)
|
||||
|
||||
if deposit_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = deposit_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to deposit cover: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Cover deposit successful!")
|
||||
|
||||
# Extract updated cover available after deposit ----------------------
|
||||
print("\n=== Cover Available After Deposit ===\n")
|
||||
loan_broker_node = next(
|
||||
node for node in deposit_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("ModifiedNode", {}).get("LedgerEntryType") == "LoanBroker"
|
||||
)
|
||||
|
||||
current_cover_available = loan_broker_node["ModifiedNode"]["FinalFields"]["CoverAvailable"]
|
||||
print(f"{current_cover_available} TSTUSD")
|
||||
|
||||
# Verify issuer of cover asset matches ----------------------
|
||||
# Only the issuer of the asset can submit clawback transactions.
|
||||
# The asset must also have clawback enabled.
|
||||
print("\n=== Verifying Asset Issuer ===\n")
|
||||
asset_issuer_info = client.request(LedgerEntry(
|
||||
mpt_issuance=mpt_id,
|
||||
ledger_index="validated",
|
||||
))
|
||||
|
||||
if asset_issuer_info.result["node"]["Issuer"] != mpt_issuer.address:
|
||||
issuer = asset_issuer_info.result["node"]["Issuer"]
|
||||
print(f"Error: {issuer} does not match account ({mpt_issuer.address}) attempting clawback!")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"MPT issuer account verified: {mpt_issuer.address}. Proceeding to clawback.")
|
||||
|
||||
# Prepare LoanBrokerCoverClawback transaction ----------------------
|
||||
print("\n=== Preparing LoanBrokerCoverClawback transaction ===\n")
|
||||
cover_clawback_tx = LoanBrokerCoverClawback(
|
||||
account=mpt_issuer.address,
|
||||
loan_broker_id=loan_broker_id,
|
||||
amount=MPTAmount(mpt_issuance_id=mpt_id, value=current_cover_available),
|
||||
)
|
||||
|
||||
print(json.dumps(cover_clawback_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Sign, submit, and wait for clawback validation ----------------------
|
||||
print("\n=== Submitting LoanBrokerCoverClawback transaction ===\n")
|
||||
clawback_response = submit_and_wait(cover_clawback_tx, client, mpt_issuer)
|
||||
|
||||
if clawback_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = clawback_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to clawback cover: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Successfully clawed back {current_cover_available} TSTUSD!")
|
||||
|
||||
# Extract final cover available ----------------------
|
||||
print("\n=== Final Cover Available After Clawback ===\n")
|
||||
loan_broker_node = next(
|
||||
node for node in clawback_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("ModifiedNode", {}).get("LedgerEntryType") == "LoanBroker"
|
||||
)
|
||||
|
||||
print(f"{loan_broker_node['ModifiedNode']['FinalFields'].get('CoverAvailable', '0')} TSTUSD")
|
||||
@@ -1,95 +0,0 @@
|
||||
# IMPORTANT: This example deposits and withdraws first-loss capital from a
|
||||
# preconfigured LoanBroker entry.
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import LoanBrokerCoverDeposit, LoanBrokerCoverWithdraw, MPTAmount
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.wallet import Wallet
|
||||
|
||||
# Set up client ----------------------
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
# If missing, lending_setup.py will generate the data.
|
||||
if not os.path.exists("lending_setup.json"):
|
||||
print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "lending_setup.py"], check=True)
|
||||
|
||||
# Load preconfigured accounts and loan_broker_id.
|
||||
with open("lending_setup.json") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# You can replace these values with your own.
|
||||
loan_broker = Wallet.from_seed(setup_data["loan_broker"]["seed"])
|
||||
loan_broker_id = setup_data["loan_broker_id"]
|
||||
mpt_id = setup_data["mpt_id"]
|
||||
|
||||
print(f"\nLoan broker address: {loan_broker.address}")
|
||||
print(f"LoanBrokerID: {loan_broker_id}")
|
||||
print(f"MPT ID: {mpt_id}")
|
||||
|
||||
# Prepare LoanBrokerCoverDeposit transaction ----------------------
|
||||
print("\n=== Preparing LoanBrokerCoverDeposit transaction ===\n")
|
||||
cover_deposit_tx = LoanBrokerCoverDeposit(
|
||||
account=loan_broker.address,
|
||||
loan_broker_id=loan_broker_id,
|
||||
amount=MPTAmount(mpt_issuance_id=mpt_id, value="2000"),
|
||||
)
|
||||
|
||||
print(json.dumps(cover_deposit_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Sign, submit, and wait for deposit validation ----------------------
|
||||
print("\n=== Submitting LoanBrokerCoverDeposit transaction ===\n")
|
||||
deposit_response = submit_and_wait(cover_deposit_tx, client, loan_broker)
|
||||
|
||||
if deposit_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = deposit_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to deposit cover: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Cover deposit successful!")
|
||||
|
||||
# Extract cover balance from the transaction result
|
||||
print("\n=== Cover Balance ===\n")
|
||||
loan_broker_node = next(
|
||||
node for node in deposit_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("ModifiedNode", {}).get("LedgerEntryType") == "LoanBroker"
|
||||
)
|
||||
# First-loss capital is stored in the LoanBroker's pseudo-account.
|
||||
print(f"LoanBroker Pseudo-Account: {loan_broker_node['ModifiedNode']['FinalFields']['Account']}")
|
||||
print(f"Cover balance after deposit: {loan_broker_node['ModifiedNode']['FinalFields']['CoverAvailable']} TSTUSD")
|
||||
|
||||
# Prepare LoanBrokerCoverWithdraw transaction ----------------------
|
||||
print("\n=== Preparing LoanBrokerCoverWithdraw transaction ===\n")
|
||||
cover_withdraw_tx = LoanBrokerCoverWithdraw(
|
||||
account=loan_broker.address,
|
||||
loan_broker_id=loan_broker_id,
|
||||
amount=MPTAmount(mpt_issuance_id=mpt_id, value="1000"),
|
||||
)
|
||||
|
||||
print(json.dumps(cover_withdraw_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Sign, submit, and wait for withdraw validation ----------------------
|
||||
print("\n=== Submitting LoanBrokerCoverWithdraw transaction ===\n")
|
||||
withdraw_response = submit_and_wait(cover_withdraw_tx, client, loan_broker)
|
||||
|
||||
if withdraw_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = withdraw_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to withdraw cover: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Cover withdraw successful!")
|
||||
|
||||
# Extract updated cover balance from the transaction result
|
||||
print("\n=== Updated Cover Balance ===\n")
|
||||
loan_broker_node = next(
|
||||
node for node in withdraw_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("ModifiedNode", {}).get("LedgerEntryType") == "LoanBroker"
|
||||
)
|
||||
print(f"LoanBroker Pseudo-Account: {loan_broker_node['ModifiedNode']['FinalFields']['Account']}")
|
||||
print(f"Cover balance after withdraw: {loan_broker_node['ModifiedNode']['FinalFields']['CoverAvailable']} TSTUSD")
|
||||
@@ -1,89 +0,0 @@
|
||||
# IMPORTANT: This example creates a loan using a preconfigured
|
||||
# loan broker, borrower, and private vault.
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import LoanSet
|
||||
from xrpl.transaction import autofill, sign, sign_loan_set_by_counterparty, submit_and_wait
|
||||
from xrpl.wallet import Wallet
|
||||
|
||||
# Set up client ----------------------
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
# If missing, lending_setup.py will generate the data.
|
||||
if not os.path.exists("lending_setup.json"):
|
||||
print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "lending_setup.py"], check=True)
|
||||
|
||||
# Load preconfigured accounts and loan_broker_id.
|
||||
with open("lending_setup.json") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# You can replace these values with your own.
|
||||
loan_broker = Wallet.from_seed(setup_data["loan_broker"]["seed"])
|
||||
borrower = Wallet.from_seed(setup_data["borrower"]["seed"])
|
||||
loan_broker_id = setup_data["loan_broker_id"]
|
||||
|
||||
print(f"\nLoan broker address: {loan_broker.address}")
|
||||
print(f"Borrower address: {borrower.address}")
|
||||
print(f"LoanBrokerID: {loan_broker_id}")
|
||||
|
||||
# Prepare LoanSet transaction ----------------------
|
||||
# Account and Counterparty accounts can be swapped, but determines signing order.
|
||||
# Account signs first, Counterparty signs second.
|
||||
print("\n=== Preparing LoanSet transaction ===\n")
|
||||
|
||||
loan_set_tx = autofill(LoanSet(
|
||||
account=loan_broker.address,
|
||||
counterparty=borrower.address,
|
||||
loan_broker_id=loan_broker_id,
|
||||
principal_requested="1000",
|
||||
interest_rate=500,
|
||||
payment_total=12,
|
||||
payment_interval=2592000,
|
||||
grace_period=604800,
|
||||
loan_origination_fee="100",
|
||||
loan_service_fee="10",
|
||||
), client)
|
||||
|
||||
print(json.dumps(loan_set_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Loan broker signs first.
|
||||
print("\n=== Adding loan broker signature ===\n")
|
||||
loan_broker_signed = sign(loan_set_tx, loan_broker)
|
||||
|
||||
print(f"TxnSignature: {loan_broker_signed.txn_signature}")
|
||||
print(f"SigningPubKey: {loan_broker_signed.signing_pub_key}\n")
|
||||
print(f"Signed loan_set_tx for borrower to sign over:\n{json.dumps(loan_broker_signed.to_xrpl(), indent=2)}")
|
||||
|
||||
# Borrower signs second.
|
||||
print("\n=== Adding borrower signature ===\n")
|
||||
fully_signed = sign_loan_set_by_counterparty(borrower, loan_broker_signed)
|
||||
|
||||
print(f"Borrower TxnSignature: {fully_signed.tx.counterparty_signature.txn_signature}")
|
||||
print(f"Borrower SigningPubKey: {fully_signed.tx.counterparty_signature.signing_pub_key}")
|
||||
print(f"\nFully signed LoanSet transaction:\n{json.dumps(fully_signed.tx.to_xrpl(), indent=2)}")
|
||||
|
||||
# Submit and wait for validation ----------------------
|
||||
print("\n=== Submitting signed LoanSet transaction ===\n")
|
||||
submit_response = submit_and_wait(fully_signed.tx, client)
|
||||
|
||||
if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = submit_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to create loan: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Loan created successfully!")
|
||||
|
||||
# Extract loan information from the transaction result.
|
||||
print("\n=== Loan Information ===\n")
|
||||
loan_node = next(
|
||||
node for node in submit_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("CreatedNode", {}).get("LedgerEntryType") == "Loan"
|
||||
)
|
||||
print(json.dumps(loan_node["CreatedNode"]["NewFields"], indent=2))
|
||||
@@ -1,64 +0,0 @@
|
||||
# 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 vault_id
|
||||
# and loan_broker values with your own.
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import LoanBrokerSet
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.wallet import Wallet
|
||||
|
||||
# Set up client ----------------------
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
# If missing, lending_setup.py will generate the data.
|
||||
if not os.path.exists("lending_setup.json"):
|
||||
print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "lending_setup.py"], check=True)
|
||||
|
||||
# Load preconfigured accounts and vault_id.
|
||||
with open("lending_setup.json") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# You can replace these values with your own.
|
||||
loan_broker = Wallet.from_seed(setup_data["loan_broker"]["seed"])
|
||||
vault_id = setup_data["vault_id"]
|
||||
|
||||
print(f"\nLoan broker/vault owner address: {loan_broker.address}")
|
||||
print(f"Vault ID: {vault_id}")
|
||||
|
||||
# Prepare LoanBrokerSet transaction ----------------------
|
||||
print("\n=== Preparing LoanBrokerSet transaction ===\n")
|
||||
loan_broker_set_tx = LoanBrokerSet(
|
||||
account=loan_broker.address,
|
||||
vault_id=vault_id,
|
||||
management_fee_rate=1000,
|
||||
)
|
||||
|
||||
print(json.dumps(loan_broker_set_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Submit, sign, and wait for validation ----------------------
|
||||
print("\n=== Submitting LoanBrokerSet transaction ===\n")
|
||||
submit_response = submit_and_wait(loan_broker_set_tx, client, loan_broker)
|
||||
|
||||
if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = submit_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to create loan broker: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Loan broker created successfully!")
|
||||
|
||||
# Extract loan broker information from the transaction result
|
||||
print("\n=== Loan Broker Information ===\n")
|
||||
loan_broker_node = next(
|
||||
node for node in submit_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("CreatedNode", {}).get("LedgerEntryType") == "LoanBroker"
|
||||
)
|
||||
print(f"LoanBroker ID: {loan_broker_node['CreatedNode']['LedgerIndex']}")
|
||||
print(f"LoanBroker Psuedo-Account Address: {loan_broker_node['CreatedNode']['NewFields']['Account']}")
|
||||
@@ -1,366 +0,0 @@
|
||||
# Setup script for lending protocol tutorials
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from xrpl.asyncio.clients import AsyncWebsocketClient
|
||||
from xrpl.asyncio.wallet import generate_faucet_wallet
|
||||
from xrpl.asyncio.transaction import submit_and_wait, autofill, sign
|
||||
from xrpl.transaction import sign_loan_set_by_counterparty
|
||||
from xrpl.models import (
|
||||
AccountObjects,
|
||||
Batch,
|
||||
BatchFlag,
|
||||
CredentialAccept,
|
||||
CredentialCreate,
|
||||
LoanBrokerSet,
|
||||
LoanSet,
|
||||
MPTAmount,
|
||||
MPTCurrency,
|
||||
MPTokenAuthorize,
|
||||
MPTokenIssuanceCreate,
|
||||
MPTokenIssuanceCreateFlag,
|
||||
Payment,
|
||||
PermissionedDomainSet,
|
||||
TicketCreate,
|
||||
VaultCreate,
|
||||
VaultDeposit,
|
||||
)
|
||||
from xrpl.models.transactions.vault_create import VaultCreateFlag
|
||||
from xrpl.models.transactions.deposit_preauth import Credential
|
||||
from xrpl.utils import encode_mptoken_metadata, str_to_hex
|
||||
|
||||
|
||||
async def main():
|
||||
async with AsyncWebsocketClient("wss://s.devnet.rippletest.net:51233") as client:
|
||||
|
||||
print("Setting up tutorial: 0/6", end="\r")
|
||||
|
||||
# Create and fund wallets
|
||||
loan_broker, borrower, depositor, credential_issuer = await asyncio.gather(
|
||||
generate_faucet_wallet(client),
|
||||
generate_faucet_wallet(client),
|
||||
generate_faucet_wallet(client),
|
||||
generate_faucet_wallet(client),
|
||||
)
|
||||
|
||||
print("Setting up tutorial: 1/6", end="\r")
|
||||
|
||||
# Issue MPT with depositor
|
||||
# Create tickets for later use with loan_broker
|
||||
# Set up credentials and domain with credential_issuer
|
||||
credential_type = str_to_hex("KYC-Verified")
|
||||
|
||||
mpt_data = {
|
||||
"ticker": "TSTUSD",
|
||||
"name": "Test USD MPT",
|
||||
"desc": "A sample non-yield-bearing stablecoin backed by U.S. Treasuries.",
|
||||
"icon": "https://example.org/tstusd-icon.png",
|
||||
"asset_class": "rwa",
|
||||
"asset_subclass": "stablecoin",
|
||||
"issuer_name": "Example Treasury Reserve Co.",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "https://exampletreasury.com/tstusd",
|
||||
"category": "website",
|
||||
"title": "Product Page",
|
||||
},
|
||||
{
|
||||
"uri": "https://exampletreasury.com/tstusd/reserve",
|
||||
"category": "docs",
|
||||
"title": "Reserve Attestation",
|
||||
},
|
||||
],
|
||||
"additional_info": {
|
||||
"reserve_type": "U.S. Treasury Bills",
|
||||
"custody_provider": "Example Custodian Bank",
|
||||
"audit_frequency": "Monthly",
|
||||
"last_audit_date": "2026-01-15",
|
||||
"pegged_currency": "USD",
|
||||
},
|
||||
}
|
||||
|
||||
ticket_create_response, mpt_issuance_response, _ = await asyncio.gather(
|
||||
submit_and_wait(
|
||||
TicketCreate(
|
||||
account=loan_broker.address,
|
||||
ticket_count=2,
|
||||
),
|
||||
client,
|
||||
loan_broker,
|
||||
),
|
||||
submit_and_wait(
|
||||
MPTokenIssuanceCreate(
|
||||
account=depositor.address,
|
||||
maximum_amount="100000000",
|
||||
transfer_fee=0,
|
||||
flags=(
|
||||
MPTokenIssuanceCreateFlag.TF_MPT_CAN_TRANSFER
|
||||
| MPTokenIssuanceCreateFlag.TF_MPT_CAN_CLAWBACK
|
||||
| MPTokenIssuanceCreateFlag.TF_MPT_CAN_TRADE
|
||||
),
|
||||
mptoken_metadata=encode_mptoken_metadata(mpt_data),
|
||||
),
|
||||
client,
|
||||
depositor,
|
||||
),
|
||||
submit_and_wait(
|
||||
Batch(
|
||||
account=credential_issuer.address,
|
||||
flags=BatchFlag.TF_ALL_OR_NOTHING,
|
||||
raw_transactions=[
|
||||
CredentialCreate(
|
||||
account=credential_issuer.address,
|
||||
subject=loan_broker.address,
|
||||
credential_type=credential_type,
|
||||
),
|
||||
CredentialCreate(
|
||||
account=credential_issuer.address,
|
||||
subject=borrower.address,
|
||||
credential_type=credential_type,
|
||||
),
|
||||
CredentialCreate(
|
||||
account=credential_issuer.address,
|
||||
subject=depositor.address,
|
||||
credential_type=credential_type,
|
||||
),
|
||||
PermissionedDomainSet(
|
||||
account=credential_issuer.address,
|
||||
accepted_credentials=[
|
||||
Credential(
|
||||
issuer=credential_issuer.address,
|
||||
credential_type=credential_type,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
client,
|
||||
credential_issuer,
|
||||
),
|
||||
)
|
||||
|
||||
# Extract ticket sequence numbers
|
||||
tickets = [
|
||||
node["CreatedNode"]["NewFields"]["TicketSequence"]
|
||||
for node in ticket_create_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("CreatedNode", {}).get("LedgerEntryType") == "Ticket"
|
||||
]
|
||||
|
||||
# Extract MPT issuance ID
|
||||
mpt_id = mpt_issuance_response.result["meta"]["mpt_issuance_id"]
|
||||
|
||||
# Get domain ID
|
||||
credential_issuer_objects = await client.request(AccountObjects(
|
||||
account=credential_issuer.address,
|
||||
ledger_index="validated",
|
||||
))
|
||||
domain_id = next(
|
||||
node["index"]
|
||||
for node in credential_issuer_objects.result["account_objects"]
|
||||
if node["LedgerEntryType"] == "PermissionedDomain"
|
||||
)
|
||||
|
||||
print("Setting up tutorial: 2/6", end="\r")
|
||||
|
||||
# Accept credentials and authorize MPT for each account
|
||||
await asyncio.gather(
|
||||
submit_and_wait(
|
||||
Batch(
|
||||
account=loan_broker.address,
|
||||
flags=BatchFlag.TF_ALL_OR_NOTHING,
|
||||
raw_transactions=[
|
||||
CredentialAccept(
|
||||
account=loan_broker.address,
|
||||
issuer=credential_issuer.address,
|
||||
credential_type=credential_type,
|
||||
),
|
||||
MPTokenAuthorize(
|
||||
account=loan_broker.address,
|
||||
mptoken_issuance_id=mpt_id,
|
||||
),
|
||||
],
|
||||
),
|
||||
client,
|
||||
loan_broker,
|
||||
),
|
||||
submit_and_wait(
|
||||
Batch(
|
||||
account=borrower.address,
|
||||
flags=BatchFlag.TF_ALL_OR_NOTHING,
|
||||
raw_transactions=[
|
||||
CredentialAccept(
|
||||
account=borrower.address,
|
||||
issuer=credential_issuer.address,
|
||||
credential_type=credential_type,
|
||||
),
|
||||
MPTokenAuthorize(
|
||||
account=borrower.address,
|
||||
mptoken_issuance_id=mpt_id,
|
||||
),
|
||||
],
|
||||
),
|
||||
client,
|
||||
borrower,
|
||||
),
|
||||
submit_and_wait(
|
||||
CredentialAccept(
|
||||
account=depositor.address,
|
||||
issuer=credential_issuer.address,
|
||||
credential_type=credential_type,
|
||||
),
|
||||
client,
|
||||
depositor,
|
||||
),
|
||||
)
|
||||
|
||||
print("Setting up tutorial: 3/6", end="\r")
|
||||
|
||||
# Create private vault and distribute MPT to accounts
|
||||
vault_create_response, _ = await asyncio.gather(
|
||||
submit_and_wait(
|
||||
VaultCreate(
|
||||
account=loan_broker.address,
|
||||
asset=MPTCurrency(mpt_issuance_id=mpt_id),
|
||||
flags=VaultCreateFlag.TF_VAULT_PRIVATE,
|
||||
domain_id=domain_id,
|
||||
),
|
||||
client,
|
||||
loan_broker,
|
||||
),
|
||||
submit_and_wait(
|
||||
Batch(
|
||||
account=depositor.address,
|
||||
flags=BatchFlag.TF_ALL_OR_NOTHING,
|
||||
raw_transactions=[
|
||||
Payment(
|
||||
account=depositor.address,
|
||||
destination=loan_broker.address,
|
||||
amount=MPTAmount(mpt_issuance_id=mpt_id, value="5000"),
|
||||
),
|
||||
Payment(
|
||||
account=depositor.address,
|
||||
destination=borrower.address,
|
||||
amount=MPTAmount(mpt_issuance_id=mpt_id, value="2500"),
|
||||
),
|
||||
],
|
||||
),
|
||||
client,
|
||||
depositor,
|
||||
),
|
||||
)
|
||||
|
||||
vault_id = next(
|
||||
node["CreatedNode"]["LedgerIndex"]
|
||||
for node in vault_create_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("CreatedNode", {}).get("LedgerEntryType") == "Vault"
|
||||
)
|
||||
|
||||
print("Setting up tutorial: 4/6", end="\r")
|
||||
|
||||
# Create LoanBroker and deposit MPT into vault
|
||||
loan_broker_set_response, _ = await asyncio.gather(
|
||||
submit_and_wait(
|
||||
LoanBrokerSet(
|
||||
account=loan_broker.address,
|
||||
vault_id=vault_id,
|
||||
),
|
||||
client,
|
||||
loan_broker,
|
||||
),
|
||||
submit_and_wait(
|
||||
VaultDeposit(
|
||||
account=depositor.address,
|
||||
vault_id=vault_id,
|
||||
amount=MPTAmount(mpt_issuance_id=mpt_id, value="50000000"),
|
||||
),
|
||||
client,
|
||||
depositor,
|
||||
),
|
||||
)
|
||||
|
||||
loan_broker_id = next(
|
||||
node["CreatedNode"]["LedgerIndex"]
|
||||
for node in loan_broker_set_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("CreatedNode", {}).get("LedgerEntryType") == "LoanBroker"
|
||||
)
|
||||
|
||||
print("Setting up tutorial: 5/6", end="\r")
|
||||
|
||||
# Create 2 identical loans with complete repayment due in 30 days
|
||||
|
||||
# Helper function to create, sign, and submit a LoanSet transaction
|
||||
async def create_loan(ticket_sequence):
|
||||
loan_set_tx = await autofill(LoanSet(
|
||||
account=loan_broker.address,
|
||||
counterparty=borrower.address,
|
||||
loan_broker_id=loan_broker_id,
|
||||
principal_requested="1000",
|
||||
interest_rate=500,
|
||||
payment_total=1,
|
||||
payment_interval=2592000,
|
||||
loan_origination_fee="100",
|
||||
loan_service_fee="10",
|
||||
sequence=0,
|
||||
ticket_sequence=ticket_sequence,
|
||||
), client)
|
||||
|
||||
loan_broker_signed = sign(loan_set_tx, loan_broker)
|
||||
fully_signed = sign_loan_set_by_counterparty(borrower, loan_broker_signed)
|
||||
submit_response = await submit_and_wait(fully_signed.tx, client)
|
||||
|
||||
return submit_response
|
||||
|
||||
submit_response_1, submit_response_2 = await asyncio.gather(
|
||||
create_loan(tickets[0]),
|
||||
create_loan(tickets[1]),
|
||||
)
|
||||
|
||||
loan_id_1 = next(
|
||||
node["CreatedNode"]["LedgerIndex"]
|
||||
for node in submit_response_1.result["meta"]["AffectedNodes"]
|
||||
if node.get("CreatedNode", {}).get("LedgerEntryType") == "Loan"
|
||||
)
|
||||
|
||||
loan_id_2 = next(
|
||||
node["CreatedNode"]["LedgerIndex"]
|
||||
for node in submit_response_2.result["meta"]["AffectedNodes"]
|
||||
if node.get("CreatedNode", {}).get("LedgerEntryType") == "Loan"
|
||||
)
|
||||
|
||||
print("Setting up tutorial: 6/6", end="\r")
|
||||
|
||||
# Write setup data to JSON file
|
||||
setup_data = {
|
||||
"description": "This file is auto-generated by lending_setup.py. It stores XRPL account info for use in lending protocol tutorials.",
|
||||
"loan_broker": {
|
||||
"address": loan_broker.address,
|
||||
"seed": loan_broker.seed,
|
||||
},
|
||||
"borrower": {
|
||||
"address": borrower.address,
|
||||
"seed": borrower.seed,
|
||||
},
|
||||
"depositor": {
|
||||
"address": depositor.address,
|
||||
"seed": depositor.seed,
|
||||
},
|
||||
"credential_issuer": {
|
||||
"address": credential_issuer.address,
|
||||
"seed": credential_issuer.seed,
|
||||
},
|
||||
"domain_id": domain_id,
|
||||
"mpt_id": mpt_id,
|
||||
"vault_id": vault_id,
|
||||
"loan_broker_id": loan_broker_id,
|
||||
"loan_id_1": loan_id_1,
|
||||
"loan_id_2": loan_id_2,
|
||||
}
|
||||
|
||||
with open("lending_setup.json", "w") as f:
|
||||
json.dump(setup_data, f, indent=2)
|
||||
|
||||
print("Setting up tutorial: Complete!")
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,130 +0,0 @@
|
||||
# IMPORTANT: This example impairs an existing loan, which has a 60 second grace period.
|
||||
# After the 60 seconds pass, this example defaults the loan.
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import LedgerEntry, LoanManage
|
||||
from xrpl.models.transactions.loan_manage import LoanManageFlag
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.utils import posix_to_ripple_time, ripple_time_to_posix
|
||||
from xrpl.wallet import Wallet
|
||||
|
||||
# Set up client ----------------------
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
# If missing, lending_setup.py will generate the data.
|
||||
if not os.path.exists("lending_setup.json"):
|
||||
print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "lending_setup.py"], check=True)
|
||||
|
||||
# Load preconfigured accounts and loan_id.
|
||||
with open("lending_setup.json") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# You can replace these values with your own.
|
||||
loan_broker = Wallet.from_seed(setup_data["loan_broker"]["seed"])
|
||||
loan_id = setup_data["loan_id_1"]
|
||||
|
||||
print(f"\nLoan broker address: {loan_broker.address}")
|
||||
print(f"LoanID: {loan_id}")
|
||||
|
||||
# Check loan status before impairment ----------------------
|
||||
print("\n=== Loan Status ===\n")
|
||||
loan_status = client.request(LedgerEntry(
|
||||
index=loan_id,
|
||||
ledger_index="validated",
|
||||
))
|
||||
|
||||
print(f"Total Amount Owed: {loan_status.result['node']['TotalValueOutstanding']} TSTUSD.")
|
||||
# Convert Ripple Epoch timestamp to local date and time
|
||||
next_payment_due_date = loan_status.result["node"]["NextPaymentDueDate"]
|
||||
payment_due = datetime.fromtimestamp(ripple_time_to_posix(next_payment_due_date))
|
||||
print(f"Payment Due Date: {payment_due}")
|
||||
|
||||
# Prepare LoanManage transaction to impair the loan ----------------------
|
||||
print("\n=== Preparing LoanManage transaction to impair loan ===\n")
|
||||
loan_manage_impair = LoanManage(
|
||||
account=loan_broker.address,
|
||||
loan_id=loan_id,
|
||||
flags=LoanManageFlag.TF_LOAN_IMPAIR,
|
||||
)
|
||||
|
||||
print(json.dumps(loan_manage_impair.to_xrpl(), indent=2))
|
||||
|
||||
# Sign, submit, and wait for impairment validation ----------------------
|
||||
print("\n=== Submitting LoanManage impairment transaction ===\n")
|
||||
impair_response = submit_and_wait(loan_manage_impair, client, loan_broker)
|
||||
|
||||
if impair_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = impair_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to impair loan: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Loan impaired successfully!")
|
||||
|
||||
# Extract loan impairment info from transaction results ----------------------
|
||||
loan_node = next(
|
||||
node for node in impair_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("ModifiedNode", {}).get("LedgerEntryType") == "Loan"
|
||||
)
|
||||
|
||||
# Check grace period and next payment due date
|
||||
grace_period = loan_node["ModifiedNode"]["FinalFields"]["GracePeriod"]
|
||||
next_payment_due_date = loan_node["ModifiedNode"]["FinalFields"]["NextPaymentDueDate"]
|
||||
default_time = next_payment_due_date + grace_period
|
||||
payment_due = datetime.fromtimestamp(ripple_time_to_posix(next_payment_due_date))
|
||||
|
||||
print(f"New Payment Due Date: {payment_due}")
|
||||
print(f"Grace Period: {grace_period} seconds")
|
||||
|
||||
# Convert current time to Ripple Epoch timestamp
|
||||
current_time = posix_to_ripple_time(int(time.time()))
|
||||
seconds_until_default = default_time - current_time
|
||||
|
||||
# Countdown until loan can be defaulted ----------------------
|
||||
print("\n=== Countdown until loan can be defaulted ===\n")
|
||||
|
||||
while seconds_until_default >= 0:
|
||||
print(f"{seconds_until_default} seconds...", end="\r")
|
||||
time.sleep(1)
|
||||
seconds_until_default -= 1
|
||||
|
||||
print("\rGrace period expired. Loan can now be defaulted.")
|
||||
|
||||
# Prepare LoanManage transaction to default the loan ----------------------
|
||||
print("\n=== Preparing LoanManage transaction to default loan ===\n")
|
||||
loan_manage_default = LoanManage(
|
||||
account=loan_broker.address,
|
||||
loan_id=loan_id,
|
||||
flags=LoanManageFlag.TF_LOAN_DEFAULT,
|
||||
)
|
||||
|
||||
print(json.dumps(loan_manage_default.to_xrpl(), indent=2))
|
||||
|
||||
# Sign, submit, and wait for default validation ----------------------
|
||||
print("\n=== Submitting LoanManage default transaction ===\n")
|
||||
default_response = submit_and_wait(loan_manage_default, client, loan_broker)
|
||||
|
||||
if default_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = default_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to default loan: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Loan defaulted successfully!")
|
||||
|
||||
# Verify loan default status from transaction results ----------------------
|
||||
print("\n=== Checking final loan status ===\n")
|
||||
loan_node = next(
|
||||
node for node in default_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("ModifiedNode", {}).get("LedgerEntryType") == "Loan"
|
||||
)
|
||||
loan_flags = loan_node["ModifiedNode"]["FinalFields"]["Flags"]
|
||||
active_flags = [f.name for f in LoanManageFlag if loan_flags & f.value]
|
||||
print(f"Final loan flags: {active_flags}")
|
||||
@@ -1,117 +0,0 @@
|
||||
# IMPORTANT: This example pays off an existing loan and then deletes it.
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import LedgerEntry, LoanDelete, LoanPay, MPTAmount
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.wallet import Wallet
|
||||
|
||||
# Set up client ----------------------
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# This step checks for the necessary setup data to run the lending protocol tutorials.
|
||||
# If missing, lending_setup.py will generate the data.
|
||||
if not os.path.exists("lending_setup.json"):
|
||||
print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "lending_setup.py"], check=True)
|
||||
|
||||
# Load preconfigured accounts, loan_id, and mpt_id.
|
||||
with open("lending_setup.json") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# You can replace these values with your own.
|
||||
borrower = Wallet.from_seed(setup_data["borrower"]["seed"])
|
||||
loan_id = setup_data["loan_id_2"]
|
||||
mpt_id = setup_data["mpt_id"]
|
||||
|
||||
print(f"\nBorrower address: {borrower.address}")
|
||||
print(f"LoanID: {loan_id}")
|
||||
print(f"MPT ID: {mpt_id}")
|
||||
|
||||
# Check initial loan status ----------------------
|
||||
print("\n=== Loan Status ===\n")
|
||||
loan_status = client.request(LedgerEntry(
|
||||
index=loan_id,
|
||||
ledger_index="validated",
|
||||
))
|
||||
|
||||
total_value_outstanding = loan_status.result["node"]["TotalValueOutstanding"]
|
||||
loan_service_fee = loan_status.result["node"]["LoanServiceFee"]
|
||||
total_payment = str(int(total_value_outstanding) + int(loan_service_fee))
|
||||
|
||||
print(f"Amount Owed: {total_value_outstanding} TSTUSD")
|
||||
print(f"Loan Service Fee: {loan_service_fee} TSTUSD")
|
||||
print(f"Total Payment Due (including fees): {total_payment} TSTUSD")
|
||||
|
||||
# Prepare LoanPay transaction ----------------------
|
||||
print("\n=== Preparing LoanPay transaction ===\n")
|
||||
loan_pay_tx = LoanPay(
|
||||
account=borrower.address,
|
||||
loan_id=loan_id,
|
||||
amount=MPTAmount(mpt_issuance_id=mpt_id, value=total_payment),
|
||||
)
|
||||
|
||||
print(json.dumps(loan_pay_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Sign, submit, and wait for payment validation ----------------------
|
||||
print("\n=== Submitting LoanPay transaction ===\n")
|
||||
pay_response = submit_and_wait(loan_pay_tx, client, borrower)
|
||||
|
||||
if pay_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = pay_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to pay loan: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Loan paid successfully!")
|
||||
|
||||
# Extract updated loan info from transaction results ----------------------
|
||||
print("\n=== Loan Status After Payment ===\n")
|
||||
loan_node = next(
|
||||
node for node in pay_response.result["meta"]["AffectedNodes"]
|
||||
if node.get("ModifiedNode", {}).get("LedgerEntryType") == "Loan"
|
||||
)
|
||||
|
||||
final_balance = loan_node["ModifiedNode"]["FinalFields"].get("TotalValueOutstanding")
|
||||
if final_balance:
|
||||
print(f"Outstanding Loan Balance: {final_balance} TSTUSD")
|
||||
else:
|
||||
print("Outstanding Loan Balance: Loan fully paid off!")
|
||||
|
||||
# Prepare LoanDelete transaction ----------------------
|
||||
# Either the loan broker or borrower can submit this transaction.
|
||||
print("\n=== Preparing LoanDelete transaction ===\n")
|
||||
loan_delete_tx = LoanDelete(
|
||||
account=borrower.address,
|
||||
loan_id=loan_id,
|
||||
)
|
||||
|
||||
print(json.dumps(loan_delete_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Sign, submit, and wait for deletion validation ----------------------
|
||||
print("\n=== Submitting LoanDelete transaction ===\n")
|
||||
delete_response = submit_and_wait(loan_delete_tx, client, borrower)
|
||||
|
||||
if delete_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = delete_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to delete loan: {result_code}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Loan deleted successfully!")
|
||||
|
||||
# Verify loan deletion ----------------------
|
||||
print("\n=== Verifying Loan Deletion ===\n")
|
||||
verify_response = client.request(LedgerEntry(
|
||||
index=loan_id,
|
||||
ledger_index="validated",
|
||||
))
|
||||
|
||||
if verify_response.is_successful():
|
||||
print("Warning: Loan still exists in the ledger.")
|
||||
elif verify_response.result.get("error") == "entryNotFound":
|
||||
print("Loan has been successfully removed from the XRP Ledger!")
|
||||
else:
|
||||
print(f"Error checking loan status: {verify_response.result.get('error')}")
|
||||
@@ -1 +0,0 @@
|
||||
xrpl-py>=4.5.0
|
||||
@@ -1,3 +0,0 @@
|
||||
# Single Asset Vault Examples
|
||||
|
||||
Shows how to create, deposit into, and withdraw from single asset vaults on the XRP Ledger.
|
||||
@@ -1,187 +0,0 @@
|
||||
# Single Asset Vault Examples (JavaScript)
|
||||
|
||||
This directory contains JavaScript examples demonstrating how to create, deposit into, and withdraw from single asset vaults on the XRP Ledger.
|
||||
|
||||
## Setup
|
||||
|
||||
Install dependencies before running any examples:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create a Vault
|
||||
|
||||
```sh
|
||||
node createVault.js
|
||||
```
|
||||
|
||||
The script should output the VaultCreate transaction, vault ID, and complete vault information:
|
||||
|
||||
```sh
|
||||
Vault owner address: rLXZNDSS7gWvQZKunRUFiaViSiHo1yd4Ms
|
||||
MPT issuance ID: 0003E3B486D3DACD8BB468AB33793B9626BD894A92AB3AB4
|
||||
Permissioned domain ID: 3BB81D0D164456A2D74720F63FD923F16DE08FB3223D3ED103D09F525A8D69D1
|
||||
|
||||
|
||||
=== VaultCreate transaction ===
|
||||
{
|
||||
"TransactionType": "VaultCreate",
|
||||
"Account": "rLXZNDSS7gWvQZKunRUFiaViSiHo1yd4Ms",
|
||||
"Asset": {
|
||||
"mpt_issuance_id": "0003E3B486D3DACD8BB468AB33793B9626BD894A92AB3AB4"
|
||||
},
|
||||
"Flags": 65536,
|
||||
"DomainID": "3BB81D0D164456A2D74720F63FD923F16DE08FB3223D3ED103D09F525A8D69D1",
|
||||
"Data": "50726976617465207661756C74",
|
||||
"MPTokenMetadata": "7B226163223A2264656669222C226169223A7B226578616D706C655F696E666F223A2274657374227D2C2264223A2250726F706F7274696F6E616C206F776E65727368697020736861726573206F6620746865207661756C742E222C2269223A226578616D706C652E636F6D2F61737365742D69636F6E2E706E67222C22696E223A22417373657420497373756572204E616D65222C226E223A225661756C7420736861726573222C2274223A22534841524531222C227573223A5B7B2263223A2277656273697465222C2274223A2241737365742057656273697465222C2275223A226578616D706C652E636F6D2F6173736574227D2C7B2263223A22646F6373222C2274223A22446F6373222C2275223A226578616D706C652E636F6D2F646F6373227D5D7D",
|
||||
"AssetsMaximum": "0",
|
||||
"WithdrawalPolicy": 1
|
||||
}
|
||||
|
||||
=== Submitting VaultCreate transaction... ===
|
||||
Vault created successfully!
|
||||
|
||||
Vault ID: 9D25282C143F0F7F71F0E6FC7ABB3BD6FB30B7DCF04DF4A1E31C701B1B332D29
|
||||
Vault pseudo-account address: rnBAKKEBBTqswakdeJJkZtBs9SRgpMkThj
|
||||
Share MPT issuance ID: 000000012DF200D67FF9DA7686FF8B6F32097337D7765211
|
||||
|
||||
=== Getting vault_info... ===
|
||||
{
|
||||
"api_version": 2,
|
||||
"id": 12,
|
||||
"result": {
|
||||
"ledger_hash": "73B53C0608A9C87C2B97314F0BAD109F236C4A95FB53FE4E8CEAEFE826A1E7AB",
|
||||
"ledger_index": 597229,
|
||||
"validated": true,
|
||||
"vault": {
|
||||
"Account": "rnBAKKEBBTqswakdeJJkZtBs9SRgpMkThj",
|
||||
"Asset": {
|
||||
"mpt_issuance_id": "0003E3B486D3DACD8BB468AB33793B9626BD894A92AB3AB4"
|
||||
},
|
||||
"Data": "50726976617465207661756C74",
|
||||
"Flags": 65536,
|
||||
"LedgerEntryType": "Vault",
|
||||
"Owner": "rLXZNDSS7gWvQZKunRUFiaViSiHo1yd4Ms",
|
||||
"OwnerNode": "0",
|
||||
"PreviousTxnID": "8B64609225F802258250824B2C6C0A8B752AB8CBB6FAF64D433DC2F35C09E131",
|
||||
"PreviousTxnLgrSeq": 597229,
|
||||
"Sequence": 597228,
|
||||
"ShareMPTID": "000000012DF200D67FF9DA7686FF8B6F32097337D7765211",
|
||||
"WithdrawalPolicy": 1,
|
||||
"index": "9D25282C143F0F7F71F0E6FC7ABB3BD6FB30B7DCF04DF4A1E31C701B1B332D29",
|
||||
"shares": {
|
||||
"DomainID": "3BB81D0D164456A2D74720F63FD923F16DE08FB3223D3ED103D09F525A8D69D1",
|
||||
"Flags": 60,
|
||||
"Issuer": "rnBAKKEBBTqswakdeJJkZtBs9SRgpMkThj",
|
||||
"LedgerEntryType": "MPTokenIssuance",
|
||||
"MPTokenMetadata": "7B226163223A2264656669222C226169223A7B226578616D706C655F696E666F223A2274657374227D2C2264223A2250726F706F7274696F6E616C206F776E65727368697020736861726573206F6620746865207661756C742E222C2269223A226578616D706C652E636F6D2F61737365742D69636F6E2E706E67222C22696E223A22417373657420497373756572204E616D65222C226E223A225661756C7420736861726573222C2274223A22534841524531222C227573223A5B7B2263223A2277656273697465222C2274223A2241737365742057656273697465222C2275223A226578616D706C652E636F6D2F6173736574227D2C7B2263223A22646F6373222C2274223A22446F6373222C2275223A226578616D706C652E636F6D2F646F6373227D5D7D",
|
||||
"OutstandingAmount": "0",
|
||||
"OwnerNode": "0",
|
||||
"PreviousTxnID": "8B64609225F802258250824B2C6C0A8B752AB8CBB6FAF64D433DC2F35C09E131",
|
||||
"PreviousTxnLgrSeq": 597229,
|
||||
"Sequence": 1,
|
||||
"index": "4C3CC0AF1FE27EBE364F02AFF889D73D1F6F7CB5ED6126D1CD605E8952E18302",
|
||||
"mpt_issuance_id": "000000012DF200D67FF9DA7686FF8B6F32097337D7765211"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "response"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deposit into a Vault
|
||||
|
||||
```sh
|
||||
node deposit.js
|
||||
```
|
||||
|
||||
The script should output the vault state before and after the deposit, along with the depositor's share balance:
|
||||
|
||||
```sh
|
||||
Depositor address: rnEmvWahVbNXzs8zGjhEfkBwo41Zn5wDDU
|
||||
Vault ID: 6AC4EC2D775C6275D314996D6ECDD16DCB9382A29FDB769951C42192FCED76EF
|
||||
Asset MPT issuance ID: 0003E3B486D3DACD8BB468AB33793B9626BD894A92AB3AB4
|
||||
Vault share MPT issuance ID: 0000000152E7CD364F869E832EDB806C4A7AD8B3D0C151C5
|
||||
|
||||
=== Getting initial vault state... ===
|
||||
- Total vault value: 1
|
||||
- Available assets: 1
|
||||
|
||||
=== Checking depositor's balance... ===
|
||||
Balance: 9937
|
||||
|
||||
=== VaultDeposit transaction ===
|
||||
{
|
||||
"TransactionType": "VaultDeposit",
|
||||
"Account": "rnEmvWahVbNXzs8zGjhEfkBwo41Zn5wDDU",
|
||||
"VaultID": "6AC4EC2D775C6275D314996D6ECDD16DCB9382A29FDB769951C42192FCED76EF",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0003E3B486D3DACD8BB468AB33793B9626BD894A92AB3AB4",
|
||||
"value": "1"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting VaultDeposit transaction... ===
|
||||
Deposit successful!
|
||||
|
||||
=== Vault state after deposit ===
|
||||
- Total vault value: 2
|
||||
- Available assets: 2
|
||||
|
||||
=== Depositor's share balance ==
|
||||
Shares held: 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Withdraw from a Vault
|
||||
|
||||
```sh
|
||||
node withdraw.js
|
||||
```
|
||||
|
||||
The script should output the vault state before and after the withdrawal, along with updated share and asset balances:
|
||||
|
||||
```sh
|
||||
Depositor address: rnEmvWahVbNXzs8zGjhEfkBwo41Zn5wDDU
|
||||
Vault ID: 6AC4EC2D775C6275D314996D6ECDD16DCB9382A29FDB769951C42192FCED76EF
|
||||
Asset MPT issuance ID: 0003E3B486D3DACD8BB468AB33793B9626BD894A92AB3AB4
|
||||
Vault share MPT issuance ID: 0000000152E7CD364F869E832EDB806C4A7AD8B3D0C151C5
|
||||
|
||||
=== Getting initial vault state... ===
|
||||
Initial vault state:
|
||||
Assets Total: 2
|
||||
Assets Available: 2
|
||||
|
||||
=== Checking depositor's share balance... ===
|
||||
Shares held: 2
|
||||
|
||||
=== Preparing VaultWithdraw transaction ===
|
||||
{
|
||||
"TransactionType": "VaultWithdraw",
|
||||
"Account": "rnEmvWahVbNXzs8zGjhEfkBwo41Zn5wDDU",
|
||||
"VaultID": "6AC4EC2D775C6275D314996D6ECDD16DCB9382A29FDB769951C42192FCED76EF",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "0003E3B486D3DACD8BB468AB33793B9626BD894A92AB3AB4",
|
||||
"value": "1"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting VaultWithdraw transaction... ===
|
||||
Withdrawal successful!
|
||||
|
||||
=== Vault state after withdrawal ===
|
||||
Assets Total: 1
|
||||
Assets Available: 1
|
||||
|
||||
=== Depositor's share balance ==
|
||||
Shares held: 1
|
||||
|
||||
=== Depositor's asset balance ==
|
||||
Balance: 9937
|
||||
```
|
||||
@@ -1,111 +0,0 @@
|
||||
import xrpl from "xrpl"
|
||||
import { execSync } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
// Auto-run setup if needed
|
||||
if (!fs.existsSync("vaultSetup.json")) {
|
||||
console.log(`\n=== Vault setup data doesn't exist. Running setup script... ===\n`)
|
||||
execSync("node vaultSetup.js", { stdio: "inherit" })
|
||||
}
|
||||
|
||||
// Load setup data
|
||||
const setupData = JSON.parse(fs.readFileSync("vaultSetup.json", "utf8"))
|
||||
|
||||
// Connect to the network
|
||||
const client = new xrpl.Client("wss://s.devnet.rippletest.net:51233")
|
||||
await client.connect()
|
||||
|
||||
// Create and fund vault owner account
|
||||
const { wallet: vaultOwner } = await client.fundWallet()
|
||||
|
||||
// You can replace these values with your own
|
||||
const mptIssuanceId = setupData.mptIssuanceId
|
||||
const domainId = setupData.domainId
|
||||
|
||||
console.log(`Vault owner address: ${vaultOwner.address}`)
|
||||
console.log(`MPT issuance ID: ${mptIssuanceId}`)
|
||||
console.log(`Permissioned domain ID: ${domainId}\n`)
|
||||
|
||||
// Prepare VaultCreate transaction ----------------------
|
||||
console.log(`\n=== VaultCreate transaction ===`)
|
||||
const vaultCreateTx = {
|
||||
TransactionType: "VaultCreate",
|
||||
Account: vaultOwner.address,
|
||||
Asset: { mpt_issuance_id: mptIssuanceId },
|
||||
Flags: xrpl.VaultCreateFlags.tfVaultPrivate, // Omit tfVaultPrivate flag for public vaults
|
||||
// To make vault shares non-transferable add the tfVaultShareNonTransferable flag:
|
||||
// Flags: xrpl.VaultCreateFlags.tfVaultPrivate | xrpl.VaultCreateFlags.tfVaultShareNonTransferable
|
||||
DomainID: domainId, // Omit for public vaults
|
||||
// Convert Vault data to a string (without excess whitespace), then string to hex.
|
||||
Data: xrpl.convertStringToHex(JSON.stringify(
|
||||
{ n: "LATAM Fund II", w: "examplefund.com" })
|
||||
),
|
||||
// Encode JSON metadata as hex string per XLS-89 MPT Metadata Schema.
|
||||
// See: https://xls.xrpl.org/xls/XLS-0089-multi-purpose-token-metadata-schema.html
|
||||
MPTokenMetadata: xrpl.encodeMPTokenMetadata({
|
||||
ticker: "SHARE1",
|
||||
name: "Vault shares",
|
||||
desc: "Proportional ownership shares of the vault.",
|
||||
icon: "example.com/asset-icon.png",
|
||||
asset_class: "defi",
|
||||
issuer_name: "Asset Issuer Name",
|
||||
uris: [
|
||||
{
|
||||
uri: "example.com/asset",
|
||||
category: "website",
|
||||
title: "Asset Website",
|
||||
},
|
||||
{
|
||||
uri: "example.com/docs",
|
||||
category: "docs",
|
||||
title: "Docs",
|
||||
},
|
||||
],
|
||||
additional_info: {
|
||||
example_info: "test",
|
||||
},
|
||||
}),
|
||||
AssetsMaximum: "0", // No cap
|
||||
WithdrawalPolicy: xrpl.VaultWithdrawalPolicy.vaultStrategyFirstComeFirstServe,
|
||||
};
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(vaultCreateTx)
|
||||
console.log(JSON.stringify(vaultCreateTx, null, 2))
|
||||
|
||||
// Submit, sign, and wait for validation ----------------------
|
||||
console.log("\n=== Submitting VaultCreate transaction... ===")
|
||||
const submit_response = await client.submitAndWait(vaultCreateTx, {
|
||||
wallet: vaultOwner,
|
||||
autofill: true,
|
||||
})
|
||||
if (submit_response.result.meta.TransactionResult !== "tesSUCCESS") {
|
||||
const result_code = submit_response.result.meta.TransactionResult;
|
||||
console.error("Error: Unable to create vault:", result_code)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log("Vault created successfully!")
|
||||
|
||||
// Extract vault information from the transaction result
|
||||
const affectedNodes = submit_response.result.meta.AffectedNodes || []
|
||||
const vaultNode = affectedNodes.find(
|
||||
(node) => node.CreatedNode?.LedgerEntryType === "Vault"
|
||||
)
|
||||
if (vaultNode) {
|
||||
console.log(`\nVault ID: ${vaultNode.CreatedNode.LedgerIndex}`)
|
||||
console.log(`Vault pseudo-account address: ${vaultNode.CreatedNode.NewFields.Account}`)
|
||||
console.log(`Share MPT issuance ID: ${vaultNode.CreatedNode.NewFields.ShareMPTID}`)
|
||||
}
|
||||
|
||||
// Call vault_info method to retrieve the vault's information
|
||||
console.log("\n=== Getting vault_info... ===")
|
||||
const vaultID = vaultNode.CreatedNode.LedgerIndex
|
||||
const vault_info_response = await client.request({
|
||||
command: "vault_info",
|
||||
vault_id: vaultID,
|
||||
ledger_index: "validated"
|
||||
})
|
||||
console.log(JSON.stringify(vault_info_response, null, 2))
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,147 +0,0 @@
|
||||
// IMPORTANT: This example deposits into an existing PRIVATE vault.
|
||||
// The depositor account used has valid credentials in the vault's Permissioned Domain.
|
||||
// Without valid credentials, the VaultDeposit transaction will fail.
|
||||
// If you want to deposit into a public vault, you can replace the vaultID and shareMPTIssuanceId
|
||||
// values with your own.
|
||||
|
||||
import xrpl from "xrpl"
|
||||
import { execSync } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
// Auto-run setup if needed
|
||||
if (!fs.existsSync("vaultSetup.json")) {
|
||||
console.log(`\n=== Vault setup data doesn't exist. Running setup script... ===\n`)
|
||||
execSync("node vaultSetup.js", { stdio: "inherit" })
|
||||
}
|
||||
|
||||
// Load setup data
|
||||
const setupData = JSON.parse(fs.readFileSync("vaultSetup.json", "utf8"))
|
||||
|
||||
// Connect to the network
|
||||
const client = new xrpl.Client("wss://s.devnet.rippletest.net:51233")
|
||||
await client.connect()
|
||||
|
||||
// You can replace these values with your own
|
||||
const depositor = xrpl.Wallet.fromSeed(setupData.depositor.seed)
|
||||
const vaultID = setupData.vaultID
|
||||
const assetMPTIssuanceId = setupData.mptIssuanceId
|
||||
const shareMPTIssuanceId = setupData.vaultShareMPTIssuanceId
|
||||
|
||||
console.log(`Depositor address: ${depositor.address}`)
|
||||
console.log(`Vault ID: ${vaultID}`)
|
||||
console.log(`Asset MPT issuance ID: ${assetMPTIssuanceId}`)
|
||||
console.log(`Vault share MPT issuance ID: ${shareMPTIssuanceId}`)
|
||||
|
||||
const depositAmount = 1
|
||||
|
||||
// Get initial vault state ----------------------
|
||||
console.log("\n=== Getting initial vault state... ===")
|
||||
const initialVaultInfo = await client.request({
|
||||
command: "vault_info",
|
||||
vault_id: vaultID,
|
||||
ledger_index: "validated"
|
||||
})
|
||||
|
||||
console.log(` - Total vault value: ${initialVaultInfo.result.vault.AssetsTotal}`)
|
||||
console.log(` - Available assets: ${initialVaultInfo.result.vault.AssetsAvailable}`)
|
||||
|
||||
// Check depositor's asset balance ----------------------
|
||||
console.log("\n=== Checking depositor's balance... ===")
|
||||
try {
|
||||
// Use ledger_entry to get specific MPT issuance balance
|
||||
const ledgerEntryResult = await client.request({
|
||||
command: "ledger_entry",
|
||||
mptoken: {
|
||||
mpt_issuance_id: assetMPTIssuanceId,
|
||||
account: depositor.address
|
||||
},
|
||||
ledger_index: "validated"
|
||||
})
|
||||
|
||||
const balance = ledgerEntryResult.result.node?.MPTAmount
|
||||
console.log(`Balance: ${balance}`)
|
||||
|
||||
// Check if balance is sufficient
|
||||
if (balance < depositAmount) {
|
||||
console.error(`Error: Insufficient balance! Have ${balance}, need ${depositAmount}`)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.data?.error === 'entryNotFound') {
|
||||
console.error(`Error: The depositor doesn't hold any assets with ID: ${assetMPTIssuanceId}`)
|
||||
} else {
|
||||
console.error(`Error checking MPT: ${error}`)
|
||||
}
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Prepare VaultDeposit transaction ----------------------
|
||||
console.log(`\n=== VaultDeposit transaction ===`)
|
||||
const vaultDepositTx = {
|
||||
TransactionType: "VaultDeposit",
|
||||
Account: depositor.address,
|
||||
VaultID: vaultID,
|
||||
Amount: {
|
||||
mpt_issuance_id: assetMPTIssuanceId,
|
||||
value: depositAmount.toString()
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(vaultDepositTx)
|
||||
console.log(JSON.stringify(vaultDepositTx, null, 2))
|
||||
|
||||
// Submit VaultDeposit transaction ----------------------
|
||||
console.log("\n=== Submitting VaultDeposit transaction... ===")
|
||||
const depositResult = await client.submitAndWait(vaultDepositTx, {
|
||||
wallet: depositor,
|
||||
autofill: true,
|
||||
})
|
||||
if (depositResult.result.meta.TransactionResult !== "tesSUCCESS") {
|
||||
const result_code = depositResult.result.meta.TransactionResult
|
||||
console.error("Error: Unable to deposit:", result_code)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log("Deposit successful!")
|
||||
|
||||
// Extract vault state from transaction metadata ----------------------
|
||||
console.log("\n=== Vault state after deposit ===")
|
||||
const affectedNodes = depositResult.result.meta.AffectedNodes
|
||||
const vaultNode = affectedNodes.find(
|
||||
(node) => {
|
||||
return (
|
||||
node.ModifiedNode &&
|
||||
node.ModifiedNode.LedgerEntryType === "Vault" &&
|
||||
node.ModifiedNode.LedgerIndex === vaultID
|
||||
)
|
||||
}
|
||||
)
|
||||
if (vaultNode) {
|
||||
const vaultFields = vaultNode.ModifiedNode.FinalFields
|
||||
console.log(` - Total vault value: ${vaultFields.AssetsTotal}`)
|
||||
console.log(` - Available assets: ${vaultFields.AssetsAvailable}`)
|
||||
}
|
||||
|
||||
// Get the depositor's share balance ----------------------
|
||||
console.log("\n=== Depositor's share balance ==")
|
||||
const depositorShareNode = affectedNodes.find((node) => {
|
||||
const shareNode = node.ModifiedNode || node.CreatedNode
|
||||
const fields = shareNode?.FinalFields || shareNode?.NewFields
|
||||
return (
|
||||
shareNode &&
|
||||
shareNode.LedgerEntryType === "MPToken" &&
|
||||
fields?.Account === depositor.address &&
|
||||
fields?.MPTokenIssuanceID === shareMPTIssuanceId
|
||||
)
|
||||
})
|
||||
if (depositorShareNode) {
|
||||
const shareNode = depositorShareNode.ModifiedNode || depositorShareNode.CreatedNode
|
||||
const shareFields = shareNode.FinalFields || shareNode.NewFields
|
||||
console.log(`Shares held: ${shareFields.MPTAmount}`)
|
||||
}
|
||||
|
||||
await client.disconnect()
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "vault-examples",
|
||||
"description": "Example code for creating and managing vaults",
|
||||
"dependencies": {
|
||||
"xrpl": "^4.5.0"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
import xrpl from 'xrpl'
|
||||
import fs from 'fs'
|
||||
|
||||
// Setup script for vault tutorials
|
||||
|
||||
process.stdout.write('Setting up tutorial: 0/5\r')
|
||||
|
||||
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
// Create and fund all wallets
|
||||
const [
|
||||
{ wallet: mptIssuer },
|
||||
{ wallet: domainOwner },
|
||||
{ wallet: depositor },
|
||||
{ wallet: vaultOwner }
|
||||
] = await Promise.all([
|
||||
client.fundWallet(),
|
||||
client.fundWallet(),
|
||||
client.fundWallet(),
|
||||
client.fundWallet()
|
||||
])
|
||||
|
||||
// Step 1: Create MPT issuance, permissioned domain, and credentials in parallel
|
||||
process.stdout.write('Setting up tutorial: 1/5\r')
|
||||
|
||||
const credType = 'VaultAccess'
|
||||
const [mptCreateResult] = await Promise.all([
|
||||
client.submitAndWait(
|
||||
{
|
||||
TransactionType: "MPTokenIssuanceCreate",
|
||||
Account: mptIssuer.address,
|
||||
Flags:
|
||||
xrpl.MPTokenIssuanceCreateFlags.tfMPTCanTransfer |
|
||||
xrpl.MPTokenIssuanceCreateFlags.tfMPTCanLock,
|
||||
AssetScale: 2,
|
||||
TransferFee: 0,
|
||||
MaximumAmount: "1000000000000",
|
||||
MPTokenMetadata: xrpl.encodeMPTokenMetadata({
|
||||
ticker: "USTST",
|
||||
name: "USTST Stablecoin",
|
||||
desc: "A test stablecoin token",
|
||||
icon: "example.org/ustst-icon.png",
|
||||
asset_class: "rwa",
|
||||
asset_subclass: "stablecoin",
|
||||
issuer_name: "Test Stablecoin Inc",
|
||||
uris: [
|
||||
{
|
||||
uri: "example.org/ustst",
|
||||
category: "website",
|
||||
title: "USTST Official Website",
|
||||
},
|
||||
{
|
||||
uri: "example.org/ustst/reserves",
|
||||
category: "attestation",
|
||||
title: "Reserve Attestation Reports",
|
||||
},
|
||||
{
|
||||
uri: "example.org/ustst/docs",
|
||||
category: "docs",
|
||||
title: "USTST Documentation",
|
||||
},
|
||||
],
|
||||
additional_info: {
|
||||
backing: "USD",
|
||||
reserve_ratio: "1:1",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ wallet: mptIssuer, autofill: true },
|
||||
),
|
||||
client.submitAndWait(
|
||||
{
|
||||
TransactionType: "Batch",
|
||||
Account: domainOwner.address,
|
||||
Flags: xrpl.BatchFlags.tfAllOrNothing,
|
||||
RawTransactions: [
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: "PermissionedDomainSet",
|
||||
Account: domainOwner.address,
|
||||
AcceptedCredentials: [
|
||||
{
|
||||
Credential: {
|
||||
Issuer: domainOwner.address,
|
||||
CredentialType: xrpl.convertStringToHex(credType),
|
||||
},
|
||||
},
|
||||
],
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn,
|
||||
},
|
||||
},
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: "CredentialCreate",
|
||||
Account: domainOwner.address,
|
||||
Subject: depositor.address,
|
||||
CredentialType: xrpl.convertStringToHex(credType),
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ wallet: domainOwner, autofill: true },
|
||||
),
|
||||
]);
|
||||
|
||||
const mptIssuanceId = mptCreateResult.result.meta.mpt_issuance_id
|
||||
|
||||
// Get domain ID
|
||||
const domainOwnerObjects = await client.request({
|
||||
command: 'account_objects',
|
||||
account: domainOwner.address,
|
||||
ledger_index: 'validated'
|
||||
})
|
||||
const domainId = domainOwnerObjects.result.account_objects.find(
|
||||
(node) => node.LedgerEntryType === 'PermissionedDomain'
|
||||
).index
|
||||
|
||||
// Step 2: Depositor accepts credential, authorizes MPT, and creates vault in parallel
|
||||
process.stdout.write('Setting up tutorial: 2/5\r')
|
||||
|
||||
const [, vaultCreateResult] = await Promise.all([
|
||||
client.submitAndWait(
|
||||
{
|
||||
TransactionType: 'Batch',
|
||||
Account: depositor.address,
|
||||
Flags: xrpl.BatchFlags.tfAllOrNothing,
|
||||
RawTransactions: [
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'CredentialAccept',
|
||||
Account: depositor.address,
|
||||
Issuer: domainOwner.address,
|
||||
CredentialType: xrpl.convertStringToHex(credType),
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
},
|
||||
{
|
||||
RawTransaction: {
|
||||
TransactionType: 'MPTokenAuthorize',
|
||||
Account: depositor.address,
|
||||
MPTokenIssuanceID: mptIssuanceId,
|
||||
Flags: xrpl.GlobalFlags.tfInnerBatchTxn
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ wallet: depositor, autofill: true }
|
||||
),
|
||||
client.submitAndWait(
|
||||
{
|
||||
TransactionType: 'VaultCreate',
|
||||
Account: vaultOwner.address,
|
||||
Asset: {
|
||||
mpt_issuance_id: mptIssuanceId
|
||||
},
|
||||
Flags: xrpl.VaultCreateFlags.tfVaultPrivate,
|
||||
DomainID: domainId,
|
||||
Data: xrpl.convertStringToHex(
|
||||
JSON.stringify({ n: "LATAM Fund II", w: "examplefund.com" })
|
||||
),
|
||||
MPTokenMetadata: xrpl.encodeMPTokenMetadata({
|
||||
ticker: 'SHARE1',
|
||||
name: 'Vault Shares',
|
||||
desc: 'Proportional ownership shares of the vault',
|
||||
icon: 'example.com/vault-shares-icon.png',
|
||||
asset_class: 'defi',
|
||||
issuer_name: 'Vault Owner',
|
||||
uris: [
|
||||
{
|
||||
uri: 'example.com/asset',
|
||||
category: 'website',
|
||||
title: 'Asset Website'
|
||||
},
|
||||
{
|
||||
uri: 'example.com/docs',
|
||||
category: 'docs',
|
||||
title: 'Docs'
|
||||
}
|
||||
],
|
||||
additional_info: {
|
||||
example_info: 'test'
|
||||
}
|
||||
}),
|
||||
AssetsMaximum: '0',
|
||||
WithdrawalPolicy: xrpl.VaultWithdrawalPolicy.vaultStrategyFirstComeFirstServe
|
||||
},
|
||||
{ wallet: vaultOwner, autofill: true }
|
||||
)
|
||||
])
|
||||
|
||||
const vaultNode = vaultCreateResult.result.meta.AffectedNodes.find(
|
||||
(node) => node.CreatedNode?.LedgerEntryType === 'Vault'
|
||||
)
|
||||
const vaultID = vaultNode.CreatedNode.LedgerIndex
|
||||
const vaultShareMPTIssuanceId = vaultNode.CreatedNode.NewFields.ShareMPTID
|
||||
|
||||
// Step 3: Issuer sends payment to depositor
|
||||
process.stdout.write('Setting up tutorial: 3/5\r')
|
||||
|
||||
const paymentResult = await client.submitAndWait(
|
||||
{
|
||||
TransactionType: 'Payment',
|
||||
Account: mptIssuer.address,
|
||||
Destination: depositor.address,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptIssuanceId,
|
||||
value: '10000'
|
||||
}
|
||||
},
|
||||
{ wallet: mptIssuer, autofill: true }
|
||||
)
|
||||
|
||||
if (paymentResult.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error('\nPayment failed:', paymentResult.result.meta.TransactionResult)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Step 4: Make an initial deposit so withdraw example has shares to work with
|
||||
process.stdout.write('Setting up tutorial: 4/5\r')
|
||||
|
||||
const initialDepositResult = await client.submitAndWait(
|
||||
{
|
||||
TransactionType: 'VaultDeposit',
|
||||
Account: depositor.address,
|
||||
VaultID: vaultID,
|
||||
Amount: {
|
||||
mpt_issuance_id: mptIssuanceId,
|
||||
value: '1000'
|
||||
}
|
||||
},
|
||||
{ wallet: depositor, autofill: true }
|
||||
)
|
||||
|
||||
if (initialDepositResult.result.meta.TransactionResult !== 'tesSUCCESS') {
|
||||
console.error('\nInitial deposit failed:', initialDepositResult.result.meta.TransactionResult)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Step 5: Save setup data to file
|
||||
process.stdout.write('Setting up tutorial: 5/5\r')
|
||||
|
||||
const setupData = {
|
||||
mptIssuer: {
|
||||
address: mptIssuer.address,
|
||||
seed: mptIssuer.seed
|
||||
},
|
||||
mptIssuanceId,
|
||||
domainOwner: {
|
||||
address: domainOwner.address,
|
||||
seed: domainOwner.seed
|
||||
},
|
||||
domainId,
|
||||
credentialType: credType,
|
||||
depositor: {
|
||||
address: depositor.address,
|
||||
seed: depositor.seed
|
||||
},
|
||||
vaultOwner: {
|
||||
address: vaultOwner.address,
|
||||
seed: vaultOwner.seed
|
||||
},
|
||||
vaultID,
|
||||
vaultShareMPTIssuanceId
|
||||
}
|
||||
|
||||
fs.writeFileSync('vaultSetup.json', JSON.stringify(setupData, null, 2))
|
||||
|
||||
process.stdout.write('Setting up tutorial: Complete!\n')
|
||||
|
||||
await client.disconnect()
|
||||
@@ -1,161 +0,0 @@
|
||||
import xrpl from "xrpl"
|
||||
import { execSync } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
// Auto-run setup if needed
|
||||
if (!fs.existsSync("vaultSetup.json")) {
|
||||
console.log(`\n=== Vault setup data doesn't exist. Running setup script... ===\n`)
|
||||
execSync("node vaultSetup.js", { stdio: "inherit" })
|
||||
}
|
||||
|
||||
// Load setup data
|
||||
const setupData = JSON.parse(fs.readFileSync("vaultSetup.json", "utf8"))
|
||||
|
||||
// Connect to the network
|
||||
const client = new xrpl.Client("wss://s.devnet.rippletest.net:51233")
|
||||
await client.connect()
|
||||
|
||||
// You can replace these values with your own
|
||||
const depositor = xrpl.Wallet.fromSeed(setupData.depositor.seed)
|
||||
const vaultID = setupData.vaultID
|
||||
const assetMPTIssuanceId = setupData.mptIssuanceId
|
||||
const shareMPTIssuanceId = setupData.vaultShareMPTIssuanceId
|
||||
|
||||
console.log(`Depositor address: ${depositor.address}`)
|
||||
console.log(`Vault ID: ${vaultID}`)
|
||||
console.log(`Asset MPT issuance ID: ${assetMPTIssuanceId}`)
|
||||
console.log(`Vault share MPT issuance ID: ${shareMPTIssuanceId}`)
|
||||
|
||||
const withdrawAmount = "1"
|
||||
|
||||
// Get initial vault state ----------------------
|
||||
console.log("\n=== Getting initial vault state... ===")
|
||||
const initialVaultInfo = await client.request({
|
||||
command: "vault_info",
|
||||
vault_id: vaultID,
|
||||
ledger_index: "validated"
|
||||
})
|
||||
|
||||
console.log(`Initial vault state:`)
|
||||
console.log(` Assets Total: ${initialVaultInfo.result.vault.AssetsTotal}`)
|
||||
console.log(` Assets Available: ${initialVaultInfo.result.vault.AssetsAvailable}`)
|
||||
|
||||
// Check depositor's share balance ----------------------
|
||||
console.log("\n=== Checking depositor's share balance... ===")
|
||||
try {
|
||||
const shareBalanceResult = await client.request({
|
||||
command: "ledger_entry",
|
||||
mptoken: {
|
||||
mpt_issuance_id: shareMPTIssuanceId,
|
||||
account: depositor.address
|
||||
},
|
||||
ledger_index: "validated"
|
||||
})
|
||||
|
||||
const shareBalance = shareBalanceResult.result.node?.MPTAmount
|
||||
console.log(`Shares held: ${shareBalance}`)
|
||||
} catch (error) {
|
||||
if (error.data?.error === 'entryNotFound') {
|
||||
console.error(`Error: The depositor doesn't hold any vault shares with ID: ${shareMPTIssuanceId}.`)
|
||||
} else {
|
||||
console.error(`Error checking MPT: ${error}`)
|
||||
}
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Prepare VaultWithdraw transaction ----------------------
|
||||
console.log(`\n=== Preparing VaultWithdraw transaction ===`)
|
||||
const vaultWithdrawTx = {
|
||||
TransactionType: "VaultWithdraw",
|
||||
Account: depositor.address,
|
||||
VaultID: vaultID,
|
||||
Amount: {
|
||||
mpt_issuance_id: assetMPTIssuanceId,
|
||||
value: withdrawAmount
|
||||
},
|
||||
// Optional: Add Destination field to send assets to a different account
|
||||
// Destination: "rGg4tHPRGJfewwJkd8immCFx9uSo2GgcoY"
|
||||
}
|
||||
|
||||
// Validate the transaction structure before submitting
|
||||
xrpl.validate(vaultWithdrawTx)
|
||||
console.log(JSON.stringify(vaultWithdrawTx, null, 2))
|
||||
|
||||
// Submit VaultWithdraw transaction ----------------------
|
||||
console.log("\n=== Submitting VaultWithdraw transaction... ===")
|
||||
const withdrawResult = await client.submitAndWait(vaultWithdrawTx, {
|
||||
wallet: depositor,
|
||||
autofill: true,
|
||||
})
|
||||
if (withdrawResult.result.meta.TransactionResult !== "tesSUCCESS") {
|
||||
const result_code = withdrawResult.result.meta.TransactionResult
|
||||
console.error("Error: Unable to withdraw from vault:", result_code)
|
||||
await client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
console.log("Withdrawal successful!")
|
||||
|
||||
// Extract vault state from transaction metadata ----------------------
|
||||
console.log("\n=== Vault state after withdrawal ===")
|
||||
const affectedNodes = withdrawResult.result.meta.AffectedNodes
|
||||
const vaultNode = affectedNodes.find(
|
||||
(node) => {
|
||||
const modifiedNode = node.ModifiedNode || node.DeletedNode
|
||||
return (
|
||||
modifiedNode &&
|
||||
modifiedNode.LedgerEntryType === "Vault" &&
|
||||
modifiedNode.LedgerIndex === vaultID
|
||||
)
|
||||
}
|
||||
)
|
||||
if (vaultNode) {
|
||||
if (vaultNode.DeletedNode) {
|
||||
console.log(` Vault empty (all assets withdrawn)`)
|
||||
} else {
|
||||
const vaultFields = vaultNode.ModifiedNode.FinalFields
|
||||
console.log(` Assets Total: ${vaultFields.AssetsTotal}`)
|
||||
console.log(` Assets Available: ${vaultFields.AssetsAvailable}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the depositor's share balance ----------------------
|
||||
console.log("\n=== Depositor's share balance ==")
|
||||
const depositorShareNode = affectedNodes.find((node) => {
|
||||
const modifiedNode = node.ModifiedNode || node.DeletedNode
|
||||
return (
|
||||
modifiedNode &&
|
||||
modifiedNode.LedgerEntryType === "MPToken" &&
|
||||
modifiedNode.FinalFields?.Account === depositor.address &&
|
||||
modifiedNode.FinalFields?.MPTokenIssuanceID === shareMPTIssuanceId
|
||||
)
|
||||
})
|
||||
if (depositorShareNode) {
|
||||
if (depositorShareNode.DeletedNode) {
|
||||
console.log(`No more shares held (redeemed all shares)`)
|
||||
} else {
|
||||
const shareFields = depositorShareNode.ModifiedNode.FinalFields
|
||||
console.log(`Shares held: ${shareFields.MPTAmount}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the depositor's asset balance ----------------------
|
||||
console.log("\n=== Depositor's asset balance ==")
|
||||
const depositorAssetNode = affectedNodes.find((node) => {
|
||||
const assetNode = node.ModifiedNode || node.CreatedNode
|
||||
const fields = assetNode?.FinalFields || assetNode?.NewFields
|
||||
return (
|
||||
assetNode &&
|
||||
assetNode.LedgerEntryType === "MPToken" &&
|
||||
fields?.Account === depositor.address &&
|
||||
fields?.MPTokenIssuanceID === assetMPTIssuanceId
|
||||
)
|
||||
})
|
||||
if (depositorAssetNode) {
|
||||
const assetNode = depositorAssetNode.ModifiedNode || depositorAssetNode.CreatedNode
|
||||
const assetFields = assetNode.FinalFields || assetNode.NewFields
|
||||
console.log(`Balance: ${assetFields.MPTAmount}`)
|
||||
}
|
||||
|
||||
await client.disconnect()
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
# Single Asset Vault Examples (Python)
|
||||
|
||||
This directory contains Python examples demonstrating how to create, deposit into, and withdraw from single asset vaults on the XRP Ledger.
|
||||
|
||||
## Setup
|
||||
|
||||
Install dependencies before running any examples:
|
||||
|
||||
```sh
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create a Vault
|
||||
|
||||
```sh
|
||||
python create_vault.py
|
||||
```
|
||||
|
||||
The script should output the VaultCreate transaction, vault ID, and complete vault information:
|
||||
|
||||
```sh
|
||||
Vault owner address: rfsTcqjyg7j2xfJFNbd9u8mt65yrGZvLnu
|
||||
MPT issuance ID: 00385B21AF216177F319AC73F25F0FCBCDA09330D1D50D03
|
||||
Permissioned domain ID: 76397457A19E093654F74848E5255E6111FDC0A2BF9FB2143F7C2C33424E1B3E
|
||||
|
||||
|
||||
=== VaultCreate transaction ===
|
||||
{
|
||||
"Account": "rfsTcqjyg7j2xfJFNbd9u8mt65yrGZvLnu",
|
||||
"TransactionType": "VaultCreate",
|
||||
"Flags": 65536,
|
||||
"SigningPubKey": "",
|
||||
"Asset": {
|
||||
"mpt_issuance_id": "00385B21AF216177F319AC73F25F0FCBCDA09330D1D50D03"
|
||||
},
|
||||
"Data": "7b226e223a20224c4154414d2046756e64204949222c202277223a20226578616d706c6566756e642e636f6d227d",
|
||||
"AssetsMaximum": "0",
|
||||
"MPTokenMetadata": "7B226163223A2264656669222C226169223A7B226578616D706C655F696E666F223A2274657374227D2C2264223A2250726F706F7274696F6E616C206F776E65727368697020736861726573206F6620746865207661756C742E222C2269223A226578616D706C652E636F6D2F61737365742D69636F6E2E706E67222C22696E223A22417373657420497373756572204E616D65222C226E223A225661756C7420736861726573222C2274223A22534841524531222C227573223A5B7B2263223A2277656273697465222C2274223A2241737365742057656273697465222C2275223A226578616D706C652E636F6D2F6173736574227D2C7B2263223A22646F6373222C2274223A22446F6373222C2275223A226578616D706C652E636F6D2F646F6373227D5D7D",
|
||||
"DomainID": "76397457A19E093654F74848E5255E6111FDC0A2BF9FB2143F7C2C33424E1B3E",
|
||||
"WithdrawalPolicy": 1
|
||||
}
|
||||
|
||||
=== Submitting VaultCreate transaction... ===
|
||||
Vault created successfully!
|
||||
|
||||
Vault ID: 3E5BB3E4789603CC20D7A874ECBA36B74188F1B991EC9199DFA129FDB44D846D
|
||||
Vault pseudo-account address: rPgYFS3qFrUYQ3qWpF9RLKc9ECkGhgADtm
|
||||
Share MPT issuance ID: 00000001F8CD8CC81FFDDC9887627F42390E85DB32D44D0E
|
||||
|
||||
=== Getting vault_info... ===
|
||||
{
|
||||
"ledger_hash": "5851C21E353DEDEC5C6CC285E1E9835C378DCBBE5BA69CF33124DAC7EE5A08AD",
|
||||
"ledger_index": 3693379,
|
||||
"validated": true,
|
||||
"vault": {
|
||||
"Account": "rPgYFS3qFrUYQ3qWpF9RLKc9ECkGhgADtm",
|
||||
"Asset": {
|
||||
"mpt_issuance_id": "00385B21AF216177F319AC73F25F0FCBCDA09330D1D50D03"
|
||||
},
|
||||
"Data": "7B226E223A20224C4154414D2046756E64204949222C202277223A20226578616D706C6566756E642E636F6D227D",
|
||||
"Flags": 65536,
|
||||
"LedgerEntryType": "Vault",
|
||||
"Owner": "rfsTcqjyg7j2xfJFNbd9u8mt65yrGZvLnu",
|
||||
"OwnerNode": "0",
|
||||
"PreviousTxnID": "4B29E4DBA09CBDCAF591792ACFFB5F8717AD230185207C10F10B2A405FB2D576",
|
||||
"PreviousTxnLgrSeq": 3693379,
|
||||
"Sequence": 3693375,
|
||||
"ShareMPTID": "00000001F8CD8CC81FFDDC9887627F42390E85DB32D44D0E",
|
||||
"WithdrawalPolicy": 1,
|
||||
"index": "3E5BB3E4789603CC20D7A874ECBA36B74188F1B991EC9199DFA129FDB44D846D",
|
||||
"shares": {
|
||||
"DomainID": "76397457A19E093654F74848E5255E6111FDC0A2BF9FB2143F7C2C33424E1B3E",
|
||||
"Flags": 60,
|
||||
"Issuer": "rPgYFS3qFrUYQ3qWpF9RLKc9ECkGhgADtm",
|
||||
"LedgerEntryType": "MPTokenIssuance",
|
||||
"MPTokenMetadata": "7B226163223A2264656669222C226169223A7B226578616D706C655F696E666F223A2274657374227D2C2264223A2250726F706F7274696F6E616C206F776E65727368697020736861726573206F6620746865207661756C742E222C2269223A226578616D706C652E636F6D2F61737365742D69636F6E2E706E67222C22696E223A22417373657420497373756572204E616D65222C226E223A225661756C7420736861726573222C2274223A22534841524531222C227573223A5B7B2263223A2277656273697465222C2274223A2241737365742057656273697465222C2275223A226578616D706C652E636F6D2F6173736574227D2C7B2263223A22646F6373222C2274223A22446F6373222C2275223A226578616D706C652E636F6D2F646F6373227D5D7D",
|
||||
"OutstandingAmount": "0",
|
||||
"OwnerNode": "0",
|
||||
"PreviousTxnID": "4B29E4DBA09CBDCAF591792ACFFB5F8717AD230185207C10F10B2A405FB2D576",
|
||||
"PreviousTxnLgrSeq": 3693379,
|
||||
"Sequence": 1,
|
||||
"index": "EAD6924CB5DDA61CC5B85A6776A32E460FBFB0C34F5076A6A52005459B38043D",
|
||||
"mpt_issuance_id": "00000001F8CD8CC81FFDDC9887627F42390E85DB32D44D0E"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deposit into a Vault
|
||||
|
||||
```sh
|
||||
python deposit.py
|
||||
```
|
||||
|
||||
The script should output the vault state before and after the deposit, along with the depositor's share balance:
|
||||
|
||||
```sh
|
||||
Depositor address: r4pfiPR5y4GTbajHXzUS29KBDHUdxR8kCK
|
||||
Vault ID: 9966AF609568AFFCB3AEDEAC340B6AABB23C0483F013E186E83AF27EDA82C925
|
||||
Asset MPT issuance ID: 00385B21AF216177F319AC73F25F0FCBCDA09330D1D50D03
|
||||
Vault share MPT issuance ID: 00000001890BF384C217368D89BBB82B814B94B2597702B1
|
||||
|
||||
=== Getting initial vault state... ===
|
||||
- Total vault value: 1000
|
||||
- Available assets: 1000
|
||||
|
||||
=== Checking depositor's balance... ===
|
||||
Balance: 9000
|
||||
|
||||
=== VaultDeposit transaction ===
|
||||
{
|
||||
"Account": "r4pfiPR5y4GTbajHXzUS29KBDHUdxR8kCK",
|
||||
"TransactionType": "VaultDeposit",
|
||||
"SigningPubKey": "",
|
||||
"VaultID": "9966AF609568AFFCB3AEDEAC340B6AABB23C0483F013E186E83AF27EDA82C925",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00385B21AF216177F319AC73F25F0FCBCDA09330D1D50D03",
|
||||
"value": "1"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting VaultDeposit transaction... ===
|
||||
Deposit successful!
|
||||
|
||||
=== Vault state after deposit ===
|
||||
- Total vault value: 1001
|
||||
- Available assets: 1001
|
||||
|
||||
=== Depositor's share balance ===
|
||||
Shares held: 1001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Withdraw from a Vault
|
||||
|
||||
```sh
|
||||
python withdraw.py
|
||||
```
|
||||
|
||||
The script should output the vault state before and after the withdrawal, along with updated share and asset balances:
|
||||
|
||||
```sh
|
||||
Depositor address: r4pfiPR5y4GTbajHXzUS29KBDHUdxR8kCK
|
||||
Vault ID: 9966AF609568AFFCB3AEDEAC340B6AABB23C0483F013E186E83AF27EDA82C925
|
||||
Asset MPT issuance ID: 00385B21AF216177F319AC73F25F0FCBCDA09330D1D50D03
|
||||
Vault share MPT issuance ID: 00000001890BF384C217368D89BBB82B814B94B2597702B1
|
||||
|
||||
=== Getting initial vault state... ===
|
||||
Initial vault state:
|
||||
Assets Total: 1001
|
||||
Assets Available: 1001
|
||||
|
||||
=== Checking depositor's share balance... ===
|
||||
Shares held: 1001
|
||||
|
||||
=== Preparing VaultWithdraw transaction ===
|
||||
{
|
||||
"Account": "r4pfiPR5y4GTbajHXzUS29KBDHUdxR8kCK",
|
||||
"TransactionType": "VaultWithdraw",
|
||||
"SigningPubKey": "",
|
||||
"VaultID": "9966AF609568AFFCB3AEDEAC340B6AABB23C0483F013E186E83AF27EDA82C925",
|
||||
"Amount": {
|
||||
"mpt_issuance_id": "00385B21AF216177F319AC73F25F0FCBCDA09330D1D50D03",
|
||||
"value": "1"
|
||||
}
|
||||
}
|
||||
|
||||
=== Submitting VaultWithdraw transaction... ===
|
||||
Withdrawal successful!
|
||||
|
||||
=== Vault state after withdrawal ===
|
||||
Assets Total: 1000
|
||||
Assets Available: 1000
|
||||
|
||||
=== Depositor's share balance ==
|
||||
Shares held: 1000
|
||||
|
||||
=== Depositor's asset balance ==
|
||||
Balance: 9000
|
||||
```
|
||||
@@ -1,115 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import VaultCreate
|
||||
from xrpl.models.requests import VaultInfo
|
||||
from xrpl.models.transactions.vault_create import VaultCreateFlag, WithdrawalPolicy
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.utils import str_to_hex, encode_mptoken_metadata
|
||||
from xrpl.wallet import generate_faucet_wallet
|
||||
|
||||
# Auto-run setup if needed
|
||||
if not os.path.exists("vault_setup.json"):
|
||||
print("\n=== Vault setup data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "vault_setup.py"], check=True)
|
||||
|
||||
# Load setup data
|
||||
with open("vault_setup.json", "r") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# Connect to the network
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# Create and fund vault owner account
|
||||
vault_owner = generate_faucet_wallet(client)
|
||||
|
||||
# You can replace these values with your own
|
||||
mpt_issuance_id = setup_data["mpt_issuance_id"]
|
||||
domain_id = setup_data["domain_id"]
|
||||
|
||||
print(f"Vault owner address: {vault_owner.address}")
|
||||
print(f"MPT issuance ID: {mpt_issuance_id}")
|
||||
print(f"Permissioned domain ID: {domain_id}\n")
|
||||
|
||||
# Prepare VaultCreate transaction ----------------------
|
||||
print("\n=== VaultCreate transaction ===")
|
||||
vault_create_tx = VaultCreate(
|
||||
account=vault_owner.address,
|
||||
asset={"mpt_issuance_id": mpt_issuance_id},
|
||||
flags=VaultCreateFlag.TF_VAULT_PRIVATE, # Omit TF_VAULT_PRIVATE flag for public vaults
|
||||
# To make vault shares non-transferable add the TF_VAULT_SHARE_NON_TRANSFERABLE flag:
|
||||
# flags=VaultCreateFlag.TF_VAULT_PRIVATE | VaultCreateFlag.TF_VAULT_SHARE_NON_TRANSFERABLE,
|
||||
domain_id=domain_id, # Omit for public vaults
|
||||
# Convert Vault data to a string (without excess whitespace), then string to hex.
|
||||
data=str_to_hex(json.dumps(
|
||||
{"n": "LATAM Fund II", "w": "examplefund.com"}
|
||||
)),
|
||||
# Encode JSON metadata as hex string per XLS-89 MPT Metadata Schema.
|
||||
# See: https://xls.xrpl.org/xls/XLS-0089-multi-purpose-token-metadata-schema.html
|
||||
mptoken_metadata=encode_mptoken_metadata({
|
||||
"ticker": "SHARE1",
|
||||
"name": "Vault shares",
|
||||
"desc": "Proportional ownership shares of the vault.",
|
||||
"icon": "example.com/asset-icon.png",
|
||||
"asset_class": "defi",
|
||||
"issuer_name": "Asset Issuer Name",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "example.com/asset",
|
||||
"category": "website",
|
||||
"title": "Asset Website",
|
||||
},
|
||||
{
|
||||
"uri": "example.com/docs",
|
||||
"category": "docs",
|
||||
"title": "Docs",
|
||||
},
|
||||
],
|
||||
"additional_info": {
|
||||
"example_info": "test",
|
||||
},
|
||||
}),
|
||||
assets_maximum="0", # No cap
|
||||
withdrawal_policy=WithdrawalPolicy.VAULT_STRATEGY_FIRST_COME_FIRST_SERVE,
|
||||
)
|
||||
|
||||
print(json.dumps(vault_create_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Submit, sign, and wait for validation ----------------------
|
||||
print("\n=== Submitting VaultCreate transaction... ===")
|
||||
submit_response = submit_and_wait(vault_create_tx, client, vault_owner, autofill=True)
|
||||
|
||||
if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = submit_response.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to create vault: {result_code}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("Vault created successfully!")
|
||||
|
||||
# Extract vault information from the transaction result
|
||||
affected_nodes = submit_response.result["meta"].get("AffectedNodes", [])
|
||||
vault_node = next(
|
||||
(node for node in affected_nodes
|
||||
if "CreatedNode" in node and node["CreatedNode"].get("LedgerEntryType") == "Vault"),
|
||||
None
|
||||
)
|
||||
|
||||
if vault_node:
|
||||
print(f"\nVault ID: {vault_node['CreatedNode']['LedgerIndex']}")
|
||||
print(f"Vault pseudo-account address: {vault_node['CreatedNode']['NewFields']['Account']}")
|
||||
print(f"Share MPT issuance ID: {vault_node['CreatedNode']['NewFields']['ShareMPTID']}")
|
||||
|
||||
# Call vault_info method to retrieve the vault's information
|
||||
print("\n=== Getting vault_info... ===")
|
||||
vault_id = vault_node["CreatedNode"]["LedgerIndex"]
|
||||
vault_info_response = client.request(
|
||||
VaultInfo(
|
||||
vault_id=vault_id,
|
||||
ledger_index="validated"
|
||||
)
|
||||
)
|
||||
print(json.dumps(vault_info_response.result, indent=2))
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
# IMPORTANT: This example deposits into an existing PRIVATE vault.
|
||||
# The depositor account used has valid credentials in the vault's Permissioned Domain.
|
||||
# Without valid credentials, the VaultDeposit transaction will fail.
|
||||
# If you want to deposit into a public vault, you can replace the vault_id and share_mpt_issuance_id
|
||||
# values with your own.
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import VaultDeposit
|
||||
from xrpl.models.requests import VaultInfo, LedgerEntry
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.wallet import Wallet
|
||||
|
||||
# Auto-run setup if needed
|
||||
if not os.path.exists("vault_setup.json"):
|
||||
print("\n=== Vault setup data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "vault_setup.py"], check=True)
|
||||
|
||||
# Load setup data
|
||||
with open("vault_setup.json", "r") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# Connect to the network
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# You can replace these values with your own
|
||||
depositor = Wallet.from_seed(setup_data["depositor"]["seed"])
|
||||
vault_id = setup_data["vault_id"]
|
||||
asset_mpt_issuance_id = setup_data["mpt_issuance_id"]
|
||||
share_mpt_issuance_id = setup_data["vault_share_mpt_issuance_id"]
|
||||
|
||||
print(f"Depositor address: {depositor.address}")
|
||||
print(f"Vault ID: {vault_id}")
|
||||
print(f"Asset MPT issuance ID: {asset_mpt_issuance_id}")
|
||||
print(f"Vault share MPT issuance ID: {share_mpt_issuance_id}")
|
||||
|
||||
deposit_amount = 1
|
||||
|
||||
# Get initial vault state
|
||||
print("\n=== Getting initial vault state... ===")
|
||||
initial_vault_info = client.request(
|
||||
VaultInfo(
|
||||
vault_id=vault_id,
|
||||
ledger_index="validated"
|
||||
)
|
||||
)
|
||||
|
||||
print(f" - Total vault value: {initial_vault_info.result['vault']['AssetsTotal']}")
|
||||
print(f" - Available assets: {initial_vault_info.result['vault']['AssetsAvailable']}")
|
||||
|
||||
# Check depositor's asset balance
|
||||
print("\n=== Checking depositor's balance... ===")
|
||||
try:
|
||||
# Use ledger_entry to get specific MPT issuance balance
|
||||
ledger_entry_result = client.request(
|
||||
LedgerEntry(
|
||||
mptoken={
|
||||
"mpt_issuance_id": asset_mpt_issuance_id,
|
||||
"account": depositor.address
|
||||
},
|
||||
ledger_index="validated"
|
||||
)
|
||||
)
|
||||
|
||||
balance = ledger_entry_result.result["node"]["MPTAmount"]
|
||||
print(f"Balance: {balance}")
|
||||
|
||||
# Check if balance is sufficient
|
||||
if int(balance) < deposit_amount:
|
||||
print(f"Error: Insufficient balance! Have {balance}, need {deposit_amount}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as error:
|
||||
error_data = getattr(error, 'data', {})
|
||||
if 'error' in error_data and error_data['error'] == 'entryNotFound':
|
||||
print(f"Error: The depositor doesn't hold any assets with ID: {asset_mpt_issuance_id}", file=sys.stderr)
|
||||
else:
|
||||
print(f"Error checking MPT: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Prepare VaultDeposit transaction
|
||||
print("\n=== VaultDeposit transaction ===")
|
||||
vault_deposit_tx = VaultDeposit(
|
||||
account=depositor.address,
|
||||
vault_id=vault_id,
|
||||
amount={
|
||||
"mpt_issuance_id": asset_mpt_issuance_id,
|
||||
"value": str(deposit_amount)
|
||||
}
|
||||
)
|
||||
|
||||
print(json.dumps(vault_deposit_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Submit VaultDeposit transaction
|
||||
print("\n=== Submitting VaultDeposit transaction... ===")
|
||||
deposit_result = submit_and_wait(vault_deposit_tx, client, depositor, autofill=True)
|
||||
|
||||
if deposit_result.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = deposit_result.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to deposit: {result_code}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("Deposit successful!")
|
||||
|
||||
# Extract vault state from transaction metadata
|
||||
print("\n=== Vault state after deposit ===")
|
||||
affected_nodes = deposit_result.result["meta"]["AffectedNodes"]
|
||||
vault_node = None
|
||||
for node in affected_nodes:
|
||||
if "ModifiedNode" in node:
|
||||
modified = node["ModifiedNode"]
|
||||
if modified["LedgerEntryType"] == "Vault" and modified["LedgerIndex"] == vault_id:
|
||||
vault_node = node
|
||||
break
|
||||
|
||||
if vault_node:
|
||||
vault_fields = vault_node["ModifiedNode"]["FinalFields"]
|
||||
print(f" - Total vault value: {vault_fields['AssetsTotal']}")
|
||||
print(f" - Available assets: {vault_fields['AssetsAvailable']}")
|
||||
|
||||
# Get the depositor's share balance
|
||||
print("\n=== Depositor's share balance ===")
|
||||
depositor_share_node = None
|
||||
for node in affected_nodes:
|
||||
if "ModifiedNode" in node:
|
||||
share_node = node["ModifiedNode"]
|
||||
fields = share_node["FinalFields"]
|
||||
elif "CreatedNode" in node:
|
||||
share_node = node["CreatedNode"]
|
||||
fields = share_node["NewFields"]
|
||||
else:
|
||||
continue
|
||||
|
||||
if (share_node["LedgerEntryType"] == "MPToken" and
|
||||
fields["Account"] == depositor.address and
|
||||
fields["MPTokenIssuanceID"] == share_mpt_issuance_id):
|
||||
depositor_share_node = node
|
||||
break
|
||||
|
||||
if depositor_share_node:
|
||||
if "ModifiedNode" in depositor_share_node:
|
||||
share_fields = depositor_share_node["ModifiedNode"]["FinalFields"]
|
||||
else:
|
||||
share_fields = depositor_share_node["CreatedNode"]["NewFields"]
|
||||
print(f"Shares held: {share_fields['MPTAmount']}")
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
xrpl-py==4.5.0
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
|
||||
from xrpl.asyncio.clients import AsyncWebsocketClient
|
||||
from xrpl.asyncio.transaction import submit_and_wait
|
||||
from xrpl.asyncio.wallet import generate_faucet_wallet
|
||||
from xrpl.models import (
|
||||
Batch, BatchFlag, CredentialAccept, CredentialCreate, MPTokenAuthorize,
|
||||
MPTokenIssuanceCreate, MPTokenIssuanceCreateFlag, Payment,
|
||||
PermissionedDomainSet, VaultDeposit
|
||||
)
|
||||
from xrpl.models.requests import AccountObjects
|
||||
from xrpl.models.transactions.deposit_preauth import Credential
|
||||
from xrpl.models.transactions.vault_create import (
|
||||
VaultCreate, VaultCreateFlag, WithdrawalPolicy
|
||||
)
|
||||
from xrpl.utils import encode_mptoken_metadata, str_to_hex
|
||||
|
||||
|
||||
async def main():
|
||||
# Setup script for vault tutorials
|
||||
print("Setting up tutorial: 0/5", end="\r")
|
||||
|
||||
async with AsyncWebsocketClient("wss://s.devnet.rippletest.net:51233") as client:
|
||||
# Create and fund all wallets concurrently
|
||||
mpt_issuer, domain_owner, depositor, vault_owner = await asyncio.gather(
|
||||
generate_faucet_wallet(client),
|
||||
generate_faucet_wallet(client),
|
||||
generate_faucet_wallet(client),
|
||||
generate_faucet_wallet(client),
|
||||
)
|
||||
|
||||
# Step 1: Create MPT issuance, permissioned domain, and credentials in parallel
|
||||
print("Setting up tutorial: 1/5", end="\r")
|
||||
|
||||
cred_type = "VaultAccess"
|
||||
mpt_create_result, _ = await asyncio.gather(
|
||||
submit_and_wait(
|
||||
MPTokenIssuanceCreate(
|
||||
account=mpt_issuer.address,
|
||||
flags=(
|
||||
MPTokenIssuanceCreateFlag.TF_MPT_CAN_TRANSFER |
|
||||
MPTokenIssuanceCreateFlag.TF_MPT_CAN_LOCK
|
||||
),
|
||||
asset_scale=2,
|
||||
transfer_fee=0,
|
||||
maximum_amount="1000000000000",
|
||||
mptoken_metadata=encode_mptoken_metadata({
|
||||
"ticker": "USTST",
|
||||
"name": "USTST Stablecoin",
|
||||
"desc": "A test stablecoin token",
|
||||
"icon": "example.org/ustst-icon.png",
|
||||
"asset_class": "rwa",
|
||||
"asset_subclass": "stablecoin",
|
||||
"issuer_name": "Test Stablecoin Inc",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "example.org/ustst",
|
||||
"category": "website",
|
||||
"title": "USTST Official Website",
|
||||
},
|
||||
{
|
||||
"uri": "example.org/ustst/reserves",
|
||||
"category": "attestation",
|
||||
"title": "Reserve Attestation Reports",
|
||||
},
|
||||
{
|
||||
"uri": "example.org/ustst/docs",
|
||||
"category": "docs",
|
||||
"title": "USTST Documentation",
|
||||
},
|
||||
],
|
||||
"additional_info": {
|
||||
"backing": "USD",
|
||||
"reserve_ratio": "1:1",
|
||||
},
|
||||
}),
|
||||
),
|
||||
client,
|
||||
mpt_issuer,
|
||||
autofill=True
|
||||
),
|
||||
submit_and_wait(
|
||||
Batch(
|
||||
account=domain_owner.address,
|
||||
flags=BatchFlag.TF_ALL_OR_NOTHING,
|
||||
raw_transactions=[
|
||||
PermissionedDomainSet(
|
||||
account=domain_owner.address,
|
||||
accepted_credentials=[
|
||||
Credential(
|
||||
issuer=domain_owner.address,
|
||||
credential_type=str_to_hex(cred_type)
|
||||
)
|
||||
],
|
||||
),
|
||||
CredentialCreate(
|
||||
account=domain_owner.address,
|
||||
subject=depositor.address,
|
||||
credential_type=str_to_hex(cred_type),
|
||||
),
|
||||
],
|
||||
),
|
||||
client,
|
||||
domain_owner,
|
||||
autofill=True
|
||||
)
|
||||
)
|
||||
|
||||
mpt_issuance_id = mpt_create_result.result["meta"]["mpt_issuance_id"]
|
||||
|
||||
# Get domain ID
|
||||
domain_owner_objects = await client.request(AccountObjects(
|
||||
account=domain_owner.address,
|
||||
ledger_index="validated",
|
||||
))
|
||||
domain_id = next(
|
||||
node["index"]
|
||||
for node in domain_owner_objects.result["account_objects"]
|
||||
if node["LedgerEntryType"] == "PermissionedDomain"
|
||||
)
|
||||
|
||||
# Step 2: Depositor accepts credential, authorizes MPT, and creates vault in parallel
|
||||
print("Setting up tutorial: 2/5", end="\r")
|
||||
|
||||
_, vault_create_result = await asyncio.gather(
|
||||
submit_and_wait(
|
||||
Batch(
|
||||
account=depositor.address,
|
||||
flags=BatchFlag.TF_ALL_OR_NOTHING,
|
||||
raw_transactions=[
|
||||
CredentialAccept(
|
||||
account=depositor.address,
|
||||
issuer=domain_owner.address,
|
||||
credential_type=str_to_hex(cred_type),
|
||||
),
|
||||
MPTokenAuthorize(
|
||||
account=depositor.address,
|
||||
mptoken_issuance_id=mpt_issuance_id,
|
||||
),
|
||||
],
|
||||
),
|
||||
client,
|
||||
depositor,
|
||||
autofill=True
|
||||
),
|
||||
submit_and_wait(
|
||||
VaultCreate(
|
||||
account=vault_owner.address,
|
||||
asset={"mpt_issuance_id": mpt_issuance_id},
|
||||
flags=VaultCreateFlag.TF_VAULT_PRIVATE,
|
||||
domain_id=domain_id,
|
||||
data=str_to_hex(json.dumps(
|
||||
{"n": "LATAM Fund II", "w": "examplefund.com"}
|
||||
)),
|
||||
mptoken_metadata=encode_mptoken_metadata({
|
||||
"ticker": "SHARE1",
|
||||
"name": "Vault Shares",
|
||||
"desc": "Proportional ownership shares of the vault",
|
||||
"icon": "example.com/vault-shares-icon.png",
|
||||
"asset_class": "defi",
|
||||
"issuer_name": "Vault Owner",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "example.com/asset",
|
||||
"category": "website",
|
||||
"title": "Asset Website",
|
||||
},
|
||||
{
|
||||
"uri": "example.com/docs",
|
||||
"category": "docs",
|
||||
"title": "Docs",
|
||||
},
|
||||
],
|
||||
"additional_info": {
|
||||
"example_info": "test",
|
||||
},
|
||||
}),
|
||||
assets_maximum="0",
|
||||
withdrawal_policy=WithdrawalPolicy.VAULT_STRATEGY_FIRST_COME_FIRST_SERVE,
|
||||
),
|
||||
client,
|
||||
vault_owner,
|
||||
autofill=True
|
||||
),
|
||||
)
|
||||
|
||||
# Extract vault_id and vault_share_mpt_issuance_id
|
||||
vault_node = next(
|
||||
node for node in vault_create_result.result["meta"]["AffectedNodes"]
|
||||
if "CreatedNode" in node and node["CreatedNode"].get("LedgerEntryType") == "Vault"
|
||||
)
|
||||
vault_id = vault_node["CreatedNode"]["LedgerIndex"]
|
||||
vault_share_mpt_issuance_id = vault_node["CreatedNode"]["NewFields"]["ShareMPTID"]
|
||||
|
||||
# Step 3: Issuer sends payment to depositor
|
||||
print("Setting up tutorial: 3/5", end="\r")
|
||||
|
||||
payment_result = await submit_and_wait(
|
||||
Payment(
|
||||
account=mpt_issuer.address,
|
||||
destination=depositor.address,
|
||||
amount={
|
||||
"mpt_issuance_id": mpt_issuance_id,
|
||||
"value": "10000",
|
||||
},
|
||||
),
|
||||
client,
|
||||
mpt_issuer,
|
||||
autofill=True
|
||||
)
|
||||
|
||||
if payment_result.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"\nPayment failed: {payment_result.result['meta']['TransactionResult']}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Step 4: Make an initial deposit so withdraw example has shares to work with
|
||||
print("Setting up tutorial: 4/5", end="\r")
|
||||
|
||||
initial_deposit_result = await submit_and_wait(
|
||||
VaultDeposit(
|
||||
account=depositor.address,
|
||||
vault_id=vault_id,
|
||||
amount={
|
||||
"mpt_issuance_id": mpt_issuance_id,
|
||||
"value": "1000",
|
||||
},
|
||||
),
|
||||
client,
|
||||
depositor,
|
||||
autofill=True
|
||||
)
|
||||
|
||||
if initial_deposit_result.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
print(f"\nInitial deposit failed: {initial_deposit_result.result['meta']['TransactionResult']}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Step 5: Save setup data to file
|
||||
print("Setting up tutorial: 5/5", end="\r")
|
||||
|
||||
setup_data = {
|
||||
"mpt_issuer": {
|
||||
"address": mpt_issuer.address,
|
||||
"seed": mpt_issuer.seed,
|
||||
},
|
||||
"mpt_issuance_id": mpt_issuance_id,
|
||||
"domain_owner": {
|
||||
"address": domain_owner.address,
|
||||
"seed": domain_owner.seed,
|
||||
},
|
||||
"domain_id": domain_id,
|
||||
"credential_type": cred_type,
|
||||
"depositor": {
|
||||
"address": depositor.address,
|
||||
"seed": depositor.seed,
|
||||
},
|
||||
"vault_owner": {
|
||||
"address": vault_owner.address,
|
||||
"seed": vault_owner.seed,
|
||||
},
|
||||
"vault_id": vault_id,
|
||||
"vault_share_mpt_issuance_id": vault_share_mpt_issuance_id,
|
||||
}
|
||||
|
||||
with open("vault_setup.json", "w") as f:
|
||||
json.dump(setup_data, f, indent=2)
|
||||
|
||||
print("Setting up tutorial: Complete!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,164 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models import VaultWithdraw
|
||||
from xrpl.models.requests import VaultInfo, LedgerEntry
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.wallet import Wallet
|
||||
|
||||
# Auto-run setup if needed
|
||||
if not os.path.exists("vault_setup.json"):
|
||||
print("\n=== Vault setup data doesn't exist. Running setup script... ===\n")
|
||||
subprocess.run([sys.executable, "vault_setup.py"], check=True)
|
||||
|
||||
# Load setup data
|
||||
with open("vault_setup.json", "r") as f:
|
||||
setup_data = json.load(f)
|
||||
|
||||
# Connect to the network
|
||||
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")
|
||||
|
||||
# You can replace these values with your own
|
||||
depositor = Wallet.from_seed(setup_data["depositor"]["seed"])
|
||||
vault_id = setup_data["vault_id"]
|
||||
asset_mpt_issuance_id = setup_data["mpt_issuance_id"]
|
||||
share_mpt_issuance_id = setup_data["vault_share_mpt_issuance_id"]
|
||||
|
||||
print(f"Depositor address: {depositor.address}")
|
||||
print(f"Vault ID: {vault_id}")
|
||||
print(f"Asset MPT issuance ID: {asset_mpt_issuance_id}")
|
||||
print(f"Vault share MPT issuance ID: {share_mpt_issuance_id}")
|
||||
|
||||
withdraw_amount = 1
|
||||
|
||||
# Get initial vault state
|
||||
print("\n=== Getting initial vault state... ===")
|
||||
initial_vault_info = client.request(
|
||||
VaultInfo(
|
||||
vault_id=vault_id,
|
||||
ledger_index="validated"
|
||||
)
|
||||
)
|
||||
|
||||
print("Initial vault state:")
|
||||
print(f" Assets Total: {initial_vault_info.result['vault']['AssetsTotal']}")
|
||||
print(f" Assets Available: {initial_vault_info.result['vault']['AssetsAvailable']}")
|
||||
|
||||
# Check depositor's share balance
|
||||
print("\n=== Checking depositor's share balance... ===")
|
||||
try:
|
||||
share_balance_result = client.request(
|
||||
LedgerEntry(
|
||||
mptoken={
|
||||
"mpt_issuance_id": share_mpt_issuance_id,
|
||||
"account": depositor.address
|
||||
},
|
||||
ledger_index="validated"
|
||||
)
|
||||
)
|
||||
|
||||
share_balance = share_balance_result.result["node"]["MPTAmount"]
|
||||
print(f"Shares held: {share_balance}")
|
||||
except Exception as error:
|
||||
error_data = getattr(error, 'data', {})
|
||||
if 'error' in error_data and error_data['error'] == 'entryNotFound':
|
||||
print(f"Error: The depositor doesn't hold any vault shares with ID: {share_mpt_issuance_id}.", file=sys.stderr)
|
||||
else:
|
||||
print(f"Error checking MPT: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Prepare VaultWithdraw transaction
|
||||
print("\n=== Preparing VaultWithdraw transaction ===")
|
||||
vault_withdraw_tx = VaultWithdraw(
|
||||
account=depositor.address,
|
||||
vault_id=vault_id,
|
||||
amount={
|
||||
"mpt_issuance_id": asset_mpt_issuance_id,
|
||||
"value": str(withdraw_amount)
|
||||
}
|
||||
# Optional: Add destination field to send assets to a different account
|
||||
# destination="rGg4tHPRGJfewwJkd8immCFx9uSo2GgcoY"
|
||||
)
|
||||
|
||||
print(json.dumps(vault_withdraw_tx.to_xrpl(), indent=2))
|
||||
|
||||
# Submit VaultWithdraw transaction
|
||||
print("\n=== Submitting VaultWithdraw transaction... ===")
|
||||
withdraw_result = submit_and_wait(vault_withdraw_tx, client, depositor, autofill=True)
|
||||
|
||||
if withdraw_result.result["meta"]["TransactionResult"] != "tesSUCCESS":
|
||||
result_code = withdraw_result.result["meta"]["TransactionResult"]
|
||||
print(f"Error: Unable to withdraw from vault: {result_code}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("Withdrawal successful!")
|
||||
|
||||
# Extract vault state from transaction metadata
|
||||
print("\n=== Vault state after withdrawal ===")
|
||||
affected_nodes = withdraw_result.result["meta"]["AffectedNodes"]
|
||||
vault_node = None
|
||||
for node in affected_nodes:
|
||||
if "ModifiedNode" in node or "DeletedNode" in node:
|
||||
modified_node = node["ModifiedNode"] if "ModifiedNode" in node else node["DeletedNode"]
|
||||
if modified_node["LedgerEntryType"] == "Vault" and modified_node["LedgerIndex"] == vault_id:
|
||||
vault_node = node
|
||||
break
|
||||
|
||||
if vault_node:
|
||||
if "DeletedNode" in vault_node:
|
||||
print(" Vault empty (all assets withdrawn)")
|
||||
else:
|
||||
vault_fields = vault_node["ModifiedNode"]["FinalFields"]
|
||||
print(f" Assets Total: {vault_fields['AssetsTotal']}")
|
||||
print(f" Assets Available: {vault_fields['AssetsAvailable']}")
|
||||
|
||||
# Get the depositor's share balance
|
||||
print("\n=== Depositor's share balance ===")
|
||||
depositor_share_node = None
|
||||
for node in affected_nodes:
|
||||
if "ModifiedNode" in node or "DeletedNode" in node:
|
||||
modified_node = node["ModifiedNode"] if "ModifiedNode" in node else node["DeletedNode"]
|
||||
if "FinalFields" in modified_node:
|
||||
fields = modified_node["FinalFields"]
|
||||
if (modified_node["LedgerEntryType"] == "MPToken" and
|
||||
fields["Account"] == depositor.address and
|
||||
fields["MPTokenIssuanceID"] == share_mpt_issuance_id):
|
||||
depositor_share_node = node
|
||||
break
|
||||
|
||||
if depositor_share_node:
|
||||
if "DeletedNode" in depositor_share_node:
|
||||
print("No more shares held (redeemed all shares)")
|
||||
else:
|
||||
share_fields = depositor_share_node["ModifiedNode"]["FinalFields"]
|
||||
print(f"Shares held: {share_fields['MPTAmount']}")
|
||||
|
||||
# Get the depositor's asset balance
|
||||
print("\n=== Depositor's asset balance ===")
|
||||
depositor_asset_node = None
|
||||
for node in affected_nodes:
|
||||
if "ModifiedNode" in node:
|
||||
asset_node = node["ModifiedNode"]
|
||||
fields = asset_node["FinalFields"]
|
||||
elif "CreatedNode" in node:
|
||||
asset_node = node["CreatedNode"]
|
||||
fields = asset_node["NewFields"]
|
||||
else:
|
||||
continue
|
||||
|
||||
if (asset_node["LedgerEntryType"] == "MPToken" and
|
||||
fields["Account"] == depositor.address and
|
||||
fields["MPTokenIssuanceID"] == asset_mpt_issuance_id):
|
||||
depositor_asset_node = node
|
||||
break
|
||||
|
||||
if depositor_asset_node:
|
||||
if "ModifiedNode" in depositor_asset_node:
|
||||
asset_fields = depositor_asset_node["ModifiedNode"]["FinalFields"]
|
||||
else:
|
||||
asset_fields = depositor_asset_node["CreatedNode"]["NewFields"]
|
||||
print(f"Balance: {asset_fields['MPTAmount']}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CalloutMediaBanner } from "shared/sections/CalloutMediaBanner";
|
||||
import { CalloutMediaBanner } from "shared/patterns/CalloutMediaBanner";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
@@ -39,12 +39,9 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<CalloutMediaBanner
|
||||
variant="green"
|
||||
heading="The Compliant Ledger Protocol"
|
||||
headingAs="h1"
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
buttons={[
|
||||
{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') },
|
||||
{ label: "Learn More", onClick: () => handleClick('responsive-demo-tertiary') }
|
||||
]}
|
||||
primaryButton={{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') }}
|
||||
tertiaryButton={{ label: "Learn More", onClick: () => handleClick('responsive-demo-tertiary') }}
|
||||
/>
|
||||
|
||||
{/* Responsive Behavior */}
|
||||
@@ -125,9 +122,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="default"
|
||||
heading="Build on XRPL"
|
||||
subheading="Start building your next decentralized application on the XRP Ledger."
|
||||
buttons={[
|
||||
{ label: "Start Building", href: "#start" }
|
||||
]}
|
||||
primaryButton={{ label: "Start Building", href: "#start" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -148,10 +143,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="light-gray"
|
||||
heading="Developer Resources"
|
||||
subheading="Access comprehensive documentation, tutorials, and code samples."
|
||||
buttons={[
|
||||
{ label: "View Docs", href: "#docs" },
|
||||
{ label: "Browse Tutorials", href: "#tutorials" }
|
||||
]}
|
||||
primaryButton={{ label: "View Docs", href: "#docs" }}
|
||||
tertiaryButton={{ label: "Browse Tutorials", href: "#tutorials" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -172,9 +165,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="lilac"
|
||||
heading="New Feature Release"
|
||||
subheading="Discover the latest enhancements and capabilities added to the XRP Ledger."
|
||||
buttons={[
|
||||
{ label: "Learn More", href: "#features" }
|
||||
]}
|
||||
primaryButton={{ label: "Learn More", href: "#features" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -195,10 +186,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="green"
|
||||
heading="The Compliant Ledger Protocol"
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#get-started" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
primaryButton={{ label: "Get Started", href: "#get-started" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -219,10 +208,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="gray"
|
||||
heading="Join the Community"
|
||||
subheading="Connect with developers building on XRPL."
|
||||
buttons={[
|
||||
{ label: "Join Discord", href: "#discord" },
|
||||
{ label: "View Events", href: "#events" }
|
||||
]}
|
||||
primaryButton={{ label: "Join Discord", href: "#discord" }}
|
||||
tertiaryButton={{ label: "View Events", href: "#events" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -268,9 +255,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<CalloutMediaBanner
|
||||
backgroundImage={sampleBackgroundImage}
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
buttons={[
|
||||
{ label: "Start Building", onClick: () => handleClick('image-white-primary') }
|
||||
]}
|
||||
primaryButton={{ label: "Start Building", onClick: () => handleClick('image-white-primary') }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -291,12 +276,9 @@ export default function CalloutMediaBannerShowcase() {
|
||||
backgroundImage={sampleLightBackgroundImage}
|
||||
textColor="black"
|
||||
heading="Build the Future of Finance"
|
||||
headingAs="h2"
|
||||
subheading="Create powerful decentralized applications with XRPL's fast, efficient, and sustainable blockchain technology."
|
||||
buttons={[
|
||||
{ label: "Start Building", onClick: () => handleClick('image-black-primary') },
|
||||
{ label: "Explore Features", onClick: () => handleClick('image-black-tertiary') }
|
||||
]}
|
||||
primaryButton={{ label: "Start Building", onClick: () => handleClick('image-black-primary') }}
|
||||
tertiaryButton={{ label: "Explore Features", onClick: () => handleClick('image-black-tertiary') }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -339,10 +321,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="default"
|
||||
heading="Complete Feature Set"
|
||||
subheading="Access all the tools you need to build on XRPL."
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
primaryButton={{ label: "Get Started", href: "#start" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -363,9 +343,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="light-gray"
|
||||
heading="Simple Call-to-Action"
|
||||
subheading="Focus user attention on a single primary action."
|
||||
buttons={[
|
||||
{ label: "Take Action", href: "#action" }
|
||||
]}
|
||||
primaryButton={{ label: "Take Action", href: "#action" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -405,10 +383,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<CalloutMediaBanner
|
||||
variant="green"
|
||||
subheading="Important information or announcement without requiring user action."
|
||||
buttons={[
|
||||
{ label: "Take Action", href: "#action" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
primaryButton={{ label: "Take Action", href: "#action" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -576,18 +552,10 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>heading</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><em>required</em></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Main heading text</div>
|
||||
</div>
|
||||
|
||||
{/* headingAs */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>headingAs</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>'h6'</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Heading element type - allows semantic HTML customization while maintaining visual styling</div>
|
||||
</div>
|
||||
|
||||
{/* subheading */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>subheading</code></div>
|
||||
@@ -596,12 +564,20 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Subheading/description text</div>
|
||||
</div>
|
||||
|
||||
{/* buttons */}
|
||||
{/* primaryButton */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>buttons</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`Array<{ label, href?, onClick?, forceColor? }>`}</code></div>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>primaryButton</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Button configurations (1-2 buttons supported)</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Primary button configuration</div>
|
||||
</div>
|
||||
|
||||
{/* tertiaryButton */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>tertiaryButton</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Tertiary button configuration</div>
|
||||
</div>
|
||||
|
||||
{/* className */}
|
||||
@@ -630,7 +606,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Location:</strong>{' '}
|
||||
<code>shared/sections/CalloutMediaBanner/</code>
|
||||
<code>shared/patterns/CalloutMediaBanner/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Color Tokens:</strong>{' '}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardsFeatured } from "shared/sections/CardsFeatured";
|
||||
import { CardsFeatured } from "shared/patterns/CardsFeatured";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
257
about/cards-icon-grid-showcase.page.tsx
Normal file
257
about/cards-icon-grid-showcase.page.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardsIconGrid } from "shared/patterns/CardsIconGrid";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'CardsIconGrid Pattern Showcase',
|
||||
description: "A comprehensive showcase of the CardsIconGrid pattern component demonstrating light and dark mode variations in the XRPL.org Design System.",
|
||||
}
|
||||
};
|
||||
|
||||
// Sample icon SVG for demonstration
|
||||
const SAMPLE_ICON = "/img/icons/card-icon-placeholder.svg";
|
||||
|
||||
// Sample cards data - Green variant
|
||||
const greenCards = [
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Digital Wallets icon",
|
||||
label: "Digital Wallets",
|
||||
href: "#wallets",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "B2B Payment Rails icon",
|
||||
label: "B2B Payment Rails",
|
||||
href: "#payments",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Compliance-First Payments icon",
|
||||
label: "Compliance-First Payments",
|
||||
href: "#compliance",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Merchant Settlement icon",
|
||||
label: "Merchant Settlement",
|
||||
href: "#settlement",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Cross-Border Payments icon",
|
||||
label: "Cross-Border Payments",
|
||||
href: "#cross-border",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Treasury Management icon",
|
||||
label: "Treasury Management",
|
||||
href: "#treasury",
|
||||
variant: "green" as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Sample cards data - Neutral variant
|
||||
const neutralCards = [
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Documentation icon",
|
||||
label: "Documentation",
|
||||
href: "#docs",
|
||||
variant: "neutral" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Tutorials icon",
|
||||
label: "Tutorials",
|
||||
href: "#tutorials",
|
||||
variant: "neutral" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "API Reference icon",
|
||||
label: "API Reference",
|
||||
href: "#api",
|
||||
variant: "neutral" as const,
|
||||
},
|
||||
];
|
||||
|
||||
export default function CardsIconGridShowcase() {
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="py-26 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">CardsIconGrid Pattern</h1>
|
||||
<p className="longform">
|
||||
A section pattern that displays a heading, optional description, and a responsive grid
|
||||
of CardIcon components. Follows the "CardIconGrid" design from Figma.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Design Tokens Reference */}
|
||||
<PageGrid className="py-10">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
|
||||
<h2 className="h4 mb-6">Design Specifications</h2>
|
||||
<div className="d-flex flex-wrap gap-8">
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Typography</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Heading:</strong> heading-md (Tobias Light)</li>
|
||||
<li><strong>Description:</strong> body-l (Booton Light)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Grid Layout</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Mobile:</strong> 1 column</li>
|
||||
<li><strong>Tablet:</strong> 2 columns</li>
|
||||
<li><strong>Desktop:</strong> 3 columns</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Colors</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Light Mode:</strong> $black (#141414)</li>
|
||||
<li><strong>Dark Mode:</strong> $white (#FFFFFF)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 6 Cards Example - Green Variant */}
|
||||
<section>
|
||||
<CardsIconGrid
|
||||
heading="Unlock new business models with embedded payments"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
cards={greenCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 3 Cards Example - Neutral Variant */}
|
||||
<section>
|
||||
<CardsIconGrid
|
||||
heading="Developer Resources"
|
||||
description="Everything you need to start building on the XRP Ledger."
|
||||
cards={neutralCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Without Description */}
|
||||
<PageGrid className="py-10">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
|
||||
<h2 className="h4 mb-4">Without Description</h2>
|
||||
<p className="mb-0">
|
||||
The description prop is optional. When omitted, only the heading appears above the cards.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<section>
|
||||
<CardsIconGrid
|
||||
heading="Funding & Support Programs"
|
||||
cards={greenCards.slice(0, 3)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Code Examples */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 10 }}>
|
||||
<h2 className="h4 mb-6">Code Examples</h2>
|
||||
|
||||
<h5 className="mb-4">Basic Usage</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#1a1a1a', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#f8f8f2' }}>{`import { CardsIconGrid } from 'shared/patterns/CardsIconGrid';
|
||||
|
||||
<CardsIconGrid
|
||||
heading="Unlock new business models"
|
||||
description="Build powerful solutions with XRPL."
|
||||
cards={[
|
||||
{
|
||||
icon: "/icons/wallet.svg",
|
||||
label: "Digital Wallets",
|
||||
href: "/docs/wallets",
|
||||
variant: "green"
|
||||
},
|
||||
{
|
||||
icon: "/icons/payments.svg",
|
||||
label: "B2B Payment Rails",
|
||||
href: "/docs/payments",
|
||||
variant: "green"
|
||||
},
|
||||
{
|
||||
icon: "/icons/compliance.svg",
|
||||
label: "Compliance-First Payments",
|
||||
href: "/docs/compliance",
|
||||
variant: "green"
|
||||
}
|
||||
]}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">Without Description</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#1a1a1a', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#f8f8f2' }}>{`<CardsIconGrid
|
||||
heading="Developer Resources"
|
||||
cards={[
|
||||
{ icon: "/icons/docs.svg", label: "Documentation", href: "/docs", variant: "neutral" },
|
||||
{ icon: "/icons/tutorials.svg", label: "Tutorials", href: "/tutorials", variant: "neutral" },
|
||||
{ icon: "/icons/api.svg", label: "API Reference", href: "/api", variant: "neutral" }
|
||||
]}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Design References */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
|
||||
<h2 className="h4 mb-6">Design References</h2>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<strong>Figma:</strong>{' '}
|
||||
<a href="https://www.figma.com/design/Ojj6UpFBw3HMb0QqRaKxAU/Section-Cards---Icon?node-id=30071-3082&m=dev" target="_blank" rel="noopener noreferrer">
|
||||
Section Cards - Icon Grid
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Documentation:</strong>{' '}
|
||||
<code>shared/patterns/CardsIconGrid/CardsIconGrid.md</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
537
about/cardstat-showcase.page.tsx
Normal file
537
about/cardstat-showcase.page.tsx
Normal file
@@ -0,0 +1,537 @@
|
||||
import * as React from 'react';
|
||||
import { CardStat } from 'shared/components/CardStat';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'CardStat Component Showcase',
|
||||
description: 'Interactive showcase of the Brand Design System CardStat component with all variants and configurations.',
|
||||
},
|
||||
};
|
||||
|
||||
export default function CardStatShowcase() {
|
||||
const [clickCount, setClickCount] = React.useState<Record<string, number>>({});
|
||||
|
||||
const handleClick = (id: string) => {
|
||||
setClickCount((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
{/* Hero Section */}
|
||||
<PageGrid className="py-26">
|
||||
<div className="d-flex flex-column-reverse col-lg-8 mx-auto">
|
||||
<h1 className="mb-0">CardStat Component</h1>
|
||||
<h6 className="eyebrow mb-3">Brand Design System</h6>
|
||||
</div>
|
||||
<p className="col-lg-8 mx-auto mt-10">
|
||||
A statistics card component following the XRPL Brand Design System. This showcase demonstrates
|
||||
all color variants, button configurations, and responsive behavior using PageGrid.
|
||||
</p>
|
||||
</PageGrid>
|
||||
|
||||
{/* Basic Usage */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Basic Usage</h2>
|
||||
<h6 className="eyebrow mb-3">Simple Statistics</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
CardStat components display prominent statistics with descriptive labels. They adapt responsively
|
||||
and can be used without buttons for purely informational displays.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="2"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="*"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Color Variants */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Color Variants</h2>
|
||||
<h6 className="eyebrow mb-3">Visual Themes</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Four color variants are available to match different types of statistics and visual contexts.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="6M"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>
|
||||
<p className="mt-4 text-muted"><strong>Lilac</strong> - User metrics, community stats</p>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="$1T"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
/>
|
||||
<p className="mt-4 text-muted"><strong>Green</strong> - Financial metrics, growth</p>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Uptime years"
|
||||
variant="light-gray"
|
||||
/>
|
||||
<p className="mt-4 text-muted"><strong>Light Gray</strong> - Technical stats, reliability</p>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="70+"
|
||||
label="Partners"
|
||||
variant="dark-gray"
|
||||
/>
|
||||
<p className="mt-4 text-muted"><strong>Dark Gray</strong> - Neutral metrics, secondary info</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* With Single Button */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-full">
|
||||
<h2 className="h4 mb-8">With Primary Button</h2>
|
||||
<h6 className="eyebrow mb-3">Single CTA</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Add a primary button for a main call-to-action. Buttons use the black variant for proper
|
||||
contrast on colored backgrounds.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 6 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
primaryButton={{
|
||||
label: "Explore",
|
||||
onClick: () => handleClick('explore-1')
|
||||
}}
|
||||
/>
|
||||
{clickCount['explore-1'] > 0 && (
|
||||
<p className="mt-4 text-muted">Clicked {clickCount['explore-1']} time{clickCount['explore-1'] !== 1 ? 's' : ''}</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 6 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: () => handleClick('learn-1')
|
||||
}}
|
||||
/>
|
||||
{clickCount['learn-1'] > 0 && (
|
||||
<p className="mt-4 text-muted">Clicked {clickCount['learn-1']} time{clickCount['learn-1'] !== 1 ? 's' : ''}</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 12 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
primaryButton={{
|
||||
label: "View Details",
|
||||
onClick: () => handleClick('view-1')
|
||||
}}
|
||||
/>
|
||||
{clickCount['view-1'] > 0 && (
|
||||
<p className="mt-4 text-muted">Clicked {clickCount['view-1']} time{clickCount['view-1'] !== 1 ? 's' : ''}</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* With Two Buttons */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-full">
|
||||
<h2 className="h4 mb-8">With Two Buttons</h2>
|
||||
<h6 className="eyebrow mb-3">Multiple CTAs</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Include both primary and secondary buttons for multiple action options. Buttons wrap responsively
|
||||
and maintain consistent spacing.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: () => handleClick('primary-1')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Get Started",
|
||||
onClick: () => handleClick('secondary-1')
|
||||
}}
|
||||
/>
|
||||
{(clickCount['primary-1'] > 0 || clickCount['secondary-1'] > 0) && (
|
||||
<p className="mt-4 text-muted">
|
||||
Primary: {clickCount['primary-1'] || 0}, Secondary: {clickCount['secondary-1'] || 0}
|
||||
</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "Explore",
|
||||
onClick: () => handleClick('primary-2')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "View Stats",
|
||||
onClick: () => handleClick('secondary-2')
|
||||
}}
|
||||
/>
|
||||
{(clickCount['primary-2'] > 0 || clickCount['secondary-2'] > 0) && (
|
||||
<p className="mt-4 text-muted">
|
||||
Primary: {clickCount['primary-2'] || 0}, Secondary: {clickCount['secondary-2'] || 0}
|
||||
</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
primaryButton={{
|
||||
label: "Read More",
|
||||
onClick: () => handleClick('primary-3')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Try It",
|
||||
onClick: () => handleClick('secondary-3')
|
||||
}}
|
||||
/>
|
||||
{(clickCount['primary-3'] > 0 || clickCount['secondary-3'] > 0) && (
|
||||
<p className="mt-4 text-muted">
|
||||
Primary: {clickCount['primary-3'] || 0}, Secondary: {clickCount['secondary-3'] || 0}
|
||||
</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Responsive Behavior */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-full">
|
||||
<h2 className="h4 mb-8">Responsive Layout</h2>
|
||||
<h6 className="eyebrow mb-3">Adaptive Grid</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Cards adapt to different screen sizes. On mobile (base), cards stack vertically. On tablet (md),
|
||||
they can be arranged in 2 columns. On desktop (lg+), up to 3-4 columns are supported.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="1M"
|
||||
superscript="+"
|
||||
label="Transactions daily"
|
||||
variant="lilac"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="150"
|
||||
superscript="+"
|
||||
label="Countries"
|
||||
variant="green"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="99.9"
|
||||
superscript="%"
|
||||
label="Uptime"
|
||||
variant="light-gray"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="24/7"
|
||||
label="Support"
|
||||
variant="dark-gray"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Mixed Configurations */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Mixed Configurations</h2>
|
||||
<h6 className="eyebrow mb-3">Flexible Usage</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Mix and match cards with different button configurations in the same layout.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: () => handleClick('mixed-1')
|
||||
}}
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
primaryButton={{
|
||||
label: "Explore",
|
||||
onClick: () => handleClick('mixed-2')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Get Started",
|
||||
onClick: () => handleClick('mixed-3')
|
||||
}}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Wide Layout */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Wide Card Layout</h2>
|
||||
<h6 className="eyebrow mb-3">Larger Spans</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Cards can span multiple columns for wider layouts on larger screens.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 6 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets using XRPL"
|
||||
variant="lilac"
|
||||
primaryButton={{
|
||||
label: "Explore Wallets",
|
||||
onClick: () => handleClick('wide-1')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Get Started",
|
||||
onClick: () => handleClick('wide-2')
|
||||
}}
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 6 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Total value moved on the network"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "View Statistics",
|
||||
onClick: () => handleClick('wide-3')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: () => handleClick('wide-4')
|
||||
}}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Usage Guidelines */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Usage Guidelines</h2>
|
||||
<h6 className="eyebrow mb-3">Best Practices</h6>
|
||||
</div>
|
||||
<div className="col-lg-8 mx-auto w-100">
|
||||
<h5 className="mb-4">When to Use</h5>
|
||||
<ul className="mb-8">
|
||||
<li><strong>Key metrics</strong> - Highlight important numbers prominently</li>
|
||||
<li><strong>Dashboard sections</strong> - Create stat-focused areas on landing pages</li>
|
||||
<li><strong>About pages</strong> - Showcase company or product statistics</li>
|
||||
<li><strong>Feature sections</strong> - Emphasize quantitative benefits</li>
|
||||
</ul>
|
||||
|
||||
<h5 className="mb-4">Color Variant Selection</h5>
|
||||
<ul className="mb-8">
|
||||
<li><strong>Lilac</strong> - User-focused statistics, community metrics</li>
|
||||
<li><strong>Green</strong> - Financial metrics, growth indicators</li>
|
||||
<li><strong>Light Gray</strong> - Technical statistics, reliability metrics</li>
|
||||
<li><strong>Dark Gray</strong> - Neutral or secondary information</li>
|
||||
</ul>
|
||||
|
||||
<h5 className="mb-4">Button Configuration</h5>
|
||||
<ul className="mb-8">
|
||||
<li><strong>No buttons</strong> - For purely informational displays</li>
|
||||
<li><strong>Single button</strong> - For one clear call-to-action</li>
|
||||
<li><strong>Two buttons</strong> - For multiple action options</li>
|
||||
</ul>
|
||||
|
||||
<h5 className="mb-4">Tips</h5>
|
||||
<ul>
|
||||
<li>Keep statistics concise using abbreviations (M, K, T, +)</li>
|
||||
<li>Use descriptive labels that clearly explain the metric</li>
|
||||
<li>Choose colors that match the type of statistic</li>
|
||||
<li>Test on all breakpoints to ensure proper responsive behavior</li>
|
||||
<li>Limit buttons to essential actions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Implementation Examples */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="col-lg-10 mx-auto d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Code Examples</h2>
|
||||
<h6 className="eyebrow mb-3">Implementation</h6>
|
||||
</div>
|
||||
<div className="col-lg-10 mx-auto">
|
||||
<h5 className="mb-4">Basic Card</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">With Primary Button</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
href: "/about"
|
||||
}}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">With Two Buttons</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: handleLearnMore
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Get Started",
|
||||
href: "/start"
|
||||
}}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">In PageGrid Layout</h5>
|
||||
<div className="p-4 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Uptime years"
|
||||
variant="light-gray"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</ PageGridRow>
|
||||
</ PageGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import * as React from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
|
||||
type BorderedColProps = React.ComponentProps<typeof PageGridCol>;
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'PageGrid Showcase',
|
||||
@@ -22,22 +19,36 @@ const GridDemo = ({ title, description, children, code }: {
|
||||
<h3 className="h4 mb-4">{title}</h3>
|
||||
{description && <p className="mb-6">{description}</p>}
|
||||
{code && (
|
||||
<div className={clsx("mb-6 br-4", "grid-showcase-code-block")}>
|
||||
<pre>{code}</pre>
|
||||
<div className="mb-6 p-4 bg-light br-4 text-black" style={{ fontFamily: 'monospace', fontSize: '14px', overflow: 'auto' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{code}</pre>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid-showcase-demo">
|
||||
<div style={{
|
||||
border: '1px dashed #ccc',
|
||||
padding: '16px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Bordered column component for visualization (theme styles: .grid-showcase-bordered-col in _landings.scss)
|
||||
const BorderedCol = ({ children, className, style, ...props }: BorderedColProps) => (
|
||||
<PageGridCol
|
||||
// Bordered column component for visualization
|
||||
const BorderedCol = ({ children, ...props }: { children: React.ReactNode } & any) => (
|
||||
<PageGridCol
|
||||
{...props}
|
||||
className={clsx("grid-showcase-bordered-col", className)}
|
||||
style={style}
|
||||
style={{
|
||||
border: '1px solid #0069ff',
|
||||
backgroundColor: 'rgba(0, 105, 255, 0.05)',
|
||||
padding: '16px',
|
||||
minHeight: '60px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
...props.style
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PageGridCol>
|
||||
@@ -266,43 +277,50 @@ export default function GridShowcase() {
|
||||
<p className="mb-6">
|
||||
The PageGrid system uses the following breakpoints:
|
||||
</p>
|
||||
<div className={clsx("grid-showcase-breakpoints-table", "mb-6")}>
|
||||
<div className="grid-showcase-breakpoints-table__grid grid-showcase-breakpoints-table__header">
|
||||
<div className="grid-showcase-breakpoints-table__cell">Breakpoint</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">Min Width</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">Columns</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">Container Padding</div>
|
||||
<div style={{ width: '100%', backgroundColor: '#FFFFFF', borderRadius: '4px', overflow: 'hidden', border: '1px solid #EEE', marginBottom: '24px' }}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1.2fr 1fr 1fr 1.5fr',
|
||||
borderBottom: '2px solid #E0E0E1',
|
||||
background: '#FAFAFA'
|
||||
}}>
|
||||
<div style={{ padding: '12px', fontWeight: 600 }}>Breakpoint</div>
|
||||
<div style={{ padding: '12px', fontWeight: 600 }}>Min Width</div>
|
||||
<div style={{ padding: '12px', fontWeight: 600 }}>Columns</div>
|
||||
<div style={{ padding: '12px', fontWeight: 600 }}>Container Padding</div>
|
||||
</div>
|
||||
|
||||
<div className="grid-showcase-breakpoints-table__grid grid-showcase-breakpoints-table__row">
|
||||
<div className="grid-showcase-breakpoints-table__cell"><code>base</code></div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">0px</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">4 columns</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">16px</div>
|
||||
{/* Rows */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr 1fr 1.5fr', borderBottom: '1px solid #E0E0E1' }}>
|
||||
<div style={{ padding: '12px' }}><code>base</code></div>
|
||||
<div style={{ padding: '12px' }}>0px</div>
|
||||
<div style={{ padding: '12px' }}>4 columns</div>
|
||||
<div style={{ padding: '12px' }}>16px</div>
|
||||
</div>
|
||||
<div className="grid-showcase-breakpoints-table__grid grid-showcase-breakpoints-table__row">
|
||||
<div className="grid-showcase-breakpoints-table__cell"><code>sm</code></div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">576px</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">8 columns</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">24px</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr 1fr 1.5fr', borderBottom: '1px solid #E0E0E1' }}>
|
||||
<div style={{ padding: '12px' }}><code>sm</code></div>
|
||||
<div style={{ padding: '12px' }}>576px</div>
|
||||
<div style={{ padding: '12px' }}>8 columns</div>
|
||||
<div style={{ padding: '12px' }}>24px</div>
|
||||
</div>
|
||||
<div className="grid-showcase-breakpoints-table__grid grid-showcase-breakpoints-table__row">
|
||||
<div className="grid-showcase-breakpoints-table__cell"><code>md</code></div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">576px</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">8 columns</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">24px</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr 1fr 1.5fr', borderBottom: '1px solid #E0E0E1' }}>
|
||||
<div style={{ padding: '12px' }}><code>md</code></div>
|
||||
<div style={{ padding: '12px' }}>576px</div>
|
||||
<div style={{ padding: '12px' }}>8 columns</div>
|
||||
<div style={{ padding: '12px' }}>24px</div>
|
||||
</div>
|
||||
<div className="grid-showcase-breakpoints-table__grid grid-showcase-breakpoints-table__row">
|
||||
<div className="grid-showcase-breakpoints-table__cell"><code>lg</code></div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">992px</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">12 columns</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">32px</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr 1fr 1.5fr', borderBottom: '1px solid #E0E0E1' }}>
|
||||
<div style={{ padding: '12px' }}><code>lg</code></div>
|
||||
<div style={{ padding: '12px' }}>992px</div>
|
||||
<div style={{ padding: '12px' }}>12 columns</div>
|
||||
<div style={{ padding: '12px' }}>32px</div>
|
||||
</div>
|
||||
<div className="grid-showcase-breakpoints-table__grid grid-showcase-breakpoints-table__row">
|
||||
<div className="grid-showcase-breakpoints-table__cell"><code>xl</code></div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">1280px</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">12 columns</div>
|
||||
<div className="grid-showcase-breakpoints-table__cell">112px (max-width: 1440px)</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr 1fr 1.5fr' }}>
|
||||
<div style={{ padding: '12px' }}><code>xl</code></div>
|
||||
<div style={{ padding: '12px' }}>1280px</div>
|
||||
<div style={{ padding: '12px' }}>12 columns</div>
|
||||
<div style={{ padding: '12px' }}>112px (max-width: 1440px)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -316,13 +334,13 @@ export default function GridShowcase() {
|
||||
<h2 className="h3 mb-6">Usage</h2>
|
||||
|
||||
<h4 className="h5 mb-4">Import</h4>
|
||||
<div className={clsx("mb-6 br-4", "grid-showcase-code-block")}>
|
||||
<pre>{`import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";`}</pre>
|
||||
<div className="p-4 bg-light br-4 mb-6" style={{ fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, color: '#000' }}>{`import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";`}</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Basic Example</h4>
|
||||
<div className={clsx("mb-6 br-4", "grid-showcase-code-block")}>
|
||||
<pre>{`<PageGrid>
|
||||
<div className="p-4 bg-light br-4 mb-6" style={{ fontFamily: 'monospace', fontSize: '14px', overflow: 'auto' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<PageGrid>
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={6}>
|
||||
Column 1
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { HeaderHeroSplitMedia } from 'shared/sections/HeaderHeroSplitMedia';
|
||||
import { HeaderHeroSplitMedia } from 'shared/patterns/HeaderHeroSplitMedia';
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
@@ -491,7 +491,7 @@ export default function HeaderHeroSplitMediaShowcase() {
|
||||
</div>
|
||||
<div className="p-6-sm p-10-until-sm br-8 mb-6" style={{ backgroundColor: '#1e1e1e', color: '#d4d4d4' }}>
|
||||
<pre style={{ margin: 0, overflow: 'auto' }}>
|
||||
<code>{`import { HeaderHeroSplitMedia } from 'shared/sections/HeaderHeroSplitMedia';
|
||||
<code>{`import { HeaderHeroSplitMedia } from 'shared/patterns/HeaderHeroSplitMedia';
|
||||
|
||||
// Full content - all props provided
|
||||
<HeaderHeroSplitMedia
|
||||
@@ -679,7 +679,7 @@ export default function HeaderHeroSplitMediaShowcase() {
|
||||
</div>
|
||||
<div>
|
||||
<strong>Documentation:</strong>{' '}
|
||||
<code>shared/sections/HeaderHeroSplitMedia/HeaderHeroSplitMedia.md</code>
|
||||
<code>shared/patterns/HeaderHeroSplitMedia/HeaderHeroSplitMedia.md</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
PageGridRow,
|
||||
PageGridCol,
|
||||
} from "shared/components/PageGrid/page-grid";
|
||||
import { SmallTilesSection } from "shared/sections/SmallTilesSection/SmallTilesSection";
|
||||
import { SmallTilesSection } from "shared/components/SmallTilesSection/SmallTilesSection";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
@@ -26,7 +26,7 @@ const logos = {
|
||||
],
|
||||
developer_tooling: ["cryptum", "evernode", "threezy", "tokenize"],
|
||||
interoperability: ["multichain"],
|
||||
wallet: ["crossmark", "edge", "gem-wallet", "xumm", "joey-wallet", "bifrost-wallet"],
|
||||
wallet: ["crossmark", "edge", "gem-wallet", "xumm", "joey-wallet"],
|
||||
nfts: [
|
||||
"aesthetes",
|
||||
"audiotarky",
|
||||
@@ -413,15 +413,6 @@ const cardsData = [
|
||||
category_name: "Wallet",
|
||||
link: "https://xaman.app/#team",
|
||||
},
|
||||
{
|
||||
id: "bifrost-wallet",
|
||||
title: "Bifrost Wallet",
|
||||
description:
|
||||
"Bifrost Wallet is a secure, independently audited self-custodial wallet for the XRP Ledger with multi-chain support. Purpose-built for XRPFi: a wallet where your XRP works for you and earns yield.",
|
||||
category_id: "wallet",
|
||||
category_name: "Wallet",
|
||||
link: "https://bifrostwallet.com/",
|
||||
},
|
||||
];
|
||||
|
||||
const featured_categories = {
|
||||
@@ -468,7 +459,7 @@ const uses = [
|
||||
{
|
||||
id: "wallet",
|
||||
title: "Wallet",
|
||||
number: 6,
|
||||
number: 5,
|
||||
description:
|
||||
"Build digital wallets to store passwords and interact with various blockchains to send and receive digital assets, including XRP."
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ const links = [
|
||||
];
|
||||
|
||||
const softwallets = [
|
||||
{ href: "https://bifrostwallet.com/", id: "wallet-bifrost", alt: "Bifrost Wallet" },
|
||||
{ href: "https://towolabs.com/", id: "wallet-towo", alt: "Towo" },
|
||||
{ href: "https://xaman.app/", id: "wallet-xumm", alt: "Xaman" },
|
||||
{ href: "https://trustwallet.com/", id: "wallet-trust", alt: "Trust Wallet" },
|
||||
{
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
---
|
||||
category: 2026
|
||||
date: "2026-01-27"
|
||||
template: '../../@theme/templates/blogpost'
|
||||
seo:
|
||||
title: Introducing Clio version 2.7.0
|
||||
description: Version 2.7.0 of Clio, an XRP Ledger API server optimized for HTTP and WebSocket API calls, is now available. This release adds new features and bug fixes.
|
||||
labels:
|
||||
- Clio Release Notes
|
||||
markdown:
|
||||
editPage:
|
||||
hide: true
|
||||
---
|
||||
# Introducing Clio version 2.7.0
|
||||
|
||||
Version 2.7.0 of Clio, an XRP Ledger API server optimized for HTTP and WebSocket API calls, is now available. This release adds new features and bug fixes.
|
||||
|
||||
## Install / Upgrade
|
||||
|
||||
| Package |
|
||||
| :------- |
|
||||
| [Clio Server Linux Release (GCC)](https://github.com/XRPLF/clio/releases/download/2.7.0/clio_server_Linux_Release_gcc.zip) |
|
||||
| [Clio Server Linux Debian Release (amd64)](https://github.com/XRPLF/clio/releases/download/2.7.0/clio_2.7.0_amd64.deb) |
|
||||
| [Clio Server macOS Release (Apple Clang 17)](https://github.com/XRPLF/clio/releases/download/2.7.0/clio_server_macOS_Release_apple-clang.zip) |
|
||||
|
||||
For other platforms, please [build from source](https://github.com/XRPLF/clio/releases/tag/2.7.0). The most recent commit in the git log should be:
|
||||
|
||||
```text
|
||||
Author: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
|
||||
Date: Thu Jan 15 19:03:47 2026 +0000
|
||||
|
||||
ci: Restart colima on macOS (#2923)
|
||||
```
|
||||
|
||||
## What's Changed
|
||||
|
||||
See the [Full Changelog on GitHub](https://github.com/XRPLF/clio/compare/2.6.0...2.7.0).
|
||||
|
||||
### Features
|
||||
|
||||
- Adds `account_mptoken_issuances` API method to retrieve all `MPTokenIssuances` created by a specified account, and `account_mptokens` API method to retrieve all `MPTokens` held by a specified account. ([#2680](https://github.com/XRPLF/clio/pull/2680))
|
||||
- Adds DynamicMPT support to `account_mptoken_issuances` handler. ([#2820](https://github.com/XRPLF/clio/pull/2820))
|
||||
|
||||
### Improvements
|
||||
|
||||
- Removed old ETL implementation and enabled ETLng by default. ([#2752](https://github.com/XRPLF/clio/pull/2752))
|
||||
- Added async framework `submit` method for running one-shot tasks that don't require a handle to retrieve the result. ([#2751](https://github.com/XRPLF/clio/pull/2751))
|
||||
- Updated the Ledger Publisher to use async framework instead of directly using `io_context`. ([#2756](https://github.com/XRPLF/clio/pull/2756))
|
||||
- Added support for normal/high priority to `WorkQueue`. ([#2721](https://github.com/XRPLF/clio/pull/2721))
|
||||
- Added ability to read and write `LedgerCache` to file. ([#2761](https://github.com/XRPLF/clio/pull/2761))
|
||||
- Added graceful shutdown for old web server. ([#2786](https://github.com/XRPLF/clio/pull/2786))
|
||||
- Prometheus requests are now handled in `WorkQueue`. ([#2790](https://github.com/XRPLF/clio/pull/2790))
|
||||
- Added observable value utility to enable reactive approach across the codebase. ([#2831](https://github.com/XRPLF/clio/pull/2831))
|
||||
- Added option to save cache asynchronously. ([#2883](https://github.com/XRPLF/clio/pull/2883))
|
||||
- Added basic support for channels. ([#2859](https://github.com/XRPLF/clio/pull/2859))
|
||||
- Added build information to `clio_server --version` command. ([#2893](https://github.com/XRPLF/clio/pull/2893))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed an issue where `account_info` was omitting the `signer_lists` field when requested for accounts with no signer lists. ([#2746](https://github.com/XRPLF/clio/pull/2746))
|
||||
- Fixed an issue where `ledger_entry` error codes didn't match with `rippled`. ([#2549](https://github.com/XRPLF/clio/pull/2549))
|
||||
- Enhanced cache saving error to include more information. ([#2794](https://github.com/XRPLF/clio/pull/2794))
|
||||
- Fixed `WorkQueue` contention issues. ([#2866](https://github.com/XRPLF/clio/pull/2866))
|
||||
- Fixed issue where failed asserts in tests produced no output. ([#2905](https://github.com/XRPLF/clio/pull/2905))
|
||||
- Added workaround for an edge case exception in `AmendmentCenter`. ([#2897](https://github.com/XRPLF/clio/pull/2897))
|
||||
- Fixed `WorkQueue` performance. ([#2887](https://github.com/XRPLF/clio/pull/2887))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Refactored duplicate `ledger_index` pattern in RPC handlers into a common function. ([#2755](https://github.com/XRPLF/clio/pull/2755))
|
||||
- Refactored `getLedgerIndex` to return `std::expected` instead of throwing exceptions. ([#2788](https://github.com/XRPLF/clio/pull/2788))
|
||||
- Added writing command to `etl::SystemState`. ([#2842](https://github.com/XRPLF/clio/pull/2842))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Removed `logging.md` from README. ([#2710](https://github.com/XRPLF/clio/pull/2710))
|
||||
- Fixed `graceful_period` description. ([#2791](https://github.com/XRPLF/clio/pull/2791))
|
||||
|
||||
### Styling
|
||||
|
||||
- Fixed pre-commit style issues. ([#2743](https://github.com/XRPLF/clio/pull/2743))
|
||||
- Fixed comment in `pre-commit-autoupdate.yml`. ([#2750](https://github.com/XRPLF/clio/pull/2750))
|
||||
- Fixed hadolint issues. ([#2777](https://github.com/XRPLF/clio/pull/2777))
|
||||
- Added black pre-commit hook. ([#2811](https://github.com/XRPLF/clio/pull/2811))
|
||||
- Updated pre-commit hooks. ([#2825](https://github.com/XRPLF/clio/pull/2825), [#2875](https://github.com/XRPLF/clio/pull/2875))
|
||||
- Used `shfmt` for shell scripts. ([#2841](https://github.com/XRPLF/clio/pull/2841))
|
||||
- Fixed clang-tidy error. ([#2901](https://github.com/XRPLF/clio/pull/2901))
|
||||
|
||||
### Testing
|
||||
|
||||
- Fixed flaky `DeadlineIsHandledCorrectly` test. ([#2716](https://github.com/XRPLF/clio/pull/2716))
|
||||
- Fixed flaky test. ([#2729](https://github.com/XRPLF/clio/pull/2729))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Pinned all GitHub actions. ([#2712](https://github.com/XRPLF/clio/pull/2712))
|
||||
- Updated CI to use intermediate environment variables for improved security. ([#2713](https://github.com/XRPLF/clio/pull/2713))
|
||||
- Updated CI to save full logs for failed sanitizer tests. ([#2715](https://github.com/XRPLF/clio/pull/2715))
|
||||
- Enabled clang asan builds. ([#2717](https://github.com/XRPLF/clio/pull/2717))
|
||||
- [DEPENDABOT] bump actions/upload-artifact from 4.6.2 to 5.0.0. ([#2722](https://github.com/XRPLF/clio/pull/2722))
|
||||
- [DEPENDABOT] bump actions/upload-artifact from 4.6.2 to 5.0.0 in /.github/actions/code-coverage. ([#2725](https://github.com/XRPLF/clio/pull/2725))
|
||||
- [DEPENDABOT] bump actions/download-artifact from 5.0.0 to 6.0.0. ([#2723](https://github.com/XRPLF/clio/pull/2723))
|
||||
- Improved pre-commit failure message. ([#2720](https://github.com/XRPLF/clio/pull/2720))
|
||||
- Updated CI to use XRPLF/get-nproc Github action. ([#2727](https://github.com/XRPLF/clio/pull/2727))
|
||||
- [DEPENDABOT] bump actions/checkout from 4.3.0 to 5.0.0. ([#2724](https://github.com/XRPLF/clio/pull/2724))
|
||||
- Added date to nightly release version. ([#2731](https://github.com/XRPLF/clio/pull/2731))
|
||||
- Fixed nightly commits link. ([#2738](https://github.com/XRPLF/clio/pull/2738))
|
||||
- Updated CI to use new prepare-runner. ([#2742](https://github.com/XRPLF/clio/pull/2742))
|
||||
- Updated tooling in Docker images. ([#2737](https://github.com/XRPLF/clio/pull/2737))
|
||||
- Installed pre-commit in the main CI image. ([#2744](https://github.com/XRPLF/clio/pull/2744))
|
||||
- Updated docker images. ([#2745](https://github.com/XRPLF/clio/pull/2745))
|
||||
- Added date to nightly release title. ([#2748](https://github.com/XRPLF/clio/pull/2748))
|
||||
- Updated prepare-runner to fix `ccache` on macOS. ([#2749](https://github.com/XRPLF/clio/pull/2749))
|
||||
- Removed backticks from release date. ([#2754](https://github.com/XRPLF/clio/pull/2754))
|
||||
- Specified apple-clang 17.0 in Conan profile. ([#2757](https://github.com/XRPLF/clio/pull/2757))
|
||||
- Fixed pre-commit hook failing on empty file. ([#2766](https://github.com/XRPLF/clio/pull/2766))
|
||||
- [DEPENDABOT] bump docker/setup-qemu-action from 3.6.0 to 3.7.0 in /.github/actions/build-docker-image. ([#2763](https://github.com/XRPLF/clio/pull/2763))
|
||||
- [DEPENDABOT] bump docker/metadata-action from 5.8.0 to 5.9.0 in /.github/actions/build-docker-image. ([#2762](https://github.com/XRPLF/clio/pull/2762))
|
||||
- Changed default `max_queue_size` to 1000. ([#2771](https://github.com/XRPLF/clio/pull/2771))
|
||||
- Specified bash as default shell in Github workflows. ([#2772](https://github.com/XRPLF/clio/pull/2772))
|
||||
- Updated CI to use `ucontext` in ASAN builds. ([#2775](https://github.com/XRPLF/clio/pull/2775))
|
||||
- Updated `xrpl` version to 3.0.0-rc1. ([#2776](https://github.com/XRPLF/clio/pull/2776))
|
||||
- Forced usage of `ucontext` with ASAN. ([#2774](https://github.com/XRPLF/clio/pull/2774))
|
||||
- Removed redundant silencing of ASAN errors in CI. ([#2779](https://github.com/XRPLF/clio/pull/2779))
|
||||
- Updated CI to use environment variables instead of input. ([#2781](https://github.com/XRPLF/clio/pull/2781))
|
||||
- Improved cache implementation. ([#2780](https://github.com/XRPLF/clio/pull/2780))
|
||||
- Updated nudb recipe to remove linker warnings. ([#2787](https://github.com/XRPLF/clio/pull/2787))
|
||||
- Updated CI to use environment variables instead of input in cache-key. ([#2789](https://github.com/XRPLF/clio/pull/2789))
|
||||
- Added defines for asan/tsan to Conan profile. ([#2784](https://github.com/XRPLF/clio/pull/2784))
|
||||
- Enabled TSAN in CI. ([#2785](https://github.com/XRPLF/clio/pull/2785))
|
||||
- Updated CI to stop downloading `ccache` on develop branch. ([#2792](https://github.com/XRPLF/clio/pull/2792))
|
||||
- Updated CI to always upload cache on develop. ([#2793](https://github.com/XRPLF/clio/pull/2793))
|
||||
- [DEPENDABOT] bump peter-evans/create-pull-request from 7.0.8 to 7.0.9. ([#2805](https://github.com/XRPLF/clio/pull/2805))
|
||||
- [DEPENDABOT] bump actions/checkout from 5.0.0 to 6.0.0. ([#2806](https://github.com/XRPLF/clio/pull/2806))
|
||||
- Updated `spdlog` and `fmt` libraries. ([#2804](https://github.com/XRPLF/clio/pull/2804))
|
||||
- Ran clang-tidy multiple times to ensure all issues were resolved. ([#2803](https://github.com/XRPLF/clio/pull/2803))
|
||||
- Fixed Repeat-based tests TSAN issues. ([#2810](https://github.com/XRPLF/clio/pull/2810))
|
||||
- Fixed `WebServerAdminTestsSuit` TSAN issues. ([#2809](https://github.com/XRPLF/clio/pull/2809))
|
||||
- Used `boost::asio::ssl::stream` instead of `boost::beast::ssl_stream`. ([#2814](https://github.com/XRPLF/clio/pull/2814))
|
||||
- Installed latest Ninja in images. ([#2813](https://github.com/XRPLF/clio/pull/2813))
|
||||
- Updated images to use latest Ninja. ([#2817](https://github.com/XRPLF/clio/pull/2817))
|
||||
- Updated lockfile. ([#2818](https://github.com/XRPLF/clio/pull/2818))
|
||||
- Added mathbunnyru to maintainers. ([#2823](https://github.com/XRPLF/clio/pull/2823))
|
||||
- Fixed TSAN async-signal-unsafe issue. ([#2824](https://github.com/XRPLF/clio/pull/2824))
|
||||
- [DEPENDABOT] bump docker/metadata-action from 5.9.0 to 5.10.0 in /.github/actions/build-docker-image. ([#2826](https://github.com/XRPLF/clio/pull/2826))
|
||||
- [DEPENDABOT] bump actions/checkout from 6.0.0 to 6.0.1. ([#2837](https://github.com/XRPLF/clio/pull/2837))
|
||||
- [DEPENDABOT] bump peter-evans/create-pull-request from 7.0.9 to 7.0.11. ([#2836](https://github.com/XRPLF/clio/pull/2836))
|
||||
- [DEPENDABOT] bump ytanikin/pr-conventional-commits from 1.4.2 to 1.5.1. ([#2835](https://github.com/XRPLF/clio/pull/2835))
|
||||
- Reduced delay in ETL taskman. ([#2802](https://github.com/XRPLF/clio/pull/2802))
|
||||
- Added systemd file to the Debian package. ([#2844](https://github.com/XRPLF/clio/pull/2844))
|
||||
- Switched to `xrpl` version 3.0.0. ([#2843](https://github.com/XRPLF/clio/pull/2843))
|
||||
- Added a Debian package to the Github release. ([#2850](https://github.com/XRPLF/clio/pull/2850))
|
||||
- Added a script to regenerate the Conan lockfile. ([#2849](https://github.com/XRPLF/clio/pull/2849))
|
||||
- [DEPENDABOT] bump tj-actions/changed-files from 46.0.5 to 47.0.1. ([#2853](https://github.com/XRPLF/clio/pull/2853))
|
||||
- [DEPENDABOT] bump peter-evans/create-pull-request from 7.0.11 to 8.0.0. ([#2854](https://github.com/XRPLF/clio/pull/2854))
|
||||
- [DEPENDABOT] bump actions/download-artifact from 6.0.0 to 7.0.0. ([#2855](https://github.com/XRPLF/clio/pull/2855))
|
||||
- [DEPENDABOT] bump actions/upload-artifact from 5.0.0 to 6.0.0. ([#2856](https://github.com/XRPLF/clio/pull/2856))
|
||||
- [DEPENDABOT] bump codecov/codecov-action from 5.5.1 to 5.5.2. ([#2857](https://github.com/XRPLF/clio/pull/2857))
|
||||
- [DEPENDABOT] bump actions/upload-artifact from 5.0.0 to 6.0.0 in /.github/actions/code-coverage. ([#2858](https://github.com/XRPLF/clio/pull/2858))
|
||||
- Updated shared Github actions. ([#2852](https://github.com/XRPLF/clio/pull/2852))
|
||||
- Removed unnecessary creation of build directory in CI. ([#2867](https://github.com/XRPLF/clio/pull/2867))
|
||||
- [DEPENDABOT] bump docker/setup-buildx-action from 3.11.1 to 3.12.0 in /.github/actions/build-docker-image. ([#2872](https://github.com/XRPLF/clio/pull/2872))
|
||||
- [DEPENDABOT] bump docker/setup-buildx-action from 3.11.1 to 3.12.0. ([#2870](https://github.com/XRPLF/clio/pull/2870))
|
||||
- [DEPENDABOT] bump actions/cache from 4.3.0 to 5.0.1. ([#2871](https://github.com/XRPLF/clio/pull/2871))
|
||||
- Updated prepare-runner in Github actions and workflows. ([#2889](https://github.com/XRPLF/clio/pull/2889))
|
||||
- Fixed branch name and commit SHA for GitHub PRs. ([#2888](https://github.com/XRPLF/clio/pull/2888))
|
||||
- Updated CI to show `ccache` stats. ([#2902](https://github.com/XRPLF/clio/pull/2902))
|
||||
- Changed build process to pass version explicitly instead of relying on tags. ([#2904](https://github.com/XRPLF/clio/pull/2904))
|
||||
- Updated `gtest` and `spdlog`. ([#2908](https://github.com/XRPLF/clio/pull/2908))
|
||||
- Updated tooling in Docker images. ([#2907](https://github.com/XRPLF/clio/pull/2907))
|
||||
- Updated CI workflows to use new Docker images and GitHub actions. ([#2909](https://github.com/XRPLF/clio/pull/2909))
|
||||
- Updated CI to use actual build date instead of date of last commit. ([#2911](https://github.com/XRPLF/clio/pull/2911))
|
||||
- Updated CI to use environment variable for `BUILD_TYPE` in `reusable-build.yml`. ([#2913](https://github.com/XRPLF/clio/pull/2913))
|
||||
- Changed build date format. ([#2914](https://github.com/XRPLF/clio/pull/2914))
|
||||
- Updated CI to restart colima on macOS. ([#2923](https://github.com/XRPLF/clio/pull/2923))
|
||||
- Reverted "refactor: Add writing command to etl::SystemState". ([#2860](https://github.com/XRPLF/clio/pull/2860))
|
||||
|
||||
## Contributors
|
||||
|
||||
The following people contributed directly to this release:
|
||||
|
||||
- [@godexsoft](https://github.com/godexsoft)
|
||||
- [@mathbunnyru](https://github.com/mathbunnyru)
|
||||
- [@kuznetsss](https://github.com/kuznetsss)
|
||||
- [@yinyiqian1](https://github.com/yinyiqian1)
|
||||
- [@emreariyurek](https://github.com/emreariyurek)
|
||||
- [@PeterChen13579](https://github.com/PeterChen13579)
|
||||
- [@bthomee](https://github.com/bthomee)
|
||||
|
||||
## Feedback
|
||||
|
||||
To report an issue or propose a new idea, please [open an issue](https://github.com/XRPLF/clio/issues).
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
category: 2026
|
||||
date: "2026-02-18"
|
||||
template: '../../@theme/templates/blogpost'
|
||||
seo:
|
||||
title: GPG Key Rotation
|
||||
description: Ripple has rotated the GPG key used to sign rippled packages.
|
||||
labels:
|
||||
- Advisories
|
||||
markdown:
|
||||
editPage:
|
||||
hide: true
|
||||
---
|
||||
# GPG Key Rotation
|
||||
|
||||
Ripple has rotated the GPG key used to sign `rippled` packages. If you have an existing installation, you should download and trust the new key to prevent issues upgrading in the future. **Automatic upgrades will not work** until you have trusted the new key.
|
||||
|
||||
## Action Needed
|
||||
|
||||
Add Ripple's package-signing GPG key, then verify the fingerprint of the newly-added key.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Red Hat / CentOS" %}
|
||||
|
||||
```bash
|
||||
sudo rpm --import https://repos.ripple.com/repos/rippled-rpm/stable/repodata/repomd.xml.key
|
||||
rpm -qi gpg-pubkey-ab06faa6 | gpg --show-keys --fingerprint
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="Ubuntu / Debian" %}
|
||||
|
||||
```bash
|
||||
sudo install -d -m 0755 /etc/apt/keyrings && \
|
||||
curl -fsSL https://repos.ripple.com/repos/api/gpg/key/public \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/ripple.gpg > /dev/null
|
||||
gpg --show-keys --fingerprint /etc/apt/keyrings/ripple.gpg
|
||||
```
|
||||
|
||||
Ensure the `signed-by` path in your Ripple source list refers to the location the key was downloaded. For example, on an Ubuntu 22.04 Jammy installation, `/etc/apt/sources.list.d/ripple.list` would contain:
|
||||
|
||||
```
|
||||
deb [signed-by=/etc/apt/keyrings/ripple.gpg] https://repos.ripple.com/repos/rippled-deb jammy stable
|
||||
```
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
The output should include an entry for Ripple such as the following:
|
||||
|
||||
```
|
||||
pub ed25519 2026-02-16 [SC] [expires: 2033-02-14]
|
||||
E057 C1CF 72B0 DF1A 4559 E857 7DEE 9236 AB06 FAA6
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub ed25519 2026-02-16 [S] [expires: 2029-02-15]
|
||||
```
|
||||
|
||||
{% admonition type="danger" name="Warning" %}
|
||||
Only trust this key if its fingerprint exactly matches: `E057 C1CF 72B0 DF1A 4559 E857 7DEE 9236 AB06 FAA6`.
|
||||
{% /admonition %}
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
category: 2026
|
||||
date: "2026-01-28"
|
||||
template: '../../@theme/templates/blogpost'
|
||||
seo:
|
||||
title: Introducing XRP Ledger version 3.1.0
|
||||
description: rippled version 3.1.0 is now available. This version introduces new amendments and bug fixes.
|
||||
labels:
|
||||
- rippled Release Notes
|
||||
markdown:
|
||||
editPage:
|
||||
hide: true
|
||||
---
|
||||
# Introducing XRP Ledger version 3.1.0
|
||||
|
||||
Version 3.1.0 of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available. This release introduces Single Asset Vaults, the Lending Protocol, and bug fixes.
|
||||
|
||||
|
||||
## Action Required
|
||||
|
||||
If you run an XRP Ledger server, upgrade to version 3.1.0 as soon as possible to ensure service continuity.
|
||||
|
||||
|
||||
## 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.0-1.el9.x86_64.rpm) | `8ac8c529718566e6ebef3cb177d170fda1efc81ee08c4ea99d7e8fa3db0a2c70` |
|
||||
| [DEB for Ubuntu / Debian (x86-64)](https://repos.ripple.com/repos/rippled-deb/pool/stable/rippled_3.1.0-1_amd64.deb) | `58574a2299db2edf567e09efa25504677cdc66e4fa26f8a84322ab05f3a02996` |
|
||||
|
||||
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:
|
||||
|
||||
```text
|
||||
commit d325f20c76fa798d0286d25e80b126ec0a2ee679
|
||||
Author: Ed Hennis <ed@ripple.com>
|
||||
Date: Tue Jan 27 21:13:06 2026 -0400
|
||||
|
||||
Set version to 3.1.0 (#6284)
|
||||
```
|
||||
|
||||
|
||||
## Full Changelog
|
||||
|
||||
|
||||
### Amendments
|
||||
|
||||
- **SingleAssetVault**: Adds vaults, which pool a single asset for use with the Lending Protocol. ([#5632](https://github.com/XRPLF/rippled/pull/5632))
|
||||
- **LendingProtocol**: Adds the ability to create loans on the XRP Ledger. Loan brokers can create fixed-term, uncollateralized loans using the pooled funds from a Single Asset Vault. The protocol is highly configurable, enabling loan brokers to tune risk appetite, depostitor protections, and economic incentives. ([#5632](https://github.com/XRPLF/rippled/pull/5632))
|
||||
- **fixBatchInnerSigs**: Fixes an issue where inner transactions of a `Batch` transaction would be flagged as having valid signatures. Inner transactions never have valid signatures. ([#6069](https://github.com/XRPLF/rippled/pull/6069))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Expand `Number` to support full integer range. ([#6192](https://github.com/XRPLF/rippled/pull/6192))
|
||||
- Fix: Reorder Batch Preflight Errors. ([#6176](https://github.com/XRPLF/rippled/pull/6176))
|
||||
- Fix dependencies so clio can use libxrpl. ([#6251](https://github.com/XRPLF/rippled/pull/6251))
|
||||
- Fix: Remove DEFAULT fields that change to the default in associateAsset (was Add Vault creation tests for showing valid range for AssetsMaximum). ([#6259](https://github.com/XRPLF/rippled/pull/6259))
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
The following RippleX teams and GitHub users contributed to this release:
|
||||
|
||||
- RippleX Engineering
|
||||
- RippleX Docs
|
||||
- RippleX Product
|
||||
- @dangell7
|
||||
|
||||
|
||||
## Bug Bounties and Responsible Disclosures
|
||||
|
||||
We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find.
|
||||
|
||||
To report a bug, please send a detailed report to: <bugs@xrpl.org>
|
||||
@@ -7,12 +7,6 @@
|
||||
page: index.page.tsx
|
||||
expanded: true
|
||||
items:
|
||||
- group: '2026'
|
||||
expanded: false
|
||||
items:
|
||||
- page: 2026/gpg-key-rotation.md
|
||||
- page: 2026/rippled-3.1.0.md
|
||||
- page: 2026/clio-2.7.0.md
|
||||
- group: '2025'
|
||||
expanded: false
|
||||
items:
|
||||
|
||||
@@ -1317,61 +1317,6 @@ const events = [
|
||||
image: require("../static/img/events/hackathon-kaigi.png"),
|
||||
end_date: "December 06, 2025",
|
||||
},
|
||||
{
|
||||
name: "XRP Community Day EMEA",
|
||||
description:
|
||||
"Join the EMEA XRP community on February 11, 2026 for XRP Community Day, a global virtual event celebrating innovation, utility, and growth across the XRP ecosystem.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/w118fmkh?utm_source=xrplorg",
|
||||
location: "Virtual - X Spaces",
|
||||
date: "February 11, 2026",
|
||||
image: require("../static/img/events/emea_xrplorg.png"),
|
||||
end_date: "February 11, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRP Community Day Americas",
|
||||
description:
|
||||
"Join the Americas XRP community on February 11, 2026 for XRP Community Day, a global virtual event celebrating innovation, utility, and growth across the XRP ecosystem.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/1powvqnc?utm_source=xrplorg",
|
||||
location: "Virtual - X Spaces",
|
||||
date: "February 11, 2026",
|
||||
image: require("../static/img/events/amer_xrplorg.png"),
|
||||
end_date: "February 11, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRP Community Day APAC",
|
||||
description:
|
||||
"Join the APAC XRP community on February 12 for XRP Community Day, a global virtual event celebrating innovation, utility, and growth across the XRP ecosystem.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/ckzg3l3r?utm_source=xrplorg",
|
||||
location: "Virtual - X Spaces",
|
||||
date: "February 12, 2026",
|
||||
image: require("../static/img/events/apac_xrplorg.png"),
|
||||
end_date: "February 12, 2026",
|
||||
},
|
||||
{
|
||||
name: "Building On The XRP Ledger",
|
||||
description:
|
||||
"This 2-day intensive hands-on training is designed for developers who are curious to learn about XRP Ledger. Meet your peers, share insights, and join a community of builders.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/lxb5ttsc",
|
||||
location: "Paris, France",
|
||||
date: "January 26 - 27, 2026",
|
||||
image: require("../static/img/events/building-xrpl.png"),
|
||||
end_date: "January 27, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRPL Meetup in London",
|
||||
description:
|
||||
"Calling all blockchain and XRP Ledger enthusiasts in London! Join XRPL Meetups to share knowledge, build real-life connections, and foster communities centered around blockchain and XRP Ledger. We're establishing local “XRPL Hubs” across Europe, and we want you to be a part of it!",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/xshnm19t",
|
||||
location: "London, UK",
|
||||
date: "February 18, 2026",
|
||||
image: require("../static/img/events/meetup-london.png"),
|
||||
end_date: "February 18, 2026",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1479,45 +1424,6 @@ export default function Events() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="container-new py-26">
|
||||
<div className="event-hero card-grid card-grid-2xN">
|
||||
<div className="pr-2 col">
|
||||
<img
|
||||
alt="xrp ledger events hero"
|
||||
src={require("../static/img/events/xrpl-hero.png")}
|
||||
className="w-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-5 pr-2 col">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="mb-8 h4 h2-sm">
|
||||
{translate("XRP Community Night Denver")}
|
||||
</h2>
|
||||
<h6 className="mb-3 eyebrow">{translate("Save the Date")}</h6>
|
||||
</div>
|
||||
<p className="mb-4">
|
||||
{translate(
|
||||
"Attending ETHDenver? Join us for an evening with the XRP community in Denver. Connect with the users, builders and projects innovating with and utilizing XRP."
|
||||
)}
|
||||
</p>
|
||||
<div className=" my-3 event-small-gray">
|
||||
{translate("Location: Denver, CO")}
|
||||
</div>
|
||||
<div className="py-2 my-3 event-small-gray">
|
||||
{translate("February 18, 2026")}
|
||||
</div>
|
||||
<div className="d-lg-block">
|
||||
<a
|
||||
className="btn btn-primary btn-arrow-out"
|
||||
target="_blank"
|
||||
href="https://luma.com/chz145tf?utm_source=xprlorg"
|
||||
>
|
||||
{translate("Register Now")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/* Upcoming Events */}
|
||||
<section className="container-new py-26" id="upcoming-events">
|
||||
<div className="p-0 pb-2 mb-4 d-flex flex-column-reverse col-lg-6 pr-lg-5">
|
||||
|
||||
@@ -406,78 +406,6 @@ const events = [
|
||||
start_date: "November 14, 2025",
|
||||
end_date: "December 06, 2025",
|
||||
},
|
||||
{
|
||||
name: "XRP Community Day EMEA",
|
||||
description:
|
||||
"Join the EMEA XRP community on February 11, 2026 for XRP Community Day, a global virtual event celebrating innovation, utility, and growth across the XRP ecosystem.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/w118fmkh?utm_source=xrplorg",
|
||||
location: "Virtual - X Spaces",
|
||||
date: "February 11, 2026",
|
||||
image: require("../static/img/events/emea_xrplorg.png"),
|
||||
start_date: "February 11, 2026",
|
||||
end_date: "February 11, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRP Community Day Americas",
|
||||
description:
|
||||
"Join the Americas XRP community on February 11, 2026 for XRP Community Day, a global virtual event celebrating innovation, utility, and growth across the XRP ecosystem.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/1powvqnc?utm_source=xrplorg",
|
||||
location: "Virtual - X Spaces",
|
||||
date: "February 11, 2026",
|
||||
image: require("../static/img/events/amer_xrplorg.png"),
|
||||
start_date: "February 11, 2026",
|
||||
end_date: "February 11, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRP Community Day APAC",
|
||||
description:
|
||||
"Join the APAC XRP community on February 12 for XRP Community Day, a global virtual event celebrating innovation, utility, and growth across the XRP ecosystem.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/ckzg3l3r?utm_source=xrplorg",
|
||||
location: "Virtual - X Spaces",
|
||||
date: "February 12, 2026",
|
||||
image: require("../static/img/events/apac_xrplorg.png"),
|
||||
start_date: "February 12, 2026",
|
||||
end_date: "February 12, 2026",
|
||||
},
|
||||
{
|
||||
name: "Building On The XRP Ledger",
|
||||
description:
|
||||
"This 2-day intensive hands-on training is designed for developers who are curious to learn about XRP Ledger. Meet your peers, share insights, and join a community of builders.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/lxb5ttsc",
|
||||
location: "Paris, France",
|
||||
date: "January 26 - 27, 2026",
|
||||
image: require("../static/img/events/building-xrpl.png"),
|
||||
start_date: "January 26, 2026",
|
||||
end_date: "January 27, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRPL Meetup in London",
|
||||
description:
|
||||
"Calling all blockchain and XRP Ledger enthusiasts in London! Join XRPL Meetups to share knowledge, build real-life connections, and foster communities centered around blockchain and XRP Ledger. We're establishing local “XRPL Hubs” across Europe, and we want you to be a part of it!",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/xshnm19t",
|
||||
location: "London, UK",
|
||||
date: "February 18, 2026",
|
||||
image: require("../static/img/events/meetup-london.png"),
|
||||
start_date: "February 18, 2026",
|
||||
end_date: "February 18, 2026",
|
||||
},
|
||||
{
|
||||
name: "XRP Community Night Denver",
|
||||
description:
|
||||
"Attending ETHDenver? Join us for an evening with the XRP community in Denver. Connect with the users, builders and projects innovating with and utilizing XRP.",
|
||||
type: "meetup",
|
||||
link: "https://luma.com/chz145tf?utm_source=xprlorg",
|
||||
location: "Denver, CO",
|
||||
date: "February 18, 2026",
|
||||
image: require("../static/img/events/denver_xrplorg.png"),
|
||||
start_date: "February 18, 2026",
|
||||
end_date: "February 18, 2026",
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
[AccountSet transactions]: /docs/references/protocol/transactions/types/accountset.md
|
||||
[AccountSet]: /docs/references/protocol/transactions/types/accountset.md
|
||||
[Address]: /docs/references/protocol/data-types/basic-data-types.md#addresses
|
||||
[Amendments entry]: /docs/references/protocol/ledger-data/ledger-entry-types/amendments.md
|
||||
[Amendments object]: /docs/references/protocol/ledger-data/ledger-entry-types/amendments.md
|
||||
[Amendments entry]: /docs/concepts/networks-and-servers/amendments.md
|
||||
[Amendments object]: /docs/concepts/networks-and-servers/amendments.md
|
||||
[Batch amendment]: /resources/known-amendments.md#batch
|
||||
[Batch]: /docs/references/protocol/transactions/types/batch.md
|
||||
[Batch transaction]: /docs/references/protocol/transactions/types/batch.md
|
||||
@@ -128,15 +128,6 @@
|
||||
[LedgerStateFix transaction]: /docs/references/protocol/transactions/types/ledgerstatefix.md
|
||||
[LedgerStateFix transactions]: /docs/references/protocol/transactions/types/ledgerstatefix.md
|
||||
[LedgerStateFix]: /docs/references/protocol/transactions/types/ledgerstatefix.md
|
||||
[LoanBrokerCoverClawback transaction]: /docs/references/protocol/transactions/types/loanbrokercoverclawback.md
|
||||
[LoanBrokerCoverDeposit transaction]: /docs/references/protocol/transactions/types/loanbrokercoverdeposit.md
|
||||
[LoanBrokerCoverWithdraw transaction]: /docs/references/protocol/transactions/types/loanbrokercoverwithdraw.md
|
||||
[LoanBrokerDelete transaction]: /docs/references/protocol/transactions/types/loanbrokerdelete.md
|
||||
[LoanBrokerSet transaction]: /docs/references/protocol/transactions/types/loanbrokerset.md
|
||||
[LoanDelete transaction]: /docs/references/protocol/transactions/types/loandelete.md
|
||||
[LoanManage transaction]: /docs/references/protocol/transactions/types/loanmanage.md
|
||||
[LoanPay transaction]: /docs/references/protocol/transactions/types/loanpay.md
|
||||
[LoanSet transaction]: /docs/references/protocol/transactions/types/loanset.md
|
||||
[Marker]: /docs/references/http-websocket-apis/api-conventions/markers-and-pagination.md
|
||||
[MPTokenIssuanceSet]: /docs/references/protocol/transactions/types/mptokenissuanceset.md
|
||||
[MPTokenIssuanceSet transaction]: /docs/references/protocol/transactions/types/mptokenissuanceset.md
|
||||
@@ -144,7 +135,6 @@
|
||||
[MPTokensV1 amendment]: /resources/known-amendments.md#mptokensv1
|
||||
[MultiSign amendment]: /resources/known-amendments.md#multisign
|
||||
[MultiSignReserve amendment]: /resources/known-amendments.md#multisignreserve
|
||||
[NFT]: /docs/references/protocol/data-types/nftoken.md
|
||||
[NFTokenAcceptOffer transaction]: /docs/references/protocol/transactions/types/nftokenacceptoffer.md
|
||||
[NFTokenAcceptOffer transactions]: /docs/references/protocol/transactions/types/nftokenacceptoffer.md
|
||||
[NFTokenAcceptOffer]: /docs/references/protocol/transactions/types/nftokenacceptoffer.md
|
||||
@@ -165,7 +155,6 @@
|
||||
[NFTokenPage entry]: /docs/references/protocol/ledger-data/ledger-entry-types/nftokenpage.md
|
||||
[NFTokenPage object]: /docs/references/protocol/ledger-data/ledger-entry-types/nftokenpage.md
|
||||
[NFToken]: /docs/references/protocol/data-types/nftoken.md
|
||||
[Negative UNL]: /docs/concepts/consensus-protocol/negative-unl.md
|
||||
[NegativeUNL amendment]: /resources/known-amendments.md#negativeunl
|
||||
[NegativeUNL entry]: /docs/references/protocol/ledger-data/ledger-entry-types/negativeunl.md
|
||||
[NegativeUNL object]: /docs/references/protocol/ledger-data/ledger-entry-types/negativeunl.md
|
||||
@@ -259,13 +248,6 @@
|
||||
[UNLModify pseudo-transaction]: /docs/references/protocol/transactions/pseudo-transaction-types/unlmodify.md
|
||||
[UNLModify pseudo-transactions]: /docs/references/protocol/transactions/pseudo-transaction-types/unlmodify.md
|
||||
[UNLModify]: /docs/references/protocol/transactions/pseudo-transaction-types/unlmodify.md
|
||||
[Vault entry]: /docs/references/protocol/ledger-data/ledger-entry-types/vault.md
|
||||
[VaultCreate transaction]: /docs/references/protocol/transactions/types/vaultcreate.md
|
||||
[VaultDelete transaction]: /docs/references/protocol/transactions/types/vaultdelete.md
|
||||
[VaultDeposit transaction]: /docs/references/protocol/transactions/types/vaultdeposit.md
|
||||
[VaultSet transaction]: /docs/references/protocol/transactions/types/vaultset.md
|
||||
[VaultWithdraw transaction]: /docs/references/protocol/transactions/types/vaultwithdraw.md
|
||||
[VaultClawback transaction]: /docs/references/protocol/transactions/types/vaultclawback.md
|
||||
[XChainAddAccountCreateAttestation transaction]: /docs/references/protocol/transactions/types/xchainaddaccountcreateattestation.md
|
||||
[XChainAddAccountCreateAttestation transactions]: /docs/references/protocol/transactions/types/xchainaddaccountcreateattestation.md
|
||||
[XChainAddAccountCreateAttestation]: /docs/references/protocol/transactions/types/xchainaddaccountcreateattestation.md
|
||||
@@ -284,7 +266,6 @@
|
||||
[XChainOwnedClaimID entry]: /docs/references/protocol/ledger-data/ledger-entry-types/xchainownedclaimid
|
||||
[XRP, in drops]: /docs/references/protocol/data-types/basic-data-types.md#specifying-currency-amounts
|
||||
[XRPFees amendment]: /resources/known-amendments.md#xrpfees
|
||||
[AccountID]: /docs/references/protocol/binary-format/#accountid-fields
|
||||
[account_channels command]: /docs/references/http-websocket-apis/public-api-methods/account-methods/account_channels.md
|
||||
[account_channels method]: /docs/references/http-websocket-apis/public-api-methods/account-methods/account_channels.md
|
||||
[account_currencies command]: /docs/references/http-websocket-apis/public-api-methods/account-methods/account_currencies.md
|
||||
@@ -314,7 +295,6 @@
|
||||
[channel_verify command]: /docs/references/http-websocket-apis/public-api-methods/payment-channel-methods/channel_verify.md
|
||||
[channel_verify method]: /docs/references/http-websocket-apis/public-api-methods/payment-channel-methods/channel_verify.md
|
||||
[common fields]: /docs/references/protocol/transactions/common-fields.md
|
||||
[common ledger entry fields]: /docs/references/protocol/ledger-data/common-fields/
|
||||
[connect command]: /docs/references/http-websocket-apis/admin-api-methods/peer-management-methods/connect.md
|
||||
[connect method]: /docs/references/http-websocket-apis/admin-api-methods/peer-management-methods/connect.md
|
||||
[consensus_info command]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/consensus_info.md
|
||||
@@ -329,7 +309,6 @@
|
||||
[fee command]: /docs/references/http-websocket-apis/public-api-methods/server-info-methods/fee.md
|
||||
[fee levels]: /docs/concepts/transactions/transaction-cost.md#fee-levels
|
||||
[fee method]: /docs/references/http-websocket-apis/public-api-methods/server-info-methods/fee.md
|
||||
[fee voting]: /docs/concepts/consensus-protocol/fee-voting.md
|
||||
[fetch_info command]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/fetch_info.md
|
||||
[fetch_info method]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/fetch_info.md
|
||||
[fix1201 amendment]: /resources/known-amendments.md#fix1201
|
||||
@@ -362,21 +341,17 @@
|
||||
[fixRemoveNFTokenAutoTrustLine amendment]: /resources/known-amendments.md#fixremovenftokenautotrustline
|
||||
[fixTakerDryOfferRemoval amendment]: /resources/known-amendments.md#fixtakerdryofferremoval
|
||||
[fixTrustLinesToSelf amendment]: /resources/known-amendments.md#fixtrustlinestoself
|
||||
[flags field]: /docs/references/protocol/transactions/common-fields#flags-field
|
||||
[get_aggregate_price command]: /docs/references/http-websocket-apis/public-api-methods/path-and-order-book-methods/get_aggregate_price.md
|
||||
[get_aggregate_price method]: /docs/references/http-websocket-apis/public-api-methods/path-and-order-book-methods/get_aggregate_price.md
|
||||
[gateway_balances command]: /docs/references/http-websocket-apis/public-api-methods/account-methods/gateway_balances.md
|
||||
[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 JavaScript]: /docs/tutorials/javascript/build-apps/get-started.md
|
||||
[Get Started Using Python]: /docs/tutorials/python/build-apps/get-started.md
|
||||
[hexadecimal]: https://en.wikipedia.org/wiki/Hexadecimal
|
||||
[identifying hash]: /docs/concepts/transactions/index.md#identifying-transactions
|
||||
[json command]: /docs/references/http-websocket-apis/public-api-methods/utility-methods/json.md
|
||||
[json method]: /docs/references/http-websocket-apis/public-api-methods/utility-methods/json.md
|
||||
[ledger command]: /docs/references/http-websocket-apis/public-api-methods/ledger-methods/ledger.md
|
||||
[ledger entry ID]: /docs/references/protocol/ledger-data/common-fields.md#ledger-entry-id
|
||||
[ledger format]: /docs/references/protocol/ledger-data/ledger-entry-types/index.md
|
||||
[ledger index]: /docs/references/protocol/data-types/basic-data-types.md#ledger-index
|
||||
[ledger method]: /docs/references/http-websocket-apis/public-api-methods/ledger-methods/ledger.md
|
||||
@@ -394,13 +369,6 @@
|
||||
[ledger_entry method]: /docs/references/http-websocket-apis/public-api-methods/ledger-methods/ledger_entry.md
|
||||
[ledger_request command]: /docs/references/http-websocket-apis/admin-api-methods/logging-and-data-management-methods/ledger_request.md
|
||||
[ledger_request method]: /docs/references/http-websocket-apis/admin-api-methods/logging-and-data-management-methods/ledger_request.md
|
||||
[Lending Protocol]: /docs/concepts/tokens/lending-protocol.md
|
||||
[Loan]: /docs/references/protocol/ledger-data/ledger-entry-types/loan.md
|
||||
[Loan entry]: /docs/references/protocol/ledger-data/ledger-entry-types/loan.md
|
||||
[Loan ledger entry]: /docs/references/protocol/ledger-data/ledger-entry-types/loan.md
|
||||
[LoanBroker]: /docs/references/protocol/ledger-data/ledger-entry-types/loanbroker.md
|
||||
[LoanBroker entry]: /docs/references/protocol/ledger-data/ledger-entry-types/loanbroker.md
|
||||
[LoanBroker ledger entry]: /docs/references/protocol/ledger-data/ledger-entry-types/loanbroker.md
|
||||
[log_level command]: /docs/references/http-websocket-apis/admin-api-methods/logging-and-data-management-methods/log_level.md
|
||||
[log_level method]: /docs/references/http-websocket-apis/admin-api-methods/logging-and-data-management-methods/log_level.md
|
||||
[logrotate command]: /docs/references/http-websocket-apis/admin-api-methods/logging-and-data-management-methods/logrotate.md
|
||||
@@ -436,12 +404,10 @@
|
||||
[public servers]: /docs/tutorials/public-servers.md
|
||||
[random command]: /docs/references/http-websocket-apis/public-api-methods/utility-methods/random.md
|
||||
[random method]: /docs/references/http-websocket-apis/public-api-methods/utility-methods/random.md
|
||||
[reserves]: /docs/concepts/accounts/reserves.md
|
||||
[result code]: /docs/references/protocol/transactions/transaction-results/index.md
|
||||
[ripple-lib]: https://github.com/XRPLF/xrpl.js
|
||||
[ripple_path_find command]: /docs/references/http-websocket-apis/public-api-methods/path-and-order-book-methods/ripple_path_find.md
|
||||
[ripple_path_find method]: /docs/references/http-websocket-apis/public-api-methods/path-and-order-book-methods/ripple_path_find.md
|
||||
[Ripple Epoch]: /docs/references/protocol/data-types/basic-data-types.md#specifying-time
|
||||
[seconds since the Ripple Epoch]: /docs/references/protocol/data-types/basic-data-types.md#specifying-time
|
||||
[server_definitions method]: /docs/references/http-websocket-apis/public-api-methods/server-info-methods/server_definitions.md
|
||||
[server_info command]: /docs/references/http-websocket-apis/public-api-methods/server-info-methods/server_info.md
|
||||
@@ -466,7 +432,6 @@
|
||||
[transaction cost]: /docs/concepts/transactions/transaction-cost.md
|
||||
[transaction_entry command]: /docs/references/http-websocket-apis/public-api-methods/transaction-methods/transaction_entry.md
|
||||
[transaction_entry method]: /docs/references/http-websocket-apis/public-api-methods/transaction-methods/transaction_entry.md
|
||||
[transaction result codes]: /docs/references/protocol/transactions/transaction-results
|
||||
[tx command]: /docs/references/http-websocket-apis/public-api-methods/transaction-methods/tx.md
|
||||
[tx method]: /docs/references/http-websocket-apis/public-api-methods/transaction-methods/tx.md
|
||||
[tx_history command]: /docs/references/http-websocket-apis/public-api-methods/transaction-methods/tx_history.md
|
||||
@@ -482,9 +447,5 @@
|
||||
[validator_list_sites method]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/validator_list_sites.md
|
||||
[validators command]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/validators.md
|
||||
[validators method]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/validators.md
|
||||
[vault_info command]: /docs/references/http-websocket-apis/public-api-methods/vault-methods/vault_info.md
|
||||
[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.js library]: https://github.com/XRPLF/xrpl.js
|
||||
[xrpl-py library]: https://github.com/XRPLF/xrpl-py
|
||||
|
||||
@@ -10,7 +10,7 @@ status: not_enabled
|
||||
|
||||
Permission delegation is the function of granting various permissions to another account to send permissions on behalf of your account. You can use permission delegation to enable flexible security paradigms such as role-based access control, instead of or alongside techniques such as [multi-signing](./multi-signing.md).
|
||||
|
||||
{% amendment-disclaimer name="PermissionDelegation" /%}
|
||||
{% amendment-disclaimer name="PermissionDelegation" /%}
|
||||
|
||||
|
||||
## Background: The Need for Permission Delegation
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
seo:
|
||||
description: A pseudo-account is a special type of XRPL account that holds assets on behalf of an on-chain protocol.
|
||||
labels:
|
||||
- Single Asset Vault
|
||||
- AMM
|
||||
- Lending Protocol
|
||||
status: not_enabled
|
||||
---
|
||||
|
||||
# Pseudo-Accounts
|
||||
|
||||
The XRP Ledger is an account-based blockchain where assets like XRP, trust line tokens, and Multi-Purpose Tokens (MPTs) are held by accounts, and are represented on-chain by an [AccountRoot](../../references/protocol/ledger-data/ledger-entry-types/accountroot) ledger entry. However, certain use cases require assets to be transferable to and from an object, which is why a pseudo-account is needed.
|
||||
|
||||
A pseudo-account is a special type of account that holds assets on behalf of an on-chain protocol. Use cases for pseudo-accounts include:
|
||||
|
||||
- **Automated Market Makers (AMM)**: The [XLS-30 amendment](../../../resources/known-amendments#amm) introduced pseudo-accounts for AMMs by adding the `AMMID` field to the `AccountRoot` ledger entry. This field links a pseudo-account to an AMM instance, allowing it to track XRP and token balances in the pool and issue `LPTokens` on behalf of the AMM instance.
|
||||
|
||||
- **Single Asset Vaults**: A single asset vault pseudo-account is used to store deposited funds and issue MPT shares. A new `VaultID` field is introduced in the `AccountRoot` ledger entry, which links the pseudo-account with the vault.
|
||||
|
||||
- **Lending Protocol**: The Lending Protocol also uses the single asset vault's pseudo-account, with each `LoanBroker` tracked in the pseudo-account's owner directory. The pseudo-account holds first-loss capital that protects vault depositors from loan defaults, as well as the loan funds themselves.
|
||||
|
||||
A pseudo-account has strict limitations. It cannot receive payments from other accounts, cannot send transactions since it has no signing authority, and exists solely to store or issue assets.
|
||||
|
||||
## Reserve Requirements
|
||||
|
||||
The cost of creating a pseudo-account depends on whether it is owned and controlled by another account:
|
||||
|
||||
- **Owned pseudo-accounts**: For objects like a `Vault` where a single account owns and controls the associated pseudo-account, the creation transaction increases the owner's XRP reserve by one [incremental owner reserve](../accounts/reserves#base-reserve-and-owner-reserve) (currently {% $env.PUBLIC_OWNER_RESERVE %}). This is in addition to any other reserve requirements of the transaction (for example, the Vault object itself).
|
||||
|
||||
- **Unowned pseudo-accounts**: For objects like an `AMM` that are not owned by any account, the creation transaction must charge a special, higher-than-normal transaction fee. This fee must be at least the value of one incremental owner reserve. This amount is burned, compensating for the permanent ledger space without tying the reserve to a specific owner.
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
@@ -1,202 +0,0 @@
|
||||
---
|
||||
seo:
|
||||
description: The XRPL Lending Protocol enables on-chain, uncollateralized fixed-term loans.
|
||||
labels:
|
||||
- Decentralized Finance
|
||||
- Lending Protocol
|
||||
status: not_enabled
|
||||
---
|
||||
# Lending Protocol
|
||||
|
||||
The Lending Protocol is an XRP Ledger DeFi primitive that enables on-chain, fixed-term, uncollateralized loans using pooled funds from a [Single Asset Vault](./single-asset-vaults.md). The protocol is highly configurable, enabling loan brokers to easily tune risk appetite, depostitor protections, and economic incentives.
|
||||
|
||||
The implementation relies on off-chain underwriting and risk management to assess the creditworthiness of borrowers, but offers peer-to-peer loans without intermediaries like banks or financial institutions. First-loss capital protection is used to help offset losses from loan defaults.
|
||||
|
||||
The current implementation of the lending protocol doesn't include automated on-chain collateral and liquidation management, instead focusing on on-chain credit origination.
|
||||
|
||||
To ensure compliance needs are met, asset issuers can [claw back](../../references/protocol/transactions/types/clawback.md) funds from the vault associated with the lending protocol. Issuers can also [freeze](./fungible-tokens/freezes.md) individual accounts or issue a global freeze.
|
||||
|
||||
{% amendment-disclaimer name="LendingProtocol" /%}
|
||||
|
||||
## Protocol Flow
|
||||
|
||||
There are three parties involved in the process of creating a loan:
|
||||
|
||||
- **Loan Brokers**: Create asset vaults and manage associated loans.
|
||||
- **Depositors**: Add assets to vaults.
|
||||
- **Borrowers**: Receive loans, making repayments as defined by their loan terms.
|
||||
|
||||
[{% inline-svg file="../../img/lending-protocol-diagram.svg" /%}](../../img/lending-protocol-diagram.svg "Diagram: The lifecycle of a loan.")
|
||||
|
||||
The lifecycle of a loan is as follows:
|
||||
|
||||
1. A loan broker creates a vault.
|
||||
2. Depositors add assets to the vault.
|
||||
3. (Optional) The loan broker deposits first-loss capital.
|
||||
4. A loan broker and borrower create a loan, defining the terms of the loan, and the requested principal (excluding fees) is transferred to the borrower.
|
||||
5. If payments are missed, the loan enters a grace period. Once the grace period expires, the loan broker has the option to default the loan.
|
||||
6. The loan is deleted when matured or defaulted.
|
||||
7. (Optional) The loan broker can withdraw first-loss capital.
|
||||
8. After all loans are paid, the loan broker can delete the `LoanBroker` ledger entry, and then the corresponding `Vault` ledger entry.
|
||||
|
||||
|
||||
## Accounting
|
||||
|
||||
### Risk Management
|
||||
|
||||
#### First-Loss Capital
|
||||
|
||||
First-Loss Capital is an optional mechanism to mitigate the risks associated with lending. To protect investors' assets, a loan broker can deposit assets as first-loss capital, which acts as a buffer in the event of loan defaults. The first-loss capital is placed into the vault to cover a percentage of losses from missed payments.
|
||||
|
||||
Three parameters control the First-Loss Capital:
|
||||
|
||||
- `CoverAvailable`: The total amount of cover deposited by the lending protocol owner.
|
||||
- `CoverRateMinimum`: The percentage of debt that must be covered by `CoverAvailable`.
|
||||
- `CoverRateLiquidation`: The maximum percentage of the minimum required cover _(DebtTotal x CoverRateMinimum)_ that will be placed in the asset vault to cover a loan default.
|
||||
|
||||
Whenever the available cover falls below the minimum required:
|
||||
|
||||
- The loan broker can't issue new loans.
|
||||
- The loan broker can't receive fees. All fees are added to the First-Loss Capital to cover the deficit.
|
||||
|
||||
Below is an example of how first-loss capital is used to cover a loan default:
|
||||
|
||||
```
|
||||
** Initial States **
|
||||
|
||||
-- Vault --
|
||||
AssetsTotal = 100,090 Tokens
|
||||
AssetsAvailable = 99,000 Tokens
|
||||
SharesTotal = 100,000 Tokens
|
||||
|
||||
-- Lending Protocol --
|
||||
DebtTotal = 1,090 Tokens
|
||||
CoverRateMinimum = 0.1 (10%)
|
||||
CoverRateLiquidation = 0.1 (10%)
|
||||
CoverAvailable = 1,000 Tokens
|
||||
|
||||
-- Loan --
|
||||
PrincipleOutstanding = 1,000 Tokens
|
||||
InterestOutstanding = 90 Tokens
|
||||
|
||||
|
||||
# First-Loss Capital liquidation maths
|
||||
|
||||
DefaultAmount = PrincipleOutstanding + InterestOutstanding
|
||||
= 1,000 + 90
|
||||
= 1,090
|
||||
|
||||
# The amount of the default that the first-loss capital scheme will cover
|
||||
DefaultCovered = min((DebtTotal x CoverRateMinimum) x CoverRateLiquidation, DefaultAmount)
|
||||
= min((1,090 * 0.1) * 0.1, 1,090) = min(10.9, 1,090)
|
||||
= 10.9 Tokens
|
||||
|
||||
Loss = DefaultAmount - DefaultCovered
|
||||
= 1,090 - 10.9
|
||||
= 1,079.1 Tokens
|
||||
|
||||
FundsReturned = DefaultCovered
|
||||
= 10.9
|
||||
|
||||
# Note, Loss + FundsReturned MUST be equal to PrincipleOutstanding + InterestOutstanding
|
||||
|
||||
** State Changes **
|
||||
|
||||
-- Vault --
|
||||
AssetsTotal = AssetsTotal - Loss
|
||||
= 100,090 - 1,079.1
|
||||
= 99,010.9 Tokens
|
||||
|
||||
AssetsAvailable = AssetsAvailable + FundsReturned
|
||||
= 99,000 + 10.9
|
||||
= 99,010.9 Tokens
|
||||
|
||||
SharesTotal = (UNCHANGED)
|
||||
|
||||
-- Lending Protocol --
|
||||
DebtTotal = DebtTotal - PrincipleOutstanding + InterestOutstanding
|
||||
= 1,090 - (1,000 + 90)
|
||||
= 0 Tokens
|
||||
|
||||
CoverAvailable = CoverAvailable - DefaultCovered
|
||||
= 1,000 - 10.9
|
||||
= 989.1 Tokens
|
||||
```
|
||||
|
||||
#### Impairment
|
||||
|
||||
If the loan broker discovers a borrower can't make an upcoming payment, impairment allows the loan broker to register a "paper loss" with the vault. The impairment mechanism moves the due date of the next payment to the time the loan is impaired, allowing the loan to default more quickly. However, if the borrower makes a payment before that date, the impairment status is automatically cleared.
|
||||
|
||||
|
||||
### Compliance
|
||||
|
||||
#### Clawback
|
||||
|
||||
Issuers (trust line token or MPT, not XRP) can claw back funds from First-Loss Capital. To ensure there is always a minimum amount of capital available to protect depositors, issuers can't claw back the entire available amount. Instead, they can claw back up to a minimum amount of First-Loss Capital that the loan broker must maintain for the lending protocol; the minimum amount is calculated as `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`.
|
||||
|
||||
#### Freeze
|
||||
|
||||
Freezing is a mechanism by which an asset issuer (trust line token or MPT, not XRP) prevents an account from sending their issued asset. _Deep freeze_ takes this a step further by preventing an account from sending _and_ receiving issued assets. Issuers can also enact a _global freeze_, which prevents everyone from sending or receiving their issued asset.
|
||||
|
||||
{% admonition type="info" name="Note" %}
|
||||
In all freeze scenarios, assets can be sent back to the issuer.
|
||||
{% /admonition %}
|
||||
|
||||
If a borrower has their account frozen or deep frozen, they can't make loan payments. This doesn't absolve a borrower of their repayment obligations, and they will eventually default on their loan.
|
||||
|
||||
Freezing a borrower's account won't affect a loan broker's functions, but it will prevent them from receiving any lending protocol fees. However, issuers can freeze a loan broker's _pseudo-account_ and prevent the loan broker from creating new loans; existing loans won't be affected. A deep freeze on a loan broker's _pseudo-account_ also prevents loans from being repaid.
|
||||
|
||||
|
||||
### Interest Rates
|
||||
|
||||
There are three interest rates associated with a loan:
|
||||
|
||||
- **Interest Rate**: The regular interest rate based on the principal amount. It is the cost of borrowing funds.
|
||||
- **Late Interest Rate**: A higher interest rate charged for a late payment.
|
||||
- **Full Payment Rate**: An interest rate charged for repaying the total loan early.
|
||||
|
||||
|
||||
### Fees
|
||||
|
||||
The lending protocol charges a number of fees that the loan broker can configure. The protocol won't charge these fees if the loan broker hasn't deposited enough first-loss capital.
|
||||
|
||||
- **Management Fee**: This is a percentage of interest charged by the loan broker. Vault depositors pay this fee.
|
||||
- **Loan Origination Fee**: A fee paid to the loan broker, taken from the principal amount loaned out.
|
||||
- **Loan Service Fee**: A fee charged on top of each loan payment.
|
||||
- **Late Payment Fee**: A fee paid on top of a late payment.
|
||||
- **Early Payment Fee**: A fee paid on top of an early payment.
|
||||
|
||||
|
||||
## Loan Payment Processing
|
||||
|
||||
Loan payments are evaluated and processed around three criteria: amount, timing, and specified flags. The combination of these criteria determine how funds are applied to the loan's principal, interest, and associated fees.
|
||||
|
||||
Each payment consists of four components:
|
||||
|
||||
- **Principal**: The portion that reduces the outstanding loan principle.
|
||||
- **Interest**: The portion that covers the cost of borrowing for the period.
|
||||
- **Fees**: The portion that covers any applicable service fees, management fees, late payment fees, or other charges.
|
||||
- **ValueChange**: The amount by which the payment changes the loan balance.
|
||||
|
||||
When the loan payment is submitted, the lending protocol then checks these parameters:
|
||||
|
||||
- **Timing**: Is the payment on time or late?
|
||||
- **Amount**: Does the payment amount meet the minimum required amount, or exceed it?
|
||||
|
||||
Based on the timing and transaction flags, the lending protocol processes the payment as one of four types:
|
||||
|
||||
- **On-Time Payments**: If the payment is on-time, it's further classified into these payment scenarios:
|
||||
- **Sequential Periodic Payments**: The payment is applied to as many complete payment cycles as possible; cycles are calculated as the amount due each payment period (including fees).
|
||||
- **Overpayments**: After all possible cycles are fully paid, any remaining amount is treated as an overpayment and applied to the principal. This type of payment requires the `lsfLoanOverpayment` flag to be enabled on the `Loan` ledger entry, as well as the `tfLoanOverpayment` flag to be enabled on the `LoanPay` transaction. If these flags are missing, the excess amount is ignored.
|
||||
- **Full Early Repayment**: The payment has the `tfLoanFullPayment` flag set, and the amount covers the remainder of the loan (including fees).
|
||||
- **Late Payments**: The payment is late on a payment cycle. Late payments must be for an exact amount, calculated as:
|
||||
|
||||
`totalDue = periodicPayment + loanServiceFee + latePaymentFee + latePaymentInterest`
|
||||
|
||||
Overpayments aren't permitted on late payments; any excess amount is ignored.
|
||||
|
||||
{% admonition type="info" name="Note" %}
|
||||
In scenarios where excess payment amounts are "ignored", the transaction succeeds, but the borrower is only charged on the expected amount.
|
||||
{% /admonition %}
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
@@ -1,251 +0,0 @@
|
||||
---
|
||||
seo:
|
||||
description: Single asset vaults aggregate assets from multiple depositors and make them available to other on-chain protocols.
|
||||
labels:
|
||||
- Single Asset Vault
|
||||
status: not_enabled
|
||||
---
|
||||
|
||||
# Single Asset Vaults
|
||||
|
||||
A single asset vault is an XRP Ledger primitive that aggregates assets from multiple depositors and makes them available to other on-chain protocols, such as the Lending Protocol. A vault asset can be [XRP](../../introduction/what-is-xrp.md), a [trust line token](../tokens/fungible-tokens/index.md), or an [MPT (Multi-Purpose Token)](../tokens/fungible-tokens/multi-purpose-tokens.md).
|
||||
|
||||
A Vault Owner account manages the vault and can create, update, or delete it as needed. When creating a vault, the Vault Owner can also specify whether shares are transferable or non-transferable. Non-transferable shares cannot be transferred to any other account, and can only be redeemed.
|
||||
|
||||
{% amendment-disclaimer name="SingleAssetVault" /%}
|
||||
|
||||
## Public vs. Private Vaults
|
||||
|
||||
A vault can be **public** or **private**, depending on the required level of access control.
|
||||
|
||||
In a public vault, anyone can deposit or redeem liquidity as long as they hold sufficient shares. In contrast, a private vault restricts access, allowing only depositors with the necessary [Credentials](../../concepts/decentralized-storage/credentials.md), managed through [Permissioned Domains](./decentralized-exchange/permissioned-domains.md), to deposit assets.
|
||||
|
||||
{% admonition type="warning" name="Warning" %}
|
||||
If a depositor's credentials expire, they can no longer deposit assets in a private vault, but can always redeem their existing shares.
|
||||
{% /admonition %}
|
||||
|
||||
To prevent the Vault Owner from locking funds away, any shareholder in a private vault can redeem their shares for assets.
|
||||
|
||||
Choosing between a public or private vault depends on your use case. For example, if depositor identity verification is required, use a private vault and issue credentials only to verified accounts.
|
||||
|
||||
## Vault Share Distribution and Redemption
|
||||
|
||||
Depositors can deposit assets to receive shares, which represent their proportional ownership of the vault, or redeem shares for assets.
|
||||
|
||||
[{% inline-svg file="../../img/single-asset-vault-img.svg" /%}](../../img/single-asset-vault-img.svg "Diagram: an example of an asset being deposited into the vault and shares being redeemed.")
|
||||
|
||||
Since the XRP Ledger is an account-based blockchain, all assets must be held by an account. A `Vault` ledger entry cannot hold assets directly, so a [pseudo-account](../accounts/pseudo-accounts.md) is created to hold assets on its behalf. This stand-alone account cannot receive funds or send transactions, and exists solely to store assets and issue shares.
|
||||
|
||||
Each share is represented on-chain as an MPT, issued by the vault's pseudo-account. Since MPTs can only exist as whole number units, the vault uses a `Scale` setting to convert fractional asset amounts into whole number shares.
|
||||
|
||||
The scale behavior varies based on the type of asset held by the vault:
|
||||
|
||||
- **XRP**: Uses a fixed scale that aligns with XRP's native structure, where one share represents one drop.
|
||||
- **Trust Line Token**: Allows configurable precision (default preserves 6 decimal places).
|
||||
- **MPT**: Uses a 1-to-1 relationship between MPT units and shares.
|
||||
|
||||
Depending on the connected protocol, vault shares may be yield-bearing, meaning shareholders could redeem shares for more or less liquidity than they originally deposited. This is because the total asset balance in the vault can grow or shrink over time, affecting the value of each share. However, the vault asset (e.g., USDC, XRP) does not generate yield on its own.
|
||||
|
||||
The value of each share depends on the total assets in the vault:
|
||||
|
||||
- If the vault earns yield over time, shares represent a larger claim, allowing depositors to redeem them for more assets.
|
||||
- If the vault incurs losses, shares hold less value, resulting in lower redemptions.
|
||||
|
||||
A vault could generate yield through mechanisms like lending or staking, with yield paid in the same asset deposited. The specific logic for this depends on how the connected on-chain protocol generates yield. For example, if a vault is used by a lending protocol, it could earn yield from interest paid by borrowers.
|
||||
|
||||
### Exchange Algorithm
|
||||
|
||||
A single asset vault uses an **exchange algorithm** to define how assets convert into shares during deposits and how shares convert back into assets during redemptions.
|
||||
|
||||
A vault's total value can fluctuate due to factors like _unrealized losses_, which impact the exchange rate for deposits and redemptions. To ensure fairness, the algorithm adjusts the exchange rate dynamically, so depositors receive shares or redeem them for assets at a rate that accurately reflects the vault’s true value.
|
||||
|
||||
#### Unrealized Loss
|
||||
|
||||
To prevent depositors from exploiting potential losses by redeeming shares early and shifting the full loss onto the remaining depositors, the vault tracks unrealized losses (or paper loss) using the `LossUnrealized` attribute in the `Vault` ledger entry.
|
||||
|
||||
Because the unrealized loss temporarily decreases the vault's value, a malicious depositor may take advantage of this by depositing assets at a lowered price and redeeming shares once the price increases.
|
||||
|
||||
For example, consider a vault with a total value of $1.0m and total shares of 1.0m. Let's assume the unrealized loss for the vault is $900k:
|
||||
|
||||
1. The new exchange rate is calculated as:
|
||||
|
||||
```js
|
||||
// ExchangeRate = (AssetsTotal - LossUnrealized) / SharesTotal
|
||||
exchangeRate = (1,000,000 - 900,000) / 1,000,000
|
||||
```
|
||||
|
||||
The exchange rate value is now **0.1**.
|
||||
|
||||
2. After the unrealized loss is cleared, the new effective exchange rate would be:
|
||||
|
||||
```js
|
||||
// ExchangeRate = AssetsTotal / SharesTotal
|
||||
exchangeRate = 1,000,000 / 1,000,000
|
||||
```
|
||||
|
||||
The exchange rate is now **1.0**.
|
||||
|
||||
A depositor could deposit $100k assets at a 0.1 exchange rate and get 1.0m shares. Once the unrealized loss is cleared, their shares would be worth $1.0m.
|
||||
|
||||
To mitigate this, the vault uses separate exchange rates for deposits and redemptions.
|
||||
|
||||
#### Exchange Rates
|
||||
|
||||
A single asset vault uses **two distinct exchange rates**:
|
||||
|
||||
- **Deposit Exchange Rate**: Protects new depositors from prior losses and ensures fair share allocation.
|
||||
- **Withdrawal Exchange Rate**: Ensures all shareholders share losses proportionally. Whether redeeming shares or withdrawing assets, the vault always calculates payouts using the actual current value (total assets minus losses), so depositors get their fair share of what's actually in the vault.
|
||||
- **Redemptions**: The vault burns shares so the depositor can receive proportional assets.
|
||||
- **Withdrawals**: The vault determines the shares to burn based on the requested asset amount.
|
||||
|
||||
These exchange rates ensure fairness and prevent manipulation, maintaining the integrity of deposits and redemptions.
|
||||
|
||||
To understand how the exchange rates are applied, here are the key variables used in the calculations:
|
||||
|
||||
- `Γ_assets`: The total balance of assets held within the vault.
|
||||
- `Γ_shares`: The total number of shares currently issued by the vault.
|
||||
- `Δ_assets`: The amount of assets being deposited, withdrawn, or redeemed.
|
||||
- `Δ_shares`: The number of shares being issued or burned.
|
||||
- `l`: The vault's total unrealized loss.
|
||||
- `σ`: The scaling factor (σ = 10<sup>Scale</sup>) used to convert fractional assets into whole number shares.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Deposit" %}
|
||||
The vault computes the number of shares a depositor will receive as follows:
|
||||
|
||||
- **Initial Deposit (Empty Vault)**: For the first deposit into an empty vault, shares are calculated using the scaling factor to properly represent fractional assets as whole numbers.
|
||||
|
||||
```js
|
||||
Δ_shares = Δ_assets * σ // σ = 10^Scale
|
||||
```
|
||||
|
||||
- **Subsequent Deposits**: For all other deposits, shares are calculated proportionally. The resulting share value is rounded **down** to the nearest whole number.
|
||||
|
||||
```js
|
||||
Δ_shares = (Δ_assets * Γ_shares) / Γ_assets
|
||||
```
|
||||
Because the share amount is rounded down, the actual assets taken from the depositor are recalculated. This ensures the depositor isn't overcharged and that new shares are valued against the vault's true value, accounting for any unrealized loss:
|
||||
|
||||
```js
|
||||
Δ_assets = (Δ_shares * (Γ_assets - l)) / Γ_shares
|
||||
```
|
||||
|
||||
After a successful deposit, the _total assets_ and _total shares_ values are updated like so:
|
||||
|
||||
```js
|
||||
Γ_assets = Γ_assets + Δ_assets // New balance of assets in the vault.
|
||||
Γ_shares = Γ_shares + Δ_shares // New share balance in the vault.
|
||||
```
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="Redeem" %}
|
||||
The vault computes the number of assets returned by burning shares as follows:
|
||||
|
||||
```js
|
||||
Δ_assets = (Δ_shares * (Γ_assets - l)) / Γ_shares
|
||||
```
|
||||
|
||||
After a successful redemption, the _total assets_ and _total shares_ values are updated like so:
|
||||
|
||||
```js
|
||||
Γ_assets = Γ_assets - Δ_assets // New balance of assets in the vault.
|
||||
Γ_shares = Γ_shares - Δ_shares // New share balance in the vault.
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="Withdraw" %}
|
||||
When a depositor requests a specific asset amount, the vault uses a two-step process to determine the final payout:
|
||||
|
||||
1. The requested asset amount (`Δ_assets_requested`) is converted into shares.
|
||||
|
||||
```js
|
||||
Δ_shares = (Δ_assets_requested * Γ_shares) / (Γ_assets - l)
|
||||
```
|
||||
|
||||
The calculated share amount is rounded to the **nearest** whole number.
|
||||
|
||||
2. The rounded number of shares is used to calculate the final asset payout (`Δ_assets_out`), using the same logic as a redemption.
|
||||
|
||||
```js
|
||||
Δ_assets_out = (Δ_shares * (Γ_assets - l)) / Γ_shares
|
||||
```
|
||||
|
||||
Due to rounding in step 1, the final payout may differ slightly from the requested amount.
|
||||
|
||||
After a successful withdrawal, the _total asset_ and _total share_ values are updated like so:
|
||||
|
||||
```js
|
||||
Γ_assets = Γ_assets - Δ_assets_out // New balance of assets in the vault.
|
||||
Γ_shares = Γ_shares - Δ_shares // New share balance in the vault.
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### Can a Depositor Transfer Shares to Another Account?
|
||||
|
||||
Vault shares are a first-class asset, meaning that they can be transferred and used in other on-ledger protocols that support MPTs. However, the payee (or the receiver) must have permission to hold both the shares and the underlying asset.
|
||||
|
||||
For example, if a private vault holds USDC, the destination account must belong to the vault’s Permissioned Domain and have permission to hold USDC. Any compliance mechanisms applied to USDC also apply to the shares. If the USDC issuer freezes the payee’s trust line, the payee cannot receive shares representing USDC.
|
||||
|
||||
{% admonition type="info" name="Note" %}
|
||||
It is important to remember that a vault must be **configured** to allow share transfers, or this will not be possible.
|
||||
{% /admonition %}
|
||||
|
||||
A depositor can transfer vault shares to another account by making a [Payment](../../references/protocol/transactions/types/payment) transaction. Nothing changes in the way the payment transaction is submitted for transferring vault shares. However, there are new failure scenarios to watch out for if the transaction fails:
|
||||
|
||||
- The vault is private and the payee lacks credentials in the vault's permissioned domain.
|
||||
- The vault shares are configured as non-transferable.
|
||||
- There is a global freeze (trust line tokens) or lock (MPTs) on the underlying asset.
|
||||
- The underlying asset is an MPT and is locked for the payer, payee, or vault pseudo-account.
|
||||
- The underlying asset is a trust line token and the trust line is frozen between the issuer and the payer, payee, or vault pseudo-account.
|
||||
|
||||
If the transfer succeeds and the payee already holds vault shares, their balance increases. Otherwise, a new MPT entry is created for their account.
|
||||
|
||||
## Compliance
|
||||
|
||||
### Frozen Assets
|
||||
|
||||
The issuer of a vault asset can enact a [freeze](./fungible-tokens/freezes) for trust line tokens or [lock an MPT](./fungible-tokens/deep-freeze#how-does-mpt-freeze/lock-behavior-differ-from-iou). When a vault asset is frozen:
|
||||
|
||||
1. Withdrawals can only be made to the asset’s issuer.
|
||||
2. The asset cannot be deposited into the vault.
|
||||
3. Its corresponding shares also cannot be transferred.
|
||||
|
||||
### Clawback
|
||||
|
||||
An asset issuer can perform a [Clawback](../../use-cases/tokenization/stablecoin-issuer#clawback) on vault assets by forcing redemption of shares held by an account. This exchanges the holder's shares for the underlying assets, which are sent directly to the issuer. This mechanism allows asset issuers to recover their issued assets from vault depositors when necessary for fraud prevention or regulatory compliance.
|
||||
|
||||
## Why Use a Single Asset Vault?
|
||||
|
||||
With a single asset vault you don't have to manage liquidity at the protocol level. Instead, you can use the vault to handle deposits, redemptions, and asset tracking separately.
|
||||
|
||||
Vaults handle asset-to-share conversion, ensure accurate pricing, and eliminate the need to add custom logic to calculate exchange rates or account for unrealized losses.
|
||||
|
||||
Depending on the connected on-chain protocol, vaults can be applied to various use cases, such as:
|
||||
|
||||
- Lending markets
|
||||
- Aggregators
|
||||
- Yield-bearing tokens
|
||||
- Asset management
|
||||
|
||||
The only supported use cases right now are _asset management_ and [_lending markets_](./lending-protocol.md).
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
|
||||
## See Also
|
||||
|
||||
- **Concepts:**
|
||||
- [Credentials](../../concepts/decentralized-storage/credentials.md) - Define access requirements for private vaults.
|
||||
- [Permissioned Domains](../tokens/decentralized-exchange/permissioned-domains.md) - Control access to private vaults.
|
||||
- [Pseudo-Accounts](../accounts/pseudo-accounts.md) - Special accounts that hold assets on behalf of on-chain protocols.
|
||||
- **References:**
|
||||
- [Vault entry][] - Data structure on the ledger that records vault information.
|
||||
- [VaultClawback transaction][] - Allow asset issuers to recover assets from the vault.
|
||||
- [VaultCreate transaction][] - Create a new vault for aggregating assets.
|
||||
- [VaultDelete transaction][] - Delete an existing vault entry.
|
||||
- [VaultDeposit transaction][] - Add assets to a vault in exchange for shares.
|
||||
- [VaultSet transaction][] - Update the configuration of an existing vault.
|
||||
- [VaultWithdraw transaction][] - Redeem liquidity from a vault.
|
||||
- [vault_info method][] - Retrieve information about a vault and its shares.
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 100 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 39 KiB |
@@ -13,7 +13,7 @@ In its default configuration, the `rippled` server automatically deletes outdate
|
||||
|
||||
## Warnings
|
||||
|
||||
Storing full history is expensive. As of 2026-01-25, the full history of the XRP Ledger occupies approximately **39 terabytes** of disk space, which must be entirely stored on fast solid state disk drives for proper server performance. Such a large amount of solid state storage is not cheap, and the total amount of history you must store increases by approximately 12 GB per day.
|
||||
Storing full history is expensive. As of 2023-07-19, the full history of the XRP Ledger occupies approximately **26 terabytes** of disk space, which must be entirely stored on fast solid state disk drives for proper server performance. Such a large amount of solid state storage is not cheap, and the total amount of history you must store increases by approximately 12 GB per day.
|
||||
|
||||
Additionally, storing full history in NuDB requires single files that are larger than the 16 TB limit of ext4 filesystems, which is the default on many Linux distributions. You must use a filesystem with a larger single-file limit, such as XFS (recommended) or ZFS.
|
||||
|
||||
|
||||
@@ -81,10 +81,10 @@ Before you install Clio, you must meet the following requirements.
|
||||
|
||||
```
|
||||
gpg: WARNING: no command supplied. Trying to guess what you mean ...
|
||||
pub ed25519 2026-02-16 [SC] [expires: 2033-02-14]
|
||||
E057C1CF72B0DF1A4559E8577DEE9236AB06FAA6
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub ed25519 2026-02-16 [S] [expires: 2029-02-15]
|
||||
pub rsa3072 2019-02-14 [SC] [expires: 2026-02-17]
|
||||
C0010EC205B35A3310DC90DE395F97FFCCAFD9A2
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub rsa3072 2019-02-14 [E] [expires: 2026-02-17]
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -49,10 +49,10 @@ Before you install `rippled`, you must meet the [System Requirements](system-req
|
||||
The output should include an entry for Ripple such as the following:
|
||||
|
||||
```
|
||||
pub ed25519 2026-02-16 [SC] [expires: 2033-02-14]
|
||||
E057C1CF72B0DF1A4559E8577DEE9236AB06FAA6
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub ed25519 2026-02-16 [S] [expires: 2029-02-15]
|
||||
pub rsa3072 2019-02-14 [SC] [expires: 2026-02-17]
|
||||
C0010EC205B35A3310DC90DE395F97FFCCAFD9A2
|
||||
uid TechOps Team at Ripple <techops+rippled@ripple.com>
|
||||
sub rsa3072 2019-02-14 [E] [expires: 2026-02-17]
|
||||
```
|
||||
|
||||
In particular, make sure that the fingerprint matches. (In the above example, the fingerprint is on the second line, starting with `C001`.)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
html: private-network-with-docker.html
|
||||
name: Run a Private Network with Docker
|
||||
parent: use-stand-alone-mode.html
|
||||
seo:
|
||||
description: Learn how to set up your own XRP private ledger network with Docker and Docker Compose.
|
||||
description: Learn how to set up your own XRP private ledger network with Docker and Docker Compose.
|
||||
labels:
|
||||
- Core Server
|
||||
---
|
||||
@@ -259,25 +261,17 @@ For each validator node, follow these steps:
|
||||
|
||||
Now that you have created the configuration files for your validators, you need to add a `validator.txt` file. This file defines which validators are trusted by your network.
|
||||
|
||||
Follow these steps to add validator configuration files to each validator:
|
||||
For each node, follow these steps:
|
||||
|
||||
1. Create a `validators.txt` file.
|
||||
1. Create a `validators.txt` file in the configuration directory.
|
||||
2. Copy the public keys from the `validator-keys.json` files that you generated at the [beginning](#generate-the-validator-keys) of the tutorial.
|
||||
3. Add the public keys of _all_ the validators. For example:
|
||||
|
||||
```
|
||||
[validators]
|
||||
nHBgaEDL8buUECuk4Rck4QBYtmUgbAoeYJLpWLzG9iXsznTRYrQu
|
||||
nHBCHX7iLDTyap3LumqBNuKgG7JLA5tc6MSJxpLs3gjkwpu836mY
|
||||
nHU5STUKTgWdreVqJDx6TopLUymzRUZshTSGcWNtjfByJkYdiiRc
|
||||
```
|
||||
|
||||
4. Copy the same `validators.txt` file into the `config` directory for _each_ validator.
|
||||
|
||||
```sh
|
||||
cp validators.txt validator_1/config/
|
||||
cp validators.txt validator_2/config/
|
||||
cp validators.txt validator_3/config/
|
||||
nHBgaEDL8buUECuk4Rck4QBYtmUgbAoeYJLpWLzG9iXsznTRYrQu
|
||||
nHBCHX7iLDTyap3LumqBNuKgG7JLA5tc6MSJxpLs3gjkwpu836mY
|
||||
nHU5STUKTgWdreVqJDx6TopLUymzRUZshTSGcWNtjfByJkYdiiRc
|
||||
```
|
||||
|
||||
## Start the Network
|
||||
@@ -291,6 +285,7 @@ To start running your private network, follow these steps:
|
||||
1. Create a `docker-compose.yml` file in the root of the private network directory, `xrpl-private-network`, and add the following content:
|
||||
|
||||
```
|
||||
version: "3.9"
|
||||
services:
|
||||
validator_1:
|
||||
platform: linux/amd64
|
||||
@@ -343,15 +338,9 @@ To start running your private network, follow these steps:
|
||||
|
||||
Now that the private ledger network is up, you need to verify that **each** validator node is running as expected:
|
||||
|
||||
1. Open a terminal in the validator_1 container:
|
||||
1. In your terminal, run `docker exec -it <validator_name> bin/bash` to execute commands in the validator Docker container. Replace `<validator_name>` with the name of the container (e.g., `validator_1`).
|
||||
|
||||
```
|
||||
docker exec -it validator_1 bin/bash
|
||||
```
|
||||
|
||||
{% admonition type="success" name="Tip" %}You can use the same syntax to execute commands in the other Docker containers. Replace `bin/bash` with the command to run and `validator_1` with the name of the container.{% /admonition %}
|
||||
|
||||
3. Run the `rippled server_info` command to check the state of the validator:
|
||||
2. Run the `rippled server_info` command to check the state of the validator:
|
||||
|
||||
```
|
||||
rippled server_info | grep server_state
|
||||
@@ -365,7 +354,7 @@ Now that the private ledger network is up, you need to verify that **each** vali
|
||||
|
||||
{% admonition type="info" name="Note" %}If the state is not updated to **proposing**, repeat step **2** after a few minutes as the ledger can take some time to update.{% /admonition %}
|
||||
|
||||
4. Verify the number of peers connected to the validator.
|
||||
3. Verify the number of peers connected to the validator.
|
||||
|
||||
```
|
||||
rippled server_info | grep peers
|
||||
@@ -377,10 +366,10 @@ Now that the private ledger network is up, you need to verify that **each** vali
|
||||
"peers" : 2
|
||||
```
|
||||
|
||||
5. Run the following command to check the genesis account information:
|
||||
4. Run the following command to check the genesis account information:
|
||||
|
||||
```
|
||||
rippled account_info rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh validated
|
||||
rippled account_info rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh validated strict
|
||||
```
|
||||
|
||||
Sample Output:
|
||||
@@ -407,9 +396,7 @@ Now that the private ledger network is up, you need to verify that **each** vali
|
||||
}
|
||||
```
|
||||
|
||||
If the ledger does not have `"validated": true`, double check that you put matching `validators.txt` files with all three public keys in each container's config directory, and restart the container if you need to make changes.
|
||||
|
||||
6. To leave the Docker container shell, enter `exit` in the terminal.
|
||||
5. To leave the Docker container shell, enter `exit` in the terminal.
|
||||
|
||||
### Perform a test transaction
|
||||
|
||||
@@ -452,7 +439,7 @@ Perform a **test** transaction to ensure you can send money to an account.
|
||||
|
||||
```
|
||||
docker exec -it validator_1 \
|
||||
rippled account_info r9wRwVgL2vWVnKhTPdtxva5vdH7FNw1zPs validated
|
||||
rippled account_info r9wRwVgL2vWVnKhTPdtxva5vdH7FNw1zPs validated strict
|
||||
```
|
||||
|
||||
Sample Output:
|
||||
|
||||
@@ -87,7 +87,6 @@ The request includes the following parameters:
|
||||
| `Field` | Type | Description |
|
||||
|:---------------|:--------|:--------------------------------------------------|
|
||||
| `tx_json` | Object | [Transaction definition](../../../protocol/transactions/index.md) in JSON format |
|
||||
| `signature_target` | String | _(Optional)_ Specifies where in the transaction metadata the signature information should be stored. Currently, the only valid value is `CounterpartySignature`. |
|
||||
| `secret` | String | _(Optional)_ The secret seed of the account supplying the transaction, used to sign it. Do not send your secret to untrusted servers or through unsecured network connections. Cannot be used with `key_type`, `seed`, `seed_hex`, or `passphrase`. |
|
||||
| `seed` | String | _(Optional)_ The secret seed of the account supplying the transaction, used to sign it. Must be in the XRP Ledger's [base58][] format. If provided, you must also specify the `key_type`. Cannot be used with `secret`, `seed_hex`, or `passphrase`. |
|
||||
| `seed_hex` | String | _(Optional)_ The secret seed of the account supplying the transaction, used to sign it. Must be in hexadecimal format. If provided, you must also specify the `key_type`. Cannot be used with `secret`, `seed`, or `passphrase`. |
|
||||
|
||||
@@ -94,7 +94,6 @@ The request includes the following parameters:
|
||||
|:-------------|:---------------------|:---------------------------------------|
|
||||
| `account` | String - [Address][] | The address which is providing the signature. |
|
||||
| `tx_json` | Object | The [Transaction](../../../protocol/transactions/index.md) to sign. Unlike using the [sign method][], all fields of the transaction must be provided, including `Fee` and `Sequence`. The transaction must include the field `SigningPubKey` with an empty string as the value. The object may optionally contain a `Signers` array with previously-collected signatures. |
|
||||
| `signature_target` | String | _(Optional)_ Specifies where in the transaction metadata the signature information should be stored. Currently, the only valid value is `CounterpartySignature`. |
|
||||
| `secret` | String | _(Optional)_ Secret key of the account supplying the transaction, used to sign it. Do not send your secret to untrusted servers or through unsecured network connections. Cannot be used with `key_type`, `seed`, `seed_hex`, or `passphrase`. |
|
||||
| `seed` | String | _(Optional)_ Secret key of the account supplying the transaction, used to sign it. Must be in the XRP Ledger's [base58][] format. If provided, you must also specify the `key_type`. Cannot be used with `secret`, `seed_hex`, or `passphrase`. |
|
||||
| `seed_hex` | String | _(Optional)_ Secret key of the account supplying the transaction, used to sign it. Must be in hexadecimal format. If provided, you must also specify the `key_type`. Cannot be used with `secret`, `seed`, or `passphrase`. |
|
||||
|
||||
@@ -115,11 +115,6 @@ Use these methods to perform convenient tasks, such as ping and random number ge
|
||||
* **[`ping`](utility-methods/ping.md)** - Confirm connectivity with the server.
|
||||
* **[`random`](utility-methods/random.md)** - Generate a random number.
|
||||
|
||||
## [Vault Methods](vault-methods/index.md)
|
||||
|
||||
Use these methods to retrieve vault information.
|
||||
|
||||
* **[`vault_info`](vault-methods/vault_info.md)** - Get information about a specific vault.
|
||||
|
||||
## Deprecated Methods
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,7 +81,6 @@ The request includes the following parameters:
|
||||
| `Field` | Type | Description |
|
||||
|:---------------|:--------|:--------------------------------------------------|
|
||||
| `tx_json` | Object | [Transaction definition](../../../protocol/transactions/index.md) in JSON format, optionally omitting any auto-fillable fields. |
|
||||
| `signature_target` | String | _(Optional)_ Specifies where in the transaction metadata the signature information should be stored. Currently, the only valid value is `CounterpartySignature`. |
|
||||
| `secret` | String | _(Optional)_ Secret key of the account supplying the transaction, used to sign it. Do not send your secret to untrusted servers or through unsecured network connections. Cannot be used with `key_type`, `seed`, `seed_hex`, or `passphrase`. |
|
||||
| `seed` | String | _(Optional)_ Secret key of the account supplying the transaction, used to sign it. Must be in the XRP Ledger's [base58][] format. If provided, you must also specify the `key_type`. Cannot be used with `secret`, `seed_hex`, or `passphrase`. |
|
||||
| `seed_hex` | String | _(Optional)_ Secret key of the account supplying the transaction, used to sign it. Must be in hexadecimal format. If provided, you must also specify the `key_type`. Cannot be used with `secret`, `seed`, or `passphrase`. |
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
metadata:
|
||||
indexPage: true
|
||||
---
|
||||
# Vault Methods
|
||||
|
||||
A `Vault` object in the XRP Ledger represents the state of a tokenized vault. Use these methods to interact with vaults.
|
||||
|
||||
{% child-pages /%}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user