UNPKG

circular-enterprise-apis

Version:
574 lines (503 loc) 18.5 kB
/******************************************************************************* CIRCULAR Enterprise APIs for Data Certification License : Open Source for private and commercial use CIRCULAR GLOBAL LEDGERS, INC. - USA Version : 1.0.1 Creation: 2/5/2025 Update : 2/5/2025 Originator: Gianluca De Novi, PhD Contributors: <names here> *******************************************************************************/ const { ec } = require('elliptic'); const sha256 = require('sha256'); const https = require('https'); const url = require('url'); /* Library Details ************************************************************/ const LIB_VERSION = '1.0.13'; const NETWORK_URL = 'https://circularlabs.io/network/getNAG?network='; const DEFAULT_CHAIN = '0x8a20baa40c45dc5055aeb26197c203e576ef389d9acb171bd62da11dc5ad72b2'; const DEFAULT_NAG = 'https://nag.circularlabs.io/NAG.php?cep='; /* HELPER FUNCTIONS ***********************************************************/ /** * Function to add a leading zero to numbers less than 10 * @param {number} num - Number to pad * @returns {string} Padded number as a string */ function padNumber(num) { return num < 10 ? '0' + num : num.toString(); } /** * Returns a formatted timestamp in UTC * @returns {string} Formatted timestamp (YYYY:MM:DD-HH:MM:SS) */ function getFormattedTimestamp() { const date = new Date(); const year = date.getUTCFullYear(); const month = padNumber(date.getUTCMonth() + 1); const day = padNumber(date.getUTCDate()); const hours = padNumber(date.getUTCHours()); const minutes = padNumber(date.getUTCMinutes()); const seconds = padNumber(date.getUTCSeconds()); return `${year}:${month}:${day}-${hours}:${minutes}:${seconds}`; } /** * Removes '0x' from hexadecimal strings if present * @param {string} word - Hexadecimal string * @returns {string} Hexadecimal string without '0x' */ function HexFix(word) { if (typeof word === 'string') { return word.startsWith('0x') ? word.slice(2) : word; } return ''; } /** * Converts a string to its hexadecimal representation * @param {string} str - Input string * @returns {string} Hexadecimal representation of the string */ function stringToHex(str) { let hexString = ''; for (let i = 0; i < str.length; i++) { const hex = str.charCodeAt(i).toString(16); hexString += ('00' + hex).slice(-2); } return hexString; } /** * Converts a hexadecimal string to a regular string * @param {string} hex - Hexadecimal string * @returns {string} Regular string */ function hexToString(hex) { hex = HexFix(hex); let str = ''; for (let i = 0; i < hex.length; i += 2) { const code = parseInt(hex.substr(i, 2), 16); if (!isNaN(code) && code !== 0) { str += String.fromCharCode(code); } } return str; } /******************************************************************************* * Circular Certificate Class for certificate chaining */ class C_CERTIFICATE { constructor() { this.data = null; this.previousTxID = null; this.previousBlock = null; this.codeVersion = LIB_VERSION; } /** * Inserts application data into the certificate * @param {string} data - Data content */ setData(data) { this.data = stringToHex(data); } /** * Extracts application data from the certificate * @returns {string} Data content */ getData() { return hexToString(this.data); } /** * Returns the certificate in JSON format * @returns {string} JSON string representation of the certificate */ getJSONCertificate() { const certificate = { data: this.data, previousTxID: this.previousTxID, previousBlock: this.previousBlock, version: this.codeVersion, }; return JSON.stringify(certificate); } /** * Returns the size of the certificate in bytes * @returns {number} Size of the certificate in bytes */ getCertificateSize() { const jsonString = this.getJSONCertificate(); return Buffer.byteLength(jsonString, 'utf8'); } } /******************************************************************************* * Circular Account Class */ class CEP_Account { constructor() { this.address = null; this.publicKey = null; this.info = null; this.codeVersion = LIB_VERSION; this.lastError = ''; this.NAG_URL = DEFAULT_NAG; this.NETWORK_NODE = ''; this.blockchain = DEFAULT_CHAIN; this.LatestTxID = ''; this.Nonce = 0; this.data = {}; this.intervalSec = 2; } /** * Opens an account by setting the address * @param {string} address - Account address */ open(address) { if (!address || typeof address !== 'string') { throw new Error('Invalid address format'); } this.address = address; } /** * Updates the account's Nonce by querying the network * @returns {boolean} True if successful, false otherwise */ async updateAccount() { if (!this.address) { throw new Error('Account is not open'); } const data = { Blockchain: HexFix(this.blockchain), Address: HexFix(this.address), Version: this.codeVersion, }; console.log("Sending data:", JSON.stringify(data)); return new Promise((resolve, reject) => { // Construct the URL without query parameters const parsedUrl = url.parse(this.NAG_URL + 'Circular_GetWalletNonce_' + this.NETWORK_NODE); console.log("Parsed URL:", parsedUrl); const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || 443, path: parsedUrl.path, // Path without query parameters method: 'POST', // Make sure to use POST method headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(JSON.stringify(data)), // Set correct content length }, timeout: 60000, // Timeout extended to 60 seconds rejectUnauthorized: true, }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { console.log(`Response status: ${res.statusCode}`); // Log the response status try { const jsonResponse = JSON.parse(body); if (jsonResponse.Result === 200 && jsonResponse.Response && jsonResponse.Response.Nonce !== undefined) { this.Nonce = jsonResponse.Response.Nonce + 1; resolve(true); } else { console.error('Invalid response format or missing Nonce field'); reject(new Error('Invalid response format or missing Nonce field')); } } catch (error) { console.error('Failed to parse response:', error); reject(new Error('Failed to parse response body')); } }); }); req.on('error', (error) => { console.error('Error in updateAccount:', error.message); reject(new Error(`Request failed with error: ${error.message}`)); }); req.write(JSON.stringify(data)); // Send the data in the body as JSON req.end(); }); } /** * Sets the blockchain network * @param {string} network - Network name (e.g., 'devnet', 'testnet', 'mainnet') */ async setNetwork(network) { return new Promise((resolve, reject) => { const nagUrl = NETWORK_URL + encodeURIComponent(network); const parsedUrl = url.parse(nagUrl); const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || 443, path: parsedUrl.path, method: 'GET', headers: { Accept: 'application/json' }, timeout: 30000 }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { try { const data = JSON.parse(body); if (data.status === 'success' && data.url) { this.NAG_URL = data.url; resolve(); } else { reject(new Error(data.message || 'Failed to get URL')); } } catch (error) { reject(error); } }); }); req.on('error', (error) => { console.error('Error fetching network URL:', error.message); reject(error); }); req.end(); }); } /** * Sets the blockchain address * @param {string} chain - Blockchain address */ setBlockchain(chain) { this.blockchain = chain; } /** * Closes the account and resets all fields */ close() { this.address = null; this.publicKey = null; this.info = null; this.lastError = ''; this.NAG_URL = DEFAULT_NAG; this.NETWORK_NODE = ''; this.blockchain = DEFAULT_CHAIN; this.LatestTxID = ''; this.data = {}; this.intervalSec = 2; } /** * Signs data using the account's private key * @param {string} data - Data to sign * @param {string} privateKey - Private key associated with the account * @returns {string} Signature */ signData(data, privateKey) { if (!this.address) { throw new Error('Account is not open'); } const ecInstance = new ec('secp256k1'); const key = ecInstance.keyFromPrivate(HexFix(privateKey), 'hex'); const msgHash = sha256(data).toString('hex'); const signature = key.sign(msgHash).toDER('hex'); return signature; } /** * Submits a certificate to the blockchain * @param {string} pdata - Data to submit * @param {string} privateKey - Private key associated with the account * @returns {Promise<Object>} Response from the blockchain */ async submitCertificate(pdata, privateKey) { if (!this.address) { throw new Error('Account is not open'); } const PayloadObject = { Action: 'CP_CERTIFICATE', Data: stringToHex(pdata), }; const jsonstr = JSON.stringify(PayloadObject); const Payload = stringToHex(jsonstr); const Timestamp = getFormattedTimestamp(); const str = HexFix(this.blockchain) + HexFix(this.address) + HexFix(this.address) + Payload + this.Nonce + Timestamp; const ID = sha256(str).toString('hex'); const Signature = this.signData(ID, privateKey); const data = { ID, From: HexFix(this.address), To: HexFix(this.address), Timestamp, Payload: String(Payload), Nonce: String(this.Nonce), Signature, Blockchain: HexFix(this.blockchain), Type: 'C_TYPE_CERTIFICATE', Version: this.codeVersion, }; return new Promise((resolve, reject) => { const parsedUrl = url.parse(this.NAG_URL + 'Circular_AddTransaction_' + this.NETWORK_NODE); const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || 443, path: parsedUrl.path, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(JSON.stringify(data)), // Set correct content length }, timeout: 30000 }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { try { resolve(JSON.parse(body)); } catch (error) { reject(error); } }); }); req.on('error', (error) => { console.error('Error in submitCertificate:', error.message); resolve({ success: false, message: 'Server unreachable or request failed', error: error.toString() }); }); req.write(JSON.stringify(data)); req.end(); }); } /** * Polls for transaction outcome * @param {string} TxID - Transaction ID * @param {number} timeoutSec - Timeout in seconds * @returns {Promise<Object>} Transaction outcome */ async GetTransactionOutcome(TxID, timeoutSec) { return new Promise((resolve, reject) => { const startTime = Date.now(); const interval = this.intervalSec * 1000; const timeout = timeoutSec * 1000; const checkTransaction = async () => { const elapsedTime = Date.now() - startTime; if (elapsedTime > timeout) { reject(new Error('Timeout exceeded')); return; } try { const data = await this.getTransactionbyID(TxID, 0, 10); if (data.Result === 200 && data.Response !== 'Transaction Not Found' && data.Response.Status !== 'Pending') { resolve(data.Response); } else { setTimeout(checkTransaction, interval); } } catch (error) { reject(error); } }; setTimeout(checkTransaction, interval); }); } /** * Searches for a transaction by its ID * @param {string} TxID - Transaction ID * @param {number} Start - Starting block * @param {number} End - End block * @returns {Promise<Object>} Transaction details */ async getTransaction(BlockID, TxID) { const data = { Blockchain: HexFix(this.blockchain), ID: HexFix(TxID), Start: String(BlockID), End: String(BlockID), Version: this.codeVersion, }; return new Promise((resolve, reject) => { const parsedUrl = url.parse(this.NAG_URL + 'Circular_GetTransactionbyID_' + this.NETWORK_NODE); const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || 443, path: parsedUrl.path, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(JSON.stringify(data)), // Set correct content length }, }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { try { resolve(JSON.parse(body)); } catch (error) { reject(error); } }); }); req.on('error', (error) => { console.error('Error in getTransactionbyID:', error.message); reject(error); }); req.write(JSON.stringify(data)); req.end(); }); } /** * Searches for a transaction by its ID * @param {string} TxID - Transaction ID * @param {number} Start - Starting block * @param {number} End - End block * @returns {Promise<Object>} Transaction details */ async getTransactionbyID(TxID, Start, End) { const data = { Blockchain: HexFix(this.blockchain), ID: HexFix(TxID), Start: String(Start), End: String(End), Version: this.codeVersion, }; return new Promise((resolve, reject) => { const parsedUrl = url.parse(this.NAG_URL + 'Circular_GetTransactionbyID_' + this.NETWORK_NODE); const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || 443, path: parsedUrl.path, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(JSON.stringify(data)), // Set correct content length }, }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { try { resolve(JSON.parse(body)); } catch (error) { reject(error); } }); }); req.on('error', (error) => { console.error('Error in getTransactionbyID:', error.message); reject(error); }); req.write(JSON.stringify(data)); req.end(); }); } } // Export classes for use in other modules module.exports = { C_CERTIFICATE, CEP_Account };