Check balance and send Ripple (XRP) using Java

Update: As mentioned in here, since rippled 1.1.0 remote sign API is not allowed by default anymore. If you try to call it onĀ wss://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:

  1. SignĀ https://ripple.com/build/rippled-apis/#sign
  2. SubmitĀ https://ripple.com/build/rippled-apis/#submit

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!Ā šŸ»

 

Leave a Reply

Your email address will not be published. Required fields are marked *