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/.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 new file mode 100644 index 0000000000..57222e2a6e --- /dev/null +++ b/.claude/rules/go-code-samples.md @@ -0,0 +1,294 @@ +--- +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 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, prompt the user for clarity. + +## Style + +### 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` +- 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 v +``` + +`go mod tidy` populates the indirect dependency block at the bottom — that block is auto-managed and shouldn't be hand-edited. + +### Pointer helper + +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. +func ptr[T any](v T) *T { return &v } +``` + +## Tutorial files + +**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. + +### 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") +2. `package main` + imports +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() + + 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 + } + } + ``` + +## Setup files + +**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. + +```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 + } + ``` + +### 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 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, err := json.MarshalIndent(setupData, "", " ") +if err != nil { + panic(err) +} +if err := os.WriteFile("lending-setup.json", jsonData, 0644); err != nil { + panic(err) +} +``` diff --git a/.claude/rules/java-code-samples.md b/.claude/rules/java-code-samples.md new file mode 100644 index 0000000000..18e102f7e5 --- /dev/null +++ b/.claude/rules/java-code-samples.md @@ -0,0 +1,345 @@ +--- +paths: + - "_code-samples/**/*.java" +--- + +# XRPL Java Code Sample Conventions + +Java samples currently exist only in **tutorial form**. + +## Style + +### Formatting +- 2-space indent +- UTF-8 source encoding (declared in `pom.xml`) + +### 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 + {topic}-samples + 1.0.0 + jar + + + 11 + UTF-8 + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.3.0 + + + + + + + org.xrpl + xrpl4j-client + {latest-stable} + + + +``` + +### logback.xml + +`src/main/resources/logback.xml` quiets xrpl4j's DEBUG chatter so tutorial output stays readable: + +```xml + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + +``` + +## Tutorial files + +**xrpl4j sync client** — `org.xrpl.xrpl4j.client.XrplClient`. Use `CompletableFuture.supplyAsync` + `allOf().join()` for parallel work (e.g., funding multiple accounts). + +### Structure + +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 { + 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 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); + } + } + + // 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); + } + System.out.println(txType + " succeeded!"); + System.out.println("Explorer: " + EXPLORER_BASE + result.hash()); + } + ``` + +### Tutorial code step guide + +- 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"); + + 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"); + + TransactionResult createResult = signSubmitAndWait( + xrplClient, issuer, createTx, CredentialCreate.class); + + requireSuccess(createResult); + ``` diff --git a/.claude/rules/javascript-code-samples.md b/.claude/rules/javascript-code-samples.md new file mode 100644 index 0000000000..e6fd3b57a3 --- /dev/null +++ b/.claude/rules/javascript-code-samples.md @@ -0,0 +1,205 @@ +--- +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 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, prompt the user for clarity. + +## Style + +### 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: 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": "^" + }, + "type": "module" +} +``` + +## 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. Connect to the network: + ```js + // Connect to the network ---------------------- + const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233') + await client.connect() + ``` +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)) + ``` +- 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 Pseudo-Account Address: ${loanBrokerNode.CreatedNode.NewFields.Account}`) + ``` + +## Setup files + +### 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 +- Destructure response arrays: `const [{ wallet: loanBroker }, { wallet: borrower }] = await Promise.all([client.fundWallet(), client.fundWallet()])` + +### 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 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)) +``` diff --git a/.claude/rules/python-code-samples.md b/.claude/rules/python-code-samples.md new file mode 100644 index 0000000000..298e07f697 --- /dev/null +++ b/.claude/rules/python-code-samples.md @@ -0,0 +1,188 @@ +--- +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 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, prompt the user for clarity. + +## Style + +### 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>= +``` + +Add other deps only when a sample requires them. + +## Tutorial files + +**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. + +### 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 — stdlib first, blank line, then `xrpl` imports +3. Set up the client: + ```python + # Set up client ---------------------- + client = JsonRpcClient("https://s.devnet.rippletest.net:51234") + ``` +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']}") + ``` + +## Setup files + +**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. + +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 `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 + +### 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 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) +``` 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