The key concepts of the SDK are described below.
The Config class holds the different parameters required by the SDK.
You don't need to initialize your own Config objects. Use the HarmonizeContext to load configuration, as shown in the following examples.
var harmonizeCtx = HarmonizeContext.load(signer);
// we get the Ripple Custody instance from the context
var harmonize = harmonizeCtx.harmonize;var harmonizeCtx = HarmonizeContext.load(signer, configFileName);
// we get the Ripple Custody instance from the context
var harmonize = harmonizeCtx.harmonize;For more information, see Configure the SDK.
The Context class represents an instance of Ripple Custody. To interact with the Ripple Custody API, you need to initialize a Ripple Custody context. There are two levels of context:
- Global application context
HarmonizeContext. - Local Ripple Custody context
Context.
The global application context includes both of the following:
- The Ripple Custody
Configinstance. - The
HarmonizeSDK instance.
The local Ripple Custody context groups together several sub-components needed by the SDK:
- The
Authenticatorinstance, used to perform the OAuth authentication with Ripple Custody. - The
Signerinstance, which abstracts the user credentials and signature implementation. - The
Requestsinstance, a helper class to makeHTTPrequests. - The
Auditinstance, which can be used to add SDK HTTP audit trails.
A harmonize instance groups together all API actions. It contains the local context, so it is meant for a specific configuration and a single user. harmonize instances are lightweight and by themselves do not consume any resources.
Actions are the operations that can be performed on the API. They are accessed through the harmonize instance and grouped by API entity. For example, for domains, a DomainActions class exists, which is accessed through harmonize.domains().
Each of the actions implements Get, List, Update, Create, Lock, and Unlock operations for its corresponding entity, if available in the API. For example, it is not possible to lock a transaction and therefore the Lock operation is not valid for the TransactionActions class.
This table lists the actions available from the harmonize instance:
| Method | Entity | Action class |
|---|---|---|
harmonize.tickers() | Ticker | TickerActions |
harmonize.ledgers() | Ledger | LedgerActions |
harmonize.genesis() | GenesisRequest | GenesisActions |
harmonize.me() | User (the current SDK user) | MeActions |
harmonize.domains() | Domain | DomainsActions |
harmonize.requests(domainId) | Request | RequestActions |
harmonize.intents(domainId) | Intent | IntentsActions |
harmonize.vaults() | Vault | VaultsActions |
harmonize.notarize() | NotarizeData | NotarizeActions |
harmonize.transactions(domainId) | ApiTransaction, ApiTransfer, TransactionOrder | TransactionActions |
harmonize.accounts(domainId) | Account | AccountsActions |
harmonize.users(domainId) | User | UsersActions |
harmonize.endpoints(domainId) | Endpoint | EndpointsActions |
harmonize.policies(domainId) | Policy | PoliciesActions |
harmonize.events(domainId) | Event | EventsActions |
All actions implement a fully and partially fluent design.
The fully fluent design flows from the action to entity creation and ends with a submit, which returns an IntentData instance. The IntentData instance can be used to wait on request processing or poll the intent status later.
The flow is {actions} -> {operation} -> {builder} -> {submit} -> {intentData}:
harmonize.domains().create()
.id(id.toString())
.alias(name)
.permissions(Domains.Permissions.builder().readAccess(
Domains.ReadAccess.builder().all("admin").build()
).build())
.policy(Policies.CreateDomainGenesisPolicy.buildDefault())
.user(Users.CreateDomainGenesisUser.builder()
.alias(UUID.randomUUID().toString())
.description("Test user")
.customProperties(new HashMap<>())
.publicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdJ0JuuWT4482+3liCAHj8Bx2vlCzam7spp8/RVaBphdvPDKtDN9PqVN5L12Qaiq14GmNxPxDXDWFbBV9vZ4xLg==")
.role("admin")
.build())
.submit()
.waitForSucceed(Timeout.minutes(1));The partially fluent design requires the creation of an object, which is subsequently passed to the operation and potentially reused.
The flow is {builder} -> {build} -> {object} and then {actions} -> {operation} <- {object, intentData}:
var domain = Domains.CreateDomain.builder()
.id(id.toString())
.alias(name)
.permissions(Domains.Permissions.builder().readAccess(
Domains.ReadAccess.builder().all("admin").build()
).build())
.policy(Policies.CreateDomainGenesisPolicy.buildDefault())
.user(Users.CreateDomainGenesisUser.builder()
.alias(UUID.randomUUID().toString())
.description("Test user")
.customProperties(new HashMap<>())
.publicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdJ0JuuWT4482+3liCAHj8Bx2vlCzam7spp8/RVaBphdvPDKtDN9PqVN5L12Qaiq14GmNxPxDXDWFbBV9vZ4xLg==")
.role("admin")
.build())
.build();
harmonize.domains().create(domain, Expire.hours(1)).waitForSucceed(Timeout.minutes(1));To maximize performance and responsiveness, the Ripple Custody API is fully asynchronous, so the default response only acknowledges reception of the request, but not whether it succeeded. You need to poll for the status of an action to check its processing status, which can be either Failed, Processing, or Succeeded.
The SDK offers helper methods to facilitate request monitoring. The SDK can wait for confirmation before processing further instructions when a request is sent. This is enabled for both action design approaches and can be achieved using the following flow:
{actions}->{operation}->{submit}->{wait-for-status}, or{actions}->{operation, object}->{wait-for-status}
For example:
harmonize.domains().create()
.id(id.toString())
...
.submit()
.waitForProcessing(Timeout.minutes(1));Or:
harmonize.domains().create(domain, Expire.hours(24)).waitForProcessing(Timeout.minutes(1));The Expire and Timeout arguments are not equivalent in the SDK:
Expire: If the action sent is not approved within theExpiretime, it is cancelled.Timeout: If a status, for example,Succeeded, is not detected within the timeout period, the SDK throws a timeout exception.
Requests that lead to the creation of intents are not executed immediately. They are passed to Ripple Custody as intent request proposals.
All SDK action classes return an IntentData instance when a proposal is sent to Ripple Custody.
The IntentData contains the following:
- Request ID
- Intent ID
- Proposal intent body
ProposeIntentBody
We will now describe further the request and proposal.
When you perform an action with the SDK, you must check that the request itself succeeded. This does not mean the intent was executed; it only means that Ripple Custody accepted the request.
The request status can be one of the following:
| Request status | Description |
|---|---|
Processing | Ripple Custody is processing the request. |
Failed | The request was rejected by the Ripple Custody for some reason. |
Succeeded | Ripple Custody accepted the request, and the intent will be processed. Note that the intent itself may still fail. |
To wait for a specific request status, use a specific method. For example, to wait for a Succeeded status, use the following method:
.waitForSucceed(Timeout.minutes(1));If the user only requires acknowledgement that Ripple Custody received the request, use the following method:
.waitForProcessingOrSucceeded(Timeout.minutes(1));If the request Failed or polling timed out, a RuntimeException is thrown.
The intent goes through multiple phases indicated by its status, which can be one of the following:
| Intent status | Description |
|---|---|
Open | The intent was received and requires approval. |
Approved | The intent was approved. |
Executed | The intent was executed, for example, if the intent was to create a user, the user was created. |
Failed | The intent failed due to some reason and was not Executed; a failed intent cannot be rejected or approved. |
Expired | The intent expired before it was approved or rejected and will not be Executed. |
Rejected | The intent was Rejected and will not be Executed. |
To wait on an intent status use:
.waitForIntentStatus(Set.of(IntentStatus.Open, IntentStatus.Executed), Timeout.minutes(1));Depending on the domain's policy, an intent proposal may need approval by one or more users. It is important to note that if an intent requires one or more user approvals, that is, it requires human interaction, the SDK user should not wait for the Executed status. A better approach is to poll the intent status using the IntentsActions at stages that make sense in the current application.
For example:
Intents.State intentState = harmonize.intents(domainId).get(intentId).map(Intents.IntentEntity::getState).orElse(null);All the waitFor methods will block the current thread and poll Ripple Custody for the intent status until the status is matched, a failure state is returned, or a timeout is reached. A failure state or a timeout result in a RuntimeException thrown.
When using TransactionActions via harmonize.transactions(domainId), the TransactionIntentData instance is returned. This class has helper methods to wait for the different transaction statuses.
A transaction has a ProcessingStatus and a LedgerTransactionStatus status.
Remember that these methods block the current thread and should only be used when it makes sense for an application.
For example, to wait for a transaction to be completed, use:
.waitForOrderTransactionProcessingStatus(Transactions.ProcessingStatus.Completed, Timeout.minutes(1));You can use the IntentActions instance to approve or reject an intent.
For example:
harmonize.intents(domainId).approve(intentId)
.waitForSucceed(Timeout.minutes(1));You then need to check for the request status, as described in Request.
All objects and entities passed to and from the API are in JSON format. The SDK maps JSON objects using the MapJSON class, from which all entities extend. This means that, at its most primitive layer, entities such as an account, domain, or user are all just simple JSON maps with keys and values.
The SDK provides convenient methods like getId that fetch the value for the key id. It is designed this way to provide maximum flexibility and backwards/forwards compatibility.
For example, the SDK can be used against API versions that return values the SDK does not yet support, and the operator can get that value using get(Class class, String key) or set a value using set(String key, Object value).