From 583b1696800db06f63749ade10348bb9ba8b884b Mon Sep 17 00:00:00 2001 From: Oliver Eggert Date: Sat, 18 Apr 2026 13:53:00 -0700 Subject: [PATCH] update code sample to use up-to-date functions and code conventions --- .../com/example/xrpl/ManageCredentials.java | 169 +++++++++++------- .../java/src/main/resources/logback.xml | 2 +- 2 files changed, 107 insertions(+), 64 deletions(-) diff --git a/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java b/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java index 563cafb17f..3155198bcf 100644 --- a/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java +++ b/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java @@ -1,7 +1,10 @@ package com.example.xrpl; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.primitives.UnsignedInteger; import okhttp3.HttpUrl; +import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.client.XrplClient; import org.xrpl.xrpl4j.client.faucet.FaucetClient; import org.xrpl.xrpl4j.client.faucet.FundAccountRequest; @@ -15,23 +18,27 @@ import org.xrpl.xrpl4j.model.client.accounts.AccountInfoRequestParams; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.fees.FeeUtils; import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams; +import org.xrpl.xrpl4j.model.client.Finality; +import org.xrpl.xrpl4j.model.client.FinalityStatus; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.client.transactions.TransactionRequestParams; import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; import org.xrpl.xrpl4j.model.transactions.Address; import org.xrpl.xrpl4j.model.transactions.CredentialAccept; import org.xrpl.xrpl4j.model.transactions.CredentialCreate; import org.xrpl.xrpl4j.model.transactions.CredentialDelete; import org.xrpl.xrpl4j.model.transactions.CredentialType; +import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.Transaction; +import org.xrpl.xrpl4j.model.transactions.TransactionResultCodes; /** - * Demonstrates the full Credential lifecycle on the XRPL Testnet: - * fund an issuer and a subject account, issue a credential from the issuer - * to the subject, have the subject accept it, then delete it. - * - *

As of 2026, Credentials are enabled on Testnet and Devnet. + * This code sample demonstrates the Credential lifecycle on the XRPL. + * It funds two accounts, issues a credential from one to the other, + * accepts the credential, and then deletes it. */ public class ManageCredentials { @@ -39,7 +46,9 @@ public class ManageCredentials { HttpUrl.get("https://s.altnet.rippletest.net:51234/"); private static final HttpUrl TESTNET_FAUCET_URL = - HttpUrl.get("https://apex-faucet.altnet.rippletest.net"); + HttpUrl.get("https://faucet.altnet.rippletest.net"); + + private static final String EXPLORER_BASE_URL = "https://testnet.xrpl.org/transactions/"; private static final CredentialType CREDENTIAL_TYPE = CredentialType.ofPlainText("driver-license"); @@ -49,76 +58,85 @@ public class ManageCredentials { private static final long FUNDING_POLL_INTERVAL_MS = 1_000L; private static final int FUNDING_MAX_ATTEMPTS = 30; private static final long VALIDATION_POLL_INTERVAL_MS = 4_000L; + private static final int VALIDATION_MAX_ATTEMPTS = 10; + + private static final ObjectMapper JSON_MAPPER = ObjectMapperFactory.create(); + + public static void main(String[] args) + throws JsonRpcClientErrorException, JsonProcessingException, InterruptedException { - public static void main(String[] args) throws Exception { XrplClient xrplClient = new XrplClient(TESTNET_RIPPLED_URL); FaucetClient faucetClient = FaucetClient.construct(TESTNET_FAUCET_URL); SignatureService signatureService = new BcSignatureService(); - // --- Create and fund wallets ----------------------------------------- - System.out.println("Creating issuer and subject wallets..."); + System.out.println("\n=== Funding issuer and subject accounts from the Testnet faucet ===\n"); KeyPair issuer = Seed.ed25519Seed().deriveKeyPair(); KeyPair subject = Seed.ed25519Seed().deriveKeyPair(); Address issuerAddress = issuer.publicKey().deriveAddress(); Address subjectAddress = subject.publicKey().deriveAddress(); - System.out.println("Issuer: " + issuerAddress); - System.out.println("Subject: " + subjectAddress); + System.out.println("Issuer address: " + issuerAddress); + System.out.println("Subject address: " + subjectAddress); fundAndAwaitAccount(faucetClient, xrplClient, issuerAddress); fundAndAwaitAccount(faucetClient, xrplClient, subjectAddress); + // Fee is fetched once and reused across transactions in this short-lived + // sample. FeeUtils picks a reasonable fee based on current ledger load. FeeResult feeResult = xrplClient.fee(); - // --- Issue credential ------------------------------------------------ - System.out.println("\nIssuing credential from issuer to subject..."); + // --- Issue credential --------------------------------------------------- + System.out.println("\n=== Preparing CredentialCreate transaction ===\n"); CredentialCreate createTx = CredentialCreate.builder() .account(issuerAddress) .subject(subjectAddress) .credentialType(CREDENTIAL_TYPE) .sequence(accountSequence(xrplClient, issuerAddress)) - .fee(feeResult.drops().openLedgerFee()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) .lastLedgerSequence(nextLastLedgerSequence(xrplClient)) .signingPublicKey(issuer.publicKey()) .build(); + printTransactionJson(createTx); - TransactionResult createResult = signAndAwaitValidation( - xrplClient, signatureService, issuer, createTx, CredentialCreate.class - ); - System.out.println("Credential issued. Hash: " + createResult.hash()); + System.out.println("\n=== Submitting CredentialCreate transaction ===\n"); + TransactionResult createResult = signSubmitAwaitFinality( + xrplClient, signatureService, issuer, createTx, CredentialCreate.class); + printFinalResult("Credential issued", createResult.hash()); - // --- Subject accepts credential -------------------------------------- - System.out.println("\nSubject accepting credential..."); + // --- Subject accepts credential ---------------------------------------- + System.out.println("\n=== Preparing CredentialAccept transaction ===\n"); CredentialAccept acceptTx = CredentialAccept.builder() .account(subjectAddress) .issuer(issuerAddress) .credentialType(CREDENTIAL_TYPE) .sequence(accountSequence(xrplClient, subjectAddress)) - .fee(feeResult.drops().openLedgerFee()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) .lastLedgerSequence(nextLastLedgerSequence(xrplClient)) .signingPublicKey(subject.publicKey()) .build(); + printTransactionJson(acceptTx); - TransactionResult acceptResult = signAndAwaitValidation( - xrplClient, signatureService, subject, acceptTx, CredentialAccept.class - ); - System.out.println("Credential accepted. Hash: " + acceptResult.hash()); + System.out.println("\n=== Submitting CredentialAccept transaction ===\n"); + TransactionResult acceptResult = signSubmitAwaitFinality( + xrplClient, signatureService, subject, acceptTx, CredentialAccept.class); + printFinalResult("Credential accepted", acceptResult.hash()); - // --- Subject deletes credential -------------------------------------- - System.out.println("\nSubject deleting credential..."); + // --- Subject deletes credential ---------------------------------------- + System.out.println("\n=== Preparing CredentialDelete transaction ===\n"); CredentialDelete deleteTx = CredentialDelete.builder() .account(subjectAddress) .issuer(issuerAddress) .credentialType(CREDENTIAL_TYPE) .sequence(accountSequence(xrplClient, subjectAddress)) - .fee(feeResult.drops().openLedgerFee()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) .lastLedgerSequence(nextLastLedgerSequence(xrplClient)) .signingPublicKey(subject.publicKey()) .build(); + printTransactionJson(deleteTx); - TransactionResult deleteResult = signAndAwaitValidation( - xrplClient, signatureService, subject, deleteTx, CredentialDelete.class - ); - System.out.println("Credential deleted. Hash: " + deleteResult.hash()); + System.out.println("\n=== Submitting CredentialDelete transaction ===\n"); + TransactionResult deleteResult = signSubmitAwaitFinality( + xrplClient, signatureService, subject, deleteTx, CredentialDelete.class); + printFinalResult("Credential deleted", deleteResult.hash()); } /** @@ -127,7 +145,7 @@ public class ManageCredentials { */ private static void fundAndAwaitAccount( FaucetClient faucetClient, XrplClient xrplClient, Address address - ) throws Exception { + ) throws InterruptedException { faucetClient.fundAccount(FundAccountRequest.of(address)); for (int attempt = 0; attempt < FUNDING_MAX_ATTEMPTS; attempt++) { @@ -139,14 +157,21 @@ public class ManageCredentials { System.out.println("Funded: " + address); return; } catch (Exception notYetVisible) { + // Intentional: faucet funding takes a few seconds to confirm, so we + // poll until account_info succeeds. Any exception here means "not yet + // visible — retry." Thread.sleep(FUNDING_POLL_INTERVAL_MS); } } - throw new IllegalStateException("Faucet funding for " + address + " did not confirm in time."); + exitWithError("Faucet funding for " + address + " did not confirm in time."); } + /** + * Fetches the next transaction {@code Sequence} for {@code address} from + * the latest validated ledger. + */ private static UnsignedInteger accountSequence(XrplClient xrplClient, Address address) - throws Exception { + throws JsonRpcClientErrorException, JsonProcessingException { AccountInfoResult info = xrplClient.accountInfo(AccountInfoRequestParams.builder() .account(address) .ledgerSpecifier(LedgerSpecifier.VALIDATED) @@ -154,7 +179,12 @@ public class ManageCredentials { return info.accountData().sequence(); } - private static UnsignedInteger nextLastLedgerSequence(XrplClient xrplClient) throws Exception { + /** + * Computes a safe {@code LastLedgerSequence} for a new transaction — the + * latest validated ledger index plus {@link #LAST_LEDGER_OFFSET}. + */ + private static UnsignedInteger nextLastLedgerSequence(XrplClient xrplClient) + throws JsonRpcClientErrorException, JsonProcessingException { UnsignedInteger validated = xrplClient.ledger(LedgerRequestParams.builder() .ledgerSpecifier(LedgerSpecifier.VALIDATED) .build()) @@ -165,52 +195,65 @@ public class ManageCredentials { } /** - * Signs, submits, and polls until the transaction is validated or its - * {@code LastLedgerSequence} has passed. + * Signs and submits a transaction, then polls {@link XrplClient#isFinal} until + * the transaction reaches a terminal state (validated success, validated + * failure, or expired). */ - private static TransactionResult signAndAwaitValidation( + private static TransactionResult signSubmitAwaitFinality( XrplClient xrplClient, SignatureService signatureService, KeyPair signer, T transaction, Class transactionType - ) throws Exception { + ) throws JsonRpcClientErrorException, JsonProcessingException, InterruptedException { SingleSignedTransaction signed = signatureService.sign(signer.privateKey(), transaction); SubmitResult submit = xrplClient.submit(signed); - if (!"tesSUCCESS".equals(submit.engineResult())) { - throw new IllegalStateException( - "Submit failed: " + submit.engineResult() + " - " + submit.engineResultMessage() - ); + if (!TransactionResultCodes.TES_SUCCESS.equals(submit.engineResult())) { + exitWithError("Submit rejected: " + submit.engineResult() + " — " + submit.engineResultMessage()); } UnsignedInteger lastLedgerSequence = transaction.lastLedgerSequence() .orElseThrow(() -> new IllegalArgumentException( - "Transaction must set LastLedgerSequence for reliable validation polling." - )); + "Transaction must set LastLedgerSequence for finality polling.")); - while (true) { + for (int attempt = 0; attempt < VALIDATION_MAX_ATTEMPTS; attempt++) { Thread.sleep(VALIDATION_POLL_INTERVAL_MS); - TransactionResult result = xrplClient.transaction( - TransactionRequestParams.of(signed.hash()), transactionType - ); - if (result.validated()) { - return result; + Finality finality = xrplClient.isFinal( + signed.hash(), + submit.validatedLedgerIndex(), + lastLedgerSequence, + transaction.sequence(), + signer.publicKey().deriveAddress()); + + if (finality.finalityStatus() == FinalityStatus.VALIDATED_SUCCESS) { + return xrplClient.transaction( + TransactionRequestParams.of(signed.hash()), transactionType); } - UnsignedInteger latest = xrplClient.ledger(LedgerRequestParams.builder() - .ledgerSpecifier(LedgerSpecifier.VALIDATED) - .build()) - .ledgerIndex() - .orElseThrow(() -> new IllegalStateException("No validated ledger index available.")) - .unsignedIntegerValue(); - - if (latest.compareTo(lastLedgerSequence) > 0) { - throw new IllegalStateException( - "LastLedgerSequence passed before " + transactionType.getSimpleName() + " was validated." - ); + if (finality.finalityStatus() != FinalityStatus.NOT_FINAL) { + exitWithError("Transaction did not succeed: " + finality.finalityStatus() + + " (" + finality.resultCode().orElse("unknown") + ")"); } } + exitWithError("Transaction did not reach finality within " + + (VALIDATION_MAX_ATTEMPTS * VALIDATION_POLL_INTERVAL_MS / 1000) + " seconds."); + return null; // unreachable — exitWithError terminates the process + } + + private static void printTransactionJson(Transaction tx) throws JsonProcessingException { + System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(tx)); + } + + private static void printFinalResult(String label, Hash256 hash) { + System.out.println(label + " successfully."); + System.out.println("Hash: " + hash); + System.out.println("Explorer: " + EXPLORER_BASE_URL + hash); + } + + private static void exitWithError(String message) { + System.err.println("Error: " + message); + System.exit(1); } } diff --git a/_code-samples/credential/java/src/main/resources/logback.xml b/_code-samples/credential/java/src/main/resources/logback.xml index 0dc2bda59d..7585f949e0 100644 --- a/_code-samples/credential/java/src/main/resources/logback.xml +++ b/_code-samples/credential/java/src/main/resources/logback.xml @@ -4,7 +4,7 @@ - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -- %msg%n + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n