const algosdk = require('algosdk') const fs = require('fs') // eslint-disable-next-line camelcase const { sha512_256 } = require('js-sha512') const tools = require('../tools/app-tools') const approvalProgramFilename = 'teal/pricekeeper.teal' const clearProgramFilename = 'teal/clearstate.teal' class PricecasterLib { constructor (algodClient, ownerAddr = undefined) { this.algodClient = algodClient this.ownerAddr = ownerAddr this.minFee = 1000 /** * Set Application Id used in all the functions of this class. * @param {number} applicationId application id * @returns {void} */ this.setAppId = function (applicationId) { this.appId = applicationId } /** * Get minimum fee to pay for transactions. * @return {Number} minimum transaction fee */ this.minTransactionFee = function () { return this.minFee } /** * Internal function. * Read application local state related to the account. * @param {String} accountAddr account to retrieve local state * @return {Array} an array containing all the {key: value} pairs of the local state */ this.readLocalState = function (accountAddr) { return tools.readAppLocalState(this.algodClient, this.appId, accountAddr) } /** * Internal function. * Read application global state. * @return {Array} an array containing all the {key: value} pairs of the global state * @returns {void} */ this.readGlobalState = function () { return tools.readAppGlobalState(this.algodClient, this.appId, this.ownerAddr) } /** * Print local state of accountAddr on stdout. * @param {String} accountAddr account to retrieve local state * @returns {void} */ this.printLocalState = async function (accountAddr) { await tools.printAppLocalState(this.algodClient, this.appId, accountAddr) } /** * Print application global state on stdout. * @returns {void} */ this.printGlobalState = async function () { await tools.printAppGlobalState(this.algodClient, this.appId, this.ownerAddr) } /** * Internal function. * Read application local state variable related to accountAddr. * @param {String} accountAddr account to retrieve local state * @param {String} key variable key to get the value associated * @return {String/Number} it returns the value associated to the key that could be an address, a number or a * base64 string containing a ByteArray */ this.readLocalStateByKey = function (accountAddr, key) { return tools.readAppLocalStateByKey(this.algodClient, this.appId, accountAddr, key) } /** * Internal function. * Read application global state variable. * @param {String} key variable key to get the value associated * @return {String/Number} it returns the value associated to the key that could be an address, * a number or a base64 string containing a ByteArray */ this.readGlobalStateByKey = function (key) { return tools.readAppGlobalStateByKey(this.algodClient, this.appId, this.ownerAddr, key) } /** * Compile program that programFilename contains. * @param {String} programFilename filepath to the program to compile * @return {String} base64 string containing the compiled program */ this.compileProgram = async function (programBytes) { const compileResponse = await this.algodClient.compile(programBytes).do() const compiledBytes = new Uint8Array(Buffer.from(compileResponse.result, 'base64')) return { compiledBytes, hash: compileResponse.hash } } /** * Internal function. * Compile application clear state program. * @return {String} base64 string containing the compiled program */ this.compileClearProgram = function () { const program = fs.readFileSync(clearProgramFilename, 'utf8') return this.compileProgram(program) } /** * Internal function. * Compile application approval program. * @return {String} base64 string containing the compiled program */ this.compileApprovalProgram = async function () { const program = fs.readFileSync(approvalProgramFilename, 'utf8') const compiledApprovalProgram = await this.compileProgram(program) this.approvalProgramHash = compiledApprovalProgram.hash return compiledApprovalProgram } /** * Helper function to retrieve the application id from a createApp transaction response. * @param {Object} txResponse object containig the transactionResponse of the createApp call * @return {Number} application id of the created application */ this.appIdFromCreateAppResponse = function (txResponse) { return txResponse['application-index'] } /** * Create an application based on the default approval and clearState programs or based on the specified files. * @param {String} sender account used to sign the createApp transaction * @param {Function} signCallback callback with prototype signCallback(sender, tx) used to sign transactions * @return {String} transaction id of the created application */ this.createApp = async function (sender, validatorAddr, symbol, signCallback) { if (symbol.length > 16) { throw new Error('Symbol exceeds 16 characters') } symbol = symbol.padEnd(16, ' ') const localInts = 0 const localBytes = 0 const globalInts = 4 const globalBytes = 3 // declare onComplete as NoOp const onComplete = algosdk.OnApplicationComplete.NoOpOC // get node suggested parameters const params = await algodClient.getTransactionParams().do() params.fee = this.minFee params.flatFee = true const compiledProgram = await this.compileApprovalProgram() const approvalProgramCompiled = compiledProgram.compiledBytes const clearProgramCompiled = (await this.compileClearProgram()).compiledBytes const enc = new TextEncoder() const appArgs = [new Uint8Array(algosdk.decodeAddress(validatorAddr).publicKey), enc.encode(symbol)] // create unsigned transaction const txApp = algosdk.makeApplicationCreateTxn( sender, params, onComplete, approvalProgramCompiled, clearProgramCompiled, localInts, localBytes, globalInts, globalBytes, appArgs ) const txId = txApp.txID().toString() // Sign the transaction const txAppSigned = signCallback(sender, txApp) // Submit the transaction await algodClient.sendRawTransaction(txAppSigned).do() return txId } /** * Internal function. * Call application specifying args and accounts. * @param {String} sender caller address * @param {Array} appArgs array of arguments to pass to application call * @param {Array} appAccounts array of accounts to pass to application call * @param {Function} signCallback callback with prototype signCallback(sender, tx) used to sign transactions * @return {String} transaction id of the transaction */ this.callApp = async function (sender, appArgs, appAccounts, signCallback) { // get node suggested parameters const params = await this.algodClient.getTransactionParams().do() params.fee = this.minFee params.flatFee = true // create unsigned transaction const txApp = algosdk.makeApplicationNoOpTxn(sender, params, this.appId, appArgs, appAccounts.length === 0 ? undefined : appAccounts) const txId = txApp.txID().toString() // Sign the transaction const txAppSigned = signCallback(sender, txApp) // Submit the transaction await this.algodClient.sendRawTransaction(txAppSigned).do() return txId } /** * Internal function. * Call application specifying args and accounts. Do it in a group of dummy TXs for maximizing computations. * @param {String} sender caller address * @param {Array} appArgs array of arguments to pass to application call * @param {Array} appAccounts array of accounts to pass to application call * @param {Function} signCallback callback with prototype signCallback(sender, tx) used to sign transactions * @param {number} dummyTxCount the number of dummyTx to submit, with the real call last. * @return {String} transaction id of the transaction */ this.callAppInDummyGroup = async function (sender, appArgs, appAccounts, signCallback, dummyTxCount) { // get node suggested parameters const params = await this.algodClient.getTransactionParams().do() params.fee = this.minFee params.flatFee = true // console.log(appArgs) const txns = [] const enc = new TextEncoder() for (let i = 0; i < dummyTxCount; ++i) { txns.push(algosdk.makeApplicationNoOpTxn(sender, params, this.appId, undefined, undefined, undefined, undefined, enc.encode(`dummy_TX_${i}`))) } const appTx = algosdk.makeApplicationNoOpTxn(sender, params, this.appId, appArgs) txns.push(appTx) algosdk.assignGroupID(txns) const txId = appTx.txID().toString() // Sign the transactions const signedTxns = [] for (const tx of txns) { signedTxns.push(signCallback(sender, tx)) } // Submit the transaction await this.algodClient.sendRawTransaction(signedTxns).do() return txId } /** * ClearState sender. Remove all the sender associated local data. * @param {String} sender account to ClearState * @param {Function} signCallback callback with prototype signCallback(sender, tx) used to sign transactions * @return {[String]} transaction id of one of the transactions of the group */ this.clearApp = async function (sender, signCallback, forcedAppId) { // get node suggested parameters const params = await this.algodClient.getTransactionParams().do() params.fee = this.minFee params.flatFee = true let appId = this.appId if (forcedAppId) { appId = forcedAppId } // create unsigned transaction const txApp = algosdk.makeApplicationClearStateTxn(sender, params, appId) const txId = txApp.txID().toString() // Sign the transaction const txAppSigned = signCallback(sender, txApp) // Submit the transaction await this.algodClient.sendRawTransaction(txAppSigned).do() return txId } /** * Permanent delete the application. * @param {String} sender owner account * @param {Function} signCallback callback with prototype signCallback(sender, tx) used to sign transactions * @param {Function} applicationId use this application id instead of the one set * @return {String} transaction id of one of the transactions of the group */ this.deleteApp = async function (sender, signCallback, applicationId) { // get node suggested parameters const params = await this.algodClient.getTransactionParams().do() params.fee = this.minFee params.flatFee = true if (!applicationId) { applicationId = this.appId } // create unsigned transaction const txApp = algosdk.makeApplicationDeleteTxn(sender, params, applicationId) const txId = txApp.txID().toString() // Sign the transaction const txAppSigned = signCallback(sender, txApp) // Submit the transaction await this.algodClient.sendRawTransaction(txAppSigned).do() return txId } /** * Helper function to wait until transaction txId is included in a block/round. * @param {String} txId transaction id to wait for * @return {VOID} VOID */ this.waitForConfirmation = async function (txId) { const status = (await this.algodClient.status().do()) let lastRound = status['last-round'] // eslint-disable-next-line no-constant-condition while (true) { const pendingInfo = await this.algodClient.pendingTransactionInformation(txId).do() if (pendingInfo['confirmed-round'] !== null && pendingInfo['confirmed-round'] > 0) { // Got the completed Transaction return pendingInfo['confirmed-round'] } lastRound += 1 await this.algodClient.statusAfterBlock(lastRound).do() } } /** * Helper function to wait until transaction txId is included in a block/round * and returns the transaction response associated to the transaction. * @param {String} txId transaction id to get transaction response * @return {Object} returns an object containing response information */ this.waitForTransactionResponse = async function (txId) { // Wait for confirmation await this.waitForConfirmation(txId) // display results return this.algodClient.pendingTransactionInformation(txId).do() } /** * Creates a message with price data for the PriceKeeper contract * @param {String} symbol Symbol, must match appid support, 16-char UTF long * @param {BigInt} price Aggregated price * @param {BigInt} confidence Confidence * @param {BigInt} exp Exponent (positive) * @param {BigInt} slot Valid-slot of price aggregation * @param {Uint8Array} sk Signing key. * @param {string} header (optional) Message header. 'PRICEDATA' if undefined. * @param {BigInt} appId (optional) AppId. Default is this contract appId. * @param {number} version (optional) Version. Default is 1 if undefined. * @param {BigInt} ts (optional) Timestamp of message. Current system ts if undefined. * @returns A base64-encoded message. */ this.createMessage = function (symbol, price, exp, confidence, slot, sk, header, appId, version, ts) { const buf = Buffer.alloc(138) buf.write(header === undefined ? 'PRICEDATA' : header, 0) buf.writeInt8(version === undefined ? 1 : version, 9) buf.writeBigUInt64BE(appId === undefined ? BigInt(this.appId) : appId, 10) buf.write(symbol, 18) buf.writeBigUInt64BE(price, 34) // (!) Libraries like Pyth publish negative exponents. Write as signed 64bit buf.writeBigInt64BE(exp, 42) buf.writeBigUInt64BE(confidence, 50) buf.writeBigUInt64BE(slot, 58) buf.writeBigUInt64BE(ts === undefined ? BigInt(Math.floor(Date.now() / 1000)) : ts, 66) const digestu8 = Buffer.from(sha512_256(buf.slice(0, 74)), 'hex') const signature = Buffer.from(algosdk.tealSign(sk, digestu8, this.approvalProgramHash)) signature.copy(buf, 74) return buf } /** * Submits message to the PriceKeeper contract. * @param {*} sender Sender account * @param {*} msgb64 Base64-encoded message. * @returns Transaction identifier (txid) */ this.submitMessage = async function (sender, msgBuffer, signCallback) { if (!algosdk.isValidAddress(sender)) { throw new Error('Invalid sender address: ' + sender) } const appArgs = [] appArgs.push(new Uint8Array(msgBuffer)) return await this.callAppInDummyGroup(sender, appArgs, [], signCallback, 3) } } } module.exports = { PricecasterLib }