mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-06-07 10:46:45 +00:00
Compare commits
120 Commits
update-xrp
...
fix_links_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dec5f720d | ||
|
|
d7456276b9 | ||
|
|
22243abc7c | ||
|
|
a301253740 | ||
|
|
d66fa36f56 | ||
|
|
f61d74be6b | ||
|
|
00255ace3a | ||
|
|
d1fc03ca37 | ||
|
|
f9aae5f1e3 | ||
|
|
f700fa72ca | ||
|
|
bade50d826 | ||
|
|
9eb2742581 | ||
|
|
56455bf27b | ||
|
|
4230d16811 | ||
|
|
1064dad9b0 | ||
|
|
79419d0b49 | ||
|
|
0e510b349d | ||
|
|
087030b39a | ||
|
|
03dae02593 | ||
|
|
b82df3bb5d | ||
|
|
9e1bc798ba | ||
|
|
c8cb28fd80 | ||
|
|
b09fe6c7b3 | ||
|
|
28af114305 | ||
|
|
d846367857 | ||
|
|
8481e05d6e | ||
|
|
b0428e30d4 | ||
|
|
4b65061304 | ||
|
|
6d99930c9a | ||
|
|
84f78d83dd | ||
|
|
5311ffb3b4 | ||
|
|
4f991e14a5 | ||
|
|
da17bb427f | ||
|
|
c10456f103 | ||
|
|
a31124c94b | ||
|
|
7f2588c514 | ||
|
|
bba796d818 | ||
|
|
7d3145b0a1 | ||
|
|
a874b034d7 | ||
|
|
0defd68316 | ||
|
|
a439eef72b | ||
|
|
e94068b0db | ||
|
|
71e5ff4bdb | ||
|
|
ccaaa55ca3 | ||
|
|
e2da8d58a0 | ||
|
|
e0cc0849ad | ||
|
|
19b376be8e | ||
|
|
59a119db66 | ||
|
|
2041b55e4b | ||
|
|
d51da2ff5c | ||
|
|
19aad7809b | ||
|
|
508a39908c | ||
|
|
aca16f3609 | ||
|
|
187542fef5 | ||
|
|
0972abb0b6 | ||
|
|
6256e1d7c8 | ||
|
|
9f7c675517 | ||
|
|
202a16288c | ||
|
|
8c68d4a7ad | ||
|
|
5e63775a97 | ||
|
|
3e96de6323 | ||
|
|
4e7d0aadc9 | ||
|
|
bbe80b34c9 | ||
|
|
3152430e47 | ||
|
|
81279b4761 | ||
|
|
aaa4668392 | ||
|
|
89de054d4d | ||
|
|
583b169680 | ||
|
|
1241a33a5d | ||
|
|
d0f6d04715 | ||
|
|
f8d7ca470d | ||
|
|
d0187414a5 | ||
|
|
aed88784d9 | ||
|
|
dc13312be6 | ||
|
|
a063951f9e | ||
|
|
6ac6893f4a | ||
|
|
25bfaca2c0 | ||
|
|
5728345a42 | ||
|
|
f60393e9fa | ||
|
|
028e523b6d | ||
|
|
d945d6a5d6 | ||
|
|
fed058fe51 | ||
|
|
c3e898c047 | ||
|
|
98db42f996 | ||
|
|
3f551f68e3 | ||
|
|
4c5f65ff54 | ||
|
|
9b72e6c6ff | ||
|
|
5e500d58ca | ||
|
|
cb48d4f789 | ||
|
|
39f5b9ab66 | ||
|
|
3d3ac6adb3 | ||
|
|
539cef2510 | ||
|
|
0da70afdff | ||
|
|
513c86dff3 | ||
|
|
b0e99161bb | ||
|
|
a441171000 | ||
|
|
892714550e | ||
|
|
b6388ccb13 | ||
|
|
6ab5de13bb | ||
|
|
38000f19d6 | ||
|
|
ad9e5e14fa | ||
|
|
663cd6df6a | ||
|
|
6bee1983eb | ||
|
|
9df53455e9 | ||
|
|
13dddb8b22 | ||
|
|
b47c96d91a | ||
|
|
93abc4dc09 | ||
|
|
ae266aba7f | ||
|
|
ab9eec63f5 | ||
|
|
8b8ed4c6ea | ||
|
|
6aaca7f568 | ||
|
|
a045e9e40c | ||
|
|
ad9327c4c0 | ||
|
|
53983bf8e2 | ||
|
|
aa3b5e173c | ||
|
|
0328e75280 | ||
|
|
4bceb09b1b | ||
|
|
0da3a1e13c | ||
|
|
5d2e8f5f98 | ||
|
|
d4726e0816 |
19
.claude/CLAUDE.md
Normal file
19
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# XRPL Dev Portal — Claude Code Instructions
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Framework:** Redocly Realm
|
||||
- **Production branch:** `master`
|
||||
- **Local preview:** `npm start`
|
||||
|
||||
## Localization
|
||||
|
||||
- Default: `en-US`
|
||||
- Japanese: `ja`
|
||||
- Translations mirror `docs/` structure under `@l10n/<language-code>/`
|
||||
|
||||
## Navigation
|
||||
|
||||
- Update `sidebars.yaml` when adding new doc pages
|
||||
- Blog posts have a separate `blog/sidebars.yaml`
|
||||
- Redirects go in `redirects.yaml`
|
||||
7
.claude/settings.json
Normal file
7
.claude/settings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Bash(git push *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
117
.claude/skills/generate-release-notes/SKILL.md
Normal file
117
.claude/skills/generate-release-notes/SKILL.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: generate-release-notes
|
||||
description: Generate and sort rippled release notes from GitHub commit history
|
||||
argument-hint: --from <ref> --to <ref> [--date YYYY-MM-DD] [--output <path>]
|
||||
allowed-tools: Bash, Read, Edit, Write, Grep, Glob
|
||||
effort: max
|
||||
---
|
||||
|
||||
# Generate rippled Release Notes
|
||||
|
||||
This skill generates a draft release notes blog post for a new rippled version, then sorts the entries into the correct subsections.
|
||||
|
||||
## Execution constraints
|
||||
|
||||
- **Do NOT write scripts** to sort or process the file. Prefer the Edit tool for targeted changes. Use Write only when replacing large sections that are impractical to edit incrementally.
|
||||
- **Output progress**: Before each major step (generating raw release notes, reviewing file, processing amendments, sorting entries, reformatting, cleanup), output a brief status message so the user can see progress.
|
||||
|
||||
## Step 1: Generate the raw release notes
|
||||
|
||||
Run the Python script from the repo root. Pass through all arguments from `$ARGUMENTS`:
|
||||
|
||||
```bash
|
||||
python3 tools/generate-release-notes.py $ARGUMENTS
|
||||
```
|
||||
|
||||
If the user didn't provide `--from` or `--to`, ask them for the base and target refs (tags or branches).
|
||||
|
||||
The script will:
|
||||
- Fetch the version string from `BuildInfo.cpp`
|
||||
- Fetch all commits between the two refs
|
||||
- Fetch PR details (title, link, labels, files, description) via GraphQL
|
||||
- Compare `features.macro` between refs to identify amendment changes
|
||||
- Auto-sort amendment entries into the Amendments section
|
||||
- Output all other entries as unsorted with full context
|
||||
|
||||
## Step 2: Review the generated file
|
||||
|
||||
Read the output file (path shown in script output). Note the **Full Changelog** structure:
|
||||
- **Amendments section**: Contains auto-sorted entries and an HTML comment listing which amendments to include or remove
|
||||
- **Empty subsections**: Features, Breaking Changes, Bug Fixes, Refactors, Documentation, Testing, CI/Build
|
||||
- **Unsorted entries**: After the **Bug Bounties and Responsible Disclosures** section is an unsorted list of entries with title, link, labels, files, and description for context
|
||||
|
||||
## Step 3: Process amendments
|
||||
|
||||
Handle Amendments first, before sorting other entries.
|
||||
|
||||
**3a. Process the auto-sorted Amendments subsection:**
|
||||
The HTML comment contains three lists — follow them exactly:
|
||||
- **Include**: Keep these entries.
|
||||
- **Exclude**: Remove these entries.
|
||||
- Entries on **neither** list: Remove these entries.
|
||||
|
||||
**3b. Scan unsorted entries for unreleased amendment work:**
|
||||
Search through ALL unsorted entries for titles, labels, descriptions, or files that reference amendments on the "Exclude" or "Other amendments not part of this release" lists. Remove entries that directly implement, enable, fix, or refactor these amendments. Keep entries that are general changes that merely reference the amendment as motivation — if the code change is useful on its own regardless of whether the amendment ships, keep it.
|
||||
|
||||
**3c. If you disagree with any amendment decisions, make a note to the user but do NOT deviate from the rules.**
|
||||
|
||||
## Step 4: Sort remaining unsorted entries into subsections
|
||||
|
||||
Move each remaining unsorted entry into the appropriate subsection.
|
||||
|
||||
Use these signals to categorize:
|
||||
|
||||
**Files changed** (strongest signal):
|
||||
- Only `.github/`, `CMakeLists.txt`, `conan*`, CI config files → **CI/Build**
|
||||
- Only `src/test/`, `*_test.cpp` files → **Testing**
|
||||
- Only `*.md`, `docs/` files → **Documentation**
|
||||
|
||||
**Labels** (strong signal):
|
||||
- `Bug` label → **Bug Fixes**
|
||||
|
||||
**Title prefixes** (medium signal):
|
||||
- `fix:` → **Bug Fixes**
|
||||
- `feat:` → **Features**
|
||||
- `refactor:` → **Refactors**
|
||||
- `docs:` → **Documentation**
|
||||
- `test:` → **Testing**
|
||||
- `ci:`, `build:`, `chore:` → **CI/Build**
|
||||
|
||||
**Description content** (when other signals are ambiguous):
|
||||
- Read the PR description to understand the change's purpose
|
||||
- PRs that change API behavior, remove features, or have "Breaking change" checked in their description → **Breaking Changes**
|
||||
|
||||
Additional sorting guidance:
|
||||
- Watch for revert pairs: If a PR was committed and then reverted (or vice versa), check that the net effect is accounted for — don't include both.
|
||||
|
||||
## Step 5: Reformat sorted entries
|
||||
|
||||
After sorting, reformat each entry to match the release notes style.
|
||||
|
||||
**Amendment entries** should follow this format:
|
||||
```markdown
|
||||
- **amendmentName**: Description of what the amendment does. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Use more detail for amendment descriptions since they are the most important. Use present tense.
|
||||
- If there are multiple entries for the same amendment, merge into one, prioritizing the entry that describes the actual amendment.
|
||||
|
||||
**Feature and Breaking Change entries** should follow this format:
|
||||
```markdown
|
||||
- Description of the change. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Keep the description concise. Use past tense.
|
||||
|
||||
**All other entries** should follow this format:
|
||||
```markdown
|
||||
- The PR title of the entry. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Copy the PR title as-is. Only fix capitalization, remove conventional commit prefixes (fix:, feat:, ci:, refactor:, docs:, test:, chore:, build:), and adjust to past tense if needed. Do NOT rewrite, paraphrase, or summarize.
|
||||
|
||||
## Step 6: Clean up
|
||||
|
||||
- Add a short and generic description of changes to the existing `seo.description` frontmatter, e.g., "This version introduces new amendments and bug fixes." Do not create long lists of detailed changes.
|
||||
- Add a more detailed summary of the release to the existing "Introducing XRP Ledger Version X.Y.Z" section. Include amendment names (organized in a list if more than 2), featuress, and breaking changes. Limit this to 1 paragraph.
|
||||
- Do NOT delete the **Credits** or **Bug Bounties and Responsible Disclosures** sections
|
||||
- Remove empty subsections that have no entries
|
||||
- Remove all HTML comments (sorting instructions)
|
||||
- Do a final review of the release notes. If you see anything strange, or were forced to take unintuitive actions by these instructions, notify the user, but don't make changes.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ yarn-error.log
|
||||
.venv/
|
||||
_code-samples/*/js/package-lock.json
|
||||
_code-samples/*/go/go.sum
|
||||
_code-samples/*/java/target/
|
||||
_code-samples/*/*/*[Ss]etup.json
|
||||
|
||||
# PHP
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
レジャーインデックスは、32ビットの符号なし整数であり、レジャーを識別するために使用します。レジャーインデックスは、レジャーの _シーケンス番号_ と呼ばれることもあります。([アカウントシーケンス](../../references/protocol/data-types/basic-data-types.md#アカウントシーケンス)とは異なります。)一番最初のレジャーでは、レジャーインデックスは1でした。新しいレジャーのレジャーインデックスは、その直前のレジャーのレジャーインデックスに1を加算した値になります。
|
||||
レジャーインデックスは、32ビットの符号なし整数であり、レジャーを識別するために使用します。レジャーインデックスは、レジャーの _シーケンス番号_ と呼ばれることもあります。([アカウントシーケンス](/docs/references/protocol/data-types/basic-data-types.md#アカウントシーケンス)とは異なります。)一番最初のレジャーでは、レジャーインデックスは1でした。新しいレジャーのレジャーインデックスは、その直前のレジャーのレジャーインデックスに1を加算した値になります。
|
||||
|
||||
レジャーインデックスがレジャーの順番を示すのに対し、[ハッシュ][]値はレジャーの正確なコンテンツを示します。2つのレジャーが同じハッシュ値を持つ場合、それらは必ず同じものです。検証済みレジャーでは、ハッシュ値とレジャーインデックスは等しく有効で、1:1の関係です。しかし、進行中のレジャーに対しては、以下の理由によりその限りでありません。
|
||||
レジャーインデックスがレジャーの順番を示すのに対し、[ハッシュ](/docs/references/protocol/data-types/basic-data-types.md#ハッシュ)値はレジャーの正確なコンテンツを示します。2つのレジャーが同じハッシュ値を持つ場合、それらは必ず同じものです。検証済みレジャーでは、ハッシュ値とレジャーインデックスは等しく有効で、1:1の関係です。しかし、進行中のレジャーに対しては、以下の理由によりその限りでありません。
|
||||
|
||||
* ネットワーク全体でのトランザクションの伝搬遅延が原因で、2つの異なる`rippled`サーバで、同じレジャーインデックスを持つ現行レジャーに対するコンテンツが異なる場合があります。
|
||||
* 決済済みレジャーバージョンが複数あり、コンセンサスによる検証が競合している場合があります。このようなレジャーバージョンでは、レジャーインデックスは同じですが、コンテンツは異なります(ハッシュも異なります)。これらの決済済みレジャーのうち、検証済みになるのは1つだけです。
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
seo:
|
||||
description: アカウントの削除
|
||||
labels:
|
||||
- アカウント
|
||||
- Accounts
|
||||
requiredAmendment: DeletableAccounts
|
||||
txIcon: cancel
|
||||
---
|
||||
# AccountDelete
|
||||
|
||||
@@ -10,7 +12,7 @@ labels:
|
||||
|
||||
AccountDeleteトランザクションは、XRP Ledgerで[アカウント](../../ledger-data/ledger-entry-types/accountroot.md)と、アカウントが所有するオブジェクトを削除し、可能であれば、アカウントの残りのXRPを指定された送金先アカウントに送信します。アカウントを削除する要件については、[アカウントの削除](../../../../concepts/accounts/deleting-accounts.md)をご覧ください。
|
||||
|
||||
_[DeletableAccounts Amendment][]が必要です。_
|
||||
{% amendment-disclaimer name="DeletableAccounts" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
seo:
|
||||
description: XRP Ledgerのアカウントのプロパティーを修正します。
|
||||
labels:
|
||||
- アカウント
|
||||
- Accounts
|
||||
txIcon: modify
|
||||
---
|
||||
# AccountSet
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
html: ammbid.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 自動マーケットメーカーのオークションスロットに入札することで、手数料の割引を受けることができます。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: modify
|
||||
---
|
||||
# AMMBid
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMBid.cpp "Source")
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
seo:
|
||||
description: 自動マーケットメーカープールに発行済みトークンを預け入れた保有者から、トークンを回収する。
|
||||
labels:
|
||||
- AMM
|
||||
- Tokens
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMMClawback
|
||||
txIcon: cancel
|
||||
---
|
||||
# AMMClawback
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ parent: transaction-types.html
|
||||
seo:
|
||||
description: 指定された資産ペアを取引するための新しい自動マーケットメーカーを作成します。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: create
|
||||
---
|
||||
# AMMCreate
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMCreate.cpp "Source")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
html: ammdelete.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 空のプールを持つ自動マーケットメーカーのインスタンスを削除します。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: cancel
|
||||
---
|
||||
# AMMDelete
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMDelete.cpp "Source")
|
||||
|
||||
@@ -4,7 +4,10 @@ parent: transaction-types.html
|
||||
seo:
|
||||
description: 自動マーケットメーカーに資金を預け、LPTokenを受け取ります。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: send
|
||||
---
|
||||
# AMMDeposit
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMDeposit.cpp "Source")
|
||||
|
||||
@@ -4,7 +4,10 @@ parent: transaction-types.html
|
||||
seo:
|
||||
description: 自動マーケットメーカーインスタンスの取引手数料へ投票する。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: modify
|
||||
---
|
||||
# AMMVote
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMVote.cpp "Source")
|
||||
|
||||
@@ -4,7 +4,10 @@ parent: transaction-types.html
|
||||
seo:
|
||||
description: LPトークを自動マーケットメーカーに返却し、プールが保有する資産の一部と引き換えマス。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: send
|
||||
---
|
||||
# AMMWithdraw
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMWithdraw.cpp "Source")
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
seo:
|
||||
description: 最大8件のトランザクションをまとめて作成・送信し、それらがすべて成功するか、すべて失敗するようにアトミックに処理されるようにします。
|
||||
labels:
|
||||
- Batch
|
||||
- Transaction Sending
|
||||
- Transaction Sending
|
||||
- Other Transactions
|
||||
requiredAmendment: Batch
|
||||
status: not_enabled
|
||||
txIcon: other
|
||||
---
|
||||
# Batch
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Batch.cpp "Source")
|
||||
|
||||
`Batch`トランザクションは、最大8つのトランザクションを単一のバッチで送信します。各トランザクションは、4つのモード(全て成功または全て失敗(All or Nothing)、一つのみ成功(Only One)、失敗まで継続(Until Failure)、および独立実行(Independent))のいずれかでアトミックに実行されます。
|
||||
|
||||
{% amendment-disclaimer name="Batch" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
### 単一アカウントの場合
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
---
|
||||
html: checkcancel.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 未清算のCheckを取り消し、送金を行わずにレジャーから削除します。
|
||||
labels:
|
||||
- Checks
|
||||
- Checks
|
||||
- Payments
|
||||
requiredAmendment: Checks
|
||||
txIcon: cancel
|
||||
---
|
||||
# CheckCancel
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CancelCheck.cpp "Source")
|
||||
|
||||
未清算のCheckを取り消し、送金を行わずにレジャーから削除します。Checkの送金元または送金先は、いつでもこのトランザクションタイプを使用してCheckを取り消すことができます。有効期限切れのCheckはすべてのアドレスが取り消すことができます。
|
||||
|
||||
_([Checks Amendment][]が必要です)_
|
||||
{% amendment-disclaimer name="Checks" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
html: checkcash.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: レジャーでCheckオブジェクトの清算を試みます。
|
||||
labels:
|
||||
- Checks
|
||||
- Checks
|
||||
- Payments
|
||||
requiredAmendment: Checks
|
||||
txIcon: finish
|
||||
---
|
||||
# CheckCash
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CashCheck.cpp "Source")
|
||||
@@ -13,7 +14,7 @@ labels:
|
||||
|
||||
Checkに相当する資金があるとは保証されないため、送金元に十分な残高がないか、または資金を送金できるだけの十分な流動性がないことが原因で、Checkの清算が失敗することがあります。このような状況が発生した場合、Checkはレジャーに残り、送金先は後でこのCheckの換金を再試行するか、または異なる額で換金を試みることができます。
|
||||
|
||||
_([Checks Amendment][]が必要です)_
|
||||
{% amendment-disclaimer name="Checks" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
---
|
||||
html: checkcreate.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: レジャーにCheckオブジェクトを作成します
|
||||
labels:
|
||||
- Checks
|
||||
- Checks
|
||||
- Payments
|
||||
requiredAmendment: Checks
|
||||
txIcon: create
|
||||
---
|
||||
# CheckCreate
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CreateCheck.cpp "Source")
|
||||
|
||||
レジャーにCheckオブジェクトを作成します。これにより指定の送金先は後日換金することができます。このトランザクションの送信者はCheckの送金元です。
|
||||
|
||||
_([Checks Amendment][]が必要です)_
|
||||
{% amendment-disclaimer name="Checks" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
seo:
|
||||
description: 発行したトークンを取り戻します。
|
||||
labels:
|
||||
- トークン
|
||||
- Tokens
|
||||
requiredAmendment: Clawback
|
||||
txIcon: cancel
|
||||
---
|
||||
# Clawback
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
---
|
||||
seo:
|
||||
description: アカウントに仮発行された資格情報を承認します。
|
||||
status: not_enabled
|
||||
labels:
|
||||
- Decentralized Storage
|
||||
- Credentials
|
||||
requiredAmendment: Credentials
|
||||
txIcon: finish
|
||||
---
|
||||
# CredentialAccept
|
||||
|
||||
CredentialAcceptトランザクションは資格情報を承認し、その資格情報を有効にします。資格情報の対象者のみがこの操作を実行できます。
|
||||
|
||||
{% amendment-disclaimer name="Credentials" /%}
|
||||
|
||||
## CredentialAccept JSONの例
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
---
|
||||
seo:
|
||||
description: アカウントに対して暫定的に資格情報を発行します。
|
||||
status: not_enabled
|
||||
labels:
|
||||
- Decentralized Storage
|
||||
- Credentials
|
||||
requiredAmendment: Credentials
|
||||
txIcon: create
|
||||
---
|
||||
|
||||
# CredentialCreate
|
||||
|
||||
CredentialCreateトランザクションは、レジャーにCredentialを作成します。Credential(資格情報)の発行者はこのトランザクションを使用して、暫定的に資格情報を発行します。Credentialは、その対象アカウントが[CredentialAcceptトランザクション][]で承認するまで有効になりません。
|
||||
|
||||
{% amendment-disclaimer name="Credentials" /%}
|
||||
|
||||
## CredentialCreate JSONの例
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
seo:
|
||||
description: レジャーから認証情報を削除し、事実上失効させます。
|
||||
status: not_enabled
|
||||
labels:
|
||||
- Decentralized Storage
|
||||
- Credentials
|
||||
requiredAmendment: Credentials
|
||||
txIcon: cancel
|
||||
---
|
||||
# CredentialDelete
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: DepositPreauthトランザクションは別のアカウントに対し、このトランザクションの送信者に支払いを送金することを事前承認します。
|
||||
labels:
|
||||
- セキュリティ
|
||||
- Accounts
|
||||
- Security
|
||||
requiredAmendment: DepositPreauth
|
||||
txIcon: modify
|
||||
---
|
||||
# DepositPreauth
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/DepositPreauth.cpp "Source")
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
---
|
||||
html: diddelete.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: DIDを削除する。
|
||||
labels:
|
||||
- DID
|
||||
- DID
|
||||
- Decentralized Storage
|
||||
requiredAmendment: DID
|
||||
txIcon: cancel
|
||||
---
|
||||
# DIDDelete
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/DID.cpp "ソース")
|
||||
|
||||
_([DID Amendment][])_
|
||||
|
||||
指定した`Account`フィールドに関連付けられている[DIDレジャーエントリ](../../ledger-data/ledger-entry-types/did.md)を削除します。
|
||||
|
||||
{% admonition type="info" name="注記" %}このトランザクションは[共通フィールド][]のみ利用します。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="DID" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
---
|
||||
html: didset.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: DIDを作成または更新します。
|
||||
labels:
|
||||
- DID
|
||||
- DID
|
||||
- Decentralized Storage
|
||||
requiredAmendment: DID
|
||||
txIcon: create
|
||||
---
|
||||
# DIDSet
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/DID.cpp "ソース")
|
||||
|
||||
_([DID Amendment][])_
|
||||
|
||||
新しい[DIDレジャーエントリ](../../ledger-data/ledger-entry-types/did.md)を作成したり、既存の項目を更新したりします。
|
||||
|
||||
{% amendment-disclaimer name="DID" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ seo:
|
||||
description: Escrowに留保されているXRPを送金元に返金します。
|
||||
labels:
|
||||
- Escrow
|
||||
- Payments
|
||||
requiredAmendment: Escrow
|
||||
txIcon: cancel
|
||||
---
|
||||
# EscrowCancel
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp "Source")
|
||||
|
||||
@@ -3,6 +3,9 @@ seo:
|
||||
description: Escrowプロセスが終了または取り消されるまでXRPを隔離します。
|
||||
labels:
|
||||
- Escrow
|
||||
- Payments
|
||||
requiredAmendment: Escrow
|
||||
txIcon: create
|
||||
---
|
||||
# EscrowCreate
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp "Source")
|
||||
|
||||
@@ -3,6 +3,9 @@ seo:
|
||||
description: エスクローされたXRPを受取人へ送金します。
|
||||
labels:
|
||||
- Escrow
|
||||
- Payments
|
||||
requiredAmendment: Escrow
|
||||
txIcon: finish
|
||||
---
|
||||
# EscrowFinish
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp "Source")
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
---
|
||||
html: transaction-types.html
|
||||
parent: transaction-formats.html
|
||||
seo:
|
||||
description: トランザクションのタイプは、どういったタイプの操作を実行することが想定されているのかを示します。
|
||||
metadata:
|
||||
indexPage: true
|
||||
indexPage: true
|
||||
labels:
|
||||
- ブロックチェーン
|
||||
- ブロックチェーン
|
||||
---
|
||||
# トランザクションのタイプ
|
||||
|
||||
トランザクションのタイプ(`TransactionType`フィールド)は、トランザクションに関する最も基本的な情報です。トランザクションで、どういったタイプの操作を実行することが想定されているのかを示します。
|
||||
トランザクションのタイプ(`TransactionType`フィールド)は、どういったタイプの操作を実行することが想定されているのかを示します。すべてのトランザクションに、[共通フィールド](../common-fields.md)が含まれています。
|
||||
|
||||
すべてのトランザクションに、特定の共通フィールドが含まれています。
|
||||
## アカウント
|
||||
|
||||
* [共通フィールド](../common-fields.md)
|
||||
{% tx-category name="Accounts" /%}
|
||||
|
||||
トランザクションのタイプごとに、実行される操作のタイプに関連した追加のフィールドが含まれています。
|
||||
## 支払い
|
||||
|
||||
{% tx-category name="Payments" /%}
|
||||
|
||||
## トークン
|
||||
|
||||
{% tx-category name="Tokens" /%}
|
||||
|
||||
## 分散型取引所(DEX)
|
||||
|
||||
{% tx-category name="DEX" /%}
|
||||
|
||||
## 分散型ストレージ
|
||||
|
||||
{% tx-category name="Decentralized Storage" /%}
|
||||
|
||||
## XRPLサイドチェーン
|
||||
|
||||
{% tx-category name="Interoperability" /%}
|
||||
|
||||
## その他
|
||||
|
||||
{% tx-category name="Other Transactions" /%}
|
||||
|
||||
{% child-pages /%}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
seo:
|
||||
description: アカウントが特定のMPTの残高を保持することを許可します。
|
||||
labels:
|
||||
- Multi-Purpose Token, MPT
|
||||
- Multi-purpose Tokens, MPTs
|
||||
- Tokens
|
||||
requiredAmendment: MPTokensV1
|
||||
txIcon: modify
|
||||
---
|
||||
|
||||
# MPTokenAuthorize
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp "ソース")
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: 新しいMulti-Purpose Tokenを発行します。
|
||||
labels:
|
||||
- Multi-Purpose Token, MPT
|
||||
- Multi-purpose Tokens, MPTs
|
||||
- Tokens
|
||||
requiredAmendment: MPTokensV1
|
||||
txIcon: create
|
||||
---
|
||||
|
||||
# MPTokenIssuanceCreate
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: Multi-Purpose Tokenを削除します。
|
||||
labels:
|
||||
- Multi-Purpose Token, MPT
|
||||
- Multi-purpose Tokens, MPTs
|
||||
- Tokens
|
||||
requiredAmendment: MPTokensV1
|
||||
txIcon: cancel
|
||||
---
|
||||
# MPTokenIssuanceDestroy
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: MPTの変更可能なプロパティを設定します。
|
||||
labels:
|
||||
- Multi-Purpose Token, MPT
|
||||
- Multi-purpose Tokens, MPTs
|
||||
- Tokens
|
||||
requiredAmendment: MPTokensV1
|
||||
txIcon: modify
|
||||
---
|
||||
# MPTokenIssuanceSet
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
seo:
|
||||
description: NFTokenの購入または売却のオファーを受け入れる。
|
||||
labels:
|
||||
- NFT, 非代替性トークン
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
- DEX
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: finish
|
||||
---
|
||||
# NFTokenAcceptOffer
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp "ソース")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
html: nftokenburn.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: TokenBurnを使用して、NFTを永久に破棄します。
|
||||
labels:
|
||||
- 非代替性トークン, NFT
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: cancel
|
||||
---
|
||||
# NFTokenBurn
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
html: nftokencanceloffer.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: NFTokenの売買のための既存のトークンへのオファーをキャンセルする。
|
||||
labels:
|
||||
- NFT, 非代替性トークン
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
- DEX
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: cancel
|
||||
---
|
||||
# NFTokenCancelOffer
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp "ソース")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
html: nftokencreateoffer.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: NFTの売買のオファーを作成する。
|
||||
labels:
|
||||
- 非代替性トークン, NFT
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
- DEX
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: create
|
||||
---
|
||||
# NFTokenCreateOffer
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: NFTokenMintを使用して新規NFTを発行する。
|
||||
labels:
|
||||
- 非代替性トークン, NFT
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: create
|
||||
---
|
||||
# NFTokenMint
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenMint.cpp "Source")
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
seo:
|
||||
description: ダイナミックNFTを変更します。
|
||||
labels:
|
||||
- 非代替性トークン, トークン, NFT
|
||||
title:
|
||||
- NFTokenModify
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
requiredAmendment: DynamicNFT
|
||||
txIcon: modify
|
||||
---
|
||||
# NFTokenModify
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenModify.cpp "ソース")
|
||||
|
||||
`NFTokenModify`は、NFTの`URI`フィールドを別のURIに変更し、NFTのサポートデータを更新するために使用されます。NFTは、`tfMutable`フラグが設定された状態でミントされている必要があります。[ダイナミックNFT](../../../../concepts/tokens/nfts/dynamic-nfts.md)をご覧ください。
|
||||
|
||||
{% amendment-disclaimer name="DynamicNFT" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
---
|
||||
html: offercancel.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: XRP LedgerからOfferオブジェクトを削除します。
|
||||
description: 分散型取引所からオファーを削除します。
|
||||
labels:
|
||||
- 分散型取引所
|
||||
- DEX
|
||||
txIcon: cancel
|
||||
---
|
||||
# OfferCancel
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CancelOffer.cpp "Source")
|
||||
|
||||
OfferCancelトランザクションは、XRP LedgerからOfferオブジェクトを削除します。
|
||||
[分散型取引所](../../../../concepts/tokens/decentralized-exchange/index.md)からオファーを削除します。
|
||||
|
||||
## {% $frontmatter.seo.title %}のJSONの例
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
seo:
|
||||
description: 通貨交換の注文を作成します。
|
||||
labels:
|
||||
- 分散型取引所
|
||||
- DEX
|
||||
txIcon: create
|
||||
---
|
||||
# OfferCreate
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CreateOffer.cpp "ソース")
|
||||
|
||||
OfferCreateトランザクションは[分散型取引所](../../../../concepts/tokens/decentralized-exchange/index.md)で[注文](../../../../concepts/tokens/decentralized-exchange/offers.md)を作成します。
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
seo:
|
||||
description: 既存の価格オラクルを削除します。
|
||||
labels:
|
||||
- オラクル
|
||||
- Oracle
|
||||
- Decentralized Storage
|
||||
requiredAmendment: PriceOracle
|
||||
txIcon: cancel
|
||||
---
|
||||
# OracleDelete
|
||||
_([PriceOracle Amendment][])_
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/DeleteOracle.cpp "ソース")
|
||||
|
||||
既存の`Oracle`レジャーエントリを削除します。
|
||||
|
||||
{% amendment-disclaimer name="PriceOracle" /%}
|
||||
|
||||
## OracleDeleteのJSONの例
|
||||
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
seo:
|
||||
description: 価格オラクルを作成または更新します。
|
||||
labels:
|
||||
- オラクル
|
||||
- Oracle
|
||||
- Decentralized Storage
|
||||
requiredAmendment: PriceOracle
|
||||
txIcon: create
|
||||
---
|
||||
# OracleSet
|
||||
_([PriceOracle Amendment][])_
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/SetOracle.cpp "ソース")
|
||||
|
||||
Oracle Document ID を使用して、新しい`Oracle`レジャーエントリを作成するか、既存のフィールドを更新します。
|
||||
|
||||
{% amendment-disclaimer name="PriceOracle" /%}
|
||||
|
||||
## OracleSetのJSONの例
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
seo:
|
||||
description: アカウント間での価値の移動します。
|
||||
description: アカウント間での価値の移動します、またはアカウントを作成する。
|
||||
labels:
|
||||
- 支払い
|
||||
- XRP
|
||||
- クロスカレンシー
|
||||
- トークン
|
||||
- Accounts
|
||||
- Payments
|
||||
- XRP
|
||||
- Cross-Currency
|
||||
txIcon: send
|
||||
---
|
||||
# Payment
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Payment.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: Payment Channelに対しXRPを請求します。
|
||||
labels:
|
||||
- Payment Channel
|
||||
- Payment Channels
|
||||
- Payments
|
||||
requiredAmendment: PayChan
|
||||
txIcon: finish
|
||||
---
|
||||
# PaymentChannelClaim
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PayChan.cpp "Source")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: 新しいペイメントチャネルを作成します。
|
||||
labels:
|
||||
- Payment Channel
|
||||
- Payment Channels
|
||||
- Payments
|
||||
requiredAmendment: PayChan
|
||||
txIcon: create
|
||||
---
|
||||
# PaymentChannelCreate
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PayChan.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: Payment ChannelにXRPを追加します。
|
||||
labels:
|
||||
- Payment Channel
|
||||
- Payment Channels
|
||||
- Payments
|
||||
requiredAmendment: PayChan
|
||||
txIcon: modify
|
||||
---
|
||||
# PaymentChannelFund
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PayChan.cpp "Source")
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
seo:
|
||||
description: 許可型ドメインのレジャーエントリを削除する
|
||||
labels:
|
||||
- コンプライアンス
|
||||
- 許可型ドメイン
|
||||
- Compliance
|
||||
- Permissioned Domains
|
||||
- Decentralized Storage
|
||||
requiredAmendment: PermissionedDomains
|
||||
txIcon: cancel
|
||||
---
|
||||
# PermissionedDomainDelete
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp "ソース")
|
||||
|
||||
所有する[許可型ドメイン][]を削除します。
|
||||
|
||||
_([PermissionedDomains amendment][]が必要です {% not-enabled /%})_
|
||||
{% amendment-disclaimer name="PermissionedDomains" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %}のJSONの例
|
||||
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
seo:
|
||||
description: 許可型ドメインを作成または更新する
|
||||
labels:
|
||||
- コンプライアンス
|
||||
- 許可型ドメイン
|
||||
- Compliance
|
||||
- Permissioned Domains
|
||||
- Decentralized Storage
|
||||
requiredAmendment: PermissionedDomains
|
||||
txIcon: create
|
||||
---
|
||||
# PermissionedDomainSet
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp "ソース")
|
||||
|
||||
[許可型ドメイン][]を作成するか、所有するドメインを変更します。
|
||||
|
||||
_([PermissionedDomains amendment][]が必要です {% not-enabled /%})_
|
||||
{% amendment-disclaimer name="PermissionedDomains" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %}のJSONの例
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
html: setregularkey.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: アカウントに関連付けられているレギュラーキーペアの割り当て、変更、削除を行います。
|
||||
labels:
|
||||
- セキュリティ
|
||||
- Security
|
||||
- Accounts
|
||||
txIcon: modify
|
||||
---
|
||||
# SetRegularKey
|
||||
|
||||
@@ -29,7 +29,6 @@ labels:
|
||||
{% tx-example txid="6AA6F6EAAAB56E65F7F738A9A2A8A7525439D65BA990E9BA08F6F4B1C2D349B4" /%}
|
||||
|
||||
{% raw-partial file="/@l10n/ja/docs/_snippets/tx-fields-intro.md" /%}
|
||||
<!--{# fix md highlighting_ #}-->
|
||||
|
||||
| フィールド | JSONの型 | [内部の型][] | 説明 |
|
||||
|:-------------|:----------|:------------------|:------------------------------|
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
---
|
||||
html: signerlistset.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: トランザクションのマルチシグに使用できる署名者のリストを作成、置換、削除します。
|
||||
labels:
|
||||
- セキュリティ
|
||||
- Security
|
||||
- Accounts
|
||||
requiredAmendment: MultiSign
|
||||
txIcon: modify
|
||||
---
|
||||
# SignerListSet
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/SetSignerList.cpp "ソース")
|
||||
|
||||
SignerListSetトランザクションは、トランザクションの[マルチシグ](../../../../concepts/accounts/multi-signing.md)に使用できる署名者のリストを作成、置換、削除します。このトランザクションタイプは[MultiSign Amendment][]により導入されました。
|
||||
|
||||
{% amendment-disclaimer name="MultiSign" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %}のJSONの例
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
html: ticketcreate.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: チケットとして1つ以上のシーケンス番号を確保する。
|
||||
labels:
|
||||
- Transaction Sending
|
||||
- Transaction Sending
|
||||
- Accounts
|
||||
requiredAmendment: TicketBatch
|
||||
txIcon: create
|
||||
---
|
||||
# TicketCreate
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CreateTicket.cpp "Source")
|
||||
|
||||
_([TicketBatch amendment][]が必要です)_
|
||||
|
||||
TicketCreateトランザクションは、1つまたは複数の[シーケンス番号](../../data-types/basic-data-types.md#アカウントシーケンス)を[Tickets](../../ledger-data/ledger-entry-types/ticket.md)として確保します。
|
||||
|
||||
{% amendment-disclaimer name="TicketBatch" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %}JSONの例
|
||||
|
||||
```json
|
||||
@@ -28,9 +28,6 @@ TicketCreateトランザクションは、1つまたは複数の[シーケンス
|
||||
|
||||
{% tx-example txid="738AEF36B48CA4A2D85C2B74910DC34DDBBCA4C83643F2DB84A58785ED5AD3E3" /%}
|
||||
|
||||
{% raw-partial file="/@l10n/ja/docs/_snippets/tx-fields-intro.md" /%}
|
||||
<!--{# fix md highlighting_ #}-->
|
||||
|
||||
| フィールド | JSONの型 | [内部の型][] | 説明 |
|
||||
|:-----------------|:-----------------|:------------------|:-------------------|
|
||||
| `TicketCount` | 数値 | UInt32 | 作成するチケットの枚数。これは正の数でなければならず、このトランザクションの実行の結果、アカウントが250枚以上のチケットを所有することはできません。 |
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
---
|
||||
html: trustset.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: トラストラインを作成または変更します。
|
||||
labels:
|
||||
- トークン
|
||||
- Tokens
|
||||
txIcon: modify
|
||||
---
|
||||
# TrustSet
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/SetTrust.cpp "Source")
|
||||
|
||||
2つのアカウントをリンクする[トラストライン](../../../../concepts/tokens/fungible-tokens/index.md)を作成または変更します。
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
---
|
||||
html: xchainaccountcreatecommit.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: ブリッジが接続するチェーンの一つでアカウントを作成します。このアカウントがそのチェーンのブリッジの入り口となります。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: create
|
||||
---
|
||||
# XChainAccountCreateCommit
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L466-L474 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
このトランザクションはXRP-XRPブリッジにのみ使用できます。
|
||||
|
||||
`XChainAccountCreateCommit`トランザクションは、発行チェーンにトランザクションを送信するために、Witnessサーバ用の新しいアカウントを作成します。
|
||||
発行チェーンにトランザクションを送信するために、Witnessサーバ用の新しいアカウントを作成します。このトランザクションはXRP-XRPブリッジにのみ使用できます。
|
||||
|
||||
{% admonition type="warning" name="注意" %}このトランザクションは、Witnessの証明書が送信先チェーンに確実に送信される場合にのみ実行されるべきです。署名が送信されない場合、証明書が受信されるまでアカウント作成はブロックされます。XRP-XRPブリッジでこのトランザクションを無効にするには、ブリッジの`MinAccountCreateAmount`フィールドを省略します。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainAccountCreateCommit JSONの例
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
---
|
||||
html: xchainaddaccountcreateattestation.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: XChainAddAccountCreateAttestationトランザクションは他のチェーンでXChainAccountCreateCommitトランザクションが発生した証明をWitnessサーバから提示します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: create
|
||||
---
|
||||
# XChainAddAccountCreateAttestation
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L447-L464 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainAddAccountCreateAttestation`トランザクションは、`XChainAccountCreateCommit`トランザクションがもう一方のチェーンで発生したというWitnessサーバからの証明を提示します。
|
||||
`XChainAccountCreateCommit`トランザクションがもう一方のチェーンで発生したというWitnessサーバからの証明を提示します。
|
||||
|
||||
この署名は署名が提供された時点のドアの署名者リストにある鍵の一つでなければなりません。署名が提出されてから定足数に達するまでの間に署名者リストが変更された場合、新しい署名セットが使用され、現在収集されている署名の一部が削除される可能性があります。
|
||||
|
||||
@@ -20,6 +18,7 @@ _([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
{% admonition type="info" name="注記" %}報酬は現在のリストにある鍵を持っているアカウントにのみ送られます。署名者の定足数は`SignatureReward`に一致する必要があります。より大きな報酬を得ようとして、一つのWitnessサーバがこの値に不正な値を指定することはできません。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainAddAccountCreateAttestation JSONの例
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
---
|
||||
html: xchainaddclaimattestation.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 送信元チェーンで発生したイベントを、送信先チェーンに証明(アテスト)します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: modify
|
||||
---
|
||||
# XChainAddClaimAttestation
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L429-L445 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainAddClaimAttestation`トランザクションは`XChainCommit`トランザクションを証明するWitnessサーバの署名を提供します。
|
||||
`XChainCommit`トランザクションを証明するWitnessサーバの署名を提供します。
|
||||
|
||||
この署名は、署名が提出された時点のドアの署名者リストにある鍵の一つでなければなりません。ただし、署名が提出されてから定足数に達するまでの間に署名者リストが変更された場合は、新しい署名セットが使用され、現在収集されている署名の一部が削除されることがあります。
|
||||
|
||||
@@ -20,6 +18,7 @@ _([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
{% admonition type="info" name="注記" %}報酬は現在のリストにある鍵を持っているアカウントにのみ送られます。署名者の定足数は`SignatureReward`に一致する必要があります。より大きな報酬を得ようとして、一つのWitnessサーバがこの値に不正な値を指定することはできません。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainAddClaimAttestation JSONの例
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
---
|
||||
html: xchainclaim.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 送信先チェーンで金額を請求することで、クロスチェーンでの価値移転を完了させます。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: finish
|
||||
---
|
||||
# XChainClaim
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L418-L427 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainClaim`トランザクションはクロスチェーンでの価値の移転を完了させます。`XChainClaim`トランザクションにより、ユーザは送信元チェーンでロックされた価値と同等の価値を送信先チェーンで請求することができます。ユーザは、送金元チェーンでロックされた価値に関連付けられたクロスチェーン請求ID(`Account`フィールド)を所有している場合にのみ、その価値を請求することができます。ユーザは誰にでも資金を送ることができます(`Destination`フィールド)。このトランザクションが必要になるのは`XChainCommit`トランザクションで`OtherChainDestination`が指定されていない場合、または自動送金で何か問題が発生した場合のみです。
|
||||
|
||||
トランザクションによって送金に成功すると、対象の`XChainOwnedClaimID`レジャーオブジェクトは削除されます。これはトランザクションのリプレイを防ぎます。トランザクションが失敗した場合、`XChainOwnedClaimID`は削除されず、異なるパラメータでトランザクションを再実行できます。
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainClaim JSONの例
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
---
|
||||
html: xchaincommit.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: クロスチェーンでの価値移転を開始します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: send
|
||||
---
|
||||
# XChainCommit
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L408-L416 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainCommit`はクロスチェーン送金の2番目のステップです。`XChainCommit`は発行チェーンでラップできるようにロックチェーンで資産を保管したり、ロックチェーンで返却できるように発行チェーンでラップされた資産をバーンしたりします。
|
||||
クロスチェーン送金の2番目のステップです。`XChainCommit`は発行チェーンでラップできるようにロックチェーンで資産を保管したり、ロックチェーンで返却できるように発行チェーンでラップされた資産をバーンしたりします。
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainCommit JSONの例
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
---
|
||||
html: xchaincreatebridge.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 2つのチェーン間にブリッジを作成します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: create
|
||||
---
|
||||
# XChainCreateBridge
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L381-L388 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainCreateBridge`トランザクションは新しい`Bridge`レジャーオブジェクトを作成し、トランザクショ ンが送信されたチェーン上に新しいクロスチェーンブリッジの入り口を定義します。これにはブリッジのドアアカウントと資産に関する情報が含まれます。
|
||||
新しい`Bridge`レジャーオブジェクトを作成し、トランザクショ ンが送信されたチェーン上に新しいクロスチェーンブリッジの入り口を定義します。これにはブリッジのドアアカウントと資産に関する情報が含まれます。
|
||||
|
||||
このトランザクションは、ロックチェーンのドアアカウントが最初に送信する必要があります。有効なブリッジをセットアップするには、Witnessサーバのセットアップに加えて、両チェーンのドアアカウントがこのトランザクションを送信しなければなりません。
|
||||
|
||||
@@ -20,6 +18,7 @@ _([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
{% admonition type="info" name="注記" %}各ドアアカウントは1つのブリッジしか持つことができません。これにより、同じ資産に対して複数のブリッジが作成され、いずれかのチェーンで資産が不一致となるのを防ぐことができます。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainCreateBridge JSONの例
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
---
|
||||
html: xchaincreateclaimid.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: クロスチェーン送金に使用するクロスチェーン請求IDを作成します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: create
|
||||
---
|
||||
# XChainCreateClaimID
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L399-L406 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainCreateClaimID`トランザクションはクロスチェーン送金に使われる新しいクロスチェーン請求IDを作成します。クロスチェーン請求IDは*1つの*クロスチェーン送金を表します。
|
||||
|
||||
このトランザクションはクロスチェーン送金の最初のステップであり、送金元チェーンではなく、送金先チェーンで送信されます。
|
||||
|
||||
また、送金元チェーン上の資金をロックまたはバーンする送金元チェーン上のアカウントも含まれます。
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainCreateClaimID JSONの例
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
---
|
||||
html: xchainmodifybridge.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: ブリッジの設定を変更します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: modify
|
||||
---
|
||||
# XChainModifyBridge
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/develop/src/ripple/protocol/impl/TxFormats.cpp#L390-L397 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainModifyBridge`トランザクションでは、ブリッジ管理者がブリッジの設定を変更することができます。変更できるのは`SignatureReward`と`MinAccountCreateAmount`だけです。
|
||||
|
||||
このトランザクションはドアアカウントから送信される必要があり、Witnessサーバを管理するエンティティがこのトランザクションのために協調し、署名を提供する必要があります。この調整はレジャーの外部で行われます。
|
||||
|
||||
{% admonition type="info" name="注記" %}このトランザクションでブリッジの署名者リストを変更することはできません。署名者リストはドアアカウント自体にあり、署名者リストがアカウント上で変更されるのと同じ方法で変更されます(`SignerListSet`トランザクションを利用)。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainModifyBridge JSONの例
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Components related to XRPL Amendment previews and statuses
|
||||
|
||||
import * as React from 'react'
|
||||
import { Link } from '@redocly/theme/components/Link/Link'
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks'
|
||||
|
||||
26
@theme/components/ChildPages.tsx
Normal file
26
@theme/components/ChildPages.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
// Component for {% child-pages /%} markdoc tag.
|
||||
// Return a list of children of the current page.
|
||||
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks'
|
||||
import { Link } from '@redocly/theme/components/Link/Link'
|
||||
import NotEnabled from './NotEnabled'
|
||||
|
||||
export default function ChildPages() {
|
||||
const { usePageSharedData } = useThemeHooks()
|
||||
const data = usePageSharedData('index-page-items') as any[]
|
||||
return (
|
||||
<div className="children-display">
|
||||
<ul>
|
||||
{data?.map((item: any) => (
|
||||
<li className="level-1" key={item.slug}>
|
||||
<Link to={item.slug}>{item.title}</Link>
|
||||
{
|
||||
item.status === "not_enabled" ? (<NotEnabled />) : ""
|
||||
}
|
||||
<p className="blurb child-blurb">{item.seo?.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
@theme/components/CodePageName.tsx
Normal file
11
@theme/components/CodePageName.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
// Component for {% code-page-name /%} Markdoc tag.
|
||||
// Returns the current page title in monospace (code) font.
|
||||
// Useful in includes / templates that may be reused across pages.
|
||||
|
||||
export default function CodePageName(props: {
|
||||
name: string
|
||||
}) {
|
||||
return (
|
||||
<code>{props.name}</code>
|
||||
)
|
||||
}
|
||||
28
@theme/components/CopyableUrl.tsx
Normal file
28
@theme/components/CopyableUrl.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState } from "react"
|
||||
|
||||
// Copyable URL component with click-to-copy functionality
|
||||
export default function CopyableUrl({ url, translate }: { url: string; translate: (text: string) => string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`quick-ref-value-btn ${copied ? "copied" : ""}`}
|
||||
onClick={handleCopy}
|
||||
title={copied ? translate("Copied!") : translate("Click to copy")}
|
||||
>
|
||||
<code className="quick-ref-value">{url}</code>
|
||||
<span className="copy-icon">{copied ? "✓" : ""}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
45
@theme/components/InteractiveBlock.tsx
Normal file
45
@theme/components/InteractiveBlock.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// Component for {% interactive-block %} markdoc tag. Used in legacy interactive
|
||||
// tutorials; not recommended for new tutorials.
|
||||
|
||||
import * as React from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import dynamicReact from '@markdoc/markdoc/dist/react'
|
||||
import { idify } from '../helpers'
|
||||
|
||||
export default function InteractiveBlock(props: {
|
||||
children: React.ReactNode
|
||||
label: string
|
||||
steps: string[]
|
||||
}) {
|
||||
const stepId = idify(props.label)
|
||||
const { pathname } = useLocation()
|
||||
|
||||
return (
|
||||
// add key={pathname} to ensure old step state gets rerendered on page navigation
|
||||
<div className="interactive-block" id={'interactive-' + stepId} key={pathname}>
|
||||
<div className="interactive-block-inner">
|
||||
<div className="breadcrumbs-wrap">
|
||||
<ul
|
||||
className="breadcrumb tutorial-step-crumbs"
|
||||
id={'bc-ul-' + stepId}
|
||||
data-steplabel={props.label}
|
||||
data-stepid={stepId}
|
||||
>
|
||||
{props.steps?.map((step, idx) => {
|
||||
const iterStepId = idify(step).toLowerCase()
|
||||
let className = `breadcrumb-item bc-${iterStepId}`
|
||||
if (idx > 0) className += ' disabled'
|
||||
if (iterStepId === stepId) className += ' current'
|
||||
return (
|
||||
<li className={className} key={iterStepId}>
|
||||
<a href={`#interactive-${iterStepId}`}>{step}</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="interactive-block-ui">{dynamicReact(props.children, React, {})}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
// Replaces Redocly's built-in language picker with our custom language picker component.
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as React from "react";
|
||||
// Replaces the top navbar with our custom XRPL.org top navbar
|
||||
|
||||
import React from 'react'
|
||||
import { useThemeConfig, useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { LanguagePicker } from "@redocly/theme/components/LanguagePicker/LanguagePicker";
|
||||
import { slugify } from "../../helpers";
|
||||
|
||||
14
@theme/components/NotEnabled.tsx
Normal file
14
@theme/components/NotEnabled.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
// Component for {% not-enabled /%} markdoc tag. Shows a flask icon with a
|
||||
// tooltip so you can indicate that a feature is not enabled on the
|
||||
// XRP Ledger Mainnet. Legacy usage, mostly; prefer {% amendment-disclaimer %}
|
||||
// for most cases.
|
||||
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks'
|
||||
|
||||
export default function NotEnabled() {
|
||||
const { useTranslate } = useThemeHooks()
|
||||
const { translate } = useTranslate()
|
||||
return (
|
||||
<span className="status not_enabled" title={translate("This feature is not currently enabled on the production XRP Ledger.")}><i className="fa fa-flask"></i></span>
|
||||
)
|
||||
}
|
||||
22
@theme/components/RepoLink.tsx
Normal file
22
@theme/components/RepoLink.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
// Create a link into the source code repository for this project.
|
||||
// This is supposed to adjust so that PR builds use the branch+fork of the PR,
|
||||
// but that part wasn't implemented for Redocly builds.
|
||||
|
||||
import * as React from 'react'
|
||||
import dynamicReact from '@markdoc/markdoc/dist/react'
|
||||
import { Link } from '@redocly/theme/components/Link/Link'
|
||||
|
||||
export default function RepoLink(props: {
|
||||
children: React.ReactNode
|
||||
path: string
|
||||
github_fork: string
|
||||
github_branch: string
|
||||
}) {
|
||||
const treeblob = props.path.indexOf(".") >= 0 ? "blob/" : "tree/"
|
||||
const sep = props.github_fork[-1] == "/" ? "" : "/"
|
||||
const href = props.github_fork+sep+treeblob+props.github_branch+"/"+props.path
|
||||
|
||||
return (
|
||||
<Link to={href}>{dynamicReact(props.children, React, {})}</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// This component replaces the default Redocly Tabs functionality.
|
||||
// Original Tabs styling is preserved, but this adds full-page tab
|
||||
// Replaces Redocly's built-in {% tabs %} component.
|
||||
// Uses the existing Tabs styling, but adds full-page tab
|
||||
// switching and preserves tab preferences between pages.
|
||||
|
||||
import React, { useRef, useState, useEffect, useCallback, useMemo } from 'react'
|
||||
|
||||
82
@theme/components/TxRefs.tsx
Normal file
82
@theme/components/TxRefs.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
// Component for {% tx-category %} Markdoc tag. Shows a list (table?) of child pages
|
||||
// with the matching labels in the frontmatter.
|
||||
// Requires the index-pages Redocly plugin to get child page data.
|
||||
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks'
|
||||
import { Link } from '@redocly/theme/components/Link/Link'
|
||||
import { AmendmentDisclaimer } from './Amendments'
|
||||
|
||||
interface TxCategoryProps {
|
||||
name: string,
|
||||
}
|
||||
|
||||
export function TxCategory(props: TxCategoryProps) {
|
||||
const { usePageSharedData } = useThemeHooks()
|
||||
const data = usePageSharedData('index-page-items') as any[]
|
||||
const matchingItems = data?.filter( (page) => {
|
||||
if (page.labels && page.labels.includes(props.name)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="tx-type-list">
|
||||
{
|
||||
matchingItems?.map((item: any) => (
|
||||
<TxTypeLink key={item.slug} page={item} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const txIcons = {
|
||||
"create": require('../../static/img/tx-icons/TransactionCreateIcon.svg'),
|
||||
"modify": require('../../static/img/tx-icons/TransactionModifyIcon.svg'),
|
||||
"finish": require('../../static/img/tx-icons/TransactionFinishIcon.svg'),
|
||||
"cancel": require('../../static/img/tx-icons/TransactionCancelIcon.svg'),
|
||||
"send": require('../../static/img/tx-icons/TransactionSendIcon.svg'),
|
||||
"other": require('../../static/img/tx-icons/TransactionUnknownIcon.svg'),
|
||||
}
|
||||
|
||||
function TxTypeLink(props: {page: any}) {
|
||||
const page = props.page
|
||||
let txIcon = txIcons["other"]
|
||||
if (page.txIcon && txIcons[page.txIcon.toLowerCase()]) {
|
||||
txIcon = txIcons[page.txIcon]
|
||||
}
|
||||
return (
|
||||
<div className="tx-type">
|
||||
<Link to={page.slug} className="tx-title"><img className="tx-type-icon" src={txIcon} alt="" /> {page.title}</Link>
|
||||
{
|
||||
page.requiredAmendment &&
|
||||
<div className="required-amendment">
|
||||
Requires: <AmendmentDisclaimer name={page.requiredAmendment} compact={true} mode="" />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
page.seo?.description &&
|
||||
<div className="tx-desc">{page.seo.description}</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TxIconLegend() {
|
||||
return (
|
||||
<div className="tx-icon-legend">
|
||||
<h4 className="tx-icon-title">Icon Legend</h4>
|
||||
<div className="d-flex flex-wrap">
|
||||
{ Object.entries(txIcons).map( ([iconName, txIcon]) => {
|
||||
return (
|
||||
<div className="tx-legend-item" key={iconName}>
|
||||
<img className="tx-type-icon" src={txIcon} alt="" />
|
||||
{iconName}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
58
@theme/components/WSToolButtons.tsx
Normal file
58
@theme/components/WSToolButtons.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Link } from '@redocly/theme/components/Link/Link'
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks'
|
||||
|
||||
type TryItServer = 's1' | 's2' | 'xrplcluster' | 'testnet' | 'devnet' | 'testnet-clio' | 'devnet-clio'
|
||||
|
||||
export function TryIt(props: {
|
||||
method: string,
|
||||
server?: TryItServer
|
||||
}) {
|
||||
const { useTranslate } = useThemeHooks()
|
||||
const { translate } = useTranslate()
|
||||
let use_server = ""
|
||||
if (props.server == "s1") {
|
||||
use_server = "?server=wss%3A%2F%2Fs1.ripple.com%2F"
|
||||
} else if (props.server == "s2") {
|
||||
use_server = "?server=wss%3A%2F%2Fs2.ripple.com%2F"
|
||||
} else if (props.server == "xrplcluster") {
|
||||
use_server = "?server=wss%3A%2F%2Fxrplcluster.com%2F"
|
||||
} else if (props.server == 'devnet') {
|
||||
use_server = "?server=wss%3A%2F%2Fs.devnet.rippletest.net%3A51233%2F"
|
||||
} else if (props.server == 'testnet') {
|
||||
use_server = "?server=wss%3A%2F%2Fs.altnet.rippletest.net%3A51233%2F"
|
||||
} else if (props.server == 'testnet-clio') {
|
||||
use_server = "?server=wss%3A%2F%2Fclio.altnet.rippletest.net%3A51233%2F"
|
||||
} else if (props.server == 'devnet-clio') {
|
||||
use_server = "?server=wss%3A%2F%2Fclio.devnet.rippletest.net%3A51233%2F"
|
||||
}
|
||||
const to_path = `/resources/dev-tools/websocket-api-tool${use_server}#${props.method}`
|
||||
return (
|
||||
<Link style={{marginBottom: "1rem", textDecoration: "none"}} className="btn btn-primary btn-arrow" to={to_path} target="_blank" role="button">{translate("component.tryit", "Try it!")}</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function TxExample(props: {
|
||||
txid: string,
|
||||
server?: TryItServer
|
||||
}) {
|
||||
const { useTranslate } = useThemeHooks()
|
||||
const { translate } = useTranslate()
|
||||
let use_server = ""
|
||||
if (props.server == "s1") {
|
||||
use_server = "&server=wss%3A%2F%2Fs1.ripple.com%2F"
|
||||
} else if (props.server == "s2") {
|
||||
use_server = "&server=wss%3A%2F%2Fs2.ripple.com%2F"
|
||||
} else if (props.server == "xrplcluster") {
|
||||
use_server = "&server=wss%3A%2F%2Fxrplcluster.com%2F"
|
||||
} else if (props.server == 'devnet') {
|
||||
use_server = "&server=wss%3A%2F%2Fs.devnet.rippletest.net%3A51233%2F"
|
||||
} else if (props.server == 'testnet') {
|
||||
use_server = "&server=wss%3A%2F%2Fs.altnet.rippletest.net%3A51233%2F"
|
||||
}
|
||||
|
||||
const ws_req = `req=%7B%22id%22%3A%22example_tx_lookup%22%2C%22command%22%3A%22tx%22%2C%22transaction%22%3A%22${props.txid}%22%2C%22binary%22%3Afalse%2C%22api_version%22%3A2%7D`
|
||||
const to_path = `/resources/dev-tools/websocket-api-tool?${ws_req}${use_server}`
|
||||
return (
|
||||
<Link style={{marginBottom: "1rem", textDecoration: "none"}} className="btn btn-primary btn-arrow" to={to_path} target="_blank" role="button">{translate("component.queryexampletx", "Query example transaction")}</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
// Components for creating grids of cards in the current site style.
|
||||
// Used in both custom .page.tsx files as well as markdoc tags {% card-grid %}
|
||||
// and {% xrpl-card %}.
|
||||
|
||||
import * as React from 'react';
|
||||
import dynamicReact from '@markdoc/markdoc/dist/react';
|
||||
import { Link } from '@redocly/theme/components/Link/Link';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
// Loading animation component used in various custom pages.
|
||||
|
||||
export interface XRPLoaderProps {
|
||||
message?: string
|
||||
show: boolean
|
||||
show?: boolean
|
||||
}
|
||||
|
||||
export default function XRPLoader(props: XRPLoaderProps) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useThemeFromClassList = (classNames) => {
|
||||
export const useThemeFromClassList = (classNames: Array<string>) => {
|
||||
const [currentTheme, setCurrentTheme] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -40,7 +40,7 @@ export const useThemeFromClassList = (classNames) => {
|
||||
return currentTheme;
|
||||
};
|
||||
|
||||
export function slugify(s) {
|
||||
export function slugify(s: string) {
|
||||
const unacceptable_chars = /[^A-Za-z0-9._ ]+/g;
|
||||
const whitespace_regex = /\s+/g;
|
||||
s = s.replace(unacceptable_chars, "");
|
||||
|
||||
@@ -1,158 +1,11 @@
|
||||
import * as React from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
// @ts-ignore
|
||||
import dynamicReact from '@markdoc/markdoc/dist/react'
|
||||
import { Link } from '@redocly/theme/components/Link/Link'
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks'
|
||||
import { idify } from '../helpers'
|
||||
import { Button } from '@redocly/theme/components/Button/Button'
|
||||
|
||||
export { default as XRPLoader } from '../components/XRPLoader'
|
||||
export { XRPLCard, CardGrid } from '../components/XRPLCard'
|
||||
export { AmendmentsTable, AmendmentDisclaimer, Badge } from '../components/Amendments'
|
||||
export { Tabs } from '../components/SyncedTabs'
|
||||
|
||||
export function IndexPageItems() {
|
||||
const { usePageSharedData } = useThemeHooks()
|
||||
const data = usePageSharedData('index-page-items') as any[]
|
||||
return (
|
||||
<div className="children-display">
|
||||
<ul>
|
||||
{data?.map((item: any) => (
|
||||
<li className="level-1" key={item.slug}>
|
||||
<Link to={item.slug}>{item.title}</Link>
|
||||
{
|
||||
item.status === "not_enabled" ? (<NotEnabled />) : ""
|
||||
}
|
||||
<p className="blurb child-blurb">{item.seo?.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function InteractiveBlock(props: {
|
||||
children: React.ReactNode
|
||||
label: string
|
||||
steps: string[]
|
||||
}) {
|
||||
const stepId = idify(props.label)
|
||||
const { pathname } = useLocation()
|
||||
|
||||
return (
|
||||
// add key={pathname} to ensure old step state gets rerendered on page navigation
|
||||
<div className="interactive-block" id={'interactive-' + stepId} key={pathname}>
|
||||
<div className="interactive-block-inner">
|
||||
<div className="breadcrumbs-wrap">
|
||||
<ul
|
||||
className="breadcrumb tutorial-step-crumbs"
|
||||
id={'bc-ul-' + stepId}
|
||||
data-steplabel={props.label}
|
||||
data-stepid={stepId}
|
||||
>
|
||||
{props.steps?.map((step, idx) => {
|
||||
const iterStepId = idify(step).toLowerCase()
|
||||
let className = `breadcrumb-item bc-${iterStepId}`
|
||||
if (idx > 0) className += ' disabled'
|
||||
if (iterStepId === stepId) className += ' current'
|
||||
return (
|
||||
<li className={className} key={iterStepId}>
|
||||
<a href={`#interactive-${iterStepId}`}>{step}</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="interactive-block-ui">{dynamicReact(props.children, React, {})}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function RepoLink(props: {
|
||||
children: React.ReactNode
|
||||
path: string
|
||||
github_fork: string
|
||||
github_branch: string
|
||||
}) {
|
||||
const treeblob = props.path.indexOf(".") >= 0 ? "blob/" : "tree/"
|
||||
const sep = props.github_fork[-1] == "/" ? "" : "/"
|
||||
const href = props.github_fork+sep+treeblob+props.github_branch+"/"+props.path
|
||||
|
||||
return (
|
||||
<Link to={href}>{dynamicReact(props.children, React, {})}</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function CodePageName(props: {
|
||||
name: string
|
||||
}) {
|
||||
return (
|
||||
<code>{props.name}</code>
|
||||
)
|
||||
}
|
||||
|
||||
type TryItServer = 's1' | 's2' | 'xrplcluster' | 'testnet' | 'devnet' | 'testnet-clio' | 'devnet-clio'
|
||||
|
||||
export function TryIt(props: {
|
||||
method: string,
|
||||
server?: TryItServer
|
||||
}) {
|
||||
const { useTranslate } = useThemeHooks()
|
||||
const { translate } = useTranslate()
|
||||
let use_server = ""
|
||||
if (props.server == "s1") {
|
||||
use_server = "?server=wss%3A%2F%2Fs1.ripple.com%2F"
|
||||
} else if (props.server == "s2") {
|
||||
use_server = "?server=wss%3A%2F%2Fs2.ripple.com%2F"
|
||||
} else if (props.server == "xrplcluster") {
|
||||
use_server = "?server=wss%3A%2F%2Fxrplcluster.com%2F"
|
||||
} else if (props.server == 'devnet') {
|
||||
use_server = "?server=wss%3A%2F%2Fs.devnet.rippletest.net%3A51233%2F"
|
||||
} else if (props.server == 'testnet') {
|
||||
use_server = "?server=wss%3A%2F%2Fs.altnet.rippletest.net%3A51233%2F"
|
||||
} else if (props.server == 'testnet-clio') {
|
||||
use_server = "?server=wss%3A%2F%2Fclio.altnet.rippletest.net%3A51233%2F"
|
||||
} else if (props.server == 'devnet-clio') {
|
||||
use_server = "?server=wss%3A%2F%2Fclio.devnet.rippletest.net%3A51233%2F"
|
||||
}
|
||||
const to_path = `/resources/dev-tools/websocket-api-tool${use_server}#${props.method}`
|
||||
return (
|
||||
<Link style={{marginBottom: "1rem", textDecoration: "none"}} className="btn btn-primary btn-arrow" to={to_path} target="_blank" role="button">{translate("component.tryit", "Try it!")}</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function TxExample(props: {
|
||||
txid: string,
|
||||
server?: TryItServer
|
||||
}) {
|
||||
const { useTranslate } = useThemeHooks()
|
||||
const { translate } = useTranslate()
|
||||
let use_server = ""
|
||||
if (props.server == "s1") {
|
||||
use_server = "&server=wss%3A%2F%2Fs1.ripple.com%2F"
|
||||
} else if (props.server == "s2") {
|
||||
use_server = "&server=wss%3A%2F%2Fs2.ripple.com%2F"
|
||||
} else if (props.server == "xrplcluster") {
|
||||
use_server = "&server=wss%3A%2F%2Fxrplcluster.com%2F"
|
||||
} else if (props.server == 'devnet') {
|
||||
use_server = "&server=wss%3A%2F%2Fs.devnet.rippletest.net%3A51233%2F"
|
||||
} else if (props.server == 'testnet') {
|
||||
use_server = "&server=wss%3A%2F%2Fs.altnet.rippletest.net%3A51233%2F"
|
||||
}
|
||||
|
||||
const ws_req = `req=%7B%22id%22%3A%22example_tx_lookup%22%2C%22command%22%3A%22tx%22%2C%22transaction%22%3A%22${props.txid}%22%2C%22binary%22%3Afalse%2C%22api_version%22%3A2%7D`
|
||||
const to_path = `/resources/dev-tools/websocket-api-tool?${ws_req}${use_server}`
|
||||
return (
|
||||
<Link style={{marginBottom: "1rem", textDecoration: "none"}} className="btn btn-primary btn-arrow" to={to_path} target="_blank" role="button">{translate("component.queryexampletx", "Query example transaction")}</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotEnabled() {
|
||||
const { useTranslate } = useThemeHooks()
|
||||
const { translate } = useTranslate()
|
||||
return (
|
||||
<span className="status not_enabled" title={translate("This feature is not currently enabled on the production XRP Ledger.")}><i className="fa fa-flask"></i></span>
|
||||
)
|
||||
}
|
||||
export { default as ChildPages } from '../components/ChildPages'
|
||||
export { default as NotEnabled } from '../components/NotEnabled'
|
||||
export { default as InteractiveBlock } from '../components/InteractiveBlock'
|
||||
export { default as RepoLink } from '../components/RepoLink'
|
||||
export { default as CodePageName } from '../components/CodePageName'
|
||||
export { TryIt, TxExample } from '../components/WSToolButtons'
|
||||
export { TxCategory, TxIconLegend } from '../components/TxRefs'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Schema, Tag } from '@markdoc/markdoc';
|
||||
|
||||
export const indexPageList: Schema & { tagName: string } = {
|
||||
export const childPages: Schema & { tagName: string } = {
|
||||
tagName: 'child-pages',
|
||||
render: 'IndexPageItems',
|
||||
render: 'ChildPages',
|
||||
selfClosing: true,
|
||||
};
|
||||
|
||||
@@ -243,3 +243,21 @@ export const amendmentDisclaimer: Schema & { tagName: string } = {
|
||||
render: 'AmendmentDisclaimer',
|
||||
selfClosing: true
|
||||
}
|
||||
|
||||
export const txCategory: Schema & { tagName: string } = {
|
||||
tagName: 'tx-category',
|
||||
attributes: {
|
||||
name: {
|
||||
type: 'String',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
render: 'TxCategory',
|
||||
selfClosing: true,
|
||||
};
|
||||
|
||||
export const txIconLegend: Schema & { tagName: string } = {
|
||||
tagName: 'tx-icon-legend',
|
||||
render: 'TxIconLegend',
|
||||
selfClosing: true,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { indexPages } from './plugins/index-pages.js';
|
||||
import { codeSamples } from './plugins/code-samples.js';
|
||||
import { blogPosts } from './plugins/blog-posts.js';
|
||||
import { tutorialLanguages } from './plugins/tutorial-languages.js'
|
||||
import { tutorialLanguages } from './plugins/tutorial-languages.js';
|
||||
import { tutorialMetadata } from './plugins/tutorial-metadata.js';
|
||||
|
||||
export default function customPlugin() {
|
||||
const indexPagesInst = indexPages();
|
||||
const codeSamplesInst = codeSamples();
|
||||
const blogPostsInst = blogPosts();
|
||||
const tutorialLanguagesInst = tutorialLanguages();
|
||||
const tutorialMetadataInst = tutorialMetadata();
|
||||
|
||||
|
||||
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
|
||||
@@ -18,12 +20,14 @@ export default function customPlugin() {
|
||||
await codeSamplesInst.processContent?.(content, actions);
|
||||
await blogPostsInst.processContent?.(content, actions);
|
||||
await tutorialLanguagesInst.processContent?.(content, actions);
|
||||
await tutorialMetadataInst.processContent?.(content, actions);
|
||||
},
|
||||
afterRoutesCreated: async (content, actions) => {
|
||||
await indexPagesInst.afterRoutesCreated?.(content, actions);
|
||||
await codeSamplesInst.afterRoutesCreated?.(content, actions);
|
||||
await blogPostsInst.afterRoutesCreated?.(content, actions);
|
||||
await tutorialLanguagesInst.processContent?.(content, actions);
|
||||
await tutorialMetadataInst.processContent?.(content, actions);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
// The purpose of this plugin is to get a list of pages that are "children" of
|
||||
// the current page and pass along their frontmatter as well as the path that
|
||||
// can be used to link to those pages from other components.
|
||||
// It uses experimental Redocly plugin interface stuff that is expected
|
||||
// to be exposed in a "nicer" way in some theoretical future release.
|
||||
// The ts-ignore and TODOs are more for Redocly's notes than for XRPLF.
|
||||
|
||||
// @ts-check
|
||||
import { readSharedData } from '@redocly/realm/dist/server/utils/shared-data.js'; // TODO: export function from root package
|
||||
const INDEX_PAGE_INFO_DATA_KEY = 'index-page-items';
|
||||
@@ -26,14 +33,21 @@ export function indexPages() {
|
||||
|
||||
const item = findItemDeep(sidebar.items, route.fsPath);
|
||||
const childrenPaths = (item.items || [])
|
||||
.map((item) => item.fsPath)
|
||||
.map(
|
||||
// @ts-ignore
|
||||
(item) => item.fsPath
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const childRoutes = childrenPaths.map((fsPath) =>
|
||||
const childRoutes = childrenPaths.map(
|
||||
// @ts-ignore
|
||||
(fsPath) =>
|
||||
actions.getRouteByFsPath(fsPath),
|
||||
);
|
||||
const childRoutesData = await Promise.all(
|
||||
childRoutes.map(async (route) => {
|
||||
childRoutes.map(
|
||||
// @ts-ignore
|
||||
async (route) => {
|
||||
const { data } = await cache.load(
|
||||
route.fsPath,
|
||||
'markdown-frontmatter',
|
||||
@@ -63,6 +77,7 @@ export function indexPages() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
function findItemDeep(items, fsPath) {
|
||||
for (const item of items) {
|
||||
if (item.fsPath === fsPath) {
|
||||
@@ -70,6 +85,7 @@ function findItemDeep(items, fsPath) {
|
||||
}
|
||||
|
||||
if (item.items) {
|
||||
// @ts-ignore
|
||||
const found = findItemDeep(item.items, fsPath);
|
||||
if (found) {
|
||||
return found;
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
// @ts-check
|
||||
|
||||
import { getInnerText } from '@redocly/realm/dist/markdoc/helpers/get-inner-text.js'
|
||||
|
||||
/**
|
||||
* Plugin to detect languages supported in tutorial pages by scanning for tab labels.
|
||||
* Plugin to detect languages supported in tutorial pages.
|
||||
*
|
||||
* Detection methods (in priority order):
|
||||
* 1. Tab labels in the markdown (for multi-language tutorials)
|
||||
* 2. Filename patterns like "-js.md", "-py.md" (for single-language tutorials)
|
||||
* 3. Title containing language name (for single-language tutorials)
|
||||
*
|
||||
* This creates shared data that maps tutorial paths to their supported languages.
|
||||
*/
|
||||
export function tutorialLanguages() {
|
||||
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
|
||||
/** @type {import("@redocly/realm/dist/server/types").ExternalPlugin } */
|
||||
const instance = {
|
||||
id: 'tutorial-languages',
|
||||
processContent: async (actions, { fs, cache }) => {
|
||||
try {
|
||||
/** @type {Record<string, string[]>} */
|
||||
@@ -21,7 +30,18 @@ export function tutorialLanguages() {
|
||||
for (const { relativePath } of tutorialFiles) {
|
||||
try {
|
||||
const { data } = await cache.load(relativePath, 'markdown-ast')
|
||||
const languages = extractLanguagesFromAst(data.ast)
|
||||
|
||||
// Try to detect languages from tab labels first
|
||||
let languages = extractLanguagesFromAst(data.ast)
|
||||
|
||||
// Fallback: detect language from filename/title for single-language tutorials
|
||||
if (languages.length === 0) {
|
||||
const title = extractFirstHeading(data.ast) || ''
|
||||
const fallbackLang = detectLanguageFromPathAndTitle(relativePath, title)
|
||||
if (fallbackLang) {
|
||||
languages = [fallbackLang]
|
||||
}
|
||||
}
|
||||
|
||||
if (languages.length > 0) {
|
||||
// Convert file path to URL path
|
||||
@@ -54,16 +74,31 @@ function extractLanguagesFromAst(ast) {
|
||||
const languages = new Set()
|
||||
|
||||
visit(ast, (node) => {
|
||||
// Look for tab nodes with a label attribute
|
||||
if (isNode(node) && node.type === 'tag' && node.tag === 'tab') {
|
||||
if (!isNode(node)) return
|
||||
|
||||
// Detect languages from tab labels
|
||||
if (node.type === 'tag' && node.tag === 'tab') {
|
||||
const label = node.attributes?.label
|
||||
if (label) {
|
||||
const normalizedLang = normalizeLanguage(label)
|
||||
if (normalizedLang) {
|
||||
languages.add(normalizedLang)
|
||||
}
|
||||
const normalized = normalizeLanguage(label)
|
||||
if (normalized) languages.add(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect languages from code-snippet language attributes
|
||||
if (node.type === 'tag' && node.tag === 'code-snippet') {
|
||||
const lang = node.attributes?.language
|
||||
if (lang) {
|
||||
const normalized = normalizeLanguage(lang)
|
||||
if (normalized) languages.add(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect languages from fenced code blocks (```js, ```python, etc.)
|
||||
if (node.type === 'fence' && node.attributes?.language) {
|
||||
const normalized = normalizeLanguage(node.attributes.language)
|
||||
if (normalized) languages.add(normalized)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(languages)
|
||||
@@ -98,6 +133,70 @@ function normalizeLanguage(label) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect language from file path and title for single-language tutorials.
|
||||
* This is a fallback when no tab labels are found in the markdown.
|
||||
*/
|
||||
function detectLanguageFromPathAndTitle(relativePath, title) {
|
||||
const pathLower = relativePath.toLowerCase()
|
||||
const titleLower = (title || '').toLowerCase()
|
||||
|
||||
// Check filename suffixes like "-js.md", "-py.md"
|
||||
if (pathLower.endsWith('-js.md') || pathLower.includes('-javascript.md') || pathLower.includes('-in-javascript.md')) {
|
||||
return 'javascript'
|
||||
}
|
||||
if (pathLower.endsWith('-py.md') || pathLower.includes('-python.md') || pathLower.includes('-in-python.md')) {
|
||||
return 'python'
|
||||
}
|
||||
if (pathLower.endsWith('-java.md') || pathLower.includes('-in-java.md')) {
|
||||
return 'java'
|
||||
}
|
||||
if (pathLower.endsWith('-go.md') || pathLower.includes('-in-go.md') || pathLower.includes('-golang.md')) {
|
||||
return 'go'
|
||||
}
|
||||
if (pathLower.endsWith('-php.md') || pathLower.includes('-in-php.md')) {
|
||||
return 'php'
|
||||
}
|
||||
|
||||
// Check title for language indicators
|
||||
if (titleLower.includes('javascript') || titleLower.includes(' js ') || titleLower.endsWith(' js')) {
|
||||
return 'javascript'
|
||||
}
|
||||
if (titleLower.includes('python')) {
|
||||
return 'python'
|
||||
}
|
||||
if (titleLower.includes('java') && !titleLower.includes('javascript')) {
|
||||
return 'java'
|
||||
}
|
||||
if (titleLower.includes('golang') || (titleLower.includes(' go ') || titleLower.endsWith(' go') || titleLower.includes('using go'))) {
|
||||
return 'go'
|
||||
}
|
||||
if (titleLower.includes('php')) {
|
||||
return 'php'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const EXIT = Symbol('Exit visitor')
|
||||
|
||||
/**
|
||||
* Extract the first heading from the markdown AST
|
||||
*/
|
||||
function extractFirstHeading(ast) {
|
||||
let heading = null
|
||||
|
||||
visit(ast, (node) => {
|
||||
if (!isNode(node)) return
|
||||
if (node.type === 'heading') {
|
||||
heading = getInnerText([node])
|
||||
return EXIT
|
||||
}
|
||||
})
|
||||
|
||||
return heading
|
||||
}
|
||||
|
||||
function isNode(value) {
|
||||
return !!(value?.$$mdtype === 'Node')
|
||||
}
|
||||
@@ -105,14 +204,16 @@ function isNode(value) {
|
||||
function visit(node, visitor) {
|
||||
if (!node) return
|
||||
|
||||
visitor(node)
|
||||
const res = visitor(node)
|
||||
if (res === EXIT) return res
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (!child || typeof child === 'string') {
|
||||
continue
|
||||
}
|
||||
visit(child, visitor)
|
||||
const res = visit(child, visitor)
|
||||
if (res === EXIT) return res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
207
@theme/plugins/tutorial-metadata.js
Normal file
207
@theme/plugins/tutorial-metadata.js
Normal file
@@ -0,0 +1,207 @@
|
||||
// @ts-check
|
||||
|
||||
import { getInnerText } from '@redocly/realm/dist/markdoc/helpers/get-inner-text.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = resolve(__dirname, '../..');
|
||||
|
||||
/**
|
||||
* Plugin to extract tutorial metadata including last modified dates.
|
||||
* Uses Redocly's built-in git integration for dates (same as "Last updated" display).
|
||||
* Only includes tutorials that appear in the sidebar navigation (sidebars.yaml).
|
||||
* This creates shared data for displaying "What's New" tutorials and
|
||||
* auto-generating tutorial sections on the landing page.
|
||||
*/
|
||||
export function tutorialMetadata() {
|
||||
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
|
||||
const instance = {
|
||||
processContent: async (actions, { fs, cache }) => {
|
||||
try {
|
||||
// Extract tutorial paths and categories from sidebars.yaml.
|
||||
// Only tutorials present in the sidebar are included.
|
||||
const { pageCategory, categories } = extractSidebarData();
|
||||
|
||||
/** @type {Array<{path: string, title: string, description: string, lastModified: string, category: string}>} */
|
||||
const tutorials = [];
|
||||
const allFiles = await fs.scan();
|
||||
|
||||
// Find all markdown files in tutorials directory
|
||||
const tutorialFiles = allFiles.filter((file) =>
|
||||
file.relativePath.match(/^docs[\/\\]tutorials[\/\\].*\.md$/)
|
||||
);
|
||||
|
||||
for (const { relativePath } of tutorialFiles) {
|
||||
try {
|
||||
// Skip tutorials not present in sidebar navigation
|
||||
const category = pageCategory.get(relativePath);
|
||||
if (!category) continue;
|
||||
|
||||
const { data: { ast } } = await cache.load(relativePath, 'markdown-ast');
|
||||
const { data: { frontmatter } } = await cache.load(relativePath, 'markdown-frontmatter');
|
||||
|
||||
// Get last modified date using Redocly's built-in git integration
|
||||
const lastModified = await fs.getLastModified(relativePath);
|
||||
if (!lastModified) continue; // Skip files without dates
|
||||
|
||||
// Extract title from first heading
|
||||
const title = extractFirstHeading(ast) || '';
|
||||
if (!title) continue;
|
||||
|
||||
// Get description from frontmatter or first paragraph
|
||||
const description = frontmatter?.seo?.description || '';
|
||||
|
||||
// Convert file path to URL path
|
||||
const urlPath = '/' + relativePath
|
||||
.replace(/[\/\\]index\.md$/, '/')
|
||||
.replace(/\.md$/, '/')
|
||||
.replace(/\\/g, '/');
|
||||
|
||||
tutorials.push({
|
||||
path: urlPath,
|
||||
title,
|
||||
description,
|
||||
lastModified,
|
||||
category,
|
||||
});
|
||||
} catch (err) {
|
||||
continue; // Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by last modified date (newest first) for "What's New"
|
||||
tutorials.sort((a, b) =>
|
||||
new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
||||
);
|
||||
|
||||
// Create shared data including sidebar-derived categories
|
||||
actions.createSharedData('tutorial-metadata', { tutorials, categories });
|
||||
actions.addRouteSharedData('/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
|
||||
actions.addRouteSharedData('/ja/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
|
||||
actions.addRouteSharedData('/es-es/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
|
||||
} catch (e) {
|
||||
console.log('[tutorial-metadata] Error:', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the first heading from the markdown AST
|
||||
*/
|
||||
function extractFirstHeading(ast) {
|
||||
let heading = null;
|
||||
|
||||
visit(ast, (node) => {
|
||||
if (!isNode(node)) return;
|
||||
if (node.type === 'heading') {
|
||||
heading = getInnerText([node]);
|
||||
return EXIT;
|
||||
}
|
||||
});
|
||||
|
||||
return heading;
|
||||
}
|
||||
|
||||
function isNode(value) {
|
||||
return !!(value?.$$mdtype === 'Node');
|
||||
}
|
||||
|
||||
const EXIT = Symbol('Exit visitor');
|
||||
|
||||
function visit(node, visitor) {
|
||||
if (!node) return;
|
||||
|
||||
const res = visitor(node);
|
||||
if (res === EXIT) return res;
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (!child || typeof child === 'string') continue;
|
||||
const res = visit(child, visitor);
|
||||
if (res === EXIT) return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tutorial page paths and categories from sidebars.yaml.
|
||||
*
|
||||
* Returns:
|
||||
* - pageCategory: Map of relativePath to category id (slug)
|
||||
* - categories: Array of { id, title } in sidebar display order
|
||||
*
|
||||
* Top-level groups under the tutorials section become categories.
|
||||
* Pages not inside a group (e.g. public-servers.md) are skipped.
|
||||
*/
|
||||
function extractSidebarData() {
|
||||
/** @type {Map<string, string>} */
|
||||
const pageCategory = new Map();
|
||||
/** @type {Array<{id: string, title: string}>} */
|
||||
const categories = [];
|
||||
|
||||
try {
|
||||
const content = readFileSync(resolve(PROJECT_ROOT, 'sidebars.yaml'), 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let inTutorials = false;
|
||||
let entryIndent = -1; // indent of the tutorials entry itself
|
||||
let topItemIndent = -1; // indent of direct children (groups/pages)
|
||||
let currentCategory = null; // current top-level group { id, title }
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
const indent = line.search(/\S/);
|
||||
|
||||
// Detect the tutorials section
|
||||
if (trimmed.includes('page: docs/tutorials/index.page.tsx')) {
|
||||
inTutorials = true;
|
||||
entryIndent = indent;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inTutorials) continue;
|
||||
|
||||
// Exit tutorials when we reach a sibling entry at the same indent
|
||||
if (indent <= entryIndent && trimmed.startsWith('- ')) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Detect the indent of top-level items (first `- ` under tutorials items)
|
||||
if (topItemIndent === -1 && trimmed.startsWith('- ')) {
|
||||
topItemIndent = indent;
|
||||
}
|
||||
|
||||
// Top-level group - start a new category
|
||||
if (indent === topItemIndent && trimmed.startsWith('- group:')) {
|
||||
const title = trimmed.replace('- group:', '').trim();
|
||||
const id = title.toLowerCase().replace(/\s+/g, '-');
|
||||
currentCategory = { id, title };
|
||||
categories.push(currentCategory);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Top-level page (no group, e.g. public-servers.md) - reset current category
|
||||
if (indent === topItemIndent && trimmed.startsWith('- page:')) {
|
||||
currentCategory = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Nested page under a group - assign to current category
|
||||
if (currentCategory) {
|
||||
const pageMatch = trimmed.match(/^- page:\s+(docs\/tutorials\/\S+\.md)/);
|
||||
if (pageMatch) {
|
||||
pageCategory.set(pageMatch[1], currentCategory.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[tutorial-metadata] Warning: Could not read sidebars.yaml:', String(err));
|
||||
}
|
||||
|
||||
return { pageCategory, categories };
|
||||
}
|
||||
@@ -183,9 +183,10 @@ ul.nav.navbar-nav {
|
||||
--link-color-visited: white;
|
||||
--link-visited-decoration: underline;
|
||||
|
||||
--bg-color: var(--color-gray-10);
|
||||
--bg-color: var(--color-gray-1); /* was --color-gray-10 */
|
||||
--bg-color-raised: var(--color-gray-2);
|
||||
--background-color: var(--bg-color);
|
||||
|
||||
|
||||
--font-family-base: 'Work Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
--heading-font-family: var(--font-family-base);
|
||||
@@ -233,6 +234,7 @@ ul.nav.navbar-nav {
|
||||
--footer-title-text-color: var(--color-gray-6);
|
||||
|
||||
--menu-item-padding-horizontal: 0px;
|
||||
--menu-item-bg-color-active: var(--color-gray-2);
|
||||
|
||||
--md-list-left-padding: 40px;
|
||||
--md-list-margin: 0 0 20px 0;
|
||||
@@ -261,7 +263,9 @@ ul.nav.navbar-nav {
|
||||
--code-block-bg-color: var(--color-gray-8);
|
||||
--code-block-controls-bg-color: var(--color-gray-8);
|
||||
--code-block-controls-border: none;
|
||||
|
||||
--md-tabs-active-tab-bg-color: var(--color-gray-7);
|
||||
--md-tabs-hover-tab-bg-color: var(--color-gray-8);
|
||||
|
||||
--inline-code-bg-color: var(--color-gray-8);
|
||||
|
||||
@@ -273,11 +277,15 @@ ul.nav.navbar-nav {
|
||||
--language-picker-background-color: var(--color-gray-8);
|
||||
--select-list-bg-color: var(--color-gray-8);
|
||||
|
||||
--button-bg-color-secondary: var(--color-gray-8);
|
||||
|
||||
--footer-title-text-color: black;
|
||||
--bg-color: var(--color-gray-9);
|
||||
--bg-color-raised: var(--color-gray-8);
|
||||
--button-content-color-link: black;
|
||||
|
||||
--menu-item-bg-color-active: var(--color-gray-8);
|
||||
|
||||
--md-table-header-bg-color: var(--color-gray-8);
|
||||
--md-table-border-color: var(--color-gray-8);
|
||||
|
||||
@@ -285,6 +293,7 @@ ul.nav.navbar-nav {
|
||||
--code-panel-bg-color: var(--color-blue-7);
|
||||
--layer-color-hover: var(--color-gray-9);
|
||||
|
||||
|
||||
--code-block-text-color: var(--color-gray-1);
|
||||
--code-block-tokens-comment-color: var(--color-gray-4);
|
||||
--code-block-tokens-constant-color: var(--color-gray-1);
|
||||
@@ -318,6 +327,9 @@ ul.nav.navbar-nav {
|
||||
--code-block-tokens-keyword-color: var(--color-magenta-8);
|
||||
--code-block-tokens-string-color: var(--color-blue-8);
|
||||
|
||||
--md-tabs-active-tab-bg-color: var(--color-gray-4);
|
||||
--md-tabs-hover-tab-bg-color: var(--color-gray-3);
|
||||
|
||||
--code-panel-bg-color: var(--color-blue-3);
|
||||
--layer-color-hover: var(--color-gray-3);
|
||||
--bg-raised-gradient: "";
|
||||
@@ -327,6 +339,12 @@ ul.nav.navbar-nav {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Keep active tab visually static on hover */
|
||||
button[role="tab"].active {
|
||||
--md-tabs-hover-tab-bg-color: var(--md-tabs-active-tab-bg-color);
|
||||
--md-tabs-hover-tab-text-color: var(--md-tabs-active-tab-text-color);
|
||||
}
|
||||
|
||||
/* Fix unnecessary horizontal scrolling of tables in Japanese */
|
||||
[lang="ja"] table.md {
|
||||
word-break: break-word;
|
||||
|
||||
1
_code-samples/airgapped-wallet/js/.gitignore
vendored
1
_code-samples/airgapped-wallet/js/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
Wallet/
|
||||
@@ -1,92 +0,0 @@
|
||||
# Airgapped Wallet
|
||||
Airgapped describes a state where a device or a system becomes fully disconnected from other devices and systems. It is the maximum protection for a system against unwanted visitors/viruses, this allows any sensitive data like a private key to be stored without worry of it being compromised as long as reasonable security practices are being practiced.
|
||||
|
||||
This airgapped XRP wallet allows users to sign a Payment transaction in a secure environment without the private key being exposed to a machine connected to the internet. The private key and seed is encrypted by password and stored securely.
|
||||
|
||||
*Note*: You should not use this airgapped wallet in production, it should only be used for educational purposes only.
|
||||
|
||||
This code sample consists of 2 parts:
|
||||
|
||||
- `airgapped-wallet.js` - This code should be stored in a standalone airgapped machine, it consist of features to generate a wallet, store a keypair securely, sign a transaction and share the signed transaction via QR code.
|
||||
- `relay-transaction.js` - This code could be stored in any online machine, no credentials is stored on this code other than a signed transaction which would be sent to an XRPL node for it to be validated on the ledger.
|
||||
|
||||
Preferably, `airgapped-wallet.js` should be on a Linux machine while `relay-transaction.js` could be on any operating system.
|
||||
|
||||
# Security Practices
|
||||
Strongly note that an airgapped system's security is not determined by its code alone but the security practices that are being followed by an operator.
|
||||
|
||||
There are channels that can be maliciously used by outside parties to infiltrate an airgapped system and steal sensitive information.
|
||||
|
||||
There are other ways malware could interact across airgapped networks, but they all involve an infected USB drive or a similar device introducing malware onto the airgapped machine. They could also involve a person physically accessing the computer, compromising it and installing malware or modifying its hardware.
|
||||
|
||||
This is why it is also recommended to encrypt sensitive information being stored in an airgapped machine.
|
||||
|
||||
The airgapped machine should have a few rules enforced to close any possible channels getting abused to leak information outside of the machine:
|
||||
|
||||
### Wifi
|
||||
|
||||
- Disable any wireless networking hardware on the airgapped machine. For example, if you have a desktop PC with a Wifi card, open the PC and remove the Wifi hardware. If you cannot do that, you could go to the system’s BIOS or UEFI firmware and disable the Wifi hardware.
|
||||
|
||||
### BlueTooth
|
||||
|
||||
- BlueTooth can be maliciously used by neighboring devices to steal data from an airgapped machine. It is recommended to remove or disable the BlueTooth hardware.
|
||||
|
||||
### USB
|
||||
|
||||
- The USB port can be used to transfer files in and out of the airgapped machine and this may act as a threat to an airgapped machine if the USB drive is infected with a malware. So after installing & setting up this airgapped wallet, it is highly recommended to block off all USB ports by using a USB blocker and not use them.
|
||||
|
||||
Do not reconnect the airgapped machine to a network, even when you need to transfer files! An effective airgapped machine should only serve 1 purpose, which is to store data and never open up a gateway for hackers to abuse and steal data.
|
||||
|
||||
# Tutorial
|
||||
For testing purposes, you would need to have 2 machines and 1 phone in hand to scan the QR code.
|
||||
|
||||
1. 1st machine would be airgapped, following the security practices written [here](#security-practices). It stores and manages an XRPL Wallet.
|
||||
2. 2nd machine would be a normal computer connected to the internet. It relays a signed transaction blob to a rippled node.
|
||||
3. The phone would be used to scan a QR code, which contains a signed transaction blob. The phone would transmit it to the 2nd machine.
|
||||
|
||||
The diagram below shows you the process of submitting a transaction to the XRPL:
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/87929946/197970678-2a1b7f7e-d91e-424e-915e-5ba7d34689cc.png" width=75% height=75%>
|
||||
</p>
|
||||
|
||||
# Setup
|
||||
- Machine 1 - An airgapped computer (during setup, it must be connected to the internet to download the files)
|
||||
- Machine 2 - A normal computer connected to the internet
|
||||
- Phone - A normal phone with a working camera to scan a QR
|
||||
|
||||
## Machine 1 Setup
|
||||
Since this machine will be airgapped, it is best to use Linux as the Operating System.
|
||||
|
||||
1. Clone all the files under the [`airgapped-wallet`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/_code-samples/airgapped-wallet/js) directory
|
||||
|
||||
2. Import all the modules required by running: `npm install`
|
||||
|
||||
3. Airgap the machine by following the security practices written [here](#security-practices).
|
||||
|
||||
4. Run `node airgapped-wallet.js`
|
||||
|
||||
5. Scan the QR code and fund the account using the [testnet faucet](https://test.bithomp.com/faucet/)
|
||||
|
||||
6. Re-run the script and input '1' to generate a new transaction by following the instructions.
|
||||
|
||||
7. Use your phone to scan the QR code, then to send the signed transaction to Machine 2 for submission
|
||||
|
||||
## Phone Setup
|
||||
The phone requires a working camera that is able to scan a QR code and an internet connection for it to be able to transmit the signed transaction blob to Machine 2.
|
||||
|
||||
Once you have signed a transaction in the airgapped machine, a QR code will be generated which will contain the signed transaction blob. Example:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/87929946/196018292-f210a9f2-c5f8-412e-98c1-361a72286378.png" width=20% height=20%>
|
||||
|
||||
Scan the QR code using the phone, copy it to the clipboard, and transmit it to Machine 2, which will then be sending it to a rippled node.
|
||||
|
||||
You can send a message to yourself using Discord, WhatsApp or even e-mail, then open up the message using Machine 2 to receive the signed transaction blob.
|
||||
|
||||
## Machine 2 Setup
|
||||
This machine will be used to transmit a signed transaction blob from Machine 1, it would require internet access.
|
||||
|
||||
1. Clone all the files under the [`airgapped-wallet`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/_code-samples/airgapped-wallet/js) directory
|
||||
|
||||
2. Import all the modules required by running `npm install`
|
||||
|
||||
3. Run `relay-transaction.js` and copy-and-paste the received output of Machine 1 when prompted
|
||||
@@ -1,223 +0,0 @@
|
||||
const crypto = require("crypto")
|
||||
const fs = require('fs')
|
||||
const fernet = require("fernet");
|
||||
const open = require('open');
|
||||
const path = require('path')
|
||||
const prompt = require('prompt')
|
||||
const { generateSeed, deriveAddress, deriveKeypair } = require("ripple-keypairs/dist/")
|
||||
const QRCode = require('qrcode')
|
||||
const xrpl = require('xrpl')
|
||||
|
||||
const demoAccountSeed = 'sskwYQmxT7SA37ceRaGXA5PhQYrDS'
|
||||
const demoAccountAddress = 'rEDd3Wy76Ta1WqfDP2DcnBKHu31SpSiUQrS'
|
||||
|
||||
const demoDestinationSeed = 'sEdVokfq7fVXXjZTii2WhtpqGbJni6s'
|
||||
const demoDestinationAddress = 'rBgNowfkmPczhMjHRYnBPsuSodDHWHQLdj'
|
||||
|
||||
const FEE = '12'
|
||||
const LEDGER_OFFSET = 300
|
||||
const WALLET_DIR = 'Wallet'
|
||||
|
||||
/**
|
||||
* Generates a new (unfunded) wallet
|
||||
*
|
||||
* @returns {{address: *, seed: *}}
|
||||
*/
|
||||
createWallet = function () {
|
||||
const seed = generateSeed()
|
||||
const {publicKey, privateKey} = deriveKeypair(seed)
|
||||
const address = deriveAddress(publicKey)
|
||||
|
||||
console.log(
|
||||
"XRP Wallet Credentials " +
|
||||
"Wallet Address: " + address +
|
||||
"Seed: " + seed
|
||||
)
|
||||
|
||||
return {address, seed}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs transaction and returns signed transaction blob in QR code
|
||||
*
|
||||
* @param xrpAmount
|
||||
* @param destination
|
||||
* @param ledgerSequence
|
||||
* @param walletSequence
|
||||
* @param password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
signTransaction = async function (xrpAmount, destination, ledgerSequence, walletSequence, password) {
|
||||
|
||||
const salt = fs.readFileSync(path.join(__dirname, WALLET_DIR , 'salt.txt')).toString()
|
||||
|
||||
const encodedSeed = fs.readFileSync(path.join(__dirname, WALLET_DIR , 'seed.txt')).toString()
|
||||
|
||||
// Hashing salted password using Password-Based Key Derivation Function 2
|
||||
const derivedKey = crypto.pbkdf2Sync(password, salt, 1000, 32, 'sha256')
|
||||
|
||||
// Generate a Fernet secret we can use for symmetric encryption
|
||||
const secret = new fernet.Secret(derivedKey.toString('base64'));
|
||||
|
||||
// Generate decryption token
|
||||
const token = new fernet.Token({
|
||||
secret: secret,
|
||||
token: encodedSeed,
|
||||
ttl: 0
|
||||
})
|
||||
const seed = token.decode();
|
||||
|
||||
const wallet = xrpl.Wallet.fromSeed(seed)
|
||||
|
||||
const paymentTx = {
|
||||
'TransactionType': 'Payment',
|
||||
'Account': wallet.classicAddress,
|
||||
'Amount': xrpl.xrpToDrops(xrpAmount),
|
||||
'Destination': destination
|
||||
}
|
||||
|
||||
// Normally we would fetch certain needed values like Fee,
|
||||
// LastLedgerSequence snd programmatically, like so:
|
||||
//
|
||||
// const preparedTx = await client.autofill(paymentTx)
|
||||
//
|
||||
// But since this is an airgapped wallet without internet
|
||||
// connection, we have to do it manually:
|
||||
//
|
||||
// paymentTx.Sequence is set in setNextValidSequenceNumber() via sugar/autofill
|
||||
// paymentTx.LastLedgerSequence is set in setLatestValidatedLedgerSequence() via sugar/autofill
|
||||
// paymentTx.Fee is set in getFeeXrp() via sugar/getFeeXrp
|
||||
|
||||
paymentTx.Sequence = walletSequence
|
||||
paymentTx.LastLedgerSequence = ledgerSequence + LEDGER_OFFSET
|
||||
paymentTx.Fee = FEE
|
||||
|
||||
const signedTx = wallet.sign(paymentTx)
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, WALLET_DIR , 'tx_blob.txt'), signedTx.tx_blob)
|
||||
QRCode.toFile(path.join(__dirname, WALLET_DIR , 'tx_blob.png'), signedTx.tx_blob)
|
||||
|
||||
open(path.join(__dirname, WALLET_DIR , 'tx_blob.png'))
|
||||
}
|
||||
|
||||
main = async function () {
|
||||
|
||||
if (!fs.existsSync(WALLET_DIR )) {
|
||||
// Create Wallet directory in case it does not exist yet
|
||||
fs.mkdirSync(path.join(__dirname, WALLET_DIR ));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(__dirname, WALLET_DIR , 'address.txt'))) {
|
||||
// Generate a new (unfunded) Wallet
|
||||
const {address, seed} = createWallet()
|
||||
|
||||
prompt.start();
|
||||
|
||||
const {password} = await prompt.get([{
|
||||
name: 'password',
|
||||
description: 'Creating a brand new Wallet, please enter a new password \n Enter Password:',
|
||||
type: 'string',
|
||||
required: true
|
||||
}])
|
||||
|
||||
prompt.stop();
|
||||
|
||||
const salt = crypto.randomBytes(20).toString('hex')
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, WALLET_DIR , 'salt.txt'), salt);
|
||||
|
||||
// Hashing salted password using Password-Based Key Derivation Function 2
|
||||
const derivedKey = crypto.pbkdf2Sync(password, salt, 1000, 32, 'sha256')
|
||||
|
||||
// Generate a Fernet secret we can use for symmetric encryption
|
||||
const secret = new fernet.Secret(derivedKey.toString('base64'));
|
||||
|
||||
// Generate encryption token with secret, time and initialization vector
|
||||
// In a real-world use case we would have current time and a random IV,
|
||||
// but for demo purposes being deterministic is just fine
|
||||
const token = new fernet.Token({
|
||||
secret: secret,
|
||||
time: Date.parse(1),
|
||||
iv: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||
})
|
||||
|
||||
const privateKey = token.encode(seed)
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, WALLET_DIR , 'seed.txt'), privateKey)
|
||||
fs.writeFileSync(path.join(__dirname, WALLET_DIR , 'address.txt'), address)
|
||||
QRCode.toFile(path.join(__dirname, WALLET_DIR , 'address.png'), address)
|
||||
|
||||
console.log(''
|
||||
+ 'Finished generating an account.\n'
|
||||
+ 'Wallet Address: ' + address + '\n'
|
||||
+ 'Please scan the QR code on your phone and use https://test.bithomp.com/faucet/ to fund the account.\n'
|
||||
+ 'After that, you\'re able to sign transactions and transmit them to Machine 2 (online machine).')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
prompt.start();
|
||||
|
||||
console.log(''
|
||||
+ '1. Transact XRP.\n'
|
||||
+ '2. Generate an XRP wallet (read only)\n'
|
||||
+ '3. Showcase XRP Wallet Address (QR Code)\n'
|
||||
+ '4. Exit')
|
||||
|
||||
const {menu} = await prompt.get([{
|
||||
name: 'menu',
|
||||
description: 'Enter Index:',
|
||||
type: 'integer',
|
||||
required: true
|
||||
}])
|
||||
|
||||
if (menu === 1) {
|
||||
const {
|
||||
password,
|
||||
xrpAmount,
|
||||
destinationAddress,
|
||||
accountSequence,
|
||||
ledgerSequence
|
||||
} = await prompt.get([{
|
||||
name: 'password',
|
||||
description: 'Enter Password',
|
||||
type: 'string',
|
||||
required: true
|
||||
}, {
|
||||
name: 'xrpAmount',
|
||||
description: 'Enter XRP To Send',
|
||||
type: 'number',
|
||||
required: true
|
||||
}, {
|
||||
name: 'destinationAddress',
|
||||
description: 'If you just want to try it out, you can use the faucet account rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe. Enter Destination',
|
||||
type: 'string',
|
||||
required: true
|
||||
}, {
|
||||
name: 'accountSequence',
|
||||
description: 'Look up the \'Next Sequence\' for the account using test.bithomp.com and enter it',
|
||||
type: 'integer',
|
||||
required: true
|
||||
}, {
|
||||
name: 'ledgerSequence',
|
||||
description: 'Look up the latest ledger sequence on testnet.xrpl.org and enter it below!',
|
||||
type: 'integer',
|
||||
required: true
|
||||
}])
|
||||
|
||||
await signTransaction(xrpAmount, destinationAddress, ledgerSequence, accountSequence, password)
|
||||
} else if (menu === 2) {
|
||||
const {address, seed} = createWallet()
|
||||
console.log('Generated readonly Wallet (address: ' + address + ' seed: ' + seed + ')')
|
||||
} else if (menu === 3) {
|
||||
const address = fs.readFileSync(path.join(__dirname, WALLET_DIR , 'address.txt')).toString()
|
||||
console.log('Wallet Address: ' + address)
|
||||
open(path.join(__dirname, WALLET_DIR , 'address.png'))
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
prompt.stop();
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "airgapped-wallet",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fernet": "^0.4.0",
|
||||
"open": "^8.4.0",
|
||||
"pbkdf2-hmac": "^1.1.0",
|
||||
"prompt": "^1.3.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"xrpl": "^4.0.0"
|
||||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
const prompt = require('prompt')
|
||||
const xrpl = require('xrpl')
|
||||
|
||||
sendTransaction = async function (tx_blob) {
|
||||
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
|
||||
await client.connect()
|
||||
|
||||
console.log("Connected to node")
|
||||
|
||||
const tx = await client.submitAndWait(tx_blob)
|
||||
|
||||
const txHash = tx.result.hash
|
||||
const txDestination = tx.result.Destination
|
||||
const txXrpAmount = xrpl.dropsToXrp(tx.result.Amount)
|
||||
const txAccount = tx.result.Account
|
||||
|
||||
console.log("XRPL Explorer: https://testnet.xrpl.org/transactions/" + txHash)
|
||||
console.log("Transaction Hash: " + txHash)
|
||||
console.log("Transaction Destination: " + txDestination)
|
||||
console.log("XRP sent: " + txXrpAmount)
|
||||
console.log("Wallet used: " + txAccount)
|
||||
|
||||
await client.disconnect()
|
||||
}
|
||||
|
||||
main = async function () {
|
||||
const {tx_blob} = await prompt.get([{
|
||||
name: 'tx_blob',
|
||||
description: 'Set tx to \'tx_blob\' received from scanning the QR code generated by the airgapped wallet',
|
||||
type: 'string',
|
||||
required: true
|
||||
}])
|
||||
|
||||
await sendTransaction(tx_blob)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -1,115 +0,0 @@
|
||||
# Airgapped Wallet
|
||||
Airgapped describes a state where a device or a system becomes fully disconnected from other devices and systems. It is the maximum protection for a system against unwanted visitors/viruses, this allows any sensitive data like a private key to be stored without worry of it being compromised as long as reasonable security practices are being practiced.
|
||||
|
||||
This airgapped XRP wallet allows users to sign a Payment transaction in a secure environment without the private key being exposed to a machine connected to the internet. The private key and seed is encrypted by password and stored securely.
|
||||
|
||||
*Note*: You should not use this airgapped wallet in production, it should only be used for educational purposes only.
|
||||
|
||||
This code sample consists of 2 parts:
|
||||
|
||||
- `airgapped-wallet.py` - This code should be stored in a standalone airgapped machine, it consist of features to generate a wallet, store a keypair securely, sign a transaction and share the signed transaction via QR code.
|
||||
- `relay-transaction.py` - This code could be stored in any online machine, no credentials is stored on this code other than a signed transaction which would be sent to an XRPL node for it to be validated on the ledger.
|
||||
|
||||
Preferably, `airgapped-wallet.py` should be on a Linux machine while `relay-transaction.py` could be on any operating system.
|
||||
|
||||
# Security Practices
|
||||
Strongly note that an airgapped system's security is not determined by its code alone but the security practices that are being followed by an operator.
|
||||
|
||||
There are channels that can be maliciously used by outside parties to infiltrate an airgapped system and steal sensitive information.
|
||||
|
||||
There are other ways malware could interact across airgapped networks, but they all involve an infected USB drive or a similar device introducing malware onto the airgapped machine. They could also involve a person physically accessing the computer, compromising it and installing malware or modifying its hardware.
|
||||
|
||||
This is why it is also recommended to encrypt sensitive information being stored in an airgapped machine.
|
||||
|
||||
The airgapped machine should have a few rules enforced to close any possible channels getting abused to leak information outside of the machine:
|
||||
### Wifi
|
||||
|
||||
- Disable any wireless networking hardware on the airgapped machine. For example, if you have a desktop PC with a Wifi card, open the PC and remove the Wifi hardware. If you cannot do that, you could go to the system’s BIOS or UEFI firmware and disable the Wifi hardware.
|
||||
|
||||
### BlueTooth
|
||||
|
||||
- BlueTooth can be maliciously used by neighboring devices to steal data from an airgapped machine. It is recommended to remove or disable the BlueTooth hardware.
|
||||
|
||||
### USB
|
||||
|
||||
- The USB port can be used to transfer files in and out of the airgapped machine and this may act as a threat to an airgapped machine if the USB drive is infected with a malware. So after installing & setting up this airgapped wallet, it is highly recommended to block off all USB ports by using a USB blocker and not use them.
|
||||
|
||||
Do not reconnect the airgapped machine to a network, even when you need to transfer files! An effective airgapped machine should only serve 1 purpose, which is to store data and never open up a gateway for hackers to abuse and steal data.
|
||||
|
||||
# Tutorial
|
||||
For testing purposes, you would need to have 2 machines and 1 phone in hand to scan the QR code.
|
||||
|
||||
1. 1st machine would be airgapped, following the security practices written [here](#security-practices). It stores and manages an XRPL Wallet.
|
||||
2. 2nd machine would be a normal computer connected to the internet. It relays a signed transaction blob to a rippled node.
|
||||
3. The phone would be used to scan a QR code, which contains a signed transaction blob. The phone would transmit it to the 2nd machine.
|
||||
|
||||
The diagram below shows you the process of submitting a transaction to the XRPL:
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/87929946/197970678-2a1b7f7e-d91e-424e-915e-5ba7d34689cc.png" width=75% height=75%>
|
||||
</p>
|
||||
|
||||
# Setup
|
||||
- Machine 1 - An airgapped computer (during setup, it must be connected to the internet to download the files)
|
||||
- Machine 2 - A normal computer connected to the internet
|
||||
- Phone - A normal phone with a working camera to scan a QR
|
||||
|
||||
## Machine 1 Setup
|
||||
Since this machine will be airgapped, it is best to use Linux as the Operating System.
|
||||
|
||||
1. Install Python 3.8:
|
||||
|
||||
**Linux Command Line**:
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3.8 python3-pip
|
||||
```
|
||||
**Website**: https://www.python.org/downloads/source/
|
||||
|
||||
2. Clone all the files under the [`airgapped-wallet`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/_code-samples/airgapped-wallet/py) directory
|
||||
|
||||
3. Import all the modules required by running:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. Airgap the machine by following the security practices written [here](#security-practices).
|
||||
|
||||
5. Run `airgapped-wallet.py`
|
||||
|
||||
6. Scan the QR code and fund the account using the [testnet faucet](https://test.bithomp.com/faucet/)
|
||||
|
||||
7. Re-run the script and input '1' to generate a new transaction by following the instructions.
|
||||
|
||||
8. Use your phone to scan the QR code, then to send the signed transaction to Machine 2 for submission
|
||||
|
||||
## Machine 2 Setup
|
||||
This machine will be used to transmit a signed transaction blob from Machine 1, it would require internet access.
|
||||
|
||||
1. Install Python 3.8
|
||||
|
||||
**Linux Command Line**:
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3.8 python3-pip
|
||||
```
|
||||
**Website**: https://www.python.org/downloads/source/
|
||||
|
||||
2. Clone all the files under the [`airgapped-wallet`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/_code-samples/airgapped-wallet/py) directory
|
||||
|
||||
3. Import all the modules required by running:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. Run `relay-transaction.py` with one argument, the signed transaction blob to submit.
|
||||
|
||||
## Phone Setup
|
||||
The phone requires a working camera that is able to scan a QR code and an internet connection for it to be able to transmit the signed transaction blob to Machine 2.
|
||||
|
||||
Once you have signed a transaction in the airgapped machine, a QR code will be generated which will contain the signed transaction blob. Example:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/87929946/196018292-f210a9f2-c5f8-412e-98c1-361a72286378.png" width=20% height=20%>
|
||||
|
||||
Scan the QR code using the phone and transmit it to Machine 2, which will then be sending it to a rippled node.
|
||||
|
||||
You can send a message to yourself using Discord, WhatsApp or even e-mail, then open up the message using Machine 2 to receive the signed transaction blob.
|
||||
@@ -1,223 +0,0 @@
|
||||
import os
|
||||
import base64
|
||||
import qrcode
|
||||
import platform
|
||||
from PIL import Image
|
||||
from pathlib import Path, PureWindowsPath, PurePath
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from xrpl.core import keypairs
|
||||
from xrpl.utils import xrp_to_drops
|
||||
from xrpl.models.transactions import Payment
|
||||
from xrpl.transaction import sign
|
||||
from xrpl.wallet.main import Wallet
|
||||
|
||||
|
||||
def create_wallet(silent: False):
|
||||
"""
|
||||
Generates a keypair
|
||||
"""
|
||||
if not silent:
|
||||
print("1. Generating seed...")
|
||||
seed = keypairs.generate_seed()
|
||||
|
||||
print("2. Deriving keypair from seed...")
|
||||
pub, priv = keypairs.derive_keypair(seed)
|
||||
|
||||
print("3. Deriving classic addresses from keypair..\n")
|
||||
address = keypairs.derive_classic_address(pub)
|
||||
|
||||
else:
|
||||
seed = keypairs.generate_seed()
|
||||
pub, priv = keypairs.derive_keypair(seed)
|
||||
address = keypairs.derive_classic_address(pub)
|
||||
|
||||
return address, seed
|
||||
|
||||
|
||||
def sign_transaction(xrp_amount, destination, ledger_seq, wallet_seq, password):
|
||||
"""
|
||||
Signs transaction and returns signed transaction blob in QR code
|
||||
"""
|
||||
print("1. Retrieving encrypted private key and salt...")
|
||||
with open(get_path("/WalletTEST/private.txt"), "r") as f:
|
||||
seed = f.read()
|
||||
seed = bytes.fromhex(seed)
|
||||
|
||||
with open(get_path("/WalletTEST/salt.txt"), "rb") as f:
|
||||
salt = f.read()
|
||||
|
||||
print("2. Initializing key...")
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
iterations=100000,
|
||||
salt=salt
|
||||
)
|
||||
|
||||
key = base64.urlsafe_b64encode(kdf.derive(bytes(password.encode())))
|
||||
crypt = Fernet(key)
|
||||
|
||||
print("3. Decrypting wallet's private key using password")
|
||||
seed = crypt.decrypt(seed)
|
||||
|
||||
print("4. Initializing wallet using decrypted private key")
|
||||
_wallet = Wallet.from_seed(seed=seed.decode())
|
||||
|
||||
validated_seq = ledger_seq
|
||||
|
||||
print("5. Constructing payment transaction...")
|
||||
my_tx_payment = Payment(
|
||||
account=_wallet.address,
|
||||
amount=xrp_to_drops(xrp=xrp_amount),
|
||||
destination=destination,
|
||||
last_ledger_sequence=validated_seq + 100,
|
||||
# +100 to catch up with the ledger when we transmit the signed tx blob to Machine 2
|
||||
sequence=wallet_seq,
|
||||
fee="10"
|
||||
)
|
||||
|
||||
print("6. Signing transaction...")
|
||||
my_tx_payment_signed = sign(transaction=my_tx_payment, wallet=_wallet)
|
||||
|
||||
img = qrcode.make(my_tx_payment_signed.to_dict())
|
||||
|
||||
print("7. Displaying signed transaction blob's QR code on the screen...")
|
||||
img.save(get_path("/WalletTEST/transactionID.png"))
|
||||
image = Image.open(get_path("/WalletTEST/transactionID.png"))
|
||||
image.show()
|
||||
|
||||
print(f"RESULT: {my_tx_payment_signed.to_dict()}")
|
||||
print("END RESULT: Successful")
|
||||
|
||||
|
||||
def get_path(file):
|
||||
"""
|
||||
Get path (filesystem management)
|
||||
"""
|
||||
|
||||
global File_
|
||||
# Checks what OS is being used
|
||||
OS = platform.system()
|
||||
usr = Path.home()
|
||||
|
||||
# Get PATH format based on the OS
|
||||
if OS == "Windows":
|
||||
File_ = PureWindowsPath(str(usr) + file)
|
||||
else: # Assuming Linux-style file format
|
||||
File_ = PurePath(str(usr) + file)
|
||||
|
||||
return str(File_)
|
||||
|
||||
|
||||
def create_wallet_directory():
|
||||
global File, Path_
|
||||
OS = platform.system()
|
||||
usr = Path.home()
|
||||
if OS == "Windows":
|
||||
# If it's Windows, use this path:
|
||||
print("- OS Detected: Windows")
|
||||
File = PureWindowsPath(str(usr) + '/WalletTEST')
|
||||
Path_ = str(PureWindowsPath(str(usr)))
|
||||
else:
|
||||
print("- OS Detected: Linux")
|
||||
# If it's Linux, use this path:
|
||||
File = PurePath(str(usr) + '/WalletTEST')
|
||||
Path_ = str(PurePath(str(usr)))
|
||||
|
||||
if not os.path.exists(File):
|
||||
print("1. Generating wallet's keypair...")
|
||||
pub, seed = create_wallet(silent=True)
|
||||
|
||||
print("2. Creating wallet's file directory...")
|
||||
os.makedirs(File)
|
||||
|
||||
print("3. Generating and saving public key's QR code...")
|
||||
img = qrcode.make(pub)
|
||||
img.save(get_path("/WalletTEST/public.png"))
|
||||
|
||||
print("4. Generating and saving wallet's salt...")
|
||||
salt = os.urandom(16)
|
||||
|
||||
with open(get_path("/WalletTEST/salt.txt"), "wb") as f:
|
||||
f.write(salt)
|
||||
|
||||
print("5. Generating wallet's filesystem password...")
|
||||
password = "This is a unit test password 123 !@# -+= }{/"
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
iterations=100000,
|
||||
salt=salt
|
||||
)
|
||||
|
||||
key = base64.urlsafe_b64encode(kdf.derive(bytes(password.encode())))
|
||||
|
||||
crypt = Fernet(key)
|
||||
|
||||
print("6. Encrypting and saving private key by password...")
|
||||
priv = crypt.encrypt(bytes(seed, encoding='utf-8'))
|
||||
seed = crypt.encrypt(bytes(seed, encoding='utf-8'))
|
||||
|
||||
with open(get_path("/WalletTEST/seed.txt"), "w") as f:
|
||||
f.write(seed.hex())
|
||||
|
||||
with open(get_path("/WalletTEST/private.txt"), "w") as f:
|
||||
f.write(priv.hex())
|
||||
|
||||
with open(get_path("/WalletTEST/public.txt"), "w") as f:
|
||||
f.write(pub)
|
||||
|
||||
if os.path.exists(File):
|
||||
print(f"0. Wallet's filesystem already exist as the unit test has been performed before. Directory: {File}")
|
||||
|
||||
|
||||
def showcase_wallet_address_qr_code():
|
||||
with open(get_path("/WalletTEST/public.txt"), "r") as f:
|
||||
print(f"0. Wallet Address: {f.read()}")
|
||||
|
||||
__path = get_path("/WalletTEST/public.png")
|
||||
print(f"1. Getting address from {__path}...")
|
||||
print("2. Displaying QR code on the screen...")
|
||||
image = Image.open(get_path("/WalletTEST/public.png"))
|
||||
image.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Airgapped Machine Unit Test (5 functions):\n")
|
||||
|
||||
print(f"UNIT TEST 1. create_wallet():")
|
||||
_address, _seed = create_wallet(silent=False)
|
||||
print(f"-- RESULTS --\n"
|
||||
f"Address: {_address}\n"
|
||||
f"Seed: {_seed}\n"
|
||||
f"END RESULT: Successful"
|
||||
)
|
||||
|
||||
print(f"\nUNIT TEST 2. create_wallet_directory():")
|
||||
create_wallet_directory()
|
||||
print("RESULT: Successful")
|
||||
|
||||
print("\nUNIT TEST 3. showcase_wallet_address_qr_code():")
|
||||
showcase_wallet_address_qr_code()
|
||||
print("RESULT: Successful")
|
||||
|
||||
print("\nUNIT TEST 4. get_path():")
|
||||
print("1. Getting files' path...\n")
|
||||
txt_file = get_path("/WalletTEST/FILE123.txt")
|
||||
png_file = get_path("/WalletTEST/PIC321.png")
|
||||
print(f"-- RESULTS --\n"
|
||||
f"txt_file: {txt_file}\n"
|
||||
f"png_file: {png_file}\n"
|
||||
f"END RESULT: Successful")
|
||||
|
||||
print("\nUNIT TEST 5. sign_transaction():")
|
||||
print("Parameters: xrp_amount, destination, ledger_seq, wallet_seq, password")
|
||||
sign_transaction(
|
||||
xrp_amount=10,
|
||||
destination="rPEpirdT9UCNbnaZMJ4ENwKAwJqrTpvgMQ",
|
||||
ledger_seq=32602000,
|
||||
wallet_seq=32600100,
|
||||
password="This is a unit test password 123 !@# -+= }{/"
|
||||
)
|
||||
@@ -1,232 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
import base64
|
||||
import qrcode
|
||||
import platform
|
||||
from PIL import Image
|
||||
from pathlib import Path, PureWindowsPath, PurePath
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from xrpl.wallet import Wallet
|
||||
from xrpl.core import keypairs
|
||||
from xrpl.utils import xrp_to_drops
|
||||
from xrpl.models.transactions import Payment
|
||||
from xrpl.transaction import sign
|
||||
|
||||
|
||||
def create_wallet():
|
||||
"""
|
||||
Generates a keypair
|
||||
"""
|
||||
|
||||
seed = keypairs.generate_seed()
|
||||
pub, priv = keypairs.derive_keypair(seed)
|
||||
|
||||
address = keypairs.derive_classic_address(pub)
|
||||
print(
|
||||
f"\n\n XRP WALLET CREDENTIALS"
|
||||
f"\n Wallet Address: {address}"
|
||||
f"\n Seed: {seed}"
|
||||
)
|
||||
|
||||
return address, seed
|
||||
|
||||
|
||||
def sign_transaction(_xrp_amount, _destination, _ledger_seq, _wallet_seq, password):
|
||||
"""
|
||||
Signs transaction and returns signed transaction blob in QR code
|
||||
"""
|
||||
|
||||
with open(get_path("/Wallet/private.txt"), "r") as f:
|
||||
_seed = f.read()
|
||||
_seed = bytes.fromhex(_seed)
|
||||
|
||||
with open(get_path("/Wallet/salt.txt"), "rb") as f:
|
||||
salt = f.read()
|
||||
|
||||
# Line 49-58: initialize key
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
iterations=100000,
|
||||
salt=salt
|
||||
)
|
||||
|
||||
key = base64.urlsafe_b64encode(kdf.derive(bytes(password.encode())))
|
||||
crypt = Fernet(key)
|
||||
|
||||
# Decrypts the wallet's private key
|
||||
_seed = crypt.decrypt(_seed)
|
||||
_wallet = Wallet.from_seed(seed=_seed.decode())
|
||||
|
||||
validated_seq = _ledger_seq
|
||||
|
||||
# Construct Payment transaction
|
||||
my_tx_payment = Payment(
|
||||
account=_wallet.address,
|
||||
amount=xrp_to_drops(xrp=_xrp_amount),
|
||||
destination=_destination,
|
||||
last_ledger_sequence=validated_seq + 100,
|
||||
# 100 ledgers usually takes about 6 minutes, so you have about that
|
||||
# long to submit it before it expires. To give more time, increase
|
||||
# this number; for unlimited time, remove last_ledger entirely.
|
||||
sequence=_wallet_seq,
|
||||
fee="10"
|
||||
)
|
||||
|
||||
# Signs transaction and displays the signed_tx blob in QR code
|
||||
# Scan the QR code and transmit the signed_tx blob to an online machine (Machine 2) to relay it to the XRPL
|
||||
my_tx_payment_signed = sign(transaction=my_tx_payment, wallet=_wallet)
|
||||
|
||||
img = qrcode.make(my_tx_payment_signed.blob())
|
||||
img.save(get_path("/Wallet/transactionID.png"))
|
||||
image = Image.open(get_path("/Wallet/transactionID.png"))
|
||||
image.show()
|
||||
|
||||
|
||||
def get_path(file):
|
||||
"""
|
||||
Get path (filesystem management)
|
||||
"""
|
||||
|
||||
global File_
|
||||
# Checks what OS is being us
|
||||
OS = platform.system()
|
||||
usr = Path.home()
|
||||
|
||||
# Get PATH format based on the OS
|
||||
if OS == "Windows":
|
||||
File_ = PureWindowsPath(str(usr) + file)
|
||||
else: # Assuming Linux-style file format, use this path:
|
||||
File_ = PurePath(str(usr) + file)
|
||||
|
||||
return str(File_)
|
||||
|
||||
|
||||
def main():
|
||||
global File, Path_
|
||||
|
||||
# Gets the machine's operating system (OS)
|
||||
OS = platform.system()
|
||||
usr = Path.home()
|
||||
if OS == "Windows":
|
||||
# If it's Windows, use this path:
|
||||
File = PureWindowsPath(str(usr) + '/Wallet')
|
||||
Path_ = str(PureWindowsPath(str(usr)))
|
||||
else: # Assuming Linux-style file format, use this path:
|
||||
File = PurePath(str(usr) + '/Wallet')
|
||||
Path_ = str(PurePath(str(usr)))
|
||||
|
||||
# If the Wallet's folder already exists, continue on
|
||||
if os.path.exists(File) and os.path.exists(get_path("/Wallet/public.txt")):
|
||||
while True:
|
||||
try:
|
||||
ask = int(input("\n 1. Transact XRP"
|
||||
"\n 2. Generate an XRP wallet (read only)"
|
||||
"\n 3. Showcase XRP Wallet Address (QR Code)"
|
||||
"\n 4. Exit"
|
||||
"\n\n Enter Index: "
|
||||
))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if ask == 1:
|
||||
password = str(input(" Enter Password: "))
|
||||
amount = float(input("\n Enter XRP To Send: "))
|
||||
destination = input("If you just want to try it out, you can use the faucet account rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
|
||||
"\n Enter Destination: ")
|
||||
wallet_sequence = int(input("Look up the 'Next Sequence' for the account using test.bithomp.com and enter it below!"
|
||||
"\n Enter Wallet Sequence: "))
|
||||
ledger_sequence = int(input("Look up the latest ledger sequence on testnet.xrpl.org and enter it below!"
|
||||
"\n Enter Ledger Sequence: "))
|
||||
|
||||
sign_transaction(_xrp_amount=amount,
|
||||
_destination=destination,
|
||||
_ledger_seq=ledger_sequence,
|
||||
_wallet_seq=wallet_sequence,
|
||||
password=password
|
||||
)
|
||||
print("This transaction is expected to expire in ~6 minutes.")
|
||||
|
||||
del destination, amount, wallet_sequence, ledger_sequence
|
||||
|
||||
if ask == 2:
|
||||
_pub, _seed = create_wallet()
|
||||
|
||||
if ask == 3:
|
||||
with open(get_path("/Wallet/public.txt"), "r") as f:
|
||||
print(f"\n Wallet Address: {f.read()}")
|
||||
|
||||
image = Image.open(get_path("/Wallet/public.png"))
|
||||
image.show()
|
||||
|
||||
if ask == 4:
|
||||
return 0
|
||||
else:
|
||||
# If the Wallet's folder does not exist, create one and store wallet data (encrypted private key, encrypted seed, account address)
|
||||
# If the Wallet's directory exists but files are missing, delete it and generate a new wallet
|
||||
if os.path.exists(File):
|
||||
confirmation = input(f"We've detected missing files on {File}, would you like to delete your wallet's credentials & generate new wallet credentials? (YES/NO):")
|
||||
if confirmation == "YES":
|
||||
confirmation_1 = input(f"All wallet credentials will be lost if you continue, are you sure? (YES/NO): ")
|
||||
if confirmation_1 == "YES":
|
||||
shutil.rmtree(File)
|
||||
else:
|
||||
print("Aborted: Wallet credentials are still intact")
|
||||
return 0
|
||||
else:
|
||||
print("- Wallet credentials are still intact")
|
||||
return 0
|
||||
|
||||
os.makedirs(File)
|
||||
|
||||
pub, seed = create_wallet()
|
||||
|
||||
img = qrcode.make(pub)
|
||||
img.save(get_path("/Wallet/public.png"))
|
||||
|
||||
print("\nCreating a brand new Wallet, please enter a new password")
|
||||
password = str(input("\n Enter Password: "))
|
||||
salt = os.urandom(16)
|
||||
|
||||
with open(get_path("/Wallet/salt.txt"), "wb") as f:
|
||||
f.write(salt)
|
||||
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
iterations=100000,
|
||||
salt=salt
|
||||
)
|
||||
|
||||
key = base64.urlsafe_b64encode(kdf.derive(bytes(password.encode())))
|
||||
|
||||
crypt = Fernet(key)
|
||||
|
||||
priv = crypt.encrypt(bytes(seed, encoding='utf-8'))
|
||||
seed = crypt.encrypt(bytes(seed, encoding='utf-8'))
|
||||
|
||||
with open(get_path("/Wallet/seed.txt"), "w") as f:
|
||||
f.write(seed.hex())
|
||||
|
||||
with open(get_path("/Wallet/private.txt"), "w") as f:
|
||||
f.write(priv.hex())
|
||||
|
||||
with open(get_path("/Wallet/public.txt"), "w") as f:
|
||||
f.write(pub)
|
||||
|
||||
openimg = Image.open(get_path("/Wallet/public.png"))
|
||||
openimg.show()
|
||||
|
||||
print("\nFinished generating an account.")
|
||||
print(f"\nWallet Address: {pub}")
|
||||
print("\nPlease scan the QR code on your phone and use https://test.bithomp.com/faucet/ to fund the account."
|
||||
"\nAfter that, you're able to sign transactions and transmit them to Machine 2 (online machine).")
|
||||
|
||||
# Loop back to the start after setup
|
||||
main()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,53 +0,0 @@
|
||||
from xrpl.clients import JsonRpcClient
|
||||
from xrpl.models.transactions import Payment
|
||||
from xrpl.transaction import submit_and_wait
|
||||
from xrpl.utils import drops_to_xrp
|
||||
import argparse
|
||||
|
||||
def connect_node(_node):
|
||||
"""
|
||||
Connects to a node
|
||||
"""
|
||||
|
||||
JSON_RPC_URL = _node
|
||||
_client = JsonRpcClient(url=JSON_RPC_URL)
|
||||
print("\n --- Connected to Node")
|
||||
return _client
|
||||
|
||||
|
||||
def send_transaction(tx_blob):
|
||||
"""
|
||||
Connects to a node -> Send Transaction
|
||||
Main Function to send transaction to the XRPL
|
||||
"""
|
||||
|
||||
client = connect_node("https://s.altnet.rippletest.net:51234/")
|
||||
# TESTNET: "https://s.altnet.rippletest.net:51234/"
|
||||
# MAINNET: "https://s2.ripple.com:51234/"
|
||||
|
||||
tx = submit_and_wait(transaction=tx_blob, client=client)
|
||||
|
||||
tx_account = tx.result["tx_json"]["Account"]
|
||||
tx_hash = tx.result["hash"]
|
||||
tx_destination = tx.result["tx_json"]['Destination']
|
||||
delivered = tx.result["meta"]["delivered_amount"]
|
||||
if type(delivered) == str:
|
||||
tx_delivered_amount = f"{drops_to_xrp(delivered)} XRP"
|
||||
else:
|
||||
tx_delivered_amount = f"{delivered['value']} {delivered['currency']}.{delivered['issuer']}"
|
||||
|
||||
|
||||
print(f"\n XRPL Explorer: https://testnet.xrpl.org/transactions/{tx_hash}"
|
||||
f"\n Wallet Used: {tx_account}"
|
||||
f"\n Transaction Hash: {tx_hash}"
|
||||
f"\n Transaction Destination: {tx_destination}"
|
||||
f"\n Amount Delivered: {tx_delivered_amount}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = argparse.ArgumentParser(description='Submit a signed transaction blob')
|
||||
p.add_argument('blob', type=str,
|
||||
help='Transaction blob (in hexadecimal) to submit')
|
||||
tx_blob = p.parse_args().blob
|
||||
send_transaction(tx_blob)
|
||||
@@ -1,4 +0,0 @@
|
||||
cryptography==44.0.1
|
||||
Pillow==10.3.0
|
||||
qrcode==7.2
|
||||
xrpl-py>=3.0.0
|
||||
10
_code-samples/assign-regular-key/go/README.md
Normal file
10
_code-samples/assign-regular-key/go/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Assign a Regular Key (Go)
|
||||
|
||||
Demonstrates how to assign a regular key pair to an XRP Ledger account. Both WebSocket (`ws/`) and JSON-RPC (`rpc/`) examples are included.
|
||||
|
||||
Quick setup and usage:
|
||||
|
||||
```sh
|
||||
go mod tidy
|
||||
go run ./ws/main.go
|
||||
```
|
||||
@@ -1,8 +1,6 @@
|
||||
module github.com/XRPLF
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.10
|
||||
go 1.24.0
|
||||
|
||||
require github.com/Peersyst/xrpl-go v0.1.11
|
||||
|
||||
@@ -20,5 +18,5 @@ require (
|
||||
github.com/tyler-smith/go-bip32 v1.0.0 // indirect
|
||||
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
)
|
||||
|
||||
@@ -46,8 +46,8 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
|
||||
golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
3
_code-samples/auction-slot/README.md
Normal file
3
_code-samples/auction-slot/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Use AMM Auction Slot to Save on Fees
|
||||
|
||||
Estimate the fees that would be paid for trading through an AMM and use the auction slot to save on fees if applicable.
|
||||
@@ -1,4 +1,4 @@
|
||||
const BigNumber = require('bignumber.js')
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
/* Convert a trading fee to a value that can be multiplied
|
||||
* by a total to "subtract" the fee from the total.
|
||||
@@ -50,7 +50,7 @@ function feeDecimal(tFee) {
|
||||
* theoretical input to the pool, it should be rounded
|
||||
* up (ceiling) to preserve the pool's constant product.
|
||||
*/
|
||||
function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) {
|
||||
export function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) {
|
||||
return ( ( pool_in_bn.multipliedBy(pool_out_bn) ).dividedBy(
|
||||
pool_out_bn.minus(asset_out_bn)
|
||||
).minus(pool_in_bn)
|
||||
@@ -76,7 +76,7 @@ function solveQuadraticEq(a,b,c) {
|
||||
* @param trading_fee int - The trading fee as an integer {0,1000} where 1000
|
||||
* represents a 1% fee.
|
||||
*/
|
||||
function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) {
|
||||
export function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) {
|
||||
// convert inputs to BigNumber
|
||||
const lpTokens = BigNumber(desired_lpt)
|
||||
const lptAMMBalance = BigNumber(lpt_balance)
|
||||
@@ -100,7 +100,7 @@ function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) {
|
||||
* XLS-30 section 4.1.1, but factors in the increase in the minimum bid as a
|
||||
* result of having new LP Tokens issued to you from your deposit.
|
||||
*/
|
||||
function auctionDeposit(old_bid, time_interval, trading_fee, lpt_balance) {
|
||||
export function auctionDeposit(old_bid, time_interval, trading_fee, lpt_balance) {
|
||||
const tfee_decimal = feeDecimal(trading_fee)
|
||||
const lptokens = BigNumber(lpt_balance)
|
||||
const b = BigNumber(old_bid)
|
||||
@@ -133,7 +133,7 @@ function auctionDeposit(old_bid, time_interval, trading_fee, lpt_balance) {
|
||||
*
|
||||
* @returns BigNumber - the minimum amount of LP tokens to win the auction slot
|
||||
*/
|
||||
function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) {
|
||||
export function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) {
|
||||
const tfee_decimal = feeDecimal(trading_fee)
|
||||
const lptokens = BigNumber(lpt_balance)
|
||||
const min_bid = lptokens.multipliedBy(tfee_decimal).dividedBy(25)
|
||||
@@ -154,10 +154,3 @@ function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) {
|
||||
).minus(lptokens).precision(15, BigNumber.FLOOR)
|
||||
return rounded_bid
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
"auctionDeposit": auctionDeposit,
|
||||
"auctionPrice": auctionPrice,
|
||||
"ammAssetIn": ammAssetIn,
|
||||
"swapOut": swapOut,
|
||||
}
|
||||
|
||||
@@ -1,170 +1,165 @@
|
||||
const xrpl = require('xrpl')
|
||||
const BigNumber = require('bignumber.js')
|
||||
const {auctionDeposit, ammAssetIn, swapOut} = require("./amm-formulas.js")
|
||||
import xrpl from 'xrpl'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { auctionDeposit, ammAssetIn, swapOut } from "./amm-formulas.js"
|
||||
|
||||
async function main() {
|
||||
// Connect ----------------------------------------------------------------
|
||||
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
|
||||
console.log("Connecting to Testnet...")
|
||||
await client.connect()
|
||||
|
||||
// // Get credentials from the faucet -------------------------------------
|
||||
console.log("Requesting test XRP from the faucet...")
|
||||
const wallet = (await client.fundWallet()).wallet
|
||||
console.log(`Got address ${wallet.address} / seed ${wallet.seed}.`)
|
||||
// Connect and get account ----------------------------------------------------
|
||||
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
|
||||
console.log("Connecting to Testnet...")
|
||||
await client.connect()
|
||||
|
||||
// Look up AMM status -----------------------------------------------------
|
||||
const from_asset = {
|
||||
"currency": "XRP"
|
||||
}
|
||||
const to_asset = {
|
||||
"currency": "TST",
|
||||
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
|
||||
}
|
||||
const amm_info = (await client.request({
|
||||
"command": "amm_info",
|
||||
"asset": from_asset,
|
||||
"asset2": to_asset
|
||||
}))
|
||||
console.dir(amm_info, {depth: null})
|
||||
const lpt = amm_info.result.amm.lp_token
|
||||
// XRP is always first if the pool is token←→XRP.
|
||||
// For a token←→token AMM, you'd need to figure out which asset is first.
|
||||
const pool_drops = amm_info.result.amm.amount
|
||||
const pool_tst = amm_info.result.amm.amount2
|
||||
const full_trading_fee = amm_info.result.amm.trading_fee
|
||||
const discounted_fee = amm_info.result.amm.auction_slot.discounted_fee
|
||||
const old_bid = amm_info.result.amm.auction_slot.price.value
|
||||
const time_interval = amm_info.result.amm.auction_slot.time_interval
|
||||
console.log("Requesting test XRP from the faucet...")
|
||||
const { wallet } = await client.fundWallet()
|
||||
console.log(`Got address ${wallet.address} / seed ${wallet.seed}.`)
|
||||
|
||||
// Calculate price in XRP to get 10 TST from the AMM ----------------------
|
||||
// Note, this ignores Offers from the non-AMM part of the DEX.
|
||||
const to_amount = {
|
||||
"currency": to_asset.currency,
|
||||
"issuer": to_asset.issuer,
|
||||
"value": "10.0"
|
||||
}
|
||||
// Look up AMM status -----------------------------------------------------
|
||||
const from_asset = {
|
||||
"currency": "XRP"
|
||||
}
|
||||
const to_asset = {
|
||||
"currency": "TST",
|
||||
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
|
||||
}
|
||||
const amm_info = (await client.request({
|
||||
"command": "amm_info",
|
||||
"asset": from_asset,
|
||||
"asset2": to_asset
|
||||
}))
|
||||
console.dir(amm_info, {depth: null})
|
||||
const lpt = amm_info.result.amm.lp_token
|
||||
// XRP is always first if the pool is token←→XRP.
|
||||
// For a token←→token AMM, you'd need to figure out which asset is first.
|
||||
const pool_drops = amm_info.result.amm.amount
|
||||
const pool_tst = amm_info.result.amm.amount2
|
||||
const full_trading_fee = amm_info.result.amm.trading_fee
|
||||
const discounted_fee = amm_info.result.amm.auction_slot.discounted_fee
|
||||
const old_bid = amm_info.result.amm.auction_slot.price.value
|
||||
const time_interval = amm_info.result.amm.auction_slot.time_interval
|
||||
|
||||
// Convert values to BigNumbers with the appropriate precision.
|
||||
// Tokens always have 15 significant digits;
|
||||
// XRP is precise to integer drops, which can be as high as 10^17
|
||||
const asset_out_bn = BigNumber(to_amount.value).precision(15)
|
||||
const pool_in_bn = BigNumber(pool_drops).precision(17)
|
||||
const pool_out_bn = BigNumber(pool_tst.value).precision(15)
|
||||
// Calculate price in XRP to get 10 TST from the AMM ----------------------
|
||||
// Note, this ignores Offers from the non-AMM part of the DEX.
|
||||
const to_amount = {
|
||||
"currency": to_asset.currency,
|
||||
"issuer": to_asset.issuer,
|
||||
"value": "10.0"
|
||||
}
|
||||
|
||||
if (to_amount.value > pool_out_bn) {
|
||||
console.log(`Requested ${to_amount.value} ${to_amount.currency} ` +
|
||||
`but AMM only holds ${pool_tst.value}. Quitting.`)
|
||||
client.disconnect()
|
||||
return
|
||||
}
|
||||
// Convert values to BigNumbers with the appropriate precision.
|
||||
// Tokens always have 15 significant digits;
|
||||
// XRP is precise to integer drops, which can be as high as 10^17
|
||||
const asset_out_bn = BigNumber(to_amount.value).precision(15)
|
||||
const pool_in_bn = BigNumber(pool_drops).precision(17)
|
||||
const pool_out_bn = BigNumber(pool_tst.value).precision(15)
|
||||
|
||||
// Use AMM's SwapOut formula to figure out how much XRP we have to pay
|
||||
// to receive the target amount of TST, under the current trading fee.
|
||||
const unrounded_amount = swapOut(asset_out_bn, pool_in_bn,
|
||||
pool_out_bn, full_trading_fee)
|
||||
// Round XRP to integer drops. Round ceiling to make you pay in enough.
|
||||
const from_amount = unrounded_amount.dp(0, BigNumber.ROUND_CEIL)
|
||||
console.log(`Expected cost of ${to_amount.value} ${to_amount.currency}: ` +
|
||||
`${xrpl.dropsToXrp(from_amount)} XRP`)
|
||||
|
||||
// Same calculation, but assume we have access to the discounted trading
|
||||
// fee from the auction slot.
|
||||
const raw_discounted = swapOut(asset_out_bn, pool_in_bn, pool_out_bn,
|
||||
discounted_fee)
|
||||
const discounted_from_amount = raw_discounted.dp(0, BigNumber.ROUND_CEIL)
|
||||
console.log(`Expected cost with auction slot discount: `+
|
||||
`${xrpl.dropsToXrp(discounted_from_amount)} XRP`)
|
||||
|
||||
// The potential savings is the difference between the necessary input
|
||||
// amounts with the full vs discounted fee.
|
||||
const potential_savings = from_amount.minus(discounted_from_amount)
|
||||
console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`)
|
||||
|
||||
// Calculate the cost of winning the auction slot, in LP Tokens -----------
|
||||
const auction_price = auctionDeposit(old_bid, time_interval,
|
||||
full_trading_fee, lpt.value
|
||||
).precision(15)
|
||||
console.log(`Auction price after deposit: ${auction_price} LP Tokens`)
|
||||
|
||||
// Calculate how much XRP to deposit to receive that many LP Tokens -------
|
||||
const deposit_for_bid = ammAssetIn(pool_in_bn, lpt.value, auction_price,
|
||||
full_trading_fee
|
||||
).dp(0, BigNumber.ROUND_CEIL)
|
||||
console.log(`Auction price as XRP single-asset deposit amount: `+
|
||||
`${xrpl.dropsToXrp(deposit_for_bid)} XRP`)
|
||||
|
||||
// Optional. Allow for costs to be 1% greater than estimated, in case other
|
||||
// transactions affect the same AMM during this time.
|
||||
const SLIPPAGE_MULT = BigNumber(1.01)
|
||||
const deposit_max = deposit_for_bid.multipliedBy(SLIPPAGE_MULT).dp(0)
|
||||
|
||||
// Compare price of deposit+bid with potential savings. -------------------
|
||||
// Don't forget XRP burned as transaction costs.
|
||||
const fee_response = (await client.request({"command":"fee"}))
|
||||
const tx_cost_drops = BigNumber(fee_response.result.drops.minimum_fee
|
||||
).multipliedBy(client.feeCushion).dp(0)
|
||||
const net_savings = potential_savings.minus(
|
||||
tx_cost_drops.multipliedBy(2).plus(deposit_max)
|
||||
)
|
||||
if (net_savings > 0) {
|
||||
console.log(`Estimated net savings from the auction slot: ` +
|
||||
`${xrpl.dropsToXrp(net_savings)} XRP`)
|
||||
} else {
|
||||
console.log(`Estimated the auction slot to be MORE EXPENSIVE by `+
|
||||
`${xrpl.dropsToXrp(net_savings.negated())} XRP. Quitting.`)
|
||||
client.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// Do a single-asset deposit to get LP Tokens to bid on the auction slot --
|
||||
const auction_bid = {
|
||||
"currency": lpt.currency,
|
||||
"issuer": lpt.issuer,
|
||||
"value": auction_price.toString()
|
||||
}
|
||||
const deposit_result = await client.submitAndWait({
|
||||
"TransactionType": "AMMDeposit",
|
||||
"Account": wallet.address,
|
||||
"Asset": from_asset,
|
||||
"Asset2": to_asset,
|
||||
"Amount": deposit_max.toString(),
|
||||
"LPTokenOut": auction_bid,
|
||||
"Flags": xrpl.AMMDepositFlags.tfOneAssetLPToken
|
||||
}, {autofill: true, wallet: wallet}
|
||||
)
|
||||
console.log("Deposit result:")
|
||||
console.dir(deposit_result, {depth: null})
|
||||
|
||||
// Actually bid on the auction slot ---------------------------------------
|
||||
const bid_result = await client.submitAndWait({
|
||||
"TransactionType": "AMMBid",
|
||||
"Account": wallet.address,
|
||||
"Asset": from_asset,
|
||||
"Asset2": to_asset,
|
||||
"BidMax": auction_bid,
|
||||
"BidMin": auction_bid, // So rounding doesn't leave dust amounts of LPT
|
||||
}, {autofill: true, wallet: wallet}
|
||||
)
|
||||
console.log("Bid result:")
|
||||
console.dir(bid_result, {depth: null})
|
||||
|
||||
// Trade using the discount -----------------------------------------------
|
||||
const spend_drops = discounted_from_amount.multipliedBy(SLIPPAGE_MULT
|
||||
).dp(0).toString()
|
||||
const offer_result = await client.submitAndWait({
|
||||
"TransactionType": "OfferCreate",
|
||||
"Account": wallet.address,
|
||||
"TakerPays": to_amount,
|
||||
"TakerGets": spend_drops
|
||||
}, {autofill: true, wallet: wallet})
|
||||
console.log("Offer result:")
|
||||
console.dir(offer_result, {depth: null})
|
||||
console.log("Offer balance changes summary:")
|
||||
console.dir(xrpl.getBalanceChanges(offer_result.result.meta), {depth:null})
|
||||
|
||||
// Done.
|
||||
if (to_amount.value > pool_out_bn) {
|
||||
console.log(`Requested ${to_amount.value} ${to_amount.currency} ` +
|
||||
`but AMM only holds ${pool_tst.value}. Quitting.`)
|
||||
client.disconnect()
|
||||
} // End of main()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
main()
|
||||
// Use AMM's SwapOut formula to figure out how much XRP we have to pay
|
||||
// to receive the target amount of TST, under the current trading fee.
|
||||
const unrounded_amount = swapOut(asset_out_bn, pool_in_bn,
|
||||
pool_out_bn, full_trading_fee)
|
||||
// Round XRP to integer drops. Round ceiling to make you pay in enough.
|
||||
const from_amount = unrounded_amount.dp(0, BigNumber.ROUND_CEIL)
|
||||
console.log(`Expected cost of ${to_amount.value} ${to_amount.currency}: ` +
|
||||
`${xrpl.dropsToXrp(from_amount)} XRP`)
|
||||
|
||||
// Same calculation, but assume we have access to the discounted trading
|
||||
// fee from the auction slot.
|
||||
const raw_discounted = swapOut(asset_out_bn, pool_in_bn, pool_out_bn,
|
||||
discounted_fee)
|
||||
const discounted_from_amount = raw_discounted.dp(0, BigNumber.ROUND_CEIL)
|
||||
console.log(`Expected cost with auction slot discount: `+
|
||||
`${xrpl.dropsToXrp(discounted_from_amount)} XRP`)
|
||||
|
||||
// The potential savings is the difference between the necessary input
|
||||
// amounts with the full vs discounted fee.
|
||||
const potential_savings = from_amount.minus(discounted_from_amount)
|
||||
console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`)
|
||||
|
||||
// Calculate the cost of winning the auction slot, in LP Tokens -----------
|
||||
const auction_price = auctionDeposit(old_bid, time_interval,
|
||||
full_trading_fee, lpt.value
|
||||
).precision(15)
|
||||
console.log(`Auction price after deposit: ${auction_price} LP Tokens`)
|
||||
|
||||
// Calculate how much XRP to deposit to receive that many LP Tokens -------
|
||||
const deposit_for_bid = ammAssetIn(pool_in_bn, lpt.value, auction_price,
|
||||
full_trading_fee
|
||||
).dp(0, BigNumber.ROUND_CEIL)
|
||||
console.log(`Auction price as XRP single-asset deposit amount: `+
|
||||
`${xrpl.dropsToXrp(deposit_for_bid)} XRP`)
|
||||
|
||||
// Optional. Allow for costs to be 1% greater than estimated, in case other
|
||||
// transactions affect the same AMM during this time.
|
||||
const SLIPPAGE_MULT = BigNumber(1.01)
|
||||
const deposit_max = deposit_for_bid.multipliedBy(SLIPPAGE_MULT).dp(0)
|
||||
|
||||
// Compare price of deposit+bid with potential savings. -------------------
|
||||
// Don't forget XRP burned as transaction costs.
|
||||
const fee_response = (await client.request({"command":"fee"}))
|
||||
const tx_cost_drops = BigNumber(fee_response.result.drops.minimum_fee
|
||||
).multipliedBy(client.feeCushion).dp(0)
|
||||
const net_savings = potential_savings.minus(
|
||||
tx_cost_drops.multipliedBy(2).plus(deposit_max)
|
||||
)
|
||||
if (net_savings > 0) {
|
||||
console.log(`Estimated net savings from the auction slot: ` +
|
||||
`${xrpl.dropsToXrp(net_savings)} XRP`)
|
||||
} else {
|
||||
console.log(`Estimated the auction slot to be MORE EXPENSIVE by `+
|
||||
`${xrpl.dropsToXrp(net_savings.negated())} XRP. Quitting.`)
|
||||
client.disconnect()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Do a single-asset deposit to get LP Tokens to bid on the auction slot --
|
||||
const auction_bid = {
|
||||
"currency": lpt.currency,
|
||||
"issuer": lpt.issuer,
|
||||
"value": auction_price.toString()
|
||||
}
|
||||
const deposit_result = await client.submitAndWait({
|
||||
"TransactionType": "AMMDeposit",
|
||||
"Account": wallet.address,
|
||||
"Asset": from_asset,
|
||||
"Asset2": to_asset,
|
||||
"Amount": deposit_max.toString(),
|
||||
"LPTokenOut": auction_bid,
|
||||
"Flags": xrpl.AMMDepositFlags.tfOneAssetLPToken
|
||||
}, {autofill: true, wallet: wallet}
|
||||
)
|
||||
console.log("Deposit result:")
|
||||
console.dir(deposit_result, {depth: null})
|
||||
|
||||
// Actually bid on the auction slot ---------------------------------------
|
||||
const bid_result = await client.submitAndWait({
|
||||
"TransactionType": "AMMBid",
|
||||
"Account": wallet.address,
|
||||
"Asset": from_asset,
|
||||
"Asset2": to_asset,
|
||||
"BidMax": auction_bid,
|
||||
"BidMin": auction_bid, // So rounding doesn't leave dust amounts of LPT
|
||||
}, {autofill: true, wallet: wallet}
|
||||
)
|
||||
console.log("Bid result:")
|
||||
console.dir(bid_result, {depth: null})
|
||||
|
||||
// Trade using the discount -----------------------------------------------
|
||||
const spend_drops = discounted_from_amount.multipliedBy(SLIPPAGE_MULT
|
||||
).dp(0).toString()
|
||||
const offer_result = await client.submitAndWait({
|
||||
"TransactionType": "OfferCreate",
|
||||
"Account": wallet.address,
|
||||
"TakerPays": to_amount,
|
||||
"TakerGets": spend_drops
|
||||
}, {autofill: true, wallet: wallet})
|
||||
console.log("Offer result:")
|
||||
console.dir(offer_result, {depth: null})
|
||||
console.log("Offer balance changes summary:")
|
||||
console.dir(xrpl.getBalanceChanges(offer_result.result.meta), {depth:null})
|
||||
|
||||
// Done.
|
||||
client.disconnect()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user