@cronstamp/clientlib
Version:
Client library for cronstamp, a blockchain-based document timestamping and verification service.
156 lines • 8.89 kB
JavaScript
import { Client, rippleTimeToUnixTime } from 'xrpl';
import BaseConverter from 'base-x';
import { AdditionalDataType, ErrorTypes, MessageType, ProcessError } from '../lifecycle/manager.js';
import { fromHexString, toBase64String } from '../util/base-convert.js';
import { BlockchainBase } from './blockchainBase.js';
import { ExponentialBackoff } from '../util/time.js';
// base-x uses a different method than standard hex or base64, with extra padding in some cases.
const R_B58_DICT = 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz';
const base58 = BaseConverter(R_B58_DICT);
export class Xrp extends BlockchainBase {
nextServerId = 0;
/**
*
* @param address 20 Byte xrp address
* @param memoData 12 Byte hex data or 32 byte hex data, if containing full hash
* @returns {string} 32 Byte b64 hash
*/
static transactionDataToHash(memoData, address) {
const memoDataBinary = fromHexString(memoData);
// old implementation encoded 20 bytes in address + 12 in memo
if (memoDataBinary.length === 12) {
let addressWithChecksum = base58.decode(address);
if (addressWithChecksum.length !== 25)
throw new ProcessError('Invalid address length (' + addressWithChecksum.length + '), expected 25 Bytes including checksum and type', ErrorTypes.DOCUMENT_ALTERED, AdditionalDataType.HASH);
const addressBinary = addressWithChecksum.subarray(1, addressWithChecksum.length - 4);
const hashBinary = new Uint8Array(addressBinary.length + memoDataBinary.length);
hashBinary.set(addressBinary);
hashBinary.set(memoDataBinary, addressBinary.length);
return toBase64String(hashBinary);
}
// new implementation encodes all 32 bytes in memo
else if (memoDataBinary.length === 32) {
return toBase64String(memoDataBinary);
}
throw new ProcessError('Invalid memoData length (' + memoDataBinary.length + '), expected 12 or 32 Bytes', ErrorTypes.DOCUMENT_ALTERED, AdditionalDataType.HASH, memoData);
}
getName() {
return 'xrp';
}
/**
* Get a connected instance of xrp client
*/
async getClient(servers) {
// get next server to use
this.nextServerId %= servers.length;
const server = servers[this.nextServerId];
this.log.info(`Connecting with XRP rpc server '${server}' with id ${this.nextServerId}.`);
this.nextServerId++;
// Connect client
let client = new Client(server, {
connectionTimeout: 20000
});
await client.connect();
return client;
}
getDisplayName() {
return 'XRP';
}
getTransactionUnixTimestamp(rippleTime) {
return rippleTimeToUnixTime(rippleTime);
}
getTransactionDate(rippleTime) {
return new Date(this.getTransactionUnixTimestamp(rippleTime));
}
async verifyRootHash(rootHash, transactionTimestamp, transactionHash, verificationProcess) {
// check for transactions in the future
if (this.getTransactionUnixTimestamp(transactionTimestamp) > Date.now()) {
this.log.warn(`The xrp transaction timestamp ${transactionTimestamp} ` +
`(${this.getTransactionDate(transactionTimestamp).toISOString()}) for the transaction ` +
`${transactionHash} is in the future.`, MessageType.PROGRESS);
}
// retry using exponential backoff for at most 15 api requests => about 4 minutes max total wait time
const exponentialBackoff = new ExponentialBackoff(15, 1.35);
let client;
let response;
do {
await exponentialBackoff.waitForNextAttempt();
response = undefined;
try {
if (!client || !client.isConnected()) {
client = await this.getClient(verificationProcess.config.BLOCKCHAIN_XRP_SERVERS);
}
response = await client.request({
command: 'tx',
transaction: transactionHash,
binary: false
});
}
catch (err) {
// Timeout or other network error: restart connection
await client?.disconnect();
client = undefined;
verificationProcess.sendMessage(`Error during fetch of result for transaction from XRP rpc api. Retrying in ${Math.round(exponentialBackoff.getDelayForNextAttempt() / 1000)} seconds.`, MessageType.PROGRESS, AdditionalDataType.ERROR, err);
}
// use a valid transaction result immediately
if (response?.result?.validated === true) {
break;
}
if (response?.result?.validated === false) {
// a validated = false likely means it will take just a few more seconds to fully validate the transaction
verificationProcess.sendMessage(`Got transaction with status "validated: false" from transaction from XRP rpc api. Waiting for additional ${Math.round(exponentialBackoff.getDelayForNextAttempt() / 1000)} seconds` +
`, for the transaction to propagate to the rpc node. Total time waited: ${Math.round((Date.now() - exponentialBackoff.startTime) / 1000)} `, MessageType.PROGRESS);
}
else if (response === null) {
// a null result for getTransaction can be because the transaction has not fully propagated to all nodes, so we retry for a fixed amount of time
verificationProcess.sendMessage(`Got "null" result for transaction from XRP rpc api. Waiting for additional ${Math.round(exponentialBackoff.getDelayForNextAttempt() / 1000)} seconds` +
`, for the transaction to propagate to the rpc node. Total time waited: ${Math.round((Date.now() - exponentialBackoff.startTime) / 1000)} `, MessageType.PROGRESS);
}
// Keep retrying while transaction is still new (which might result in desynced nodes taking longer to reflect the validation of the transaction) or if network errors occurred
} while (Date.now() < this.getTransactionUnixTimestamp(transactionTimestamp) + exponentialBackoff.getTotalTime() ||
(!client && exponentialBackoff.isActive()));
await client?.disconnect();
if (response) {
verificationProcess.sendMessage('Got result from xrp network: ', MessageType.PROGRESS, AdditionalDataType.HTTP_RESPONSE_JSON, response.result);
}
else {
return verificationProcess.sendError('The xrp transaction could not be validated!');
}
if (response.result.tx_json.TransactionType !== 'Payment') {
return verificationProcess.sendError('The xrp transaction type does not match the expected transaction type (Payment)!', AdditionalDataType.ERROR, response.result);
}
if (!response.result.validated) {
return verificationProcess.sendError('The transaction is not yet fully validated in the blockchain.');
}
let destination = response.result.tx_json.Destination;
let memoData = response.result.tx_json.Memos[0].Memo.MemoData;
let blockchainRootHash = Xrp.transactionDataToHash(memoData, destination);
let rippleTime = response.result.tx_json.date;
let date = this.getTransactionDate(rippleTime);
verificationProcess.sendMessage('The transaction with destination ' +
destination +
' and additional memo data ' +
memoData +
' translates to the hash ' +
blockchainRootHash, MessageType.PROGRESS);
if (rippleTime !== transactionTimestamp) {
return verificationProcess.sendError('The certificate time does not match the time in the blockchain! ' +
`Cert: ${transactionTimestamp} (` +
this.getTransactionDate(transactionTimestamp).toISOString() +
`) Blockchain: ${rippleTime} (` +
date.toISOString() +
')', AdditionalDataType.TIMESTAMP, rippleTime);
}
if (blockchainRootHash !== rootHash) {
return verificationProcess.sendError('The certificate is invalid! The hash in the blockchain and certificate do not match. Cert: ' +
rootHash +
' Blockchain: ' +
blockchainRootHash +
' This means the document did not exist at time ' +
date.toISOString(), AdditionalDataType.HASH, blockchainRootHash);
}
return verificationProcess.sendMessage('The certificate is valid! The hash in the blockchain and certificate match. This means the document existed at time ' +
date.toISOString(), MessageType.SUCCESS);
}
}
//# sourceMappingURL=xrp.js.map