UNPKG

circular-protocol-api

Version:
1,035 lines (814 loc) 29.5 kB
const elliptic = require('elliptic'); const sha256 = require('sha256'); const https = require('https'); const { URL } = require('url'); /******************************************************************************* CIRCULAR LAYER 1 BLOCKCHAIN PROTOCOL INTERFACE LIBRARY License : Open Source for private and commercial use CIRCULAR GLOBAL LEDGERS, INC. - USA Version : 1.0.14 Creation: 7/12/2022 Update : 28/01/2025 Originator: Gianluca De Novi, PhD Contributors: Danny De Novi, redorc83 *******************************************************************************/ /* * Circular Class */ let CircularProtocolAPI = (function(){ // Support Node Software Version const version = '1.0.8'; // Application NAG Key let NAG_KEY=''; // Default NAG Link let NAG_URL='https://nag.circularlabs.io/NAG.php?cep='; function getNAGURL() { return NAG_URL } function getNAGKey() { return NAG_KEY; } function hashString(str) { return sha256(str); } // Library Errors Variable let LastError; // Helper function for error handling function handleError(error) { console.error('Error:', error); LastError = error.message || 'An unknown error occurred'; return { success: false, error: LastError }; } /* * Retrieves the Library Error */ function GetError(){ return LastError; } /* HELPER FUNCTIONS ***********************************************************/ /* * Function to add a leading zero to numbers less than 10 * num : number to pad * */ function padNumber(num) { return num < 10 ? '0' + num : num; } function makeRequest(action, data) { // Build the full URL using the base URL and action const fullUrl = `${getNAGURL()}${action}`; const parsedUrl = new URL(fullUrl); const dim = Buffer.byteLength(JSON.stringify(data)); return new Promise((resolve, reject) => { const options = { hostname: parsedUrl.hostname, path: parsedUrl.pathname + parsedUrl.search, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': dim }, timeout: 30000 }; const req = https.request(options, (res) => { let responseData = ''; res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { try { const parsedData = JSON.parse(responseData); resolve(parsedData); } catch (err) { reject(new Error(`Error parsing response data: ${err.message}`)); } }); }); req.on('error', (e) => { reject(new Error(`Request error: ${e.message}`)); }); req.on('timeout', () => { req.abort(); reject(new Error('Request timed out')); }); req.write(JSON.stringify(data)); req.end(); }); } // /* * Generate Timestamp formated * YYYY:MM:DD-hh:mm:ss */ function getFormattedTimestamp() { let date = new Date(); let year = date.getUTCFullYear(); let month = padNumber(date.getUTCMonth() + 1); let day = padNumber(date.getUTCDate()); let hours = padNumber(date.getUTCHours()); let minutes = padNumber(date.getUTCMinutes()); let seconds = padNumber(date.getUTCSeconds()); // Formats the date and time in a specific format and returns it return `${year}:${month}:${day}-${hours}:${minutes}:${seconds}`; } /* * Sign a message using secp256k1 * message: Message to sign * provateKey: Private key in hex format (minus '0x') */ function signMessage(message, privateKey) { const EC = elliptic.ec; const ec = new EC('secp256k1'); const key = ec.keyFromPrivate(hexFix(privateKey), 'hex'); const msgHash = sha256(message); // The signature is a DER-encoded hex string const signature = key.sign(msgHash).toDER('hex'); return signature; } /* * Verify Message Signature */ function verifySignature(publicKey, message, signature) { const EC = elliptic.ec; const ec = new EC('secp256k1'); const key = ec.keyFromPublic(publicKey, 'hex'); const msgHash = sha256(message); return key.verify(msgHash, signature, 'hex'); } /* * Returns a public key from a private key */ function getPublicKey(privateKey) { const EC = elliptic.ec; const ec = new EC('secp256k1'); const key = ec.keyFromPrivate(privateKey, 'hex'); const publicKey = key.getPublic('hex'); return publicKey; } /* * Convert a string in its hexadecimal representation without '0x' */ 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 in a regulare string */ function hexToString(hex) { let str = ''; hex = hexFix(hex); for (let i = 0; i < hex.length; i += 2) { let code = parseInt(hex.substr(i, 2), 16); if (!isNaN(code) && code !== 0) { str += String.fromCharCode(code); } } return str; } /* * * removes '0x' from hexadecimal numbers if the have it * */ function hexFix(word) { if (typeof word === 'string') { let Word = word; if (word.startsWith('0x')) { Word = Word.slice(2); } return Word; } return ''; } /* NAG FUNCTIONS **************************************************************/ /* * Sets the Application NAG Key */ function setNAGKey(nagKey) { NAG_KEY = nagKey; } /* * Sets the Network Access Gateway (NAG) URL * If not used, the default URL is selected */ function setNAGURL(nURL) { NAG_URL = nURL; } /* Smart Contracts ************************************************************/ /* * Test the execution of a smart contract project * * Blockchain: Blockchain where the smart contract will be tested * From: Developer's wallet address * Project: Hyper Code Lighe Smart Contract Project */ async function testContract(blockchain, from, project) { try { const data = { "Blockchain": hexFix(blockchain), "From": hexFix(from), "Timestamp": getFormattedTimestamp(), "Project": stringToHex(project), "Version": version }; const response = await makeRequest('Circular_TestContract_', data); if (!response || response.error) { throw new Error(response ? response.error : 'Unknown error occurred during contract test.'); } return response; } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Local Smart Contract Call * * Blockchain: Blockchain where the Smart Contract is deployed * From: Caller wallet Address * Address: Smart Contract Address * Request: Smart Contract Local endpoint */ async function callContract(blockchain, from, address, request) { try { const data = { "Blockchain": hexFix(blockchain), "From": hexFix(from), "Address": hexFix(address), "Request": stringToHex(Request), "Timestamp": getFormattedTimestamp(), "Version": version }; const response = await makeRequest('Circular_CallContract_', data); if (!response || response.error) { throw new Error(response ? response.error : 'Unknown error occurred during contract call.'); } return response; } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* WALLET FUNCTIONS **********************************************************/ /* * Checks if a Wallet is registered on the chain * * Blockchain: Blockchain where the wallet is registered * Address: Wallet Address */ async function checkWallet(blockchain, address) { try { const data = { "Blockchain": hexFix(blockchain), "Address": hexFix(address), "Version": version }; const response = await makeRequest('Circular_CheckWallet_', data); if (!response || response.error) { throw new Error(response ? response.error : 'Unknown error occurred while checking wallet.'); } return response; } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Retrieves a Wallet * * Blockchain: Blockchain where the wallet is registered * Address: Wallet Address */ async function getWallet(blockchain, address) { try { const data = { "Blockchain": hexFix(blockchain), "Address": hexFix(address), "Version": version }; const response = await makeRequest('Circular_GetWallet_', data); if (!response || response.error) { throw new Error(response ? response.error : 'Unknown error occurred while retrieving wallet.'); } return response; } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Retrieves a Wallet * * Blockchain: Blockchain where the wallet is registered * Address: Wallet Address */ async function getLatestTransactions(blockchain, address) { try { const data = { "Blockchain": hexFix(blockchain), "Address": hexFix(address), "Version": version }; const response = await makeRequest('Circular_GetLatestTransactions_', data); if (!response || response.error) { throw new Error(response ? response.error : 'Unknown error occurred while retrieving latest transactions.'); } return response; } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Retrieves the balance of a specified asset in a Wallet * Blockchain: Blockchain where the wallet is registered * Address: Wallet address * Asset: Asset Name (example 'CIRX') */ async function getWalletBalance(blockchain, address, asset) { try { const data = { "Blockchain": hexFix(blockchain), "Address": hexFix(address), "Asset": asset, "Version": version }; const response = await makeRequest('Circular_GetWalletBalance_', data); if (!response || response.error) { throw new Error(response ? response.error : 'Unknown error occurred while retrieving wallet balance.'); } return response; } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Register a wallet on a desired blockchain. * The same wallet can be registered on multiple blockchains * Blockchain: Blockchain where the wallet will be registered * PublicKey: Wallet PublicKey * * Without registration on the blockchain the wallet will not be reachable */ async function registerWallet(blockchain, publicKey) { try { blockchain = hexFix(blockchain); publicKey = hexFix(publicKey); const From = sha256(publicKey); const To = From; const Nonce = '0'; const Type = 'C_TYPE_REGISTERWALLET'; const PayloadObj = { "Action": "CP_REGISTERWALLET", "PublicKey": publicKey }; const jsonstr = JSON.stringify(PayloadObj); const Payload = stringToHex(jsonstr); const Timestamp = getFormattedTimestamp(); const ID = sha256(blockchain + From + To + Payload + Nonce + Timestamp); const Signature = ""; return await sendTransaction(ID, From, To, Timestamp, Type, Payload, Nonce, Signature, blockchain); } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* DOMAINS MANAGEMENT *********************************************************/ /* * Resolves the domain name returning the wallet address associated to the domain name * A single wallet can have multiple domains associations * Blockchain : Blockchain where the domain and wallet are registered * Name: Domain Name */ async function getDomain(blockchain, name) { try { const data = { "Blockchain": hexFix(blockchain), "Domain": name, "Version": version }; return await makeRequest('Circular_ResolveDomain_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } /// PARAMETRIC ASSETS MANAGEMENT /////////////////////////////////////////////////////////////////////////////////////// /* * Retrieves the list of all assets minted on a specific blockchain * Blockchain: Blockchin where to request the list */ async function getAssetList(blockchain) { try { const data = { "Blockchain": hexFix(blockchain), "Version": version }; return await makeRequest('Circular_GetAssetList_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Retrieves an Asset Descriptor * Blockchain: Blockchain where the asset is minted * Name: Asset Name (example 'CIRX') */ async function getAsset(blockchain, name) { try { const data = { "Blockchain": hexFix(blockchain), "AssetName": name, "Version": version }; return await makeRequest('Circular_GetAsset_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Retrieve The total, circulating and residual supply of a specified asset * Blockchain: Blockchain where the asset is minted * Name: Asset Name (example 'CIRX') */ async function getAssetSupply(blockchain, name) { try { const data = { "Blockchain": hexFix(blockchain), "AssetName": name, "Version": version }; return await makeRequest('Circular_GetAssetSupply_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } // VOUCHERS MANAGEMENT////////////////////////////////////////////////////////// /* * Retrieves an existing Voucher * Blockchain: blockchain where the voucher was minted * Code: voucher code */ async function getVoucher(blockchain, code) { try { code = String(code); if (code.startsWith('0x')) { code = code.slice(2); } const data = { "Blockchain": hexFix(blockchain), "Code": code, "Version": version }; return await makeRequest('Circular_GetVoucher_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } // BLOCKS MANAGEMENT ////////////////////////////////////////////////////////////////////////////////// /* * Retrieve All blocks in a specified range * Blockchain: blockchain where to search the blocks * Start: Initial block * End: End block * * If End = 0, then Start is the number of blocks from the last one minted going backward. */ async function getBlockRange(blockchain, start, end) { try { const data = { "Blockchain": hexFix(blockchain), "Start": String(start), "End": String(end), "Version": version }; return await makeRequest('Circular_GetBlockRange_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Retrieve a desired block * Blockchain: blockchain where to search the block * Num: Block number */ async function getBlock(blockchain, num) { try { const data = { "Blockchain": hexFix(blockchain), "BlockNumber": String(num), "Version": version }; return await makeRequest('Circular_GetBlock_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Retrieves the blockchain block height * * Blockchain: blockchain where to count the blocks */ async function getBlockCount(blockchain) { try { const data = { "Blockchain": hexFix(blockchain), "Version": version }; return await makeRequest('Circular_GetBlockHeight_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } // ANALYTICS //////////////////////////////////////////////////////////////////////////////////////////////// /* * Retrieves the Blockchain Amalytics * * Blockchain: selected blockchain */ async function getAnalytics(blockchain) { try { const data = { "Blockchain": hexFix(blockchain), "Version": version }; return await makeRequest('Circular_GetAnalytics_', data); } catch (error) { handleError(error); return { success: false, error: error.message }; } } /* * Get The list of blockchains available in the network * */ async function getBlockchains() { try { let data = {}; return await makeRequest('Circular_GetBlockchains_', data); } catch (error) { return handleError(error); } } /// TRANSACTIONS //////////////////////////////////////////////////////////////////////////////////////////// /* * * Searches a transaction by ID between the pending transactions * * Blockchain: Blockchain where to search the transaction * TxID: Transaction ID * */ async function getPendingTransaction(blockchain, txID) { try { let data = { "Blockchain": hexFix(blockchain), "ID": hexFix(txID), "Version": version }; return await makeRequest('Circular_GetPendingTransaction_', data); } catch (error) { return handleError(error); } } /* * Searches a Transaction by its ID * The transaction will be searched initially between the pending transactions and then in the blockchain * * Blockchain: blockchain where to search the transaction * TxID: transaction ID * Start: Starting block * End: End block * * if End = 0 Start indicates the number of blocks starting from the last block minted */ async function getTransactionbyID(blockchain, txID, start, end) { try { let data = { "Blockchain": hexFix(blockchain), "ID": hexFix(txID), "Start": String(start), "End": String(end), "Version": version }; return await makeRequest('Circular_GetTransactionbyID_', data); } catch (error) { return handleError(error); } } /* * Searches all transactions broadcasted by a specified node * * Blockchain: blockchain where to search the transaction * NodeID: ID of the node that has broadcasted the transaction * Start: Starting block * End: End block * * if End = 0 Start indicates the number of blocks starting from the last block minted */ async function getTransactionbyNode(blockchain, nodeID, start, end) { try { let data = { "Blockchain": hexFix(blockchain), "NodeID": hexFix(nodeID), "Start": String(start), "End": String(end), "Version": version }; return await makeRequest('Circular_GetTransactionbyNode_', data); } catch (error) { return handleError(error); } } /* * Searches all transactions Involving a specified address * * Blockchain: blockchain where to search the transaction * Address: Can be the sender or the recipient of the transaction * Start: Starting block * End: End block * * if End = 0 Start indicates the number of blocks starting from the last block minted */ async function getTransactionbyAddress(blockchain, address, start, end) { try { let data = { "Blockchain": hexFix(blockchain), "Address": hexFix(address), "Start": String(start), "End": String(end), "Version": version }; return await makeRequest('Circular_GetTransactionbyAddress_', data); } catch (error) { return handleError(error); } } async function getWalletNonce(blockchain, address) { try { let data = { "Blockchain": hexFix(blockchain), "Address": hexFix(address), "Version": version }; return await makeRequest('Circular_GetWalletNonce_', data); } catch (error) { return handleError(error); } } /* * Searches all transactions Involving a specified address in a specified timeframe * * Blockchain: blockchain where to search the transaction * Address: Can be the sender or the recipient of the transaction * StartDate: Starting date * endDate: End date * */ async function getTransactionbyDate(blockchain, address, startDate, endDate) { try { let data = { "Blockchain": hexFix(blockchain), "Address": hexFix(address), "StartDate": startDate, "EndDate": endDate, "Version": version }; return await makeRequest('Circular_GetTransactionbyDate_', data); } catch (error) { return handleError(error); } } /* * Submits a transaction to a desired blockchain * * ID: Transaction ID * From: Transaction Sender * To: Transaction recipient * Timestamp: Formatted Timestamo YYYY:MM:DD-hh:mm:ss * Type: Transaction Type Refer to Documentation * Payload: Transaction payload * Nonce: Wallet nonce * Signature: transaction Signature * Blockchain: Blockchain where the transaction will be submitted */ async function sendTransaction( id, from, to, timestamp, type, payload, nonce, signature, blockchain ) { try { let data = { "ID": hexFix(id), "From": hexFix(from), "To": hexFix(to), "Timestamp": timestamp, "Payload": String(hexFix(payload)), "Nonce": String(nonce), "Signature": hexFix(signature), "Blockchain": hexFix(blockchain), "Type": type, "Version": version }; return await makeRequest('Circular_AddTransaction_', data); } catch (error) { return handleError(error); } } // Send a transaction to the blockchain let intervalSec = 5; /* * Recursive transaction finality polling * will search a transaction every intervalSec seconds with a desired timeout. * * Blockchain: blockchain where the transaction was submitted * TxID: Transaction ID * timeoutSec: Waiting timeout * */ function getTransactionOutcome(blockchain, txID, timeoutSec) { return new Promise((resolve, reject) => { const startTime = Date.now(); const interval = intervalSec * 1000; // Convert seconds to milliseconds const timeout = timeoutSec * 1000; // Convert seconds to milliseconds const checkTransaction = () => { const elapsedTime = Date.now() - startTime; console.log('Checking transaction...', { elapsedTime, timeout }); if (elapsedTime > timeout) { console.log('Timeout exceeded'); reject(new Error('Timeout exceeded')); return; } getTransactionbyID(blockchain, txID, 0, 10).then(data => { console.log('Data received:', data); if (data.Result === 200 && data.Response !== 'Transaction Not Found' && data.Response.Status!=='Pending') { resolve(data.Response); // Resolve if transaction is found and not 'Transaction Not Found' } else { console.log('Transaction not yet confirmed or not found, polling again...'); setTimeout(checkTransaction, interval); // Continue polling } }) .catch(error => { console.log('Error fetching transaction:', error); reject(error); // Reject on error }); }; setTimeout(checkTransaction, interval); // Start polling }); } // Public API return { checkWallet : checkWallet, getWallet : getWallet, getLatestTransactions : getLatestTransactions, getWalletBalance : getWalletBalance, testContract : testContract, callContract : callContract, setNAGKey : setNAGKey, setNAGURL : setNAGURL, hexFix : hexFix, stringToHex : stringToHex, hexToString : hexToString, registerWallet : registerWallet, getDomain : getDomain, getAssetList : getAssetList, getAsset : getAsset, getVoucher : getVoucher, getAssetSupply : getAssetSupply, signMessage : signMessage, getPublicKey : getPublicKey, getFormattedTimestamp : getFormattedTimestamp, verifySignature : verifySignature, getBlock : getBlock, getBlockRange : getBlockRange, getBlockCount : getBlockCount, getAnalytics : getAnalytics, getBlockchains : getBlockchains, getPendingTransaction : getPendingTransaction, getTransactionbyID : getTransactionbyID, getTransactionbyNode : getTransactionbyNode, getTransactionbyAddress : getTransactionbyAddress, getTransactionbyDate : getTransactionbyDate, sendTransaction : sendTransaction, getTransactionOutcome : getTransactionOutcome, getWalletNonce : getWalletNonce, hashString : hashString, }; })(); module.exports = CircularProtocolAPI;