postchain-client
Version:
Client library for accessing a Postchain node through REST.
570 lines • 27 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as logger from "../logger";
import { SerializedTransactionFormatException, InvalidTxRidException, TxRejectedError, UnexpectedStatusError, GetBridFromChainException, } from "./errors";
import * as gtxTool from "../gtx/gtx";
import { BlockchainUrlUndefinedException, DirectoryNodeUrlPoolException, InvalidTransactionFormatException, MissingBlockchainIdentifierError, } from "./errors";
import { ensureBuffer, toBuffer } from "../formatter";
import { createClient } from "./blockchainClient";
import { AnchoringStatus, ChainConfirmationLevel, FailoverStrategy, Method, ResponseStatus, TransactionEvent, } from "./enums";
import { createNodeManager } from "./nodeManager";
import { AnchoringTransactionSchema } from "./validation/anchoringTransaction";
import { SystemChainException } from "../ICCF/error";
import { getAnchoringClient } from "../ICCF/IccfProofTxMaterialBuilder";
import { awaitGetAnchoringTransactionForBlockRid, calculateBlockRID } from "../ICCF/utils";
import { isString } from "./validation/requests";
import { encodeTransaction } from "../utils/encodeTransaction";
import { validateMerkleHash } from "./validation/merkleHash";
import { requestWithFailoverStrategy } from "./requestWithFailoverStrategy";
import { MERKLE_HASH_VERSIONS } from "../utils/constants";
export function getClientConfigFromSettings(settings) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
return __awaiter(this, void 0, void 0, function* () {
const nodeUrlPoolToUse = yield getNodeUrlsFromSettings(settings);
if (nodeUrlPoolToUse === null || nodeUrlPoolToUse.length === 0) {
const id = (_b = (_a = settings.blockchainRid) !== null && _a !== void 0 ? _a : settings.blockchainIid) !== null && _b !== void 0 ? _b : "Unknown";
throw new BlockchainUrlUndefinedException(id);
}
const endpointPool = createEndpointObjects(nodeUrlPoolToUse);
const nodeManager = createNodeManager({
nodeUrls: nodeUrlPoolToUse,
useStickyNode: (_c = settings.useStickyNode) !== null && _c !== void 0 ? _c : false,
unavailableDuration: (_d = settings.failOverConfig) === null || _d === void 0 ? void 0 : _d.unreachableDuration,
});
const blockchainRidToUse = yield (() => __awaiter(this, void 0, void 0, function* () {
var _k;
if (settings.blockchainRid) {
return settings.blockchainRid;
}
if (settings.blockchainIid !== undefined) {
return yield getBlockchainRidFromIid({
nodeManager,
endpointPool,
chainId: settings.blockchainIid,
merkleHashVersion: (_k = settings.merkleHashVersion) !== null && _k !== void 0 ? _k : 0,
});
}
throw new MissingBlockchainIdentifierError();
}))();
const directoryChainRid = yield (() => __awaiter(this, void 0, void 0, function* () {
var _l;
if (settings.directoryChainRid) {
return settings.directoryChainRid;
}
const directoryChainIid = 0;
return yield getBlockchainRidFromIid({
nodeManager,
endpointPool,
chainId: directoryChainIid,
merkleHashVersion: (_l = settings.merkleHashVersion) !== null && _l !== void 0 ? _l : 0,
});
}))();
return {
endpointPool,
nodeManager: nodeManager,
blockchainRid: blockchainRidToUse,
merkleHashVersion: (_e = settings.merkleHashVersion) !== null && _e !== void 0 ? _e : 0,
dappStatusPolling: setStatusPolling(settings.dappStatusPolling),
clusterAnchoringStatusPolling: setStatusPolling(settings.clusterAnchoringStatusPolling),
systemAnchoringStatusPolling: setStatusPolling(settings.systemAnchoringStatusPolling),
failoverStrategy: ((_f = settings.failOverConfig) === null || _f === void 0 ? void 0 : _f.strategy) || defaultFailoverConfig.strategy,
attemptsPerEndpoint: ((_g = settings.failOverConfig) === null || _g === void 0 ? void 0 : _g.attemptsPerEndpoint) || defaultFailoverConfig.attemptsPerEndpoint,
attemptInterval: ((_h = settings.failOverConfig) === null || _h === void 0 ? void 0 : _h.attemptInterval) || defaultFailoverConfig.attemptInterval,
unreachableDuration: ((_j = settings.failOverConfig) === null || _j === void 0 ? void 0 : _j.unreachableDuration) || defaultFailoverConfig.unreachableDuration,
directoryChainRid: settings.directoryChainRid || directoryChainRid,
};
});
}
export function getMerkleHashVersionFromDapp(config) {
return __awaiter(this, void 0, void 0, function* () {
try {
const response = yield requestWithFailoverStrategy(Method.GET, `/config/${config.blockchainRid}/features`, config);
const validatedResponse = validateMerkleHash(response.rspBody);
return validatedResponse.merkle_hash_version;
}
catch (error) {
console.warn("MERKLE_HASH_WARNING: version not found or endpoint unreachable", error);
return MERKLE_HASH_VERSIONS.ONE;
}
});
}
export function validTxRid(txRID) {
if (txRID.length != 32) {
const error = new InvalidTxRidException(txRID);
logger.error(error.toString());
return false;
}
return true;
}
export function nodeDiscovery({ nodeManager, directoryEndpointPool, failOverConfig, blockchainRid, blockchainIid, }) {
return __awaiter(this, void 0, void 0, function* () {
if (directoryEndpointPool.length === 0) {
throw new DirectoryNodeUrlPoolException();
}
if (!blockchainRid && blockchainIid === undefined) {
throw new MissingBlockchainIdentifierError();
}
const directoryIid = 0;
const directoryBRID = yield getBlockchainRidFromIid({
nodeManager,
endpointPool: directoryEndpointPool,
chainId: directoryIid,
failOverConfig,
merkleHashVersion: 0,
});
const blockchainRidToUse = yield (() => __awaiter(this, void 0, void 0, function* () {
if (blockchainRid) {
return blockchainRid;
}
if (blockchainIid !== undefined) {
return yield getBlockchainRidFromIid({
nodeManager,
endpointPool: directoryEndpointPool,
chainId: blockchainIid,
failOverConfig,
merkleHashVersion: 0,
});
}
throw new MissingBlockchainIdentifierError();
}))();
const D1Client = yield createClient({
nodeUrlPool: getUrlsFromEndpoints(directoryEndpointPool),
blockchainRid: directoryBRID,
merkleHashVersion: 0,
});
return yield getBlockchainApiUrls(D1Client, ensureBuffer(blockchainRidToUse));
});
}
export function getBlockchainApiUrls(directoryClient, blockchainRid) {
return __awaiter(this, void 0, void 0, function* () {
const queryObject = {
name: "cm_get_blockchain_api_urls",
args: { blockchain_rid: blockchainRid },
};
return yield directoryClient.query(queryObject);
});
}
export function convertToRellOperation(operations) {
return operations.map(operation => {
var _a;
return {
opName: operation.name,
args: (_a = operation.args) !== null && _a !== void 0 ? _a : [],
};
});
}
export function getSerializedGTX(gtx) {
const gtxBytes = encodeTransaction(gtx);
if (!Buffer.isBuffer(gtxBytes)) {
throw new SerializedTransactionFormatException();
}
return gtxBytes;
}
export function getGTXFromBufferOrTransactionOrOperation(transaction, blockchainRid) {
if (Buffer.isBuffer(transaction)) {
return gtxTool.deserialize(transaction);
}
else if ("operations" in transaction) {
return {
blockchainRid: ensureBuffer(blockchainRid),
operations: convertToRellOperation(transaction.operations),
signers: transaction.signers,
signatures: [],
};
}
else if ("name" in transaction) {
return {
blockchainRid: ensureBuffer(blockchainRid),
operations: convertToRellOperation([transaction]),
signers: [],
signatures: [],
};
}
else {
throw new InvalidTransactionFormatException();
}
}
export const callbackPromiseBuilder = (reject, resolve, callback) => {
return (error, result) => {
var _a;
if (error) {
callback === null || callback === void 0 ? void 0 : callback(error, null);
reject(error);
}
else {
callback === null || callback === void 0 ? void 0 : callback(null, (_a = result) !== null && _a !== void 0 ? _a : null);
resolve(result !== null && result !== void 0 ? result : null);
}
};
};
export const handlePostResponsePromisified = (error, statusCode, rspBody) => {
return new Promise((resolve, reject) => {
handlePostResponse(error, statusCode, rspBody, _error => {
if (_error) {
reject(_error);
}
else {
resolve();
}
});
});
};
function ensureArray(input) {
if (typeof input === "string") {
return [input];
}
return input;
}
export const formatTransactionInfoResponse = (transactionInfoResponse) => {
return {
blockRid: toBuffer(transactionInfoResponse.blockRID),
blockHeight: transactionInfoResponse.blockHeight,
blockHeader: toBuffer(transactionInfoResponse.blockHeader),
witness: toBuffer(transactionInfoResponse.witness),
timestamp: transactionInfoResponse.timestamp,
txRid: toBuffer(transactionInfoResponse.txRID),
txHash: toBuffer(transactionInfoResponse.txHash),
txData: toBuffer(transactionInfoResponse.txData),
};
};
export const formatBlockInfoResponse = (blockInfoResponse) => {
return {
rid: toBuffer(blockInfoResponse.rid),
prevBlockRid: toBuffer(blockInfoResponse.prevBlockRID),
header: toBuffer(blockInfoResponse.header),
transactions: blockInfoResponse.transactions.map(formatTransaction),
height: blockInfoResponse.height,
witness: toBuffer(blockInfoResponse.witness),
witnesses: blockInfoResponse.witnesses.map(witness => {
return toBuffer(witness);
}),
timestamp: blockInfoResponse.timestamp,
};
};
const formatTransaction = (transaction) => {
const formattedTransaction = {
rid: toBuffer(transaction.rid),
hash: toBuffer(transaction.hash),
};
if (transaction.data !== undefined) {
formattedTransaction.data = toBuffer(transaction.data);
}
return formattedTransaction;
};
export function formatRejectedTransactionFromResponse(rejectedTransactionResponse) {
return {
txRid: toBuffer(rejectedTransactionResponse.txRID),
rejectReason: rejectedTransactionResponse.rejectReason,
rejectTimestamp: rejectedTransactionResponse.rejectTimestamp,
};
}
export const isKeyPair = (keypair) => {
return (typeof keypair === "object" &&
keypair !== null &&
"privKey" in keypair &&
"pubKey" in keypair &&
keypair.privKey instanceof Buffer &&
keypair.pubKey instanceof Buffer);
};
function getNodeUrlsFromSettings(settings) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (settings.directoryNodeUrlPool) {
// If directoryNodeUrlPool is provided, use nodeDiscovery
const nodeManager = createNodeManager({
nodeUrls: ensureArray(settings.directoryNodeUrlPool),
unavailableDuration: (_a = settings.failOverConfig) === null || _a === void 0 ? void 0 : _a.unreachableDuration,
});
const discoveredNodes = yield nodeDiscovery({
nodeManager,
directoryEndpointPool: createEndpointObjects(ensureArray(settings.directoryNodeUrlPool)),
failOverConfig: settings.failOverConfig,
blockchainRid: settings.blockchainRid,
blockchainIid: settings.blockchainIid,
});
return discoveredNodes;
}
else if (typeof settings.nodeUrlPool === "string") {
// If nodeUrlPool is a string, convert it to an array
return [settings.nodeUrlPool];
}
else if (Array.isArray(settings.nodeUrlPool)) {
// If nodeUrlPool is already an array, use it as-is
return settings.nodeUrlPool;
}
else {
// Default to an empty array if no valid configuration is provided
return [];
}
});
}
export function getSystemClient(directoryNodeUrlPool, directoryChainRid) {
return __awaiter(this, void 0, void 0, function* () {
return yield createClient({
directoryNodeUrlPool,
blockchainRid: directoryChainRid,
merkleHashVersion: 0,
});
});
}
export function getSystemAnchoringChain(directoryClient) {
return __awaiter(this, void 0, void 0, function* () {
try {
const queryObject = {
name: "cm_get_system_anchoring_chain",
};
const systemAnchoringChain = yield directoryClient.query(queryObject);
if (!systemAnchoringChain) {
throw new SystemChainException("Directory chain client must be provided.");
}
return systemAnchoringChain;
}
catch (error) {
throw new SystemChainException(error.message);
}
});
}
export const defaultFailoverConfig = {
strategy: FailoverStrategy.AbortOnError,
attemptsPerEndpoint: 3,
attemptInterval: 500,
unreachableDuration: 30000,
};
export const createEndpointObjects = (endpointPoolUrls) => {
const endpoints = endpointPoolUrls.map(endpointUrl => {
return { url: endpointUrl, whenAvailable: 0 };
});
return endpoints;
};
export const getUrlsFromEndpoints = (endpointPool) => {
return endpointPool.map(endpoint => endpoint.url);
};
export function awaitDappConfirmation(txRID, dappStatusPolling, getTransactionStatus) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
let lastKnownResult;
for (let i = 0; i < dappStatusPolling.count; i++) {
lastKnownResult = yield getTransactionStatus(txRID);
if (lastKnownResult.status === ResponseStatus.Confirmed) {
return lastKnownResult;
}
else if (lastKnownResult.status === ResponseStatus.Rejected) {
throw new TxRejectedError((_a = lastKnownResult.rejectReason) !== null && _a !== void 0 ? _a : "");
}
yield sleep(dappStatusPolling.interval);
}
// TS issue. This could be fixed by implementing new retry strategy
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
return lastKnownResult;
});
}
export function awaitClusterAnchoringChainConfirmation(txRID, clusterAnchoringStatusPolling, getClusterAnchoringTransactionConfirmation) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
let clusterAnchoringConfirmation;
for (let i = 0; i < clusterAnchoringStatusPolling.count; i++) {
clusterAnchoringConfirmation = yield getClusterAnchoringTransactionConfirmation(txRID);
const anchoringTransactionValidaiton = AnchoringTransactionSchema.safeParse(clusterAnchoringConfirmation);
if (anchoringTransactionValidaiton.success) {
return {
status: AnchoringStatus.ClusterAnchored,
clusterAnchoredTx: anchoringTransactionValidaiton.data,
};
}
else if (!anchoringTransactionValidaiton.success) {
throw new TxRejectedError((_a = AnchoringStatus.FailedAnchoring) !== null && _a !== void 0 ? _a : "");
}
yield sleep(clusterAnchoringStatusPolling.interval);
}
// TS issue. This could be fixed by implementing new retry strategy
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
return clusterAnchoringConfirmation;
});
}
export function handleDappConfirmations(transactionRid, doStatusPolling, confirmationLevel, promiEvent, dappStatusPolling, getTransactionStatus) {
return __awaiter(this, void 0, void 0, function* () {
const transactionReceipt = {
status: ResponseStatus.Pending,
statusCode: null,
transactionRid: transactionRid,
};
promiEvent.emit(TransactionEvent.DappReceived, transactionReceipt);
if (doStatusPolling === false || confirmationLevel === ChainConfirmationLevel.None) {
return transactionReceipt;
}
const dappConfirmationStatus = yield awaitDappConfirmation(transactionRid, dappStatusPolling, getTransactionStatus);
transactionReceipt.statusCode = dappConfirmationStatus.statusCode;
transactionReceipt.status = dappConfirmationStatus.status;
promiEvent.emit(TransactionEvent.DappConfirmed, transactionReceipt);
if (confirmationLevel === ChainConfirmationLevel.Dapp) {
return transactionReceipt;
}
return transactionReceipt;
});
}
export function handleSystemConfirmations(transactionReceipt, confirmationLevel, promiEvent, statusPollingConfig, anchoringClient, systemAnchoringChainBrid, getClusterAnchoringTransactionConfirmation, getSystemAnchoringTransactionConfirmation) {
return __awaiter(this, void 0, void 0, function* () {
const clusterAnchoringChainConfirmation = yield awaitClusterAnchoringChainConfirmation(transactionReceipt.transactionRid, statusPollingConfig.clusterAnchoringStatusPolling, getClusterAnchoringTransactionConfirmation);
transactionReceipt.status = clusterAnchoringChainConfirmation.status;
transactionReceipt.clusterAnchoredTx = clusterAnchoringChainConfirmation.clusterAnchoredTx;
transactionReceipt.clusterAnchoringClientBrid = anchoringClient.config.blockchainRid;
promiEvent.emit(TransactionEvent.ClusterAnchoringConfirmation, transactionReceipt);
if (confirmationLevel === ChainConfirmationLevel.ClusterAnchoring) {
return transactionReceipt;
}
const systemAnchoringConfirmation = yield getSystemAnchoringTransactionConfirmation(clusterAnchoringChainConfirmation.clusterAnchoredTx.txRid, anchoringClient, systemAnchoringChainBrid);
if (!systemAnchoringConfirmation) {
return transactionReceipt;
}
transactionReceipt.status = AnchoringStatus.SystemAnchored;
transactionReceipt.systemAnchoredTx = systemAnchoringConfirmation;
transactionReceipt.systemAnchoringClientBrid = systemAnchoringChainBrid;
promiEvent.emit(TransactionEvent.SystemAnchoringConfirmation, transactionReceipt);
if (confirmationLevel === ChainConfirmationLevel.SystemAnchoring) {
return transactionReceipt;
}
return transactionReceipt;
});
}
export function getAnchoringClientAndSystemChainRid(client) {
return __awaiter(this, void 0, void 0, function* () {
const directoryClient = yield getSystemClient(getUrlsFromEndpoints(client.config.endpointPool), client.config.directoryChainRid);
const anchoringClient = yield getAnchoringClient(directoryClient, client.config.blockchainRid);
const systemAnchoringChainRidBuffer = yield getSystemAnchoringChain(directoryClient);
const systemAnchoringChainBridString = systemAnchoringChainRidBuffer.toString("hex");
return { anchoringClient, systemAnchoringChainBridString };
});
}
export function getSystemAnchoringTransaction(dappClientEndpointPool, anchoredTxRid, anchoringClient, systemAnchoringChainRid, systemAnchoringStatusPolling, merkleHashVersion) {
return __awaiter(this, void 0, void 0, function* () {
const systemAnchoringChainClient = yield getSystemClient(getUrlsFromEndpoints(dappClientEndpointPool), systemAnchoringChainRid);
const clusterAnchoringProof = yield anchoringClient.getConfirmationProof(anchoredTxRid);
const clusterBlockRid = calculateBlockRID(clusterAnchoringProof, merkleHashVersion);
const systemAnchoringTransactionConfirmation = yield awaitGetAnchoringTransactionForBlockRid(systemAnchoringChainClient, toBuffer(anchoringClient.config.blockchainRid), clusterBlockRid, systemAnchoringStatusPolling);
const systemAnchoringTransactionValidation = AnchoringTransactionSchema.safeParse(systemAnchoringTransactionConfirmation);
if (!systemAnchoringTransactionValidation.success) {
return null;
}
return systemAnchoringTransactionValidation.data;
});
}
export function setStatusPolling(statusPolling) {
var _a, _b;
return {
interval: (_a = statusPolling === null || statusPolling === void 0 ? void 0 : statusPolling.interval) !== null && _a !== void 0 ? _a : 500,
count: (_b = statusPolling === null || statusPolling === void 0 ? void 0 : statusPolling.count) !== null && _b !== void 0 ? _b : 20,
};
}
export function linkPromiEvents(event1, event2) {
const transactionEvents = Object.values(TransactionEvent).filter((event) => typeof event === "string");
/* eslint-disable @typescript-eslint/no-explicit-any */
const forwardEvent = (eventName) => (receipt) => event2.emit(eventName, receipt);
for (const eventName of transactionEvents) {
event1.on(eventName, forwardEvent(eventName));
}
}
/**
* @param error response error
* @param statusCode response status code
* @param responseObject the responsebody from the server
* @param callback the callback function to propagate the error and response back to the caller
*/
export function handleGetResponse(error, statusCode, responseObject, callback) {
try {
const responseObjectPrintable = convertToPrintable(responseObject);
logger.debug(`error: ${error}, status code: ${statusCode}, response body: ${responseObjectPrintable}`);
if (error) {
return callback(error, null);
}
if (statusCode !== 200) {
return callback(new UnexpectedStatusError(statusCode !== null && statusCode !== void 0 ? statusCode : 400, responseObjectPrintable), null);
}
return callback(null, responseObject);
}
catch (error) {
callback(error, null);
logger.error(`client.handleGetResponse(): Failed to call the callback function. ${error}`);
}
}
/**
* @param error response error
* @param statusCode response status code
* @param responseObject the responsebody from the server
* @param callback the callback function to propagate the error and response back to the caller
*/
export function handlePostResponse(error, statusCode, responseObject, callback) {
const responseObjectPrintable = convertToPrintable(responseObject);
logger.debug(`error: ${error}, status code: ${statusCode}, response body: ${responseObjectPrintable}`);
try {
if (error) {
logger.error(`In client post(). ${error}`);
callback(error, null);
}
else if (statusCode != 200) {
let errorMessage = `Unexpected status code from server. Code: ${statusCode}.`;
if (responseObjectPrintable) {
errorMessage += ` Message: ${responseObjectPrintable}.`;
}
logger.error(errorMessage);
callback(new UnexpectedStatusError(statusCode !== null && statusCode !== void 0 ? statusCode : 400, responseObjectPrintable), responseObject);
}
else {
logger.info(`Calling responseCallback with responseObject: ${responseObjectPrintable}`);
callback(null, responseObject);
}
}
catch (error) {
logger.error(`client.handlePostResponse(): Failed to call callback function ${error}`);
}
}
export function getBlockchainRidFromIid({ endpointPool, chainId, failOverConfig = {}, nodeManager, }) {
return __awaiter(this, void 0, void 0, function* () {
const mergedFailOverConfig = Object.assign(Object.assign({}, defaultFailoverConfig), failOverConfig);
const config = {
endpointPool,
nodeManager,
dappStatusPolling: setStatusPolling(),
clusterAnchoringStatusPolling: setStatusPolling(),
systemAnchoringStatusPolling: setStatusPolling(),
failoverStrategy: mergedFailOverConfig.strategy,
attemptsPerEndpoint: mergedFailOverConfig.attemptsPerEndpoint,
attemptInterval: mergedFailOverConfig.attemptInterval,
unreachableDuration: mergedFailOverConfig.unreachableDuration,
merkleHashVersion: 0,
};
const { error, statusCode, rspBody } = yield requestWithFailoverStrategy(Method.GET, `/brid/iid_${chainId}`, config);
const blockchainRid = isString(rspBody);
if (!blockchainRid) {
throw error;
}
if (error) {
throw new GetBridFromChainException(chainId, error.message);
}
else if (statusCode !== 200) {
throw new GetBridFromChainException(chainId, blockchainRid);
}
return blockchainRid;
});
}
export const sleep = (ms) => new Promise(r => setTimeout(r, ms));
export function convertToPrintable(responseObject) {
if (typeof responseObject === "bigint") {
return `${responseObject}n`;
}
else if (typeof responseObject === "object") {
return JSON.stringify(responseObject, (key, value) => typeof value === "bigint" ? `${value}n` : value);
}
else {
return String(responseObject);
}
}
//# sourceMappingURL=utils.js.map