Polling
Polling is a way to retrieve the list of payments that both senders and receivers need to process.
Overview flow chart
A typical workflow for polling for payments that are ready for processing, is as follows:
Step-by-step implementation
- Payment states
- Create the request
- Read the response
- Send payments for processing
- Create the pollers independently from each other
1. Payment states
You can use the same technique to poll for payments in any state or substate.
Senders poll for payments in the following states:
-
EXECUTED
(with sub-states) -
FAILED
-
COMPLETED
Receivers poll for payments in the following states:
-
ACCEPTED
-
EXECUTED
-
FAILED
For more information on Ripple Payments states, see Payment states .
A substate is a label that provides communication between the SENDING
and RECEIVING
RippleNet instances. A substate label is only present when the payment is in the EXECUTED
state.
Note:
For more information on Ripple Payments states, see Payment states .
In the following section, we'll focus on building the poller for payments in the ACCEPTED
state.
2. Create the request
Use the GET /v4/payments
operation to poll the payments by states.
Request example:
GET /v4/payments?page=0&size=100&states=ACCEPTED&sort_field=CREATED_AT&sort_direction=DESC
Authorization: Bearer eyJhbGci...zI1NiOlsi
The GET /v4/payments
operation accepts many parameters. Find all the parameters and their descriptions in the API reference documentation.
It's important to use them as they allow you to filter the payments you poll.
In the request example considered here, these are parameter values we'll use:
-
page=0
-
size=100
-
states=ACCEPTED
-
sort_field=CREATED_AT
-
sort_direction=DESC
page
The page
parameter controls the pagination process, which we explain in 3. Read the response below.
The default page
value is 0.
size
The size
parameter indicates the number of payments on one page.
The default size
value is 10 and its maximum is 100.
As a best practice, set the size
value to 100 in your middleware.
states
The states
parameter lets you specify a list of payment states that you want to poll.
In our example, to poll for ACCEPTED
payments, we set states=ACCEPTED
.
Best practice:
Set states
to one state only, as each poller needs to be independent from other pollers. See also 5. Create the pollers independently from each other.
Note: The state
parameter is deprecated, use states
.
sort_field and sort_direction
The sort_field
and sort_direction
parameters control the sorting of the returned list of payments.
The default value for sort_field
is MODIFIED_AT
.
The default value for sort_direction
is DESC
.
Best practice:
Use these two fields with the following values sort_field=CREATED_AT
and sort_direction=DESC
. This causes the oldest payments to appear first in the response list as you need to process the oldest payments before they expire.
3. Read the response
Before you send the payments for processing (which is a different action than polling), read and analyze the response.
Here is a sample response from GET /v4/payments
:
Click to view the full JSON response ...
{ "first": true, "last": true, "number": "0", "numberOfElements": "1", "size": "100", "totalElements": "1", "totalPages": "1", "sort": [ { "direction": "DESC", "property": "CREATED_AT", "ignoreCase": false, "nullHandling": "NATIVE", "ascending": false, "descending": true } ], "content": [ { "payment_id": "f293947c-9067-47d7-a69e-6658070ed482", "contract_hash": "c6db748bf84b7ed38b0fdc3bd6bdcca8d072af27b473ad0df47aa450b6c70396", "payment_state": "ACCEPTED", "modified_at": "2021-03-03T04:40:58.629Z", "contract": { "sender_end_to_end_id": "test", "created_at": "2021-03-03T04:40:57.812Z", "expires_at": "2021-03-26T08:14:14.839Z", "quote": { "quote_id": "642fd286-d3a7-4a75-a14b-86ca9a504922", "created_at": "2021-03-03T04:40:54.839Z", "expires_at": "2021-03-26T08:14:14.839Z", "type": "SENDER_AMOUNT", "price_guarantee": "FIRM", "sender_address": "trans_usd_sender@test.cloud.blueprint1", "receiver_address": "trans_gbp_receiver@test.cloud.blueprint2", "amount": "10.000000000", "currency_code": "USD", "currency_code_filter": null, "service_type": null, "quote_elements": [ { "quote_element_id": "23b21542-4b54-4c3c-a2dd-f551fe026194", "quote_element_type": "TRANSFER", "quote_element_order": "1", "sender_address": "trans_usd_sender@test.cloud.blueprint1", "receiver_address": "conct_usd_sender2@test.cloud.blueprint1", "sending_amount": "10.000000000", "receiving_amount": "10.000000000", "sending_fee": "0.000000000", "receiving_fee": "0.000000000", "sending_currency_code": null, "receiving_currency_code": null, "fx_rate": null, "transfer_currency_code": "USD" }, { "quote_element_id": "3e31fa45-731a-4a35-8cd9-0100b19f7b66", "quote_element_type": "TRANSFER", "quote_element_order": "2", "sender_address": "conct_usd_sender@test.cloud.blueprint2", "receiver_address": "alias_usd_receiver@test.cloud.blueprint2", "sending_amount": "10.000000000", "receiving_amount": "10.000000000", "sending_fee": "0.000000000", "receiving_fee": "0.000000000", "sending_currency_code": null, "receiving_currency_code": null, "fx_rate": null, "transfer_currency_code": "USD" }, { "quote_element_id": "66b80732-66a7-4a77-ab2d-3853d6849e98", "quote_element_type": "EXCHANGE", "quote_element_order": "3", "sender_address": "alias_usd_receiver@test.cloud.blueprint2", "receiver_address": "conct_gbp_receiver@test.cloud.blueprint2", "sending_amount": "10.000000000", "receiving_amount": "12.000000000", "sending_fee": "0.000000000", "receiving_fee": "0.000000000", "sending_currency_code": "USD", "receiving_currency_code": "GBP", "fx_rate": { "rate": "1.200000000", "base_currency_code": "USD", "counter_currency_code": "GBP", "type": "buy" }, "transfer_currency_code": null }, { "quote_element_id": "051885bf-a4be-4661-800e-40e05ac1f349", "quote_element_type": "TRANSFER", "quote_element_order": "4", "sender_address": "conct_gbp_receiver@test.cloud.blueprint2", "receiver_address": "trans_gbp_receiver@test.cloud.blueprint2", "sending_amount": "12.000000000", "receiving_amount": "12.000000000", "sending_fee": "0.000000000", "receiving_fee": "0.000000000", "sending_currency_code": null, "receiving_currency_code": null, "fx_rate": null, "transfer_currency_code": "GBP" } ], "liquidity_warning": null, "payment_method": null, "payment_method_fields": null, "payout_method_info": null }, "fee_info": null }, "ripplenet_info": [], "execution_condition": "PrefixSha256Condition{subtypes=[ED25519-SHA-256], type=PREFIX-SHA-256, fingerprint=VK9hSv6zUK9czMZCiZ9n_BWZrDGEbRQumi641B76VFQ, cost=132360}", "crypto_transaction_id": null, "validator": "test.cloud.blueprint1", "payment_type": "REGULAR", "returns_payment_with_id": null, "returned_by_payment_with_id": null, "execution_results": [], "internal_info": { "connector_role": "RECEIVING", "labels": [], "internal_id": null }, "user_info": [ { "node_address": "test.cloud.blueprint1", "accepted": [ { "json": { "end_to_end_id": "20201018041040", "DbtrAcct": {}, "DbtrAgt": {}, "DbtrAgtAcct": {}, "InstForCdtrAgt": {}, "Purp": {}, "RgltryRptg": {}, "RltdRmtInf": {}, "UltmtCdtr": {}, "Cdtr": { "Nm": "Peppa Potts", "PstlAdr": { "AdrLine": "Somewhere in Malaysia", "Ctry": "MY", "PstCd": "12345" }, "Id": { "PrvtId": { "Othr": { "Id": "AA8423234", "SchmeNm": { "Cd": "ID" } } } } }, "CdtrAcct": { "Id": { "Othr": { "Id": "0123456789" } }, "Tp": { "Cd": "Saving" } }, "CdtrAgt": { "FinInstnId": { "BICFI": "ABCDEF", "Nm": "Bank A" } }, "Dbtr": { "Nm": "Robert Downey", "PstlAdr": { "AdrLine": "Tampines St 81", "Ctry": "SG", "PstCd": "543210" }, "Id": { "PrvtId": { "Othr": { "Id": "AA8463912", "SchmeNm": { "Cd": "ID" } } } } }, "RmtInf": { "Ustrd": "20201018041010" }, "PmtTpInf": { "CtgyPurp": { "Prtry": "IR02836" } } }, "created_at": "2021-03-03T04:40:58.449Z", "subState": "" } ], "locked": [], "lock_declined": [], "retry_accept": [], "retry_settlement": [], "settlement": [], "settlement_declined": [], "failed": [], "executed": [], "completed": [], "forwarded": [], "returned": [] } ] } ] }
The first fields are important as they tell you if you need to use pagination to read the full results, as described next.
Pagination
In the example below, the values differ from the example above to illustrate the concept of pagination:
{
"first": true,
"last": false,
"number": "0",
"numberOfElements": "100",
"size": "100",
"totalElements": "300",
"totalPages": "3",
...
}
This tells us the RippleNet API is returning three pages (numbered from 0 to 2) with 100 payments per page, so we need to use pagination to retrieve all the payments.
If totalElements
is higher than size
, then totalPages
will be higher than 1.
The last
field, if true
, indicates that the page is the last one. This is the field you can use to programmatically handle the pagination.
Implement logic in your middleware to keep calling GET /v4/payments
, incrementing page
by 1 for each call, until last=true
.
Click to view example Java implementation of pagination ...
Payments payments = paymentsServices.getPaymentsFromAllPages(getPaymentsParams); ... @Override public Payments getPaymentsFromAllPages(GetPaymentsParams getPaymentsParams) throws RippleNetProblemException, RnAuthException { int page = 0; getPaymentsParams.setPage(page); // We retrieve the payments on page 0 first Payments payments = getPayments(getPaymentsParams); int numberOfElements = payments.getNumberOfElements(); // If the page is not the last one while (!payments.isLast()) { // We increase the page by 1 page++; getPaymentsParams.setPage(page); // We retrieve the payments on the incremented page Payments paymentsTemporary = getPayments(getPaymentsParams); // And for each payment on the new page for (Payment payment : paymentsTemporary.getContent()) { // we add it to the global list of Payments payments.addContentItem(payment); // we also update the total number of payments/elements in the payments object numberOfElements++; payments.setNumberOfElements(numberOfElements); } // Finally, at the end of the while loop, we update the first/last booleans // If page 1, for example, is the last, then the while loop will exit payments.setLast(paymentsTemporary.isLast()); payments.setFirst(paymentsTemporary.isFirst()); } LOGGER.info("Total amount of payments retrieved: {}", numberOfElements); LOGGER.info("Last page where payments were retrieved is page {}", getPaymentsParams.getPage()); // Return the list of Payments return payments; }
4. Send payments for processing
After retrieving all the payments with your poller, send the payments for processing.
For example, for a payment in ACCEPTED
state:
- Send the payment for compliance review.
-
Validate that its
user_info
has been received. - Lock, reject, or fail the payment.
Click to view example Java implementation of payment processing ...
if (payments != null) { if (payments.getNumberOfElements() > 0) { paymentsProcessor.processPayments(payments, hasSubStates); } else { LOGGER.info("No payments in {} states retrieved", getPaymentsParams.getStates()); } }
5. Create the pollers independently from each other
Best practice:
To make it easier to control the execution of the pollers independently, a suggested poller-based workflow starts with a separate poller for each state, where each poller is in a separate file. To avoid confusion (and for clarity), name your pollers based on the state the payment is in when the poller processes the payment. For example, for the poller for EXECUTED
payments, name it ExecutedPaymentsPoller.
The following actions need to be possible on one poller without affecting the others:
- Enable or disable a poller.
- Define an initial delay after startup.
- Define a fixed delay between two executions of the same poller.
Create your pollers as follows:
-
Create a separate poller for each state by duplicating the
ACCEPTED
poller, and adapting the parameters (states
, etc.) for each poller.Place each poller in its own file. For example:
- AcceptedPaymentsPoller
- ExecutedPaymentsPoller
- CompletedPaymentsPoller
- FailedPaymentsPoller
-
For each poller, implement a property to enable or disable the poller. For example, (in Java):
@ConditionalOnProperty(value = "ripplenet.poller.accepted.enabled", matchIfMissing = false, havingValue = "true")
-
Implement a property to define an
initial delay
after startup (including a default value as well):
@Scheduled(..., initialDelayString = "${ripplenet.poller.accepted.initial.delay:30000}")
-
Implement a property to define a
fixed delay
between two executions of the same poller (including a default value as well). The
fixedDelay
property makes sure that there is a delay of n millisecond between the time a poller finishes executing, and the start time of the next execution of the same poller:@Scheduled(fixedDelayString = "${ripplenet.poller.accepted.fixed.delay:30000}", ...)
-
Finally, include all these properties in
application.properties
to easily update their values without recompiling your code:ripplenet.poller.accepted.enabled=false ripplenet.poller.accepted.fixed.delay=30000 ripplenet.poller.accepted.initial.delay=30000
Design considerations
You can adjust the fixed delay between two executions of the same poller, depending on the number of payments you have to process. Here are some aspects to consider:
Design type | Pros | Cons |
---|---|---|
Fixed delay < 30 seconds |
|
|
60 seconds > Fixed delay >= 30 seconds - Recommended |
|
|
Fixed delay > 60 seconds |
|
|
Error handling
Here are some considerations with respect to handling HTTP errors:
HTTP errors
GET /v4/payments
HTTP Status Code | Error Message | Description |
---|---|---|
400 |
invalid_request |
Invalid Request parameters |
401 |
Error 401 Unauthorized | Bad credentials, wrong authentication method, OR the token may be expired |
403 |
Forbidden | No handler found for GET |
500 |
Server not available | Service is temporarily unavailable. |
Handlers
Name | Code | Action |
---|---|---|
Unauthorized | 401 | Payments will stop, throw an alert. Credentials could have been altered by an administrator or renew the token. |
Server not available | 500 | Payments will stop. Retry three times every two seconds. If it fails, raise an alert. |
Handlers sample code
To handle retries with a 500 HTTP status code, the RippleNet Reference Implementation uses the Spring @Retryable annotation.
@Retryable(value = {RippleNetProblemTemporaryException.class},
maxAttempts = 3,
backoff = @Backoff(maxDelay = 2000, random = true))
Payments getPayments(GetPaymentsParams getPaymentsParams)
throws RippleNetProblemException, RnAuthException;