Update: As mentioned in here, since
rippled 1.1.0
remotesign
API is not allowed by default anymore. If you try to call it onwss://s.altnet.rippletest.net:51233
you will get “Signing is not supported by this server
” error. Check this post on how to sign transaction locally using Java
Ripple (XRP) provides some options to interact with their currency/coin. One of the most recommended way is by using their official node.js package, ripple-lib
as suggested in their doc. But let say, for some reason we don’t want to use node.js, then one of the best option is using their websocket API which are well-documented (as expected from “company-maintained” blockchain ?). In this post I will show how I do it with Java (yes, this is only one of many possible ways).
Ripple websocket API is publicly available and does not required registration or token to access it
Initially, I want to use ripple-lib-java (official but not really maintained) to handle everything. But unfortunately, due to lack of documentation (and maybe my laziness), I didn’t manage to find how. So I only use it to handle the cryptographic parts and use Java-WebSocket to directly communicate with their test/sandbox API at wss://s.altnet.rippletest.net:51233
.
Since I want to have each processes work sequentially, I make a simple blocking/synchronous websocket client implementation:
import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import java.net.URI; class BlockingWebSocketClient extends WebSocketClient { private static long timeout = 20000; private List<String> results = new ArrayList<>(); private String message; public BlockingWebSocketClient(URI serverUri) { super(serverUri); } public String sendBlocking(String msg) throws InterruptedException { this.message = msg; this.connect(); synchronized (this.results) { this.results.wait(timeout); } this.close(); return this.results.get(0); } @Override public void onOpen(ServerHandshake handshakedata) { System.out.println("connected. sending message"); this.send(this.message); } @Override public void onMessage(String message) { System.out.println("response received"); synchronized (this.results) { this.results.add(message); this.results.notify(); } } @Override public void onClose(int code, String reason, boolean remote) { System.out.println("connection closed"); } @Override public void onError(Exception ex) { ex.printStackTrace(); this.close(); } }
Then to check XRP balance, I invoke account_info
passing a valid XRP address command as described here:
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; import java.math.BigDecimal; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; ... public BigDecimal getBalance(String address) throws Exception { String command = "account_info"; ObjectNode payload = this.mapper.createObjectNode(); payload.put("command", command); payload.put("account", address); payload.put("strict", true); payload.put("ledger_index", "current"); payload.put("queue", true); String response = null; BigDecimal balance = null; try { BlockingWebSocketClient client = new BlockingWebSocketClient(new URI("wss://s.altnet.rippletest.net:51233")); response = client.sendBlocking(payload.toString()); JsonNode body = this.mapper.readTree(response); Amount amount = Amount.fromDropString(body.get("result").get("account_data").get("Balance").textValue()); balance = amount.value(); } catch (NullPointerException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (InterruptedException e) { throw new Exception(String.format("Websocket %s error: %s", command, e.getMessage())); } catch (JsonProcessingException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (IOException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (URISyntaxException e) { throw new Exception(String.format("Unexpected URL used in library: %s\n%s", this.getUrl(), e.getMessage())); } return balance; }
For an XRP address (Account ID) to be valid, it is not only need to be generated with proper cryptographics but also need to have a certain minimum balance (XRP 20 as per February 2018) as explained here. If you try to check balance of an empty account ID using above API, it will throw an
invalid
error.
While it’s very straight-forward to check a balance, sending coin is slightly more challenging. Sending process involves 2 (two) API calls:
To sign the transaction, I use sign
command:
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; import java.math.BigDecimal; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; ... public Result sign(BigDecimal xrpAmount, String accountId, String secret, String addressTo) throws Exception { String command = "sign"; ObjectNode tx = this.mapper.createObjectNode(); Amount amount = new Amount(xrpAmount); tx.put("TransactionType", "Payment"); tx.put("Account", accountId); tx.put("Destination", addressTo); tx.put("Amount", amount.toDropsString()); ObjectNode payload = this.mapper.createObjectNode(); payload.set("tx_json", tx); payload.put("command", command); payload.put("secret", secret); payload.put("offline", false); payload.put("fee_mult_max", 1000); String response = null; Result result; try { BlockingWebSocketClient client = new BlockingWebSocketClient(new URI("wss://s.altnet.rippletest.net:51233")); response = client.sendBlocking(payload.toString()); JsonNode body = this.mapper.readTree(response); if (body.has("error_message")) { result = new Result(false, new String[]{body.get("error_message").textValue()}); } else { // validate property body.get("result").get("tx_blob").textValue(); result = new Result(body); } } catch (InterruptedException e) { throw new Exception(String.format("Websocket %s error: %s", command, e.getMessage())); } catch (JsonProcessingException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (IOException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (NullPointerException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (URISyntaxException e) { throw new Exception(String.format("Unexpected URL used in library: %s\n%s", this.getUrl(), e.getMessage())); } return result; }
Then I call the method above before submitting a transaction using submit
command:
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.ripple.core.coretypes.AccountID; import com.ripple.core.coretypes.Amount; import com.ripple.crypto.ecdsa.IKeyPair; import com.ripple.crypto.ecdsa.Seed; import java.io.IOException; import java.math.BigDecimal; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; ... public Result send(BigDecimal xrpAmount, String privateKey, String addressTo) throws Exception { Seed seed = Seed.fromBase58(privateKey); IKeyPair iKeyPair = seed.keyPair(); byte[] pub160Hash = iKeyPair.pub160Hash(); AccountID accountID = AccountID.fromBytes(pub160Hash); Result signResult = this.sign(xrpAmount, accountID.toString(), privateKey, addressTo); if (!signResult.isSuccess()) { return signResult; } String txBlob = signResult.getBody().get("result").get("tx_blob").textValue(); String command = "submit"; ObjectNode payload = this.mapper.createObjectNode(); payload.put("command", command); payload.put("tx_blob", txBlob); String response = null; Result result; try { BlockingWebSocketClient client = new BlockingWebSocketClient(new URI(this.getUrl())); response = client.sendBlocking(payload.toString()); System.out.println(response); JsonNode body = this.mapper.readTree(response); if (body.has("error_message")) { result = new Result(false, new String[]{body.get("error_message").textValue()}); } else { if (!"success".equalsIgnoreCase(body.get("status").textValue()) || body.get("result").get("engine_result_code").asInt() != 0) { result = new Result(false, new String[]{"Failed without error_message"}, body); } else { result = new Result(body); } } } catch (InterruptedException e) { throw new Exception(String.format("Websocket %s error: %s", command, e.getMessage())); } catch (JsonProcessingException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (IOException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (NullPointerException e) { throw new Exception(String.format("Unexpected websocket %s response: %s\n%s", command, response, e.getMessage())); } catch (URISyntaxException e) { throw new Exception(String.format("Unexpected URL used in library: %s\n%s", this.getUrl(), e.getMessage())); } return result; }
There is actuall a way to do this in a single API call but it is not suggested for production.
That is all. Cheers! ?
Getting verified SSL information with Python (3.x) is very easy. Code examples for it are…
By default, Spring Data Couchbase implements single-bucket configuration. In this default implementation, all POJO (Plain…
Last year, Google released Firebase Auth Emulator as a new component in Firebase Emulator. In…
One of the authentication protocol that is supported by most of Google Cloud services is…
If you need to to add a spatial information querying in your application, PostGIS is…
Amazon Web Service Transcribe provides API to automatically convert an audio speech file (mp3/wav) into…