UNPKG

@cronstamp/clientlib

Version:

Client library for cronstamp, a blockchain-based document timestamping and verification service.

156 lines 8.89 kB
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