コンテンツへスキップ
最終更新:

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/<topic>/java/ as a self-contained Maven project:

_code-samples/<topic>/java/
├── README.md
├── pom.xml
├── target/                                 # Maven build output; gitignored
└── src/main/
    ├── java/com/example/xrpl/
    │   └── <ClassName>.java                # Tutorial samples (one class per user action)
    └── resources/
        └── logback.xml

Run any sample with mvn exec:java -Dexec.mainClass=com.example.xrpl.<ClassName> from the language root directory.

README

README.md is the entry point for a reader running the samples.

  1. Title: # <Topic> 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.<ClassName>
  • 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)
  1. --- separator between tutorial sections

pom.xml

Java 11, UTF-8, single xrpl4j dependency, exec plugin for mvn exec:java:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>{topic}-samples</artifactId> <!-- e.g., credential-samples; change per directory -->
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.release>11</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.3.0</version>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>org.xrpl</groupId>
      <artifactId>xrpl4j-client</artifactId>
      <version>{latest-stable}</version>
    </dependency>
  </dependencies>
</project>

logback.xml

src/main/resources/logback.xml quiets xrpl4j's DEBUG chatter so tutorial output stays readable:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Quiets xrpl4j's DEBUG chatter so tutorial output stays readable.
     Raise xrpl4j to DEBUG to see wire-level transaction details. -->
<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <logger name="org.xrpl.xrpl4j" level="WARN"/>

  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
  </root>
</configuration>

Tutorial files

xrpl4j sync clientorg.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:
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/";
  1. main() wraps run() and unwraps CompletionException so async failures print the same clean message as sync ones:
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);
  }
}
  1. private static void run() holds the main flow.
  2. 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:
// ----- 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<KeyPair> issuerFuture = CompletableFuture.supplyAsync(
  () -> createAndFundWallet(xrplClient));
CompletableFuture<KeyPair> 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);
  1. Tutorial code steps.
  2. 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:
// ===== 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 <T extends Transaction> TransactionResult<T> signSubmitAndWait(
  XrplClient xrplClient,
  KeyPair signer,
  T transaction,
  Class<T> transactionType
) {
  SignatureService<PrivateKey> signatureService = new BcSignatureService();

  UnsignedInteger lastLedgerSequence = transaction.lastLedgerSequence()
    .orElseThrow(() -> new IllegalArgumentException(
      "Must set LastLedgerSequence for polling expiration"));

  try {
    SingleSignedTransaction<T> signed = signatureService.sign(signer.privateKey(), transaction);
    SubmitResult<T> 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:
    // ----- 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:
    // ----- Sign, submit, and wait for CredentialCreate validation -----
    System.out.println("\n=== Submitting CredentialCreate transaction ===\n");
    
    TransactionResult<CredentialCreate> createResult = signSubmitAndWait(
      xrplClient, issuer, createTx, CredentialCreate.class);
    
    requireSuccess(createResult);