From 21b03a8bf0949297678d39cc3134694be87b441b Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Tue, 19 May 2026 15:31:17 -0700 Subject: [PATCH 01/10] add mcp servers and rules for using them --- .claude/CLAUDE.md | 27 +++++++++++++++++++++++++++ .mcp.json | 12 ++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .mcp.json diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index fbe67a7ef7..1e8475d80a 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -6,6 +6,33 @@ - **Production branch:** `master` - **Local preview:** `npm start` +## XRPL Reference Sources + +For up-to-date XRPL protocol, API, or SDK info, use the `context7` MCP server. The following authoritative sources are indexed: + +### Documentation Sites + +- `/websites/xrpl` — xrpl.org (this dev portal's published content) +- `/websites/opensource_ripple` — opensource.ripple.com (Ripple's open-source projects) +- `/websites/xrplevm` — docs.xrplevm.org (XRPL EVM sidechain) + +### Protocol Implementation + +- `/xrplf/rippled` — rippled (C++ reference implementation; authoritative for protocol behavior, transaction validation, and ledger entry structure) + +### SDK Libraries + +- `/xrplf/xrpl-py` — Python +- `/xrplf/xrpl.js` — JavaScript / TypeScript +- `/xrplf/xrpl-go` — Go +- `/xrplf/xrpl4j` — Java + +Since the library IDs are listed above, skip `mcp__context7__resolve-library-id` and call `mcp__context7__query-docs` directly with the relevant ID. Prefer this over web search or memory when writing code samples, documenting protocol behavior, or answering SDK API questions. + +### Live xrpl.org Content + +Use the `xrpl-dev-portal` MCP server (xrpl.org content only) as a fallback if `context7` is unavailable. **Only `mcp__xrpl-dev-portal__search` is functional**. Do not call the other tools. + ## Localization - Default: `en-US` diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000000..956198021d --- /dev/null +++ b/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "context7": { + "type": "http", + "url": "https://mcp.context7.com/mcp" + }, + "xrpl-dev-portal": { + "type": "http", + "url": "https://xrpl.org/mcp" + } + } +} \ No newline at end of file From 59c2c07d2271da06a9e61fad29066f29544c8cba Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Tue, 19 May 2026 15:44:39 -0700 Subject: [PATCH 02/10] first pass of code sample rules --- .claude/rules/go-code-samples.md | 147 +++++++++++++++++++++++ .claude/rules/java-code-samples.md | 89 ++++++++++++++ .claude/rules/javascript-code-samples.md | 108 +++++++++++++++++ .claude/rules/python-code-samples.md | 93 ++++++++++++++ 4 files changed, 437 insertions(+) create mode 100644 .claude/rules/go-code-samples.md create mode 100644 .claude/rules/java-code-samples.md create mode 100644 .claude/rules/javascript-code-samples.md create mode 100644 .claude/rules/python-code-samples.md diff --git a/.claude/rules/go-code-samples.md b/.claude/rules/go-code-samples.md new file mode 100644 index 0000000000..6caa46873e --- /dev/null +++ b/.claude/rules/go-code-samples.md @@ -0,0 +1,147 @@ +--- +paths: + - "_code-samples/**/*.go" +--- + +# XRPL Go Code Sample Conventions + +Code samples come in **two flavors** with very different conventions. Identify which you're writing first. + +| Flavor | Folder pattern | Audience | Priority | +|---|---|---|---| +| **Tutorial** | `-/main.go` (e.g., `create-loan-broker/main.go`) | A dev reading & learning the protocol | Clarity over speed | +| **Setup** | `-setup/main.go` (e.g., `lending-setup/main.go`) | A dev who never opens this file — runs to prep accounts/credentials/MPT/vault | Speed over clarity | + +If a file isn't clearly one or the other, default to tutorial conventions. + +--- + +## Shared across both flavors + +- Library: `github.com/Peersyst/xrpl-go v0.1.17` in `go.mod` +- `go 1.24.3` minimum +- Each command is its own `kebab-case` subdir with one `main.go`; users run with `go run ./` from the language root +- One `go.mod` per sample folder at the language root (e.g., `_code-samples/lending-protocol/go/go.mod`) +- Variables: `camelCase` with acronyms uppercased — `loanBrokerWallet`, `mptID`, `vaultID`, `loanBrokerID`, `credIssuerWallet` +- Wallet variables always end in `Wallet` (e.g., `loanBrokerWallet`) to distinguish from `loanBrokerID` +- Setup JSON keys use `camelCase` (`loanBroker`, `credentialIssuer`, `mptID`, `vaultID`, `loanBrokerID`) — matches the JS convention + +### Pointer helper +For any `main.go` that sets optional pointer fields, include this helper near the top: +```go +// ptr is a helper that returns a pointer to the given value, +// used for setting optional transaction fields in Go. +func ptr[T any](v T) *T { return &v } +``` + +--- + +## Tutorial files + +### Transport +- **WebSocket** — `github.com/Peersyst/xrpl-go/xrpl/websocket` +- Devnet endpoint: `wss://s.devnet.rippletest.net:51233` +- Pattern: + ```go + client := websocket.NewClient( + websocket.NewClientConfig(). + WithHost("wss://s.devnet.rippletest.net:51233"), + ) + defer client.Disconnect() + if err := client.Connect(); err != nil { panic(err) } + ``` + +### Structure +1. Multi-line `// IMPORTANT:` header explaining what the script demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") +2. `package main` + imports +3. Client connection (with `defer client.Disconnect()`) +4. Auto-run setup if data is missing: + ```go + if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) { + fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n") + cmd := exec.Command("go", "run", "./lending-setup") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { panic(err) } + } + ``` +5. Load wallet seeds and IDs from setup JSON via `wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))` +6. One transaction per major step, each with its own visible `=== Header ===` + +### Output style +- Section comments in code use `// Section description ----------------------` (with the dash visual) +- Print a `=== Section Name ===` banner before each major step: + ```go + fmt.Printf("\n=== Preparing LoanBrokerSet transaction ===\n\n") + ``` +- Print every transaction as JSON before submitting: + ```go + flatTx := tx.Flatten() + txJSON, _ := json.MarshalIndent(flatTx, "", " ") + fmt.Printf("%s\n", string(txJSON)) + ``` +- Submit with the explicit options form: `client.SubmitTxAndWait(flatTx, &wstypes.SubmitOptions{Autofill: true, Wallet: &w})` + +### Result handling +- Always check `Meta.TransactionResult` explicitly: + ```go + if resp.Meta.TransactionResult != "tesSUCCESS" { + fmt.Printf("Error: Unable to create loan broker: %s\n", resp.Meta.TransactionResult) + os.Exit(1) + } + ``` +- Use `panic(err)` for unexpected errors (network/marshal failures), `os.Exit(1)` for expected protocol failures with a printed message + +--- + +## Setup files + +### Transport +- **RPC, not WebSocket** — `github.com/Peersyst/xrpl-go/xrpl/rpc` +- Devnet endpoint: `https://s.devnet.rippletest.net:51234` +- Pattern: + ```go + cfg, err := rpc.NewClientConfig( + "https://s.devnet.rippletest.net:51234", + rpc.WithFaucetProvider(faucet.NewDevnetFaucetProvider()), + ) + if err != nil { panic(err) } + client := rpc.NewClient(cfg) + + submitOpts := func(w *wallet.Wallet) *rpctypes.SubmitOptions { + return &rpctypes.SubmitOptions{Autofill: true, Wallet: w} + } + ``` +- No `defer client.Disconnect()` — RPC is HTTP + +### Speed-first patterns (required when possible) +- Use goroutines + buffered channels for fan-out parallelism (not `errgroup` or `sync.WaitGroup`) +- Each goroutine handles one transaction and sends its result (or `struct{}{}` for void) into a channel; main drains the channels in order +- When fanning out parallel transactions from the same account, create tickets first via `TicketCreate` with `TicketCount: N`, then set `Sequence: 0` and `TicketSequence: ...` on the `BaseTx` of each parallel tx +- (Future) When the `Batch` transaction is re-enabled, prefer it over tickets for atomic multi-tx groups +- `FundWallet` returns before the account is queryable on ledger — poll `account.InfoRequest` with `LedgerIndex: common.Validated` (up to ~20 seconds) before using the wallet + +### Output style +- Top comment: single line, `// Setup script for lending protocol tutorials` above `package main` +- Only output is a carriage-return progress indicator: `fmt.Print("Setting up tutorial: N/7\r")` between phases +- No `=== Section ===` banners, no transaction dumps — the user never sees this file's output beyond the progress counter +- Section comments in code are short: `// Section description` (no dash visual) + +### Output file +At the end, write all wallet/ID data the tutorials will need. Use an anonymous struct with `json:"camelCase"` tags so field order is preserved: +```go +setupData := struct { + Description string `json:"description"` + LoanBroker any `json:"loanBroker"` + DomainID string `json:"domainID"` + MptID string `json:"mptID"` + VaultID string `json:"vaultID"` + LoanBrokerID string `json:"loanBrokerID"` +}{ ... } +jsonData, _ := json.MarshalIndent(setupData, "", " ") +os.WriteFile("lending-setup.json", jsonData, 0644) +``` +- Output filename uses `kebab-case` (matches the subdir name): `lending-setup.json` + +### Error handling +- Use `panic(err)` on every error path — these are tutorial samples and a panic surfaces the failing line clearly. Don't silently `continue` or `_ = err`. diff --git a/.claude/rules/java-code-samples.md b/.claude/rules/java-code-samples.md new file mode 100644 index 0000000000..3203c5afd9 --- /dev/null +++ b/.claude/rules/java-code-samples.md @@ -0,0 +1,89 @@ +--- +paths: + - "_code-samples/**/*.java" +--- + +# XRPL Java Code Sample Conventions + +Java samples currently exist only in tutorial form. If a setup-style script is added later, mirror the speed-first conventions from the other language rules (parallelism, minimal output, no transaction printing) and update this rule. + +--- + +## Project & build + +- Maven (not Gradle); each sample is a self-contained Maven project under its language folder (e.g., `_code-samples/credential/java/`) +- Standard layout: `src/main/java/com/example/xrpl/.java` + `src/main/resources/logback.xml` +- `pom.xml` at the language root; Java 11 (`11`); UTF-8 +- Single dependency: `org.xrpl:xrpl4j-client` 6.0.0 +- Plugin: `org.codehaus.mojo:exec-maven-plugin` 3.3.0 +- Install: `mvn install` +- Run: `mvn exec:java -Dexec.mainClass=com.example.xrpl.` + +## Logging + +- `src/main/resources/logback.xml` sets `org.xrpl.xrpl4j` to `WARN` (root `INFO`) to quiet wire-level chatter +- Include an XML comment explaining how to raise to `DEBUG` for transaction-level inspection + +## Naming + +- Class/file: `PascalCase` verb-noun (e.g., `ManageCredentials.java`); one class per sample +- Package: `com.example.xrpl` +- Imports: alphabetical within groups; no wildcard imports; `java.*` last + +## Network + +- **Testnet, not devnet** (py/js/go samples use devnet; Java uses testnet) +- JSON-RPC: `https://s.altnet.rippletest.net:51234/` +- Faucet: `https://faucet.altnet.rippletest.net` +- Explorer base: `https://testnet.xrpl.org/transactions/` +- Declared as `private static final HttpUrl` constants near the top of the class + +## Class structure + +- Class-level Javadoc summarizing what the sample demonstrates +- `public static void main(String[] args)` wraps `run()` in a try/catch: + - Unwrap `CompletionException` so async failures print the same clean message as sync failures + - Print `"Error: " + cause.getMessage()` to `System.err`, then `System.exit(1)` +- `private static void run()` holds the main flow +- Helpers below a `// ===== Helper functions =====` divider, each prefixed with a one-line comment explaining the helper + +## Concurrency + +- Use `CompletableFuture.supplyAsync(() -> ...)` + `CompletableFuture.allOf(...).join()` for parallel work (e.g., funding multiple accounts in parallel) + +## Wallets + +- Create: `Seed.ed25519Seed().deriveKeyPair()` +- Fund: `FaucetClient.construct(FAUCET_URL).fundAccount(FundAccountRequest.of(address))` +- Poll `xrplClient.accountInfo(... LedgerSpecifier.VALIDATED ...)` up to 20 attempts at 1s each before using the wallet — faucet funding isn't queryable immediately +- On `InterruptedException`: always `Thread.currentThread().interrupt()` before rethrowing as `RuntimeException` + +## Transactions + +- Builder pattern: `CredentialCreate.builder().account(addr).subject(...).build()` +- Always set: `sequence`, `fee`, `lastLedgerSequence`, `signingPublicKey` +- Use shared helpers (one per concern): + - `accountSequence(client, address)` — fetches sequence from the validated ledger + - `recommendedFee(client)` — wraps `FeeUtils.computeNetworkFees(client.fee()).recommendedFee()` + - `lastLedgerSequence(client)` — validated ledger index + 20 buffer +- Sign with `BcSignatureService` (Bouncy Castle): `new BcSignatureService().sign(signer.privateKey(), tx)` + +## Submission & finality + +1. Reject early if `submit.engineResult()` ≠ `tesSUCCESS` +2. Poll `xrplClient.isFinal(...)` every 1 second until `finalityStatus()` is `VALIDATED_SUCCESS` +3. Fetch the full result via `xrplClient.transaction(...)` — `isFinal` only returns status, not the transaction body + +## Output + +- Section comments in code: `// ----- Section description -----` (dashes on **both** sides — Java's style; py/js/go use single-side dashes) +- Section banners: `System.out.println("\n=== Section Name ===\n");` +- Print every transaction as pretty JSON before submitting via `ObjectMapperFactory.create().writerWithDefaultPrettyPrinter().writeValueAsString(tx)` +- On success: print ` succeeded!` and `Explorer: ` so the reader can inspect on-ledger + +## Error handling + +- Wrap checked exceptions with context: `throw new RuntimeException("Failed to fetch account sequence for " + address + ". " + e.getMessage(), e)` +- All failures bubble to `main()`'s catch; clean stderr message + exit 1 +- Don't try/catch around individual transactions inside `run()` — let failures be visible +- Helper `requireSuccess(result)` throws `IllegalStateException` on non-`tesSUCCESS` codes; called after each submission diff --git a/.claude/rules/javascript-code-samples.md b/.claude/rules/javascript-code-samples.md new file mode 100644 index 0000000000..57b42c7ef8 --- /dev/null +++ b/.claude/rules/javascript-code-samples.md @@ -0,0 +1,108 @@ +--- +paths: + - "_code-samples/**/*.js" +--- + +# XRPL JavaScript Code Sample Conventions + +Code samples come in **two flavors** with very different conventions. Identify which you're writing first. + +| Flavor | Filename pattern | Audience | Priority | +|---|---|---|---| +| **Tutorial** | `.js` (e.g., `createLoanBroker.js`) | A dev reading & learning the protocol | Clarity over speed | +| **Setup** | `Setup.js` (e.g., `lendingSetup.js`) | A dev who never opens this file — runs to prep accounts/credentials/MPT/vault | Speed over clarity | + +If a file isn't clearly one or the other, default to tutorial conventions. + +--- + +## Shared across both flavors + +- Library: `xrpl ^4.6.0` in `package.json` +- ESM modules: `"type": "module"`; `import xrpl from 'xrpl'` +- Top-level `await` (Node 18+); no `main()` wrapper +- Devnet WebSocket: `wss://s.devnet.rippletest.net:51233` +- Style: 2-space indent, single quotes, no semicolons +- File names: `camelCase` (e.g., `createLoan.js`) +- Variables: `camelCase` with acronyms uppercased — `loanBroker`, `mptID`, `vaultID`, `loanBrokerID`, `credentialIssuer` +- Transaction object keys are XRPL native PascalCase (`TransactionType`, `Account`, `Amount`) — never transform them +- Setup JSON keys use `camelCase` (`loanBroker`, `credentialIssuer`, `mptID`, `vaultID`, `loanBrokerID`) +- End with `await client.disconnect()` + +--- + +## Tutorial files + +### Structure +1. Multi-line `// IMPORTANT:` header explaining what the script demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") +2. Imports +3. `const client = new xrpl.Client('wss://...')` + `await client.connect()` +4. Auto-run setup script if the JSON data is missing: + ```js + if (!fs.existsSync('lendingSetup.json')) { + console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`) + execSync('node lendingSetup.js', { stdio: 'inherit' }) + } + ``` +5. Load wallets and IDs from setup JSON via `xrpl.Wallet.fromSeed(setupData.X.seed)` +6. One transaction per major step, each with its own visible `=== Header ===` + +### Output style +- Section comments in code use `// Section description ----------------------` (with the dash visual) +- Print a `=== Section Name ===` banner before each major step: + ```js + console.log(`\n=== Preparing LoanBrokerSet transaction ===\n`) + ``` +- Build transactions as plain object literals; validate before submitting: + ```js + xrpl.validate(loanBrokerSetTx) + console.log(JSON.stringify(loanBrokerSetTx, null, 2)) + ``` +- Submit with the explicit options form: `await client.submitAndWait(tx, { wallet, autofill: true })` + +### Result handling +- Always check `result.meta.TransactionResult` explicitly: + ```js + if (submitResponse.result.meta.TransactionResult !== 'tesSUCCESS') { + const resultCode = submitResponse.result.meta.TransactionResult + console.error('Error: Unable to create loan broker:', resultCode) + await client.disconnect() + process.exit(1) + } + ``` +- Always `await client.disconnect()` before `process.exit(1)` +- Don't wrap in try/catch — let failures be visible + +### Dual-signed transactions +For `LoanSet`-style transactions that require both broker and counterparty signatures: +```js +const tx = await client.autofill({ ... }) +const brokerSigned = loanBroker.sign(tx) +const decoded = xrpl.decode(brokerSigned.tx_blob) +const fullySigned = xrpl.signLoanSetByCounterparty(borrower, decoded) +await client.submitAndWait(fullySigned.tx) +``` + +--- + +## Setup files + +### Speed-first patterns (required when possible) +- Run independent transactions concurrently with `await Promise.all([...])` +- When fanning out parallel transactions from the same account, batch them first via `TicketCreate` with `TicketCount: N`, then pass `Sequence: 0` and `TicketSequence: ticketArr[i]` on each parallel tx +- (Future) When the `Batch` transaction is re-enabled, prefer it over tickets for atomic multi-tx groups +- Destructure response arrays: `const [{ wallet: loanBroker }, { wallet: borrower }] = await Promise.all([client.fundWallet(), client.fundWallet()])` +- Use object property shorthand when key and variable match: `{ domainID, mptID, vaultID, loanBrokerID }` + +### Output style +- Top comment: single line, `// Setup script for lending protocol tutorials` +- Only output is a carriage-return progress indicator: `process.stdout.write('Setting up tutorial: N/7\r')` between phases +- No `=== Section ===` banners, no `xrpl.validate(tx)`, no transaction dumps — the user never sees this file's output beyond the progress counter +- Section comments in code are short: `// Section description` (no dash visual) +- If a library call emits a warning the reader doesn't need (e.g., `LoanSet` autofill warning), silence it locally with a one-line comment explaining why: `console.warn = () => {}` + +### Output file +At the end, write all wallet/ID data the tutorials will need: +```js +fs.writeFileSync('lendingSetup.json', JSON.stringify(setupData, null, 2)) +``` diff --git a/.claude/rules/python-code-samples.md b/.claude/rules/python-code-samples.md new file mode 100644 index 0000000000..a76aafd193 --- /dev/null +++ b/.claude/rules/python-code-samples.md @@ -0,0 +1,93 @@ +--- +paths: + - "_code-samples/**/*.py" +--- + +# XRPL Python Code Sample Conventions + +Code samples come in **two flavors** with very different conventions. Identify which you're writing first. + +| Flavor | Filename pattern | Audience | Priority | +|---|---|---|---| +| **Tutorial** | `_.py` (e.g., `create_loan_broker.py`) | A dev reading & learning the protocol | Clarity over speed | +| **Setup** | `_setup.py` (e.g., `lending_setup.py`) | A dev who never opens this file — runs to prep accounts/credentials/MPT/vault | Speed over clarity | + +If a file isn't clearly one or the other, default to tutorial conventions. + +--- + +## Shared across both flavors + +- Library: `xrpl-py>=4.5.0` in `requirements.txt` +- File names use `snake_case` +- Variables use `snake_case` (e.g., `loan_broker`, `mpt_id`, `vault_id`, `loan_broker_id`) +- Tutorial scripts read setup data; setup scripts write it. Both use the same JSON key style (`snake_case`: `loan_broker`, `credential_issuer`, `mpt_id`, `domain_id`, `loan_broker_id`). + +--- + +## Tutorial files + +### Client and runtime +- **Sync API only** — `from xrpl.clients import JsonRpcClient`, `from xrpl.transaction import submit_and_wait`, `from xrpl.wallet import Wallet`. No `asyncio`. +- Devnet RPC endpoint: `https://s.devnet.rippletest.net:51234` +- No `main()` function, no `if __name__ == "__main__":` — script runs top-to-bottom + +### Structure +1. Multi-line `# IMPORTANT:` header explaining what the script demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") +2. Imports +3. Client setup +4. Auto-run setup script if the JSON data is missing: + ```python + if not os.path.exists("lending_setup.json"): + print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n") + subprocess.run([sys.executable, "lending_setup.py"], check=True) + ``` +5. Load wallets and IDs from setup JSON via `Wallet.from_seed(setup_data["..."]["seed"])` +6. One transaction per major step, each with its own visible `=== Header ===` + +### Output style +- Section comments in code use `# Section description ----------------------` (with the dash visual) +- Print a `=== Section Name ===` banner before each major step: + ```python + print("\n=== Preparing LoanBrokerSet transaction ===\n") + ``` +- Print every transaction as JSON before submitting: `print(json.dumps(tx.to_xrpl(), indent=2))` +- Print a `=== Section Name ===` banner before extracting results too + +### Result handling +- Always check `submit_response.result["meta"]["TransactionResult"]` explicitly: + ```python + if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = submit_response.result["meta"]["TransactionResult"] + print(f"Error: Unable to create loan broker: {result_code}") + sys.exit(1) + ``` +- Don't wrap in try/except — let failures be visible + +--- + +## Setup files + +### Client and runtime +- **Async API only** — `from xrpl.asyncio.clients import AsyncWebsocketClient`, `xrpl.asyncio.wallet.generate_faucet_wallet`, `xrpl.asyncio.transaction` (`submit_and_wait`, `autofill`, `sign`) +- Devnet WebSocket endpoint: `wss://s.devnet.rippletest.net:51233` +- Wrap in `async def main(): ...` with `async with AsyncWebsocketClient(URL) as client:` at the top, and `asyncio.run(main())` at the bottom + +### Speed-first patterns (required when possible) +- Run independent transactions concurrently with `await asyncio.gather(...)` +- When fanning out parallel transactions from the same account, batch them first via `TicketCreate(ticket_count=N)`, then pass `ticket_sequence=...` and `sequence=0` on each parallel tx +- (Future) When the `Batch` transaction is re-enabled, prefer it over tickets for atomic multi-tx groups +- Imports of models go in one alphabetized parenthesized block + +### Output style +- Top comment: single line, `# Setup script for lending protocol tutorials` +- Only output is a carriage-return progress indicator: `print("Setting up tutorial: N/7", end="\r")` between phases +- No `=== Section ===` banners, no transaction dumps — the user never sees this file's output beyond the progress counter +- Section comments in code are short: `# Section description` (no dash visual) + +### Output file +At the end, write all wallet/ID data the tutorials will need: +```python +with open("lending_setup.json", "w") as f: + json.dump(setup_data, f, indent=2) +``` From d4de90ac86d825da376fd6070939f7d7d44324ab Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Tue, 19 May 2026 15:52:32 -0700 Subject: [PATCH 03/10] add actual java code samples --- .claude/rules/java-code-samples.md | 211 ++++++++++++++++++++++++----- 1 file changed, 178 insertions(+), 33 deletions(-) diff --git a/.claude/rules/java-code-samples.md b/.claude/rules/java-code-samples.md index 3203c5afd9..7ffa5a03e8 100644 --- a/.claude/rules/java-code-samples.md +++ b/.claude/rules/java-code-samples.md @@ -13,7 +13,7 @@ Java samples currently exist only in tutorial form. If a setup-style script is a - Maven (not Gradle); each sample is a self-contained Maven project under its language folder (e.g., `_code-samples/credential/java/`) - Standard layout: `src/main/java/com/example/xrpl/.java` + `src/main/resources/logback.xml` -- `pom.xml` at the language root; Java 11 (`11`); UTF-8 +- Java 11 (`11`); UTF-8 - Single dependency: `org.xrpl:xrpl4j-client` 6.0.0 - Plugin: `org.codehaus.mojo:exec-maven-plugin` 3.3.0 - Install: `mvn install` @@ -21,8 +21,24 @@ Java samples currently exist only in tutorial form. If a setup-style script is a ## Logging -- `src/main/resources/logback.xml` sets `org.xrpl.xrpl4j` to `WARN` (root `INFO`) to quiet wire-level chatter -- Include an XML comment explaining how to raise to `DEBUG` for transaction-level inspection +`src/main/resources/logback.xml` quiets xrpl4j wire-level chatter so tutorial output stays readable: + +```xml + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + +``` ## Naming @@ -32,58 +48,187 @@ Java samples currently exist only in tutorial form. If a setup-style script is a ## Network -- **Testnet, not devnet** (py/js/go samples use devnet; Java uses testnet) -- JSON-RPC: `https://s.altnet.rippletest.net:51234/` -- Faucet: `https://faucet.altnet.rippletest.net` -- Explorer base: `https://testnet.xrpl.org/transactions/` -- Declared as `private static final HttpUrl` constants near the top of the class +**Testnet, not devnet** (py/js/go samples use devnet; Java uses testnet). Declare constants near the top of the class: + +```java +private static final HttpUrl NETWORK_URL = HttpUrl.get("https://s.altnet.rippletest.net:51234/"); +private static final HttpUrl FAUCET_URL = HttpUrl.get("https://faucet.altnet.rippletest.net"); +private static final String EXPLORER_BASE = "https://testnet.xrpl.org/transactions/"; +``` ## Class structure -- Class-level Javadoc summarizing what the sample demonstrates -- `public static void main(String[] args)` wraps `run()` in a try/catch: - - Unwrap `CompletionException` so async failures print the same clean message as sync failures - - Print `"Error: " + cause.getMessage()` to `System.err`, then `System.exit(1)` +`main()` wraps `run()` and unwraps `CompletionException` so async failures print the same clean message as sync ones: + +```java +public static void main(String[] args) { + try { + run(); + } catch (Exception e) { + Throwable cause = (e instanceof CompletionException && e.getCause() != null) + ? e.getCause() : e; + System.err.println("Error: " + cause.getMessage()); + System.exit(1); + } +} +``` + - `private static void run()` holds the main flow -- Helpers below a `// ===== Helper functions =====` divider, each prefixed with a one-line comment explaining the helper +- Helpers below a `// ===== Helper functions =====` divider, each prefixed with a one-line comment ## Concurrency -- Use `CompletableFuture.supplyAsync(() -> ...)` + `CompletableFuture.allOf(...).join()` for parallel work (e.g., funding multiple accounts in parallel) +Use `CompletableFuture.supplyAsync` + `allOf().join()` for parallel work (e.g., funding multiple accounts): + +```java +CompletableFuture issuerFuture = CompletableFuture.supplyAsync( + () -> createAndFundWallet(xrplClient)); +CompletableFuture subjectFuture = CompletableFuture.supplyAsync( + () -> createAndFundWallet(xrplClient)); +CompletableFuture.allOf(issuerFuture, subjectFuture).join(); + +KeyPair issuer = issuerFuture.join(); +KeyPair subject = subjectFuture.join(); +``` ## Wallets -- Create: `Seed.ed25519Seed().deriveKeyPair()` -- Fund: `FaucetClient.construct(FAUCET_URL).fundAccount(FundAccountRequest.of(address))` -- Poll `xrplClient.accountInfo(... LedgerSpecifier.VALIDATED ...)` up to 20 attempts at 1s each before using the wallet — faucet funding isn't queryable immediately -- On `InterruptedException`: always `Thread.currentThread().interrupt()` before rethrowing as `RuntimeException` +Faucet funding isn't queryable immediately — poll until the account is visible on a validated ledger: + +```java +private static KeyPair createAndFundWallet(XrplClient xrplClient) { + KeyPair keyPair = Seed.ed25519Seed().deriveKeyPair(); + Address address = keyPair.publicKey().deriveAddress(); + FaucetClient.construct(FAUCET_URL).fundAccount(FundAccountRequest.of(address)); + + for (int attempt = 0; attempt < 20; attempt++) { + try { + xrplClient.accountInfo(AccountInfoRequestParams.builder() + .account(address) + .ledgerSpecifier(LedgerSpecifier.VALIDATED) + .build()); + return keyPair; + } catch (JsonRpcClientErrorException notYetVisible) { + try { Thread.sleep(1_000L); } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Account polling interrupted for " + address + ". " + e.getMessage(), e); + } + } + } + throw new IllegalStateException("Faucet funding for " + address + " did not confirm in time."); +} +``` ## Transactions -- Builder pattern: `CredentialCreate.builder().account(addr).subject(...).build()` -- Always set: `sequence`, `fee`, `lastLedgerSequence`, `signingPublicKey` -- Use shared helpers (one per concern): - - `accountSequence(client, address)` — fetches sequence from the validated ledger - - `recommendedFee(client)` — wraps `FeeUtils.computeNetworkFees(client.fee()).recommendedFee()` - - `lastLedgerSequence(client)` — validated ledger index + 20 buffer -- Sign with `BcSignatureService` (Bouncy Castle): `new BcSignatureService().sign(signer.privateKey(), tx)` +Builder pattern; always set `sequence`, `fee`, `lastLedgerSequence`, `signingPublicKey` from shared helpers: + +```java +CredentialCreate createTx = CredentialCreate.builder() + .account(issuerAddress) + .subject(subjectAddress) + .credentialType(CREDENTIAL_TYPE) + .sequence(accountSequence(xrplClient, issuerAddress)) + .fee(recommendedFee(xrplClient)) + .lastLedgerSequence(lastLedgerSequence(xrplClient)) + .signingPublicKey(issuer.publicKey()) + .build(); +``` + +Shared helpers (one per concern; each wraps `JsonRpcClientErrorException` with context): + +```java +// Next transaction sequence from the validated ledger. +private static UnsignedInteger accountSequence(XrplClient client, Address address) { /* ... */ } + +// Recommended fee for a standard (non-multisig, non-batch) transaction. +private static XrpCurrencyAmount recommendedFee(XrplClient client) { + return FeeUtils.computeNetworkFees(client.fee()).recommendedFee(); +} + +// Validated ledger index + 20-ledger buffer. +private static UnsignedInteger lastLedgerSequence(XrplClient client) { /* ... */ } +``` ## Submission & finality -1. Reject early if `submit.engineResult()` ≠ `tesSUCCESS` -2. Poll `xrplClient.isFinal(...)` every 1 second until `finalityStatus()` is `VALIDATED_SUCCESS` -3. Fetch the full result via `xrplClient.transaction(...)` — `isFinal` only returns status, not the transaction body +Sign with `BcSignatureService`, submit, poll `isFinal` until validated, then fetch the full result (`isFinal` only returns status): + +```java +private static TransactionResult signSubmitAndWait( + XrplClient xrplClient, KeyPair signer, T transaction, Class transactionType +) { + SignatureService signatureService = new BcSignatureService(); + UnsignedInteger lastLedgerSequence = transaction.lastLedgerSequence() + .orElseThrow(() -> new IllegalArgumentException("Must set LastLedgerSequence for polling expiration")); + + try { + SingleSignedTransaction signed = signatureService.sign(signer.privateKey(), transaction); + SubmitResult submit = xrplClient.submit(signed); + + if (!TransactionResultCodes.TES_SUCCESS.equals(submit.engineResult())) { + throw new IllegalStateException( + "Submission rejected. " + submit.engineResult() + " — " + submit.engineResultMessage()); + } + + Finality finality; + do { + Thread.sleep(1_000L); + finality = xrplClient.isFinal( + signed.hash(), submit.validatedLedgerIndex(), lastLedgerSequence, + transaction.sequence(), signer.publicKey().deriveAddress()); + } while (finality.finalityStatus() == FinalityStatus.NOT_FINAL); + + if (finality.finalityStatus() != FinalityStatus.VALIDATED_SUCCESS) { + throw new IllegalStateException("Transaction failed with status " + finality.finalityStatus() + + ". Result code: " + finality.resultCode().orElse("unknown")); + } + + // isFinal only returns status; fetch the full transaction body separately + return xrplClient.transaction(TransactionRequestParams.of(signed.hash()), transactionType); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Transaction polling interrupted. " + e.getMessage(), e); + } catch (JsonRpcClientErrorException | JsonProcessingException e) { + throw new RuntimeException("Transaction processing failed. " + e.getMessage(), e); + } +} +``` ## Output -- Section comments in code: `// ----- Section description -----` (dashes on **both** sides — Java's style; py/js/go use single-side dashes) - Section banners: `System.out.println("\n=== Section Name ===\n");` -- Print every transaction as pretty JSON before submitting via `ObjectMapperFactory.create().writerWithDefaultPrettyPrinter().writeValueAsString(tx)` -- On success: print ` succeeded!` and `Explorer: ` so the reader can inspect on-ledger +- Section comments in code: `// ----- Section description -----` (dashes on **both** sides — Java's style; py/js/go use single-side dashes) +- Print every transaction as pretty JSON before submitting: + +```java +private static void printTransactionJson(Transaction tx) { + try { + System.out.println(ObjectMapperFactory.create().writerWithDefaultPrettyPrinter().writeValueAsString(tx)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize transaction JSON. " + e.getMessage(), e); + } +} +``` + +- On success, print "succeeded!" line + explorer link via `requireSuccess`: + +```java +private static void requireSuccess(TransactionResult result) { + String code = result.metadata().get().transactionResult(); + String txType = result.transaction().transactionType().value(); + if (!TransactionResultCodes.TES_SUCCESS.equals(code)) { + throw new IllegalStateException(txType + " failed with error code " + code); + } + System.out.println(txType + " succeeded!"); + System.out.println("Explorer: " + EXPLORER_BASE + result.hash()); +} +``` ## Error handling -- Wrap checked exceptions with context: `throw new RuntimeException("Failed to fetch account sequence for " + address + ". " + e.getMessage(), e)` +- Wrap checked exceptions with context: `throw new RuntimeException("Failed to ... " + e.getMessage(), e)` +- On `InterruptedException`: always `Thread.currentThread().interrupt()` before rethrowing - All failures bubble to `main()`'s catch; clean stderr message + exit 1 - Don't try/catch around individual transactions inside `run()` — let failures be visible -- Helper `requireSuccess(result)` throws `IllegalStateException` on non-`tesSUCCESS` codes; called after each submission From f5e798570d483c42ee1589c8274311da501e1923 Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Wed, 20 May 2026 14:50:09 -0700 Subject: [PATCH 04/10] additional clean up --- .claude/rules/go-code-samples.md | 6 ------ .claude/rules/java-code-samples.md | 2 -- .claude/rules/javascript-code-samples.md | 6 ------ .claude/rules/python-code-samples.md | 6 ------ 4 files changed, 20 deletions(-) diff --git a/.claude/rules/go-code-samples.md b/.claude/rules/go-code-samples.md index 6caa46873e..af001f3497 100644 --- a/.claude/rules/go-code-samples.md +++ b/.claude/rules/go-code-samples.md @@ -14,8 +14,6 @@ Code samples come in **two flavors** with very different conventions. Identify w If a file isn't clearly one or the other, default to tutorial conventions. ---- - ## Shared across both flavors - Library: `github.com/Peersyst/xrpl-go v0.1.17` in `go.mod` @@ -34,8 +32,6 @@ For any `main.go` that sets optional pointer fields, include this helper near th func ptr[T any](v T) *T { return &v } ``` ---- - ## Tutorial files ### Transport @@ -92,8 +88,6 @@ func ptr[T any](v T) *T { return &v } ``` - Use `panic(err)` for unexpected errors (network/marshal failures), `os.Exit(1)` for expected protocol failures with a printed message ---- - ## Setup files ### Transport diff --git a/.claude/rules/java-code-samples.md b/.claude/rules/java-code-samples.md index 7ffa5a03e8..0639199245 100644 --- a/.claude/rules/java-code-samples.md +++ b/.claude/rules/java-code-samples.md @@ -7,8 +7,6 @@ paths: Java samples currently exist only in tutorial form. If a setup-style script is added later, mirror the speed-first conventions from the other language rules (parallelism, minimal output, no transaction printing) and update this rule. ---- - ## Project & build - Maven (not Gradle); each sample is a self-contained Maven project under its language folder (e.g., `_code-samples/credential/java/`) diff --git a/.claude/rules/javascript-code-samples.md b/.claude/rules/javascript-code-samples.md index 57b42c7ef8..3634f1b7e1 100644 --- a/.claude/rules/javascript-code-samples.md +++ b/.claude/rules/javascript-code-samples.md @@ -14,8 +14,6 @@ Code samples come in **two flavors** with very different conventions. Identify w If a file isn't clearly one or the other, default to tutorial conventions. ---- - ## Shared across both flavors - Library: `xrpl ^4.6.0` in `package.json` @@ -29,8 +27,6 @@ If a file isn't clearly one or the other, default to tutorial conventions. - Setup JSON keys use `camelCase` (`loanBroker`, `credentialIssuer`, `mptID`, `vaultID`, `loanBrokerID`) - End with `await client.disconnect()` ---- - ## Tutorial files ### Structure @@ -83,8 +79,6 @@ const fullySigned = xrpl.signLoanSetByCounterparty(borrower, decoded) await client.submitAndWait(fullySigned.tx) ``` ---- - ## Setup files ### Speed-first patterns (required when possible) diff --git a/.claude/rules/python-code-samples.md b/.claude/rules/python-code-samples.md index a76aafd193..56974ea09c 100644 --- a/.claude/rules/python-code-samples.md +++ b/.claude/rules/python-code-samples.md @@ -14,8 +14,6 @@ Code samples come in **two flavors** with very different conventions. Identify w If a file isn't clearly one or the other, default to tutorial conventions. ---- - ## Shared across both flavors - Library: `xrpl-py>=4.5.0` in `requirements.txt` @@ -23,8 +21,6 @@ If a file isn't clearly one or the other, default to tutorial conventions. - Variables use `snake_case` (e.g., `loan_broker`, `mpt_id`, `vault_id`, `loan_broker_id`) - Tutorial scripts read setup data; setup scripts write it. Both use the same JSON key style (`snake_case`: `loan_broker`, `credential_issuer`, `mpt_id`, `domain_id`, `loan_broker_id`). ---- - ## Tutorial files ### Client and runtime @@ -64,8 +60,6 @@ If a file isn't clearly one or the other, default to tutorial conventions. ``` - Don't wrap in try/except — let failures be visible ---- - ## Setup files ### Client and runtime From f1310f97bb81b1ec97d6b10095a9f88b214fb847 Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Thu, 28 May 2026 16:48:23 -0700 Subject: [PATCH 05/10] major revamp of js rules --- .claude/rules/javascript-code-samples.md | 209 +++++++++++++++++------ 1 file changed, 159 insertions(+), 50 deletions(-) diff --git a/.claude/rules/javascript-code-samples.md b/.claude/rules/javascript-code-samples.md index 3634f1b7e1..aaa3164643 100644 --- a/.claude/rules/javascript-code-samples.md +++ b/.claude/rules/javascript-code-samples.md @@ -5,98 +5,207 @@ paths: # XRPL JavaScript Code Sample Conventions +These are not concrete rules. If the user gives you directions that contradict these rules, note it to the user but their instructions take priority. + Code samples come in **two flavors** with very different conventions. Identify which you're writing first. | Flavor | Filename pattern | Audience | Priority | |---|---|---|---| | **Tutorial** | `.js` (e.g., `createLoanBroker.js`) | A dev reading & learning the protocol | Clarity over speed | -| **Setup** | `Setup.js` (e.g., `lendingSetup.js`) | A dev who never opens this file — runs to prep accounts/credentials/MPT/vault | Speed over clarity | +| **Setup** | `Setup.js` (e.g., `lendingSetup.js`) | A dev who never opens this file — runs to prep network data (accounts, tokens, etc.) for all tutorials in the subject folder | Speed over clarity | -If a file isn't clearly one or the other, default to tutorial conventions. +If a file isn't clearly one or the other, prompt the user for clarity. -## Shared across both flavors +## Style -- Library: `xrpl ^4.6.0` in `package.json` -- ESM modules: `"type": "module"`; `import xrpl from 'xrpl'` -- Top-level `await` (Node 18+); no `main()` wrapper -- Devnet WebSocket: `wss://s.devnet.rippletest.net:51233` -- Style: 2-space indent, single quotes, no semicolons +### Formatting +- 2-space indent +- Single quotes +- No semicolons + +### Naming - File names: `camelCase` (e.g., `createLoan.js`) - Variables: `camelCase` with acronyms uppercased — `loanBroker`, `mptID`, `vaultID`, `loanBrokerID`, `credentialIssuer` -- Transaction object keys are XRPL native PascalCase (`TransactionType`, `Account`, `Amount`) — never transform them -- Setup JSON keys use `camelCase` (`loanBroker`, `credentialIssuer`, `mptID`, `vaultID`, `loanBrokerID`) -- End with `await client.disconnect()` +- Transaction object keys: XRPL native PascalCase (`TransactionType`, `Account`, `Amount`) — never transform them +- Setup JSON keys: `camelCase` (`loanBroker`, `credentialIssuer`, `mptID`, `vaultID`, `loanBrokerID`) + +## Structure + +### Folder layout + +Each code sample lives at `_code-samples//js/`: + +``` +_code-samples//js/ +├── README.md +├── package.json +├── package-lock.json +├── Setup.js # Optional — runs once to prep network state +├── Setup.json # Auto-generated by the setup script and is gitignored +└── .js # Tutorial scripts (one per user action) +``` + +### README + +`README.md` is the entry point for a reader running the samples. + +1. Title: `# Examples (JavaScript)` +2. One-sentence description listing what the directory demonstrates +3. `## Setup` section with a single `npm i` fenced block +4. One `##` section per tutorial script, in the order a reader should run them: + - Heading describes the action (e.g., `## Create a Loan Broker`), not the filename + - Fenced ```sh``` block with `node .js` + - One-sentence summary of what the script will output + - Fenced ```sh``` block showing actual expected console output (real addresses, tx IDs, JSON dumps — captured from a successful sample code run) +5. `---` separator between tutorial sections + +The expected-output blocks document the golden path. Update them when a script's output format changes. + +### package.json + +Minimal — no `scripts`, no `devDependencies`, no `version` unless an external dep requires one: + +```json +{ + "name": "-examples", + "description": "Example code for .", + "dependencies": { + "xrpl": "^4.6.0" + }, + "type": "module" +} +``` ## Tutorial files +Before creating or updating a sample code file, confirm with the user: +1. High-level steps required +2. Which network to use. Devnet (`wss://s.devnet.rippletest.net:51233`) or Testnet (`wss://s.altnet.rippletest.net:51233`) + ### Structure + 1. Multi-line `// IMPORTANT:` header explaining what the script demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") 2. Imports -3. `const client = new xrpl.Client('wss://...')` + `await client.connect()` -4. Auto-run setup script if the JSON data is missing: - ```js - if (!fs.existsSync('lendingSetup.json')) { - console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`) - execSync('node lendingSetup.js', { stdio: 'inherit' }) - } - ``` -5. Load wallets and IDs from setup JSON via `xrpl.Wallet.fromSeed(setupData.X.seed)` -6. One transaction per major step, each with its own visible `=== Header ===` - -### Output style -- Section comments in code use `// Section description ----------------------` (with the dash visual) -- Print a `=== Section Name ===` banner before each major step: +3. Connect to the network: ```js - console.log(`\n=== Preparing LoanBrokerSet transaction ===\n`) + // Connect to the network ---------------------- + const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233') + await client.connect() ``` -- Build transactions as plain object literals; validate before submitting: +4. (Optional) If the tutorial is using setup script data: ```js + // This step checks for the necessary setup data to run the lending tutorials. + // If missing, lendingSetup.js will generate the data. + if (!fs.existsSync('lendingSetup.json')) { + console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`) + execSync('node lendingSetup.js', { stdio: 'inherit' }) + } + + // Load preconfigured accounts and VaultID. + const setupData = JSON.parse(fs.readFileSync('lendingSetup.json', 'utf8')) + + // You can replace these values with your own + const loanBroker = xrpl.Wallet.fromSeed(setupData.loanBroker.seed) + const vaultID = setupData.vaultID + + console.log(`\nLoan broker/vault owner address: ${loanBroker.address}`) + console.log(`Vault ID: ${vaultID}`) + ``` +5. (Optional) If no setup data is used, fund new wallets for the tutorial. + ```js + const { wallet } = await client.fundWallet() + ``` + If creating multiple wallets, parallelize the process: + ```js + const [ + { wallet: loanBroker }, + { wallet: borrower }, + { wallet: depositor }, + { wallet: credentialIssuer } + ] = await Promise.all([ + client.fundWallet(), + client.fundWallet(), + client.fundWallet(), + client.fundWallet() + ]) + ``` +6. Tutorial code steps. +7. Disconnect at the end of the code sample. + ```js + await client.disconnect() + ``` + +### Tutorial code step guide + +- Before each major step, add a comment and print a section banner. +- Build transactions as plain object literals and validate before submitting: + ```js + // Prepare LoanBrokerSet transaction ---------------------- + console.log(`\n=== Preparing LoanBrokerSet transaction ===\n`) + const loanBrokerSetTx = { + TransactionType: 'LoanBrokerSet', + Account: loanBroker.address, + VaultID: vaultID, + ManagementFeeRate: 1000 + } + + // Validate the transaction structure before submitting xrpl.validate(loanBrokerSetTx) console.log(JSON.stringify(loanBrokerSetTx, null, 2)) ``` -- Submit with the explicit options form: `await client.submitAndWait(tx, { wallet, autofill: true })` - -### Result handling -- Always check `result.meta.TransactionResult` explicitly: +- Autofill transactions and handle results by checking for `tesSUCCESS` and exiting on failure: ```js + // Submit, sign, and wait for validation ---------------------- + console.log(`\n=== Submitting LoanBrokerSet transaction ===\n`) + const submitResponse = await client.submitAndWait(loanBrokerSetTx, { + wallet: loanBroker, + autofill: true + }) if (submitResponse.result.meta.TransactionResult !== 'tesSUCCESS') { const resultCode = submitResponse.result.meta.TransactionResult console.error('Error: Unable to create loan broker:', resultCode) await client.disconnect() process.exit(1) } + console.log('Loan broker created successfully!') + ``` +- Extract metadata relevant to the tutorial: + ```js + // Extract loan broker information from the transaction result + console.log(`\n=== Loan Broker Information ===\n`) + const loanBrokerNode = submitResponse.result.meta.AffectedNodes.find(node => + node.CreatedNode?.LedgerEntryType === 'LoanBroker' + ) + console.log(`LoanBroker ID: ${loanBrokerNode.CreatedNode.LedgerIndex}`) + console.log(`LoanBroker Psuedo-Account Address: ${loanBrokerNode.CreatedNode.NewFields.Account}`) ``` -- Always `await client.disconnect()` before `process.exit(1)` -- Don't wrap in try/catch — let failures be visible - -### Dual-signed transactions -For `LoanSet`-style transactions that require both broker and counterparty signatures: -```js -const tx = await client.autofill({ ... }) -const brokerSigned = loanBroker.sign(tx) -const decoded = xrpl.decode(brokerSigned.tx_blob) -const fullySigned = xrpl.signLoanSetByCounterparty(borrower, decoded) -await client.submitAndWait(fullySigned.tx) -``` ## Setup files -### Speed-first patterns (required when possible) +### Speed-first patterns when possible - Run independent transactions concurrently with `await Promise.all([...])` - When fanning out parallel transactions from the same account, batch them first via `TicketCreate` with `TicketCount: N`, then pass `Sequence: 0` and `TicketSequence: ticketArr[i]` on each parallel tx -- (Future) When the `Batch` transaction is re-enabled, prefer it over tickets for atomic multi-tx groups - Destructure response arrays: `const [{ wallet: loanBroker }, { wallet: borrower }] = await Promise.all([client.fundWallet(), client.fundWallet()])` -- Use object property shorthand when key and variable match: `{ domainID, mptID, vaultID, loanBrokerID }` -### Output style -- Top comment: single line, `// Setup script for lending protocol tutorials` -- Only output is a carriage-return progress indicator: `process.stdout.write('Setting up tutorial: N/7\r')` between phases +### Setup code guide +- Top comment: single line, `// Setup script for tutorials` +- Only output is a carriage-return progress indicator: `process.stdout.write('Setting up tutorial: N/D\r')` between phases, where N is the step number and D is the total steps - No `=== Section ===` banners, no `xrpl.validate(tx)`, no transaction dumps — the user never sees this file's output beyond the progress counter - Section comments in code are short: `// Section description` (no dash visual) - If a library call emits a warning the reader doesn't need (e.g., `LoanSet` autofill warning), silence it locally with a one-line comment explaining why: `console.warn = () => {}` ### Output file -At the end, write all wallet/ID data the tutorials will need: +At the end, write all data the tutorials will need: ```js +const setupData = { + description: 'This file is auto-generated by lendingSetup.js. It stores XRPL account info for use in lending protocol tutorials.', + loanBroker: { + address: loanBroker.address, + seed: loanBroker.seed + }, + domainID, + mptID +} + fs.writeFileSync('lendingSetup.json', JSON.stringify(setupData, null, 2)) ``` From d067d89eb0a86730f1d4fc4fe57d25d1840fc608 Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Thu, 28 May 2026 17:27:54 -0700 Subject: [PATCH 06/10] major revamp of python code rules --- .claude/rules/javascript-code-samples.md | 2 +- .claude/rules/python-code-samples.md | 197 +++++++++++++++++------ 2 files changed, 153 insertions(+), 46 deletions(-) diff --git a/.claude/rules/javascript-code-samples.md b/.claude/rules/javascript-code-samples.md index aaa3164643..61a50c4436 100644 --- a/.claude/rules/javascript-code-samples.md +++ b/.claude/rules/javascript-code-samples.md @@ -42,7 +42,7 @@ _code-samples//js/ ├── package-lock.json ├── Setup.js # Optional — runs once to prep network state ├── Setup.json # Auto-generated by the setup script and is gitignored -└── .js # Tutorial scripts (one per user action) +└── .js # Tutorial scripts (one per user action) ``` ### README diff --git a/.claude/rules/python-code-samples.md b/.claude/rules/python-code-samples.md index 56974ea09c..79f1cf61bd 100644 --- a/.claude/rules/python-code-samples.md +++ b/.claude/rules/python-code-samples.md @@ -5,83 +5,190 @@ paths: # XRPL Python Code Sample Conventions +These are not concrete rules. If the user gives you directions that contradict these rules, note it to the user but their instructions take priority. + Code samples come in **two flavors** with very different conventions. Identify which you're writing first. | Flavor | Filename pattern | Audience | Priority | |---|---|---|---| | **Tutorial** | `_.py` (e.g., `create_loan_broker.py`) | A dev reading & learning the protocol | Clarity over speed | -| **Setup** | `_setup.py` (e.g., `lending_setup.py`) | A dev who never opens this file — runs to prep accounts/credentials/MPT/vault | Speed over clarity | +| **Setup** | `_setup.py` (e.g., `lending_setup.py`) | A dev who never opens this file — runs to prep network data (accounts, tokens, etc.) for all tutorials in the subject folder | Speed over clarity | -If a file isn't clearly one or the other, default to tutorial conventions. +If a file isn't clearly one or the other, prompt the user for clarity. -## Shared across both flavors +## Style -- Library: `xrpl-py>=4.5.0` in `requirements.txt` -- File names use `snake_case` -- Variables use `snake_case` (e.g., `loan_broker`, `mpt_id`, `vault_id`, `loan_broker_id`) -- Tutorial scripts read setup data; setup scripts write it. Both use the same JSON key style (`snake_case`: `loan_broker`, `credential_issuer`, `mpt_id`, `domain_id`, `loan_broker_id`). +### Formatting +- 4-space indent +- Double quotes +- Trailing commas on multi-line collections and call args + +### Naming +- File names: `snake_case` (e.g., `create_loan.py`) +- Variables: `snake_case` — `loan_broker`, `mpt_id`, `vault_id`, `loan_broker_id`, `credential_issuer` +- Transaction model fields: `snake_case` per `xrpl-py` (e.g., `account=`, `vault_id=`, `management_fee_rate=`) — `xrpl-py` handles the PascalCase translation on the wire +- Setup JSON keys: `snake_case` (`loan_broker`, `credential_issuer`, `mpt_id`, `vault_id`, `loan_broker_id`) + +## Structure + +### Folder layout + +Each code sample lives at `_code-samples//py/`: + +``` +_code-samples//py/ +├── README.md +├── requirements.txt +├── _setup.py # Optional — runs once to prep network state +├── _setup.json # Auto-generated by the setup script and is gitignored +└── _.py # Tutorial scripts (one per user action) +``` + +### README + +`README.md` is the entry point for a reader running the samples. + +1. Title: `# Examples (Python)` +2. One-sentence description listing what the directory demonstrates +3. `## Setup` section with a single fenced block showing venv creation + `pip install -r requirements.txt`: + ```sh + python3 -m venv .venv + source .venv/bin/activate + pip install -r requirements.txt + ``` +4. One `##` section per tutorial script, in the order a reader should run them: + - Heading describes the action (e.g., `## Create a Loan Broker`), not the filename + - Fenced ```sh``` block with `python3 .py` + - One-sentence summary of what the script will output + - Fenced ```sh``` block showing actual expected console output (real addresses, tx IDs, JSON dumps — captured from a successful sample code run) +5. `---` separator between tutorial sections + +The expected-output blocks document the golden path. Update them when a script's output format changes. + +### requirements.txt + +Minimal — pin only what's needed: + +``` +xrpl-py>=4.5.0 +``` + +Add other deps only when a sample requires them. ## Tutorial files -### Client and runtime -- **Sync API only** — `from xrpl.clients import JsonRpcClient`, `from xrpl.transaction import submit_and_wait`, `from xrpl.wallet import Wallet`. No `asyncio`. -- Devnet RPC endpoint: `https://s.devnet.rippletest.net:51234` -- No `main()` function, no `if __name__ == "__main__":` — script runs top-to-bottom +**Sync API only** — `xrpl.clients.JsonRpcClient`, `xrpl.transaction.submit_and_wait`, `xrpl.wallet.Wallet`, `xrpl.wallet.generate_faucet_wallet`. No `asyncio`, no `main()` wrapper, no `if __name__ == "__main__":` — scripts run top-to-bottom and exit. + +Before creating or updating a sample code file, confirm with the user: +1. High-level steps required +2. Which network to use. Devnet (`https://s.devnet.rippletest.net:51234`) or Testnet (`https://s.altnet.rippletest.net:51234`) ### Structure + 1. Multi-line `# IMPORTANT:` header explaining what the script demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") -2. Imports -3. Client setup -4. Auto-run setup script if the JSON data is missing: - ```python - if not os.path.exists("lending_setup.json"): - print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n") - subprocess.run([sys.executable, "lending_setup.py"], check=True) - ``` -5. Load wallets and IDs from setup JSON via `Wallet.from_seed(setup_data["..."]["seed"])` -6. One transaction per major step, each with its own visible `=== Header ===` - -### Output style -- Section comments in code use `# Section description ----------------------` (with the dash visual) -- Print a `=== Section Name ===` banner before each major step: +2. Imports — stdlib first, blank line, then `xrpl` imports +3. Set up the client: ```python - print("\n=== Preparing LoanBrokerSet transaction ===\n") + # Set up client ---------------------- + client = JsonRpcClient("https://s.devnet.rippletest.net:51234") ``` -- Print every transaction as JSON before submitting: `print(json.dumps(tx.to_xrpl(), indent=2))` -- Print a `=== Section Name ===` banner before extracting results too - -### Result handling -- Always check `submit_response.result["meta"]["TransactionResult"]` explicitly: +4. (Optional) If the tutorial is using setup script data: ```python + # This step checks for the necessary setup data to run the lending tutorials. + # If missing, lending_setup.py will generate the data. + if not os.path.exists("lending_setup.json"): + print("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n") + subprocess.run([sys.executable, "lending_setup.py"], check=True) + + # Load preconfigured accounts and vault_id. + with open("lending_setup.json") as f: + setup_data = json.load(f) + + # You can replace these values with your own. + loan_broker = Wallet.from_seed(setup_data["loan_broker"]["seed"]) + vault_id = setup_data["vault_id"] + + print(f"\nLoan broker/vault owner address: {loan_broker.address}") + print(f"Vault ID: {vault_id}") + ``` +5. (Optional) If no setup data is used, fund new wallets for the tutorial. + ```python + wallet = generate_faucet_wallet(client) + ``` + Funding multiple wallets requires sequential calls. +6. Tutorial code steps. + +### Tutorial code step guide + +- Before each major step, add a comment and print a section banner. +- Build transactions as `xrpl-py` model instances and print the wire form before submitting: + ```python + # Prepare LoanBrokerSet transaction ---------------------- + print("\n=== Preparing LoanBrokerSet transaction ===\n") + loan_broker_set_tx = LoanBrokerSet( + account=loan_broker.address, + vault_id=vault_id, + management_fee_rate=1000, + ) + + print(json.dumps(loan_broker_set_tx.to_xrpl(), indent=2)) + ``` +- Submit with `submit_and_wait` and handle results by checking for `tesSUCCESS` and exiting on failure: + ```python + # Submit, sign, and wait for validation ---------------------- + print("\n=== Submitting LoanBrokerSet transaction ===\n") + submit_response = submit_and_wait(loan_broker_set_tx, client, loan_broker) + if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS": result_code = submit_response.result["meta"]["TransactionResult"] print(f"Error: Unable to create loan broker: {result_code}") sys.exit(1) + + print("Loan broker created successfully!") + ``` +- Extract metadata relevant to the tutorial: + ```python + # Extract loan broker information from the transaction result + print("\n=== Loan Broker Information ===\n") + loan_broker_node = next( + node for node in submit_response.result["meta"]["AffectedNodes"] + if node.get("CreatedNode", {}).get("LedgerEntryType") == "LoanBroker" + ) + print(f"LoanBroker ID: {loan_broker_node['CreatedNode']['LedgerIndex']}") + print(f"LoanBroker Psuedo-Account Address: {loan_broker_node['CreatedNode']['NewFields']['Account']}") ``` -- Don't wrap in try/except — let failures be visible ## Setup files -### Client and runtime -- **Async API only** — `from xrpl.asyncio.clients import AsyncWebsocketClient`, `xrpl.asyncio.wallet.generate_faucet_wallet`, `xrpl.asyncio.transaction` (`submit_and_wait`, `autofill`, `sign`) -- Devnet WebSocket endpoint: `wss://s.devnet.rippletest.net:51233` -- Wrap in `async def main(): ...` with `async with AsyncWebsocketClient(URL) as client:` at the top, and `asyncio.run(main())` at the bottom +**Async API only** — `xrpl.asyncio.clients.AsyncWebsocketClient`, `xrpl.asyncio.wallet.generate_faucet_wallet`, `xrpl.asyncio.transaction` (`submit_and_wait`, `autofill`, `sign`). Wrap in `async def main():` + `async with AsyncWebsocketClient(WSS_URL) as client:`, and call `asyncio.run(main())` at the bottom. -### Speed-first patterns (required when possible) +WebSocket endpoints: Devnet (`wss://s.devnet.rippletest.net:51233`) or Testnet (`wss://s.altnet.rippletest.net:51233`). + +### Speed-first patterns when possible - Run independent transactions concurrently with `await asyncio.gather(...)` -- When fanning out parallel transactions from the same account, batch them first via `TicketCreate(ticket_count=N)`, then pass `ticket_sequence=...` and `sequence=0` on each parallel tx -- (Future) When the `Batch` transaction is re-enabled, prefer it over tickets for atomic multi-tx groups -- Imports of models go in one alphabetized parenthesized block +- When fanning out parallel transactions from the same account, batch them first via `TicketCreate(ticket_count=N)`, then pass `sequence=0` and `ticket_sequence=...` on each parallel tx +- Destructure gather results: `loan_broker, borrower = await asyncio.gather(generate_faucet_wallet(client), generate_faucet_wallet(client))` +- Group `xrpl.models` imports into a single alphabetized parenthesized block -### Output style -- Top comment: single line, `# Setup script for lending protocol tutorials` -- Only output is a carriage-return progress indicator: `print("Setting up tutorial: N/7", end="\r")` between phases -- No `=== Section ===` banners, no transaction dumps — the user never sees this file's output beyond the progress counter +### Setup code guide +- Top comment: single line, `# Setup script for tutorials` +- Only output is a carriage-return progress indicator: `print("Setting up tutorial: N/D", end="\r")` between phases, where N is the step number and D is the total steps +- No `=== Section ===` banners, no transaction dumps — the reader never sees this file's output beyond the progress counter - Section comments in code are short: `# Section description` (no dash visual) ### Output file -At the end, write all wallet/ID data the tutorials will need: +At the end, write all data the tutorials will need: ```python +setup_data = { + "description": "This file is auto-generated by lending_setup.py. It stores XRPL account info for use in lending protocol tutorials.", + "loan_broker": { + "address": loan_broker.address, + "seed": loan_broker.seed, + }, + "domain_id": domain_id, + "mpt_id": mpt_id, +} + with open("lending_setup.json", "w") as f: json.dump(setup_data, f, indent=2) ``` From fe2236c3f7e2d4ca97e450b17484deaad477298c Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Fri, 29 May 2026 18:20:58 -0700 Subject: [PATCH 07/10] major revamp of go code rules --- .claude/rules/go-code-samples.md | 349 ++++++++++++++++++++++--------- 1 file changed, 254 insertions(+), 95 deletions(-) diff --git a/.claude/rules/go-code-samples.md b/.claude/rules/go-code-samples.md index af001f3497..383fc205b3 100644 --- a/.claude/rules/go-code-samples.md +++ b/.claude/rules/go-code-samples.md @@ -5,27 +5,82 @@ paths: # XRPL Go Code Sample Conventions +These are not concrete rules. If the user gives you directions that contradict these rules, note it to the user but their instructions take priority. + Code samples come in **two flavors** with very different conventions. Identify which you're writing first. | Flavor | Folder pattern | Audience | Priority | |---|---|---|---| | **Tutorial** | `-/main.go` (e.g., `create-loan-broker/main.go`) | A dev reading & learning the protocol | Clarity over speed | -| **Setup** | `-setup/main.go` (e.g., `lending-setup/main.go`) | A dev who never opens this file — runs to prep accounts/credentials/MPT/vault | Speed over clarity | +| **Setup** | `-setup/main.go` (e.g., `lending-setup/main.go`) | A dev who never opens this file — runs to prep network data (accounts, tokens, etc.) for all tutorials in the subject folder | Speed over clarity | -If a file isn't clearly one or the other, default to tutorial conventions. +If a file isn't clearly one or the other, prompt the user for clarity. -## Shared across both flavors +## Style -- Library: `github.com/Peersyst/xrpl-go v0.1.17` in `go.mod` -- `go 1.24.3` minimum -- Each command is its own `kebab-case` subdir with one `main.go`; users run with `go run ./` from the language root -- One `go.mod` per sample folder at the language root (e.g., `_code-samples/lending-protocol/go/go.mod`) +### Formatting +- `gofmt`-formatted (tabs, standard layout) + +### Naming +- Folder/binary names: `kebab-case` (e.g., `create-loan-broker/`) - Variables: `camelCase` with acronyms uppercased — `loanBrokerWallet`, `mptID`, `vaultID`, `loanBrokerID`, `credIssuerWallet` -- Wallet variables always end in `Wallet` (e.g., `loanBrokerWallet`) to distinguish from `loanBrokerID` -- Setup JSON keys use `camelCase` (`loanBroker`, `credentialIssuer`, `mptID`, `vaultID`, `loanBrokerID`) — matches the JS convention +- Transaction struct fields: native XRPL `PascalCase` (`Account`, `VaultID`, `ManagementFeeRate`) — matches both Go's exported-field rule and the XRPL wire format. Common fields (`Account`, `Sequence`, `Fee`, `TicketSequence`) go in the embedded `BaseTx` substruct. +- Setup JSON keys: `camelCase` (`loanBroker`, `credentialIssuer`, `mptID`, `vaultID`, `loanBrokerID`) + +## Structure + +### Folder layout + +Each code sample lives at `_code-samples//go/`. Every command is its own `kebab-case` subdir containing one `main.go`: + +``` +_code-samples//go/ +├── README.md +├── go.mod +├── go.sum # Auto-generated by `go mod tidy`; gitignored +├── -setup/ +│ └── main.go # Optional — runs once to prep network state +├── -setup.json # Auto-generated by the setup script; gitignored +└── -/ + └── main.go # Tutorial commands (one per user action) +``` + +Run any command with `go run ./-` from the language root directory. + +### README + +`README.md` is the entry point for a reader running the samples. + +1. Title: `# Examples (Go)` +2. One-sentence description listing what the directory demonstrates +3. `## Setup` section with the note "All commands should be run from this `go/` directory." and a `go mod tidy` fenced block +4. One `##` section per tutorial command, in the order a reader should run them: + - Heading describes the action (e.g., `## Create a Loan Broker`), not the folder name + - Fenced ```sh``` block with `go run ./-` + - One-sentence summary of what the command will output + - Fenced ```sh``` block showing actual expected console output (real addresses, tx IDs, JSON dumps — captured from a successful sample code run) +5. `---` separator between tutorial sections + +The expected-output blocks document the golden path. Update them when a command's output format changes. + +### go.mod + +One `go.mod` per sample at the language root. Pin the xrpl-go version: + +``` +module github.com/XRPLF + +go 1.24.3 + +require github.com/Peersyst/xrpl-go v0.1.17 +``` + +`go mod tidy` populates the indirect dependency block at the bottom — that block is auto-managed and shouldn't be hand-edited. ### Pointer helper -For any `main.go` that sets optional pointer fields, include this helper near the top: + +Any `main.go` that sets optional pointer fields includes this helper near the top of the file: + ```go // ptr is a helper that returns a pointer to the given value, // used for setting optional transaction fields in Go. @@ -34,108 +89,212 @@ func ptr[T any](v T) *T { return &v } ## Tutorial files -### Transport -- **WebSocket** — `github.com/Peersyst/xrpl-go/xrpl/websocket` -- Devnet endpoint: `wss://s.devnet.rippletest.net:51233` -- Pattern: - ```go - client := websocket.NewClient( - websocket.NewClientConfig(). - WithHost("wss://s.devnet.rippletest.net:51233"), - ) - defer client.Disconnect() - if err := client.Connect(); err != nil { panic(err) } - ``` +**WebSocket client** — `github.com/Peersyst/xrpl-go/xrpl/websocket`. Always wrap with `defer client.Disconnect()` right after `NewClient` so the connection closes on any exit path. + +Before creating or updating a sample code file, confirm with the user: +1. High-level steps required +2. Which network to use. Devnet (`wss://s.devnet.rippletest.net:51233`) or Testnet (`wss://s.altnet.rippletest.net:51233`) ### Structure -1. Multi-line `// IMPORTANT:` header explaining what the script demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") + +1. Multi-line `// IMPORTANT:` header explaining what the command demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") 2. `package main` + imports -3. Client connection (with `defer client.Disconnect()`) -4. Auto-run setup if data is missing: - ```go - if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) { - fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n") - cmd := exec.Command("go", "run", "./lending-setup") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { panic(err) } - } - ``` -5. Load wallet seeds and IDs from setup JSON via `wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))` -6. One transaction per major step, each with its own visible `=== Header ===` +3. Connect to the network: + ```go + // Connect to the network ---------------------- + client := websocket.NewClient( + websocket.NewClientConfig(). + WithHost("wss://s.devnet.rippletest.net:51233"), + ) + defer client.Disconnect() -### Output style -- Section comments in code use `// Section description ----------------------` (with the dash visual) -- Print a `=== Section Name ===` banner before each major step: - ```go - fmt.Printf("\n=== Preparing LoanBrokerSet transaction ===\n\n") - ``` -- Print every transaction as JSON before submitting: - ```go - flatTx := tx.Flatten() - txJSON, _ := json.MarshalIndent(flatTx, "", " ") - fmt.Printf("%s\n", string(txJSON)) - ``` -- Submit with the explicit options form: `client.SubmitTxAndWait(flatTx, &wstypes.SubmitOptions{Autofill: true, Wallet: &w})` - -### Result handling -- Always check `Meta.TransactionResult` explicitly: - ```go - if resp.Meta.TransactionResult != "tesSUCCESS" { - fmt.Printf("Error: Unable to create loan broker: %s\n", resp.Meta.TransactionResult) - os.Exit(1) + if err := client.Connect(); err != nil { + panic(err) + } + ``` +4. (Optional) If the tutorial is using setup data: + ```go + // Check for setup data; run lending-setup if missing + if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) { + fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n") + cmd := exec.Command("go", "run", "./lending-setup") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + panic(err) + } + } + + // Load preconfigured accounts and VaultID + data, err := os.ReadFile("lending-setup.json") + if err != nil { + panic(err) + } + var setup map[string]any + if err := json.Unmarshal(data, &setup); err != nil { + panic(err) + } + + // You can replace these values with your own + loanBrokerWallet, err := wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string)) + if err != nil { + panic(err) + } + vaultID := setup["vaultID"].(string) + ``` +5. (Optional) If the tutorial funds its own wallets instead of loading them from setup data, add `WithFaucetProvider` to the client config in step 3 and fund wallets after `client.Connect()`: + ```go + // In step 3, extend the client config chain: + client := websocket.NewClient( + websocket.NewClientConfig(). + WithHost("wss://s.devnet.rippletest.net:51233"). + WithFaucetProvider(faucet.NewDevnetFaucetProvider()), + ) + + // After client.Connect(): + testWallet, err := wallet.New(crypto.ED25519()) + if err != nil { + panic(err) + } + if err := client.FundWallet(&testWallet); err != nil { + panic(err) + } + ``` +6. Tutorial code steps. + +### Tutorial code step guide + +- Before each major step, add a comment and print a section banner. +- Build transactions as model structs, call `.Flatten()`, and print before submitting: + ```go + // Prepare LoanBrokerSet transaction ---------------------- + fmt.Printf("\n=== Preparing LoanBrokerSet transaction ===\n\n") + mgmtFeeRate := types.InterestRate(1000) + loanBrokerSetTx := transaction.LoanBrokerSet{ + BaseTx: transaction.BaseTx{ + Account: loanBrokerWallet.ClassicAddress, + }, + VaultID: vaultID, + ManagementFeeRate: &mgmtFeeRate, + } + + // Flatten() converts the struct to a map and adds the TransactionType field + flatLoanBrokerSetTx := loanBrokerSetTx.Flatten() + loanBrokerSetTxJSON, _ := json.MarshalIndent(flatLoanBrokerSetTx, "", " ") + fmt.Printf("%s\n", string(loanBrokerSetTxJSON)) + ``` +- Submit with `SubmitTxAndWait` and handle results by checking for `tesSUCCESS` and exiting on failure: + ```go + // Submit, sign, and wait for validation ---------------------- + fmt.Printf("\n=== Submitting LoanBrokerSet transaction ===\n\n") + loanBrokerSetResponse, err := client.SubmitTxAndWait(flatLoanBrokerSetTx, &wstypes.SubmitOptions{ + Autofill: true, + Wallet: &loanBrokerWallet, + }) + if err != nil { + panic(err) + } + + if loanBrokerSetResponse.Meta.TransactionResult != "tesSUCCESS" { + fmt.Printf("Error: Unable to create loan broker: %s\n", loanBrokerSetResponse.Meta.TransactionResult) + os.Exit(1) + } + fmt.Printf("Loan broker created successfully!\n") + ``` +- Extract metadata relevant to the tutorial: + ```go + // Extract loan broker information from the transaction result + fmt.Printf("\n=== Loan Broker Information ===\n\n") + for _, node := range loanBrokerSetResponse.Meta.AffectedNodes { + if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "LoanBroker" { + fmt.Printf("LoanBroker ID: %s\n", node.CreatedNode.LedgerIndex) + fmt.Printf("LoanBroker Pseudo-Account Address: %s\n", node.CreatedNode.NewFields["Account"]) + break + } } ``` -- Use `panic(err)` for unexpected errors (network/marshal failures), `os.Exit(1)` for expected protocol failures with a printed message ## Setup files -### Transport -- **RPC, not WebSocket** — `github.com/Peersyst/xrpl-go/xrpl/rpc` -- Devnet endpoint: `https://s.devnet.rippletest.net:51234` -- Pattern: - ```go - cfg, err := rpc.NewClientConfig( - "https://s.devnet.rippletest.net:51234", - rpc.WithFaucetProvider(faucet.NewDevnetFaucetProvider()), - ) - if err != nil { panic(err) } - client := rpc.NewClient(cfg) +**RPC client** — `github.com/Peersyst/xrpl-go/xrpl/rpc` with a faucet provider. Setup uses RPC, not WebSocket: xrpl-go's WS client is built on `gorilla/websocket`, which doesn't allow concurrent writes on a single connection. - submitOpts := func(w *wallet.Wallet) *rpctypes.SubmitOptions { - return &rpctypes.SubmitOptions{Autofill: true, Wallet: w} +```go +cfg, err := rpc.NewClientConfig( + "https://s.devnet.rippletest.net:51234", + rpc.WithFaucetProvider(faucet.NewDevnetFaucetProvider()), +) +if err != nil { + panic(err) +} +client := rpc.NewClient(cfg) + +submitOpts := func(w *wallet.Wallet) *rpctypes.SubmitOptions { + return &rpctypes.SubmitOptions{Autofill: true, Wallet: w} +} +``` + +RPC endpoints: Devnet (`https://s.devnet.rippletest.net:51234`) or Testnet (`https://s.altnet.rippletest.net:51234`). + +### Speed-first patterns when possible +- Use goroutines + buffered channels for fan-out parallelism (not `errgroup` or `sync.WaitGroup`) +- Each goroutine handles one independent task — often a single transaction, sometimes a multi-step pipeline wrapped in a helper closure +- When fanning out parallel transactions from the same account, create tickets first via `TicketCreate` with `TicketCount: N`, then set `Sequence: 0` and `TicketSequence: ...` on the `BaseTx` of each parallel tx +- xrpl-go doesn't include a fund-and-wait helper, use this: + ```go + // Create and fund wallets concurrently + createAndFund := func(ch chan<- wallet.Wallet) { + w, err := wallet.New(crypto.ED25519()) + if err != nil { + panic(err) + } + if err := client.FundWallet(&w); err != nil { + panic(err) + } + // Poll until account is validated on ledger + funded := false + for range 20 { + _, err := client.Request(&account.InfoRequest{ + Account: w.GetAddress(), + LedgerIndex: common.Validated, + }) + if err == nil { + funded = true + break + } + time.Sleep(time.Second) + } + if !funded { + panic("Issue funding account: " + w.GetAddress().String()) + } + ch <- w } ``` -- No `defer client.Disconnect()` — RPC is HTTP -### Speed-first patterns (required when possible) -- Use goroutines + buffered channels for fan-out parallelism (not `errgroup` or `sync.WaitGroup`) -- Each goroutine handles one transaction and sends its result (or `struct{}{}` for void) into a channel; main drains the channels in order -- When fanning out parallel transactions from the same account, create tickets first via `TicketCreate` with `TicketCount: N`, then set `Sequence: 0` and `TicketSequence: ...` on the `BaseTx` of each parallel tx -- (Future) When the `Batch` transaction is re-enabled, prefer it over tickets for atomic multi-tx groups -- `FundWallet` returns before the account is queryable on ledger — poll `account.InfoRequest` with `LedgerIndex: common.Validated` (up to ~20 seconds) before using the wallet - -### Output style -- Top comment: single line, `// Setup script for lending protocol tutorials` above `package main` -- Only output is a carriage-return progress indicator: `fmt.Print("Setting up tutorial: N/7\r")` between phases -- No `=== Section ===` banners, no transaction dumps — the user never sees this file's output beyond the progress counter +### Setup code guide +- Top comment: single line, `// Setup script for tutorials` above `package main` +- Only output is a carriage-return progress indicator: `fmt.Print("Setting up tutorial: N/D\r")` between phases, where N is the step number and D is the total steps +- No `=== Section ===` banners, no transaction dumps — the reader never sees this file's output beyond the progress counter - Section comments in code are short: `// Section description` (no dash visual) +- Use `panic(err)` on every error path — setup is fail-fast, and a panic surfaces the failing line clearly. Don't silently `continue` or `_ = err`. ### Output file -At the end, write all wallet/ID data the tutorials will need. Use an anonymous struct with `json:"camelCase"` tags so field order is preserved: +At the end, write all data the tutorials will need. Use an anonymous struct with `json:"camelCase"` tags so field order is preserved: + ```go setupData := struct { - Description string `json:"description"` - LoanBroker any `json:"loanBroker"` - DomainID string `json:"domainID"` - MptID string `json:"mptID"` - VaultID string `json:"vaultID"` - LoanBrokerID string `json:"loanBrokerID"` + Description string `json:"description"` + LoanBroker any `json:"loanBroker"` + DomainID string `json:"domainID"` + MptID string `json:"mptID"` + VaultID string `json:"vaultID"` + LoanBrokerID string `json:"loanBrokerID"` }{ ... } -jsonData, _ := json.MarshalIndent(setupData, "", " ") -os.WriteFile("lending-setup.json", jsonData, 0644) -``` -- Output filename uses `kebab-case` (matches the subdir name): `lending-setup.json` -### Error handling -- Use `panic(err)` on every error path — these are tutorial samples and a panic surfaces the failing line clearly. Don't silently `continue` or `_ = err`. +jsonData, err := json.MarshalIndent(setupData, "", " ") +if err != nil { + panic(err) +} +if err := os.WriteFile("lending-setup.json", jsonData, 0644); err != nil { + panic(err) +} +``` From 5302657867c8135624705f368b1e86a3d6c5f597 Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Fri, 29 May 2026 23:15:13 -0700 Subject: [PATCH 08/10] major overhaul of java rules --- .claude/rules/java-code-samples.md | 488 ++++++++++++++++++----------- 1 file changed, 304 insertions(+), 184 deletions(-) diff --git a/.claude/rules/java-code-samples.md b/.claude/rules/java-code-samples.md index 0639199245..db41de9e86 100644 --- a/.claude/rules/java-code-samples.md +++ b/.claude/rules/java-code-samples.md @@ -5,21 +5,102 @@ paths: # XRPL Java Code Sample Conventions -Java samples currently exist only in tutorial form. If a setup-style script is added later, mirror the speed-first conventions from the other language rules (parallelism, minimal output, no transaction printing) and update this rule. +These are conventions, not hard rules. If the user gives you directions that contradict these conventions, note it to the user but their instructions take priority. -## Project & build +Java samples currently exist only in **tutorial form**. -- Maven (not Gradle); each sample is a self-contained Maven project under its language folder (e.g., `_code-samples/credential/java/`) -- Standard layout: `src/main/java/com/example/xrpl/.java` + `src/main/resources/logback.xml` -- Java 11 (`11`); UTF-8 -- Single dependency: `org.xrpl:xrpl4j-client` 6.0.0 -- Plugin: `org.codehaus.mojo:exec-maven-plugin` 3.3.0 -- Install: `mvn install` -- Run: `mvn exec:java -Dexec.mainClass=com.example.xrpl.` +## Style -## Logging +### Formatting +- 2-space indent +- UTF-8 source encoding (declared in `pom.xml`) -`src/main/resources/logback.xml` quiets xrpl4j wire-level chatter so tutorial output stays readable: +### Naming +- Class/file: `PascalCase` verb-noun (e.g., `ManageCredentials.java`); one public class per file +- Variables: `camelCase` (e.g., `issuerAddress`, `subjectFuture`) +- Constants: `UPPER_SNAKE_CASE` for `static final` (e.g., `NETWORK_URL`, `FAUCET_URL`, `EXPLORER_BASE`, `CREDENTIAL_TYPE`) +- Package: `com.example.xrpl` +- Imports: two blocks separated by a blank line — all non-`java.*` imports together (alphabetized: `com.*`, `okhttp3.*`, `org.*`, etc.), then `java.*` last. No wildcard imports. + +## Structure + +### Folder layout + +Each code sample lives at `_code-samples//java/` as a self-contained Maven project: + +``` +_code-samples//java/ +├── README.md +├── pom.xml +├── target/ # Maven build output; gitignored +└── src/main/ + ├── java/com/example/xrpl/ + │ └── .java # Tutorial samples (one class per user action) + └── resources/ + └── logback.xml +``` + +Run any sample with `mvn exec:java -Dexec.mainClass=com.example.xrpl.` from the language root directory. + +### README + +`README.md` is the entry point for a reader running the samples. + +1. Title: `# Example (Java)` +2. One-sentence description listing what the directory demonstrates +3. `## Setup` section with an `mvn install` fenced block +4. One `##` section per tutorial sample, in the order a reader should run them: + - Heading is a human-readable phrase for the action (e.g., `## Manage Credentials`, `## Issue a Token`) — not a code identifier like `## ManageCredentials` + - Fenced ```sh``` block with `mvn exec:java -Dexec.mainClass=com.example.xrpl.` + - One-sentence summary of what the script will output + - Fenced ```sh``` block showing actual expected console output (real addresses, tx hashes, JSON dumps, explorer links — captured from a successful sample code run) +5. `---` separator between tutorial sections + +### pom.xml + +Java 11, UTF-8, single xrpl4j dependency, exec plugin for `mvn exec:java`: + +```xml + + + 4.0.0 + + com.example + -samples + 1.0.0 + jar + + + 11 + UTF-8 + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.3.0 + + + + + + + org.xrpl + xrpl4j-client + 6.0.0 + + + +``` + +### logback.xml + +`src/main/resources/logback.xml` quiets xrpl4j's DEBUG chatter so tutorial output stays readable: ```xml @@ -31,202 +112,241 @@ Java samples currently exist only in tutorial form. If a setup-style script is a %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + ``` -## Naming +## Tutorial files -- Class/file: `PascalCase` verb-noun (e.g., `ManageCredentials.java`); one class per sample -- Package: `com.example.xrpl` -- Imports: alphabetical within groups; no wildcard imports; `java.*` last +**xrpl4j sync client** — `org.xrpl.xrpl4j.client.XrplClient`. Use `CompletableFuture.supplyAsync` + `allOf().join()` for parallel work (e.g., funding multiple accounts). -## Network +Before creating or updating a sample code file, confirm with the user: +1. Topic directory name (e.g., `credential`) and class name(s) (e.g., `ManageCredentials`) +2. High-level steps required +3. Which network to use. Devnet (`https://s.devnet.rippletest.net:51234`) or Testnet (`https://s.altnet.rippletest.net:51234`) -**Testnet, not devnet** (py/js/go samples use devnet; Java uses testnet). Declare constants near the top of the class: +### Structure -```java -private static final HttpUrl NETWORK_URL = HttpUrl.get("https://s.altnet.rippletest.net:51234/"); -private static final HttpUrl FAUCET_URL = HttpUrl.get("https://faucet.altnet.rippletest.net"); -private static final String EXPLORER_BASE = "https://testnet.xrpl.org/transactions/"; -``` - -## Class structure - -`main()` wraps `run()` and unwraps `CompletionException` so async failures print the same clean message as sync ones: - -```java -public static void main(String[] args) { - try { - run(); - } catch (Exception e) { - Throwable cause = (e instanceof CompletionException && e.getCause() != null) - ? e.getCause() : e; - System.err.println("Error: " + cause.getMessage()); - System.exit(1); - } -} -``` - -- `private static void run()` holds the main flow -- Helpers below a `// ===== Helper functions =====` divider, each prefixed with a one-line comment - -## Concurrency - -Use `CompletableFuture.supplyAsync` + `allOf().join()` for parallel work (e.g., funding multiple accounts): - -```java -CompletableFuture issuerFuture = CompletableFuture.supplyAsync( - () -> createAndFundWallet(xrplClient)); -CompletableFuture subjectFuture = CompletableFuture.supplyAsync( - () -> createAndFundWallet(xrplClient)); -CompletableFuture.allOf(issuerFuture, subjectFuture).join(); - -KeyPair issuer = issuerFuture.join(); -KeyPair subject = subjectFuture.join(); -``` - -## Wallets - -Faucet funding isn't queryable immediately — poll until the account is visible on a validated ledger: - -```java -private static KeyPair createAndFundWallet(XrplClient xrplClient) { - KeyPair keyPair = Seed.ed25519Seed().deriveKeyPair(); - Address address = keyPair.publicKey().deriveAddress(); - FaucetClient.construct(FAUCET_URL).fundAccount(FundAccountRequest.of(address)); - - for (int attempt = 0; attempt < 20; attempt++) { +1. Class-level Javadoc explaining what the sample demonstrates (and any preconditions, if applicable) +2. `package com.example.xrpl;` + imports (alphabetical within groups, `java.*` last) +3. Class declaration with `NETWORK_URL`, `FAUCET_URL`, `EXPLORER_BASE`, and tutorial-specific constants at top: + ```java + private static final HttpUrl NETWORK_URL = HttpUrl.get("https://s.altnet.rippletest.net:51234/"); + private static final HttpUrl FAUCET_URL = HttpUrl.get("https://faucet.altnet.rippletest.net"); + private static final String EXPLORER_BASE = "https://testnet.xrpl.org/transactions/"; + ``` +4. `main()` wraps `run()` and unwraps `CompletionException` so async failures print the same clean message as sync ones: + ```java + public static void main(String[] args) { try { - xrplClient.accountInfo(AccountInfoRequestParams.builder() + run(); + } catch (Exception e) { + // Unwrap CompletionException so async failures print the same clean message + // as sync failures. CompletableFuture.join() wraps exceptions in CompletionException + Throwable cause = (e instanceof CompletionException && e.getCause() != null) + ? e.getCause() : e; + System.err.println("Error: " + cause.getMessage()); + System.exit(1); + } + } + ``` +5. `private static void run()` holds the main flow. +6. Connect to network and fund however many accounts the sample needs. Fund in parallel via `CompletableFuture.supplyAsync` + `allOf().join()` when there's more than one. Two-account example: + ```java + // ----- Connect to Testnet and fund accounts ----- + XrplClient xrplClient = new XrplClient(NETWORK_URL); + System.out.println("\n=== Funding issuer and subject accounts on Testnet ===\n"); + + CompletableFuture issuerFuture = CompletableFuture.supplyAsync( + () -> createAndFundWallet(xrplClient)); + CompletableFuture subjectFuture = CompletableFuture.supplyAsync( + () -> createAndFundWallet(xrplClient)); + CompletableFuture.allOf(issuerFuture, subjectFuture).join(); + + KeyPair issuer = issuerFuture.join(); + KeyPair subject = subjectFuture.join(); + Address issuerAddress = issuer.publicKey().deriveAddress(); + Address subjectAddress = subject.publicKey().deriveAddress(); + System.out.println("Issuer: " + issuerAddress); + System.out.println("Subject: " + subjectAddress); + ``` +7. Tutorial code steps. +8. Useful helpers below a `// ===== Helper functions =====` divider, each prefixed with a one-line comment. Copy any helpers the sample uses — the signatures and bodies below are canonical; only include the ones you call: + ```java + // ===== Helper functions ===== + + // Generates a new Ed25519 keypair, funds it from the Testnet faucet, and + // returns the keypair once the account is visible on a validated ledger. + private static KeyPair createAndFundWallet(XrplClient xrplClient) { + KeyPair keyPair = Seed.ed25519Seed().deriveKeyPair(); + Address address = keyPair.publicKey().deriveAddress(); + FaucetClient faucetClient = FaucetClient.construct(FAUCET_URL); + faucetClient.fundAccount(FundAccountRequest.of(address)); + + for (int attempt = 0; attempt < 20; attempt++) { + try { + xrplClient.accountInfo(AccountInfoRequestParams.builder() + .account(address) + .ledgerSpecifier(LedgerSpecifier.VALIDATED) + .build()); + return keyPair; + } catch (JsonRpcClientErrorException notYetVisible) { + try { + Thread.sleep(1_000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Account polling interrupted for " + address + ". " + e.getMessage(), e); + } + } + } + throw new IllegalStateException("Faucet funding for " + address + " did not confirm in time."); + } + + // Fetches the next transaction sequence number of an address from + // the latest validated ledger. + private static UnsignedInteger accountSequence(XrplClient xrplClient, Address address) { + try { + AccountInfoResult info = xrplClient.accountInfo(AccountInfoRequestParams.builder() .account(address) .ledgerSpecifier(LedgerSpecifier.VALIDATED) .build()); - return keyPair; - } catch (JsonRpcClientErrorException notYetVisible) { - try { Thread.sleep(1_000L); } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Account polling interrupted for " + address + ". " + e.getMessage(), e); + return info.accountData().sequence(); + } catch (JsonRpcClientErrorException e) { + throw new RuntimeException("Failed to fetch account sequence for " + address + ". " + e.getMessage(), e); + } + } + + // Fetches the current network fee and returns the recommended fee for + // a standard (non-multisig, non-batch) transaction. + private static XrpCurrencyAmount recommendedFee(XrplClient xrplClient) { + try { + return FeeUtils.computeNetworkFees(xrplClient.fee()).recommendedFee(); + } catch (JsonRpcClientErrorException e) { + throw new RuntimeException("Failed to fetch network fee. " + e.getMessage(), e); + } + } + + // Computes a safe LastLedgerSequence for a new transaction. The + // latest validated ledger index plus a small buffer (20 ledgers). + private static UnsignedInteger lastLedgerSequence(XrplClient xrplClient) { + try { + UnsignedInteger validatedLedger = xrplClient.ledger(LedgerRequestParams.builder() + .ledgerSpecifier(LedgerSpecifier.VALIDATED) + .build()) + .ledgerIndexSafe() + .unsignedIntegerValue(); + return validatedLedger.plus(UnsignedInteger.valueOf(20)); + } catch (JsonRpcClientErrorException e) { + throw new RuntimeException("Failed to compute LastLedgerSequence. " + e.getMessage(), e); + } + } + + // Prints a transaction as a formatted JSON. + private static void printTransactionJson(Transaction tx) { + try { + System.out.println(ObjectMapperFactory.create().writerWithDefaultPrettyPrinter().writeValueAsString(tx)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize transaction JSON. " + e.getMessage(), e); + } + } + + // Signs and submits a transaction, then polls the network until + // the transaction reaches a validated state. + private static TransactionResult signSubmitAndWait( + XrplClient xrplClient, + KeyPair signer, + T transaction, + Class transactionType + ) { + SignatureService signatureService = new BcSignatureService(); + + UnsignedInteger lastLedgerSequence = transaction.lastLedgerSequence() + .orElseThrow(() -> new IllegalArgumentException( + "Must set LastLedgerSequence for polling expiration")); + + try { + SingleSignedTransaction signed = signatureService.sign(signer.privateKey(), transaction); + SubmitResult submit = xrplClient.submit(signed); + + if (!TransactionResultCodes.TES_SUCCESS.equals(submit.engineResult())) { + throw new IllegalStateException( + "Submission rejected. " + submit.engineResult() + " — " + submit.engineResultMessage()); } + + Finality finality; + do { + Thread.sleep(1_000L); + finality = xrplClient.isFinal( + signed.hash(), + submit.validatedLedgerIndex(), + lastLedgerSequence, + transaction.sequence(), + signer.publicKey().deriveAddress() + ); + } while (finality.finalityStatus() == FinalityStatus.NOT_FINAL); + + if (finality.finalityStatus() != FinalityStatus.VALIDATED_SUCCESS) { + throw new IllegalStateException( + "Transaction failed with status " + finality.finalityStatus() + + ". Result code: " + finality.resultCode().orElse("unknown")); + } + + // Retrieve the transaction result; isFinal() only returns finality status + return xrplClient.transaction( + TransactionRequestParams.of(signed.hash()), transactionType); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Transaction polling interrupted. " + e.getMessage(), e); + } catch (JsonRpcClientErrorException | JsonProcessingException e) { + throw new RuntimeException("Transaction processing failed. " + e.getMessage(), e); } } - throw new IllegalStateException("Faucet funding for " + address + " did not confirm in time."); -} -``` -## Transactions - -Builder pattern; always set `sequence`, `fee`, `lastLedgerSequence`, `signingPublicKey` from shared helpers: - -```java -CredentialCreate createTx = CredentialCreate.builder() - .account(issuerAddress) - .subject(subjectAddress) - .credentialType(CREDENTIAL_TYPE) - .sequence(accountSequence(xrplClient, issuerAddress)) - .fee(recommendedFee(xrplClient)) - .lastLedgerSequence(lastLedgerSequence(xrplClient)) - .signingPublicKey(issuer.publicKey()) - .build(); -``` - -Shared helpers (one per concern; each wraps `JsonRpcClientErrorException` with context): - -```java -// Next transaction sequence from the validated ledger. -private static UnsignedInteger accountSequence(XrplClient client, Address address) { /* ... */ } - -// Recommended fee for a standard (non-multisig, non-batch) transaction. -private static XrpCurrencyAmount recommendedFee(XrplClient client) { - return FeeUtils.computeNetworkFees(client.fee()).recommendedFee(); -} - -// Validated ledger index + 20-ledger buffer. -private static UnsignedInteger lastLedgerSequence(XrplClient client) { /* ... */ } -``` - -## Submission & finality - -Sign with `BcSignatureService`, submit, poll `isFinal` until validated, then fetch the full result (`isFinal` only returns status): - -```java -private static TransactionResult signSubmitAndWait( - XrplClient xrplClient, KeyPair signer, T transaction, Class transactionType -) { - SignatureService signatureService = new BcSignatureService(); - UnsignedInteger lastLedgerSequence = transaction.lastLedgerSequence() - .orElseThrow(() -> new IllegalArgumentException("Must set LastLedgerSequence for polling expiration")); - - try { - SingleSignedTransaction signed = signatureService.sign(signer.privateKey(), transaction); - SubmitResult submit = xrplClient.submit(signed); - - if (!TransactionResultCodes.TES_SUCCESS.equals(submit.engineResult())) { - throw new IllegalStateException( - "Submission rejected. " + submit.engineResult() + " — " + submit.engineResultMessage()); + // Checks for a tesSUCCESS result code. If true, prints an explorer + // link. Otherwise, throws an error. + private static void requireSuccess(TransactionResult result) { + String code = result.metadata().get().transactionResult(); + String txType = result.transaction().transactionType().value(); + if (!TransactionResultCodes.TES_SUCCESS.equals(code)) { + throw new IllegalStateException(txType + " failed with error code " + code); } - - Finality finality; - do { - Thread.sleep(1_000L); - finality = xrplClient.isFinal( - signed.hash(), submit.validatedLedgerIndex(), lastLedgerSequence, - transaction.sequence(), signer.publicKey().deriveAddress()); - } while (finality.finalityStatus() == FinalityStatus.NOT_FINAL); - - if (finality.finalityStatus() != FinalityStatus.VALIDATED_SUCCESS) { - throw new IllegalStateException("Transaction failed with status " + finality.finalityStatus() - + ". Result code: " + finality.resultCode().orElse("unknown")); - } - - // isFinal only returns status; fetch the full transaction body separately - return xrplClient.transaction(TransactionRequestParams.of(signed.hash()), transactionType); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Transaction polling interrupted. " + e.getMessage(), e); - } catch (JsonRpcClientErrorException | JsonProcessingException e) { - throw new RuntimeException("Transaction processing failed. " + e.getMessage(), e); + System.out.println(txType + " succeeded!"); + System.out.println("Explorer: " + EXPLORER_BASE + result.hash()); } -} -``` + ``` -## Output +### Tutorial code step guide -- Section banners: `System.out.println("\n=== Section Name ===\n");` -- Section comments in code: `// ----- Section description -----` (dashes on **both** sides — Java's style; py/js/go use single-side dashes) -- Print every transaction as pretty JSON before submitting: +- Before each major step, add a section comment and print a banner. Strict format: `// ----- Title -----` on its own line, then `System.out.println("\n=== Title ===\n");` immediately after. The title text should match between the two. +- Build transactions with the builder pattern; always set `sequence`, `fee`, `lastLedgerSequence`, `signingPublicKey` from shared helpers, then print as pretty JSON: + ```java + // ----- Prepare CredentialCreate transaction ----- + System.out.println("\n=== Preparing CredentialCreate transaction ===\n"); -```java -private static void printTransactionJson(Transaction tx) { - try { - System.out.println(ObjectMapperFactory.create().writerWithDefaultPrettyPrinter().writeValueAsString(tx)); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to serialize transaction JSON. " + e.getMessage(), e); - } -} -``` + CredentialCreate createTx = CredentialCreate.builder() + .account(issuerAddress) + .subject(subjectAddress) + .credentialType(CREDENTIAL_TYPE) + .sequence(accountSequence(xrplClient, issuerAddress)) + .fee(recommendedFee(xrplClient)) + .lastLedgerSequence(lastLedgerSequence(xrplClient)) + .signingPublicKey(issuer.publicKey()) + .build(); + printTransactionJson(createTx); + ``` +- Sign, submit, and wait via the shared `signSubmitAndWait` helper, then verify success with `requireSuccess`: + ```java + // ----- Sign, submit, and wait for CredentialCreate validation ----- + System.out.println("\n=== Submitting CredentialCreate transaction ===\n"); -- On success, print "succeeded!" line + explorer link via `requireSuccess`: + TransactionResult createResult = signSubmitAndWait( + xrplClient, issuer, createTx, CredentialCreate.class); -```java -private static void requireSuccess(TransactionResult result) { - String code = result.metadata().get().transactionResult(); - String txType = result.transaction().transactionType().value(); - if (!TransactionResultCodes.TES_SUCCESS.equals(code)) { - throw new IllegalStateException(txType + " failed with error code " + code); - } - System.out.println(txType + " succeeded!"); - System.out.println("Explorer: " + EXPLORER_BASE + result.hash()); -} -``` - -## Error handling - -- Wrap checked exceptions with context: `throw new RuntimeException("Failed to ... " + e.getMessage(), e)` -- On `InterruptedException`: always `Thread.currentThread().interrupt()` before rethrowing -- All failures bubble to `main()`'s catch; clean stderr message + exit 1 -- Don't try/catch around individual transactions inside `run()` — let failures be visible + requireSuccess(createResult); + ``` From 1d883201818eac7b8541abe931398c6939957a4c Mon Sep 17 00:00:00 2001 From: oeggert <117319296+oeggert@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:19:39 -0700 Subject: [PATCH 09/10] Update .claude/rules/javascript-code-samples.md Co-authored-by: Maria Shodunke --- .claude/rules/javascript-code-samples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/rules/javascript-code-samples.md b/.claude/rules/javascript-code-samples.md index 61a50c4436..0ca090a82f 100644 --- a/.claude/rules/javascript-code-samples.md +++ b/.claude/rules/javascript-code-samples.md @@ -177,7 +177,7 @@ Before creating or updating a sample code file, confirm with the user: node.CreatedNode?.LedgerEntryType === 'LoanBroker' ) console.log(`LoanBroker ID: ${loanBrokerNode.CreatedNode.LedgerIndex}`) - console.log(`LoanBroker Psuedo-Account Address: ${loanBrokerNode.CreatedNode.NewFields.Account}`) + console.log(`LoanBroker Pseudo-Account Address: ${loanBrokerNode.CreatedNode.NewFields.Account}`) ``` ## Setup files From 1146c5faf5de34161224cf5e3072ef5a8c67b310 Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Fri, 5 Jun 2026 12:13:01 -0700 Subject: [PATCH 10/10] create baseline code guide --- .claude/rules/code-guide.md | 20 ++++++++++++++++++++ .claude/rules/go-code-samples.md | 8 +------- .claude/rules/java-code-samples.md | 11 ++--------- .claude/rules/javascript-code-samples.md | 8 +------- .claude/rules/python-code-samples.md | 8 +------- 5 files changed, 25 insertions(+), 30 deletions(-) create mode 100644 .claude/rules/code-guide.md diff --git a/.claude/rules/code-guide.md b/.claude/rules/code-guide.md new file mode 100644 index 0000000000..228838bdc8 --- /dev/null +++ b/.claude/rules/code-guide.md @@ -0,0 +1,20 @@ +--- +paths: + - "_code-samples/**" +--- + +# XRPL Sample Code Baseline + +This guide and language-specific rules are not concrete. If the user gives you directions that contradict these rules, note it to the user but their instructions take priority. + +1. Before creating or updating a sample code file, confirm with the user: + - High-level steps required + - Which network to use: + | | Devnet | Testnet | + |:-----|:-------|:--------| + | HTTP | `https://s.devnet.rippletest.net:51234` | `https://s.altnet.rippletest.net:51234` | + | WSS | `wss://s.devnet.rippletest.net:51233` | `wss://s.altnet.rippletest.net:51233` | +2. Check the SDK library documentation via the `context7` MCP server. + - Get the latest stable version to pin the SDK at. + - Get all relevant documentation for the transactions and helpers used. + - Prefer the SDK's built-in helpers over custom code. Only use your own helpers if the library has no equivalent. diff --git a/.claude/rules/go-code-samples.md b/.claude/rules/go-code-samples.md index 383fc205b3..57222e2a6e 100644 --- a/.claude/rules/go-code-samples.md +++ b/.claude/rules/go-code-samples.md @@ -5,8 +5,6 @@ paths: # XRPL Go Code Sample Conventions -These are not concrete rules. If the user gives you directions that contradict these rules, note it to the user but their instructions take priority. - Code samples come in **two flavors** with very different conventions. Identify which you're writing first. | Flavor | Folder pattern | Audience | Priority | @@ -72,7 +70,7 @@ module github.com/XRPLF go 1.24.3 -require github.com/Peersyst/xrpl-go v0.1.17 +require github.com/Peersyst/xrpl-go v ``` `go mod tidy` populates the indirect dependency block at the bottom — that block is auto-managed and shouldn't be hand-edited. @@ -91,10 +89,6 @@ func ptr[T any](v T) *T { return &v } **WebSocket client** — `github.com/Peersyst/xrpl-go/xrpl/websocket`. Always wrap with `defer client.Disconnect()` right after `NewClient` so the connection closes on any exit path. -Before creating or updating a sample code file, confirm with the user: -1. High-level steps required -2. Which network to use. Devnet (`wss://s.devnet.rippletest.net:51233`) or Testnet (`wss://s.altnet.rippletest.net:51233`) - ### Structure 1. Multi-line `// IMPORTANT:` header explaining what the command demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") diff --git a/.claude/rules/java-code-samples.md b/.claude/rules/java-code-samples.md index db41de9e86..18e102f7e5 100644 --- a/.claude/rules/java-code-samples.md +++ b/.claude/rules/java-code-samples.md @@ -5,8 +5,6 @@ paths: # XRPL Java Code Sample Conventions -These are conventions, not hard rules. If the user gives you directions that contradict these conventions, note it to the user but their instructions take priority. - Java samples currently exist only in **tutorial form**. ## Style @@ -69,7 +67,7 @@ Java 11, UTF-8, single xrpl4j dependency, exec plugin for `mvn exec:java`: 4.0.0 com.example - -samples + {topic}-samples 1.0.0 jar @@ -92,7 +90,7 @@ Java 11, UTF-8, single xrpl4j dependency, exec plugin for `mvn exec:java`: org.xrpl xrpl4j-client - 6.0.0 + {latest-stable} @@ -125,11 +123,6 @@ Java 11, UTF-8, single xrpl4j dependency, exec plugin for `mvn exec:java`: **xrpl4j sync client** — `org.xrpl.xrpl4j.client.XrplClient`. Use `CompletableFuture.supplyAsync` + `allOf().join()` for parallel work (e.g., funding multiple accounts). -Before creating or updating a sample code file, confirm with the user: -1. Topic directory name (e.g., `credential`) and class name(s) (e.g., `ManageCredentials`) -2. High-level steps required -3. Which network to use. Devnet (`https://s.devnet.rippletest.net:51234`) or Testnet (`https://s.altnet.rippletest.net:51234`) - ### Structure 1. Class-level Javadoc explaining what the sample demonstrates (and any preconditions, if applicable) diff --git a/.claude/rules/javascript-code-samples.md b/.claude/rules/javascript-code-samples.md index 0ca090a82f..e6fd3b57a3 100644 --- a/.claude/rules/javascript-code-samples.md +++ b/.claude/rules/javascript-code-samples.md @@ -5,8 +5,6 @@ paths: # XRPL JavaScript Code Sample Conventions -These are not concrete rules. If the user gives you directions that contradict these rules, note it to the user but their instructions take priority. - Code samples come in **two flavors** with very different conventions. Identify which you're writing first. | Flavor | Filename pattern | Audience | Priority | @@ -70,7 +68,7 @@ Minimal — no `scripts`, no `devDependencies`, no `version` unless an external "name": "-examples", "description": "Example code for .", "dependencies": { - "xrpl": "^4.6.0" + "xrpl": "^" }, "type": "module" } @@ -78,10 +76,6 @@ Minimal — no `scripts`, no `devDependencies`, no `version` unless an external ## Tutorial files -Before creating or updating a sample code file, confirm with the user: -1. High-level steps required -2. Which network to use. Devnet (`wss://s.devnet.rippletest.net:51233`) or Testnet (`wss://s.altnet.rippletest.net:51233`) - ### Structure 1. Multi-line `// IMPORTANT:` header explaining what the script demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") diff --git a/.claude/rules/python-code-samples.md b/.claude/rules/python-code-samples.md index 79f1cf61bd..298e07f697 100644 --- a/.claude/rules/python-code-samples.md +++ b/.claude/rules/python-code-samples.md @@ -5,8 +5,6 @@ paths: # XRPL Python Code Sample Conventions -These are not concrete rules. If the user gives you directions that contradict these rules, note it to the user but their instructions take priority. - Code samples come in **two flavors** with very different conventions. Identify which you're writing first. | Flavor | Filename pattern | Audience | Priority | @@ -70,7 +68,7 @@ The expected-output blocks document the golden path. Update them when a script's Minimal — pin only what's needed: ``` -xrpl-py>=4.5.0 +xrpl-py>= ``` Add other deps only when a sample requires them. @@ -79,10 +77,6 @@ Add other deps only when a sample requires them. **Sync API only** — `xrpl.clients.JsonRpcClient`, `xrpl.transaction.submit_and_wait`, `xrpl.wallet.Wallet`, `xrpl.wallet.generate_faucet_wallet`. No `asyncio`, no `main()` wrapper, no `if __name__ == "__main__":` — scripts run top-to-bottom and exit. -Before creating or updating a sample code file, confirm with the user: -1. High-level steps required -2. Which network to use. Devnet (`https://s.devnet.rippletest.net:51234`) or Testnet (`https://s.altnet.rippletest.net:51234`) - ### Structure 1. Multi-line `# IMPORTANT:` header explaining what the script demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault")