use crate::address;
use crate::config;
use crate::javascript::JavaScript;
use crate::transaction;
use crate::transaction::{XAmount, XPayment, XRawTransactionStatus, XTransactionStatus};
use crate::wallet::XWallet;
use crate::x::prelude::*;
use anyhow::{bail, Error};
use fehler::throws;
use hex;
use std::str;
use std::thread;
use std::time::Duration;
use tokio::runtime::{Builder, Runtime};
type StdError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[derive(PartialEq, Debug)]
pub struct XrplReliableSendResponse {
pub transaction_status: XTransactionStatus,
pub transaction_hash: String,
pub transaction_info: String,
}
pub(self) fn drops_to_decimal(drops: u64) -> f32 {
drops as f32 / 1_000_000.
}
pub struct XrplClient {
rt: Runtime,
client: XrpLedgerApiServiceClient<tonic::transport::Channel>,
}
impl XrplClient {
#[throws(_)]
pub(crate) fn connect<D>(url: D) -> Self
where
D: std::convert::TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let mut rt = Builder::new()
.basic_scheduler()
.enable_all()
.build()
.unwrap();
let client = rt.block_on(XrpLedgerApiServiceClient::connect(url))?;
Self { rt, client }
}
#[throws(_)]
pub(self) fn get_fees(&mut self) -> GetFeeResponse {
let request = tonic::Request::new(GetFeeRequest {});
let response = self.rt.block_on(self.client.get_fee(request))?;
response.into_inner()
}
#[throws(_)]
#[allow(dead_code)]
pub(self) fn get_base_fee(&mut self) -> u64 {
let fees = self.get_fees()?;
fees.fee.unwrap().base_fee.unwrap().drops
}
#[throws(_)]
#[allow(dead_code)]
pub(self) fn get_open_ledger_fee(&mut self) -> u64 {
let fees = self.get_fees()?;
fees.fee.unwrap().open_ledger_fee.unwrap().drops
}
#[throws(_)]
pub(self) fn get_latest_validated_ledger_sequence(&mut self) -> u32 {
let fees = self.get_fees()?;
fees.ledger_current_index
}
#[throws(_)]
pub(self) fn get_account_info(&mut self, address: &str) -> AccountRoot {
let request = tonic::Request::new(GetAccountInfoRequest {
account: Some(AccountAddress {
address: address.to_owned(),
}),
signer_lists: false, strict: false, ledger: None, queue: false, });
let response = self.rt.block_on(self.client.get_account_info(request))?;
response.into_inner().account_data.unwrap()
}
#[throws(_)]
pub(self) fn get_account_sequence(
&mut self,
jscontext: &mut JavaScript,
x_address: &str,
) -> u32 {
let decoded_address = address::decode_x_address(jscontext, x_address)?;
let account_info = self.get_account_info(&decoded_address.address)?;
account_info.sequence.unwrap().value
}
#[throws(_)]
pub(crate) fn get_balance(
&mut self,
jscontext: &mut JavaScript,
x_address: &str,
) -> f32 {
let decoded_address = address::decode_x_address(jscontext, x_address)?;
let response = self.get_account_info(&decoded_address.address)?;
if let currency_amount::Amount::XrpAmount(d) =
response.balance.unwrap().value.unwrap().amount.unwrap()
{
drops_to_decimal(d.drops)
} else {
0.00
}
}
#[throws(_)]
pub(self) fn get_raw_transaction(
&mut self,
transaction_hash: Vec<u8>,
) -> GetTransactionResponse {
let request = tonic::Request::new(GetTransactionRequest {
hash: transaction_hash,
binary: false,
ledger_range: Some(LedgerRange {
ledger_index_min: 1,
ledger_index_max: 0, }),
});
let response = self.rt.block_on(self.client.get_transaction(request))?;
response.into_inner()
}
#[throws(_)]
pub(self) fn get_raw_transaction_status(
&mut self,
transaction_hash: &str,
) -> XRawTransactionStatus {
let trx_hash_vec = hex::decode(transaction_hash)?;
let response = self.get_raw_transaction(trx_hash_vec)?;
let last_ledger_sequence =
if let get_transaction_response::SerializedTransaction::Transaction(t) =
response.serialized_transaction.unwrap()
{
t.last_ledger_sequence.unwrap().value
} else {
0
};
if let get_transaction_response::SerializedMeta::Meta(c) = response.serialized_meta.unwrap()
{
XRawTransactionStatus {
transaction_result: c.transaction_result.unwrap(),
last_ledger_sequence,
validated: response.validated,
}
} else {
bail!("Unknown raw transaction status");
}
}
#[throws(_)]
pub(crate) fn get_transaction_status(&mut self, transaction_hash: &str) -> XTransactionStatus {
let transaction_status = self.get_raw_transaction_status(transaction_hash)?;
transaction::from_raw_status(transaction_status)
}
#[throws(_)]
pub(crate) fn send(
&mut self,
jscontext: &mut JavaScript,
amount: f32,
from_address: &str,
to_address: &str,
source_wallet: XWallet,
) -> XrplReliableSendResponse {
let ledger_close_time_seconds = 4;
let payment = XPayment {
amount: XAmount::new(amount),
from_address: from_address.to_owned(),
to_address: to_address.to_owned(),
};
if !address::is_valid_x_address(jscontext, &payment.to_address).unwrap()
|| !address::is_valid_x_address(jscontext, &payment.from_address).unwrap()
{
bail!("Please use the X-Address format. See: https://xrpaddress.info.");
}
let account_sequence = self.get_account_sequence(jscontext, &from_address)?;
let latest_ledger = self.get_latest_validated_ledger_sequence()?;
let last_validated_ledger_sequence = latest_ledger + config::MAX_LEDGER_VERSION_OFFSET;
let transaction = transaction::build_payment_transaction(
payment,
12,
account_sequence,
last_validated_ledger_sequence,
&source_wallet,
)?;
let signed_transaction =
transaction::sign_transaction(jscontext, &transaction, &source_wallet)?;
let request = tonic::Request::new(SubmitTransactionRequest {
signed_transaction: hex::decode(signed_transaction.result).unwrap(),
fail_hard: false,
});
let result = self.rt.block_on(self.client.submit_transaction(request))?;
let response = result.into_inner();
let result_transaction_status;
let result_transaction_hash = hex::encode(&response.hash).to_uppercase();
let result_transaction_info = if !response.engine_result.unwrap().result.starts_with("tes")
{
result_transaction_status = XTransactionStatus::FAILED;
response.engine_result_message
} else {
let mut latest_validated_ledger_sequence =
self.get_latest_validated_ledger_sequence()?;
let mut transaction_status =
self.get_raw_transaction_status(&result_transaction_hash)?;
while latest_validated_ledger_sequence <= last_validated_ledger_sequence
&& !transaction_status.validated
{
thread::sleep(Duration::from_secs(ledger_close_time_seconds));
latest_validated_ledger_sequence = self.get_latest_validated_ledger_sequence()?;
transaction_status = self.get_raw_transaction_status(&result_transaction_hash)?;
if transaction_status.last_ledger_sequence == 0 {
bail!("The transaction did not have a last_ledger_sequence field so transaction status cannot be reliably determined.");
}
}
result_transaction_status = transaction::from_raw_status(transaction_status);
"".to_owned()
};
XrplReliableSendResponse {
transaction_status: result_transaction_status,
transaction_hash: result_transaction_hash,
transaction_info: result_transaction_info,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
pub const DEFAULT_SERVER_URL: &str = "http://test.xrp.xpring.io:50051";
#[throws(_)]
#[test]
fn test_xrp_client_ok() {
match XrplClient::connect(DEFAULT_SERVER_URL) {
Ok(_result) => {
assert!(true);
}
Err(_error) => {
assert!(false);
}
}
}
#[throws(_)]
#[test]
fn test_xrp_client_invalid_url() {
match XrplClient::connect("xrp") {
Ok(_result) => {
assert!(false);
}
Err(error) => {
assert_eq!(
"transport error: error trying to connect: invalid URL, scheme is missing",
error.to_string()
);
}
}
}
#[throws(_)]
#[test]
fn test_xpring_get_base_fee() {
let mut client = XrplClient::connect(DEFAULT_SERVER_URL)?;
let response = client.get_base_fee().unwrap();
assert_eq!(response, 10);
}
#[throws(_)]
#[test]
fn test_xpring_get_balance() {
let mut client = XrplClient::connect(DEFAULT_SERVER_URL)?;
let out_dir = std::env::var("OUT_DIR").unwrap();
let mut jscontext = JavaScript::new(format!("{}/xpring.js", out_dir))?;
let response = client
.get_balance(
&mut jscontext,
"TVr7v7JGN5suv7Zgdu9aL4PtCkwayZNYWvjSG23uMMWMvzZ",
)
.unwrap();
assert_eq!(response, 1000.00);
}
#[throws(_)]
#[test]
fn test_xpring_raw_transaction_status() {
let mut client = XrplClient::connect(DEFAULT_SERVER_URL)?;
let out_dir = std::env::var("OUT_DIR").unwrap();
let mut jscontext = JavaScript::new(format!("{}/xpring.js", out_dir))?;
let w = XWallet::new(
"0314ACE51F9B116BCF3C1E38A9BD92706AF4334165870139144E947B27BB0103E8".to_owned(),
"009F56FC7B02354C428673EA14854616FED71888270C44911CBD87B84A5A59650F".to_owned(),
false,
);
let payment = client.send(
&mut jscontext,
12.12,
"T7jkn8zYC2NhPdcbVxkiEXZGy56YiEE4P7uXRgpy5j4Q6S1",
"T7QqSicoC1nB4YRyzWzctWW7KjwiYUtDzVaLwFd4N7W1AUU",
w,
)?;
thread::sleep(Duration::from_secs(4));
let response = client.get_raw_transaction_status(
&payment.transaction_hash,
);
assert_eq!(response.unwrap().transaction_result.result.starts_with("t"), true);
}
#[throws(_)]
#[test]
fn test_send() {
let mut client = XrplClient::connect(DEFAULT_SERVER_URL)?;
let out_dir = std::env::var("OUT_DIR").unwrap();
let mut jscontext = JavaScript::new(format!("{}/xpring.js", out_dir))?;
let w = XWallet::new(
"0314ACE51F9B116BCF3C1E38A9BD92706AF4334165870139144E947B27BB0103E8".to_owned(),
"009F56FC7B02354C428673EA14854616FED71888270C44911CBD87B84A5A59650F".to_owned(),
false,
);
match client.send(
&mut jscontext,
12.12,
"T7jkn8zYC2NhPdcbVxkiEXZGy56YiEE4P7uXRgpy5j4Q6S1",
"T7QqSicoC1nB4YRyzWzctWW7KjwiYUtDzVaLwFd4N7W1AUU",
w,
) {
Ok(_result) => {
assert!(true);
}
Err(_error) => {
assert!(false);
}
}
}
}