update code sample to use up-to-date functions and code conventions

This commit is contained in:
Oliver Eggert
2026-04-18 13:53:00 -07:00
parent 1241a33a5d
commit 583b169680
2 changed files with 107 additions and 64 deletions

View File

@@ -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.
*
* <p>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<PrivateKey> 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<CredentialCreate> 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<CredentialCreate> 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<CredentialAccept> 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<CredentialAccept> 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<CredentialDelete> 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<CredentialDelete> 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 <T extends Transaction> TransactionResult<T> signAndAwaitValidation(
private static <T extends Transaction> TransactionResult<T> signSubmitAwaitFinality(
XrplClient xrplClient,
SignatureService<PrivateKey> signatureService,
KeyPair signer,
T transaction,
Class<T> transactionType
) throws Exception {
) throws JsonRpcClientErrorException, JsonProcessingException, InterruptedException {
SingleSignedTransaction<T> signed = signatureService.sign(signer.privateKey(), transaction);
SubmitResult<T> 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<T> 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);
}
}

View File

@@ -4,7 +4,7 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -- %msg%n</pattern>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>