From 43e68eb9b8a0df9cf62732993964b982b955e1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A1n=20Di=20Pietro?= Date: Fri, 1 Oct 2021 15:06:33 -0300 Subject: [PATCH] Creation successful. Fixes + test deployment Betanet --- lib/{priceCasterLib.js => pricecaster.js} | 70 +++-- package.json | 2 +- .../{pricedata.teal.tmpl => pricekeeper.teal} | 45 ++- test/sc-test.js | 50 ++++ tools/app-tools.js | 270 ++++++++++++++++++ 5 files changed, 403 insertions(+), 34 deletions(-) rename lib/{priceCasterLib.js => pricecaster.js} (84%) rename teal/{pricedata.teal.tmpl => pricekeeper.teal} (83%) create mode 100644 test/sc-test.js create mode 100644 tools/app-tools.js diff --git a/lib/priceCasterLib.js b/lib/pricecaster.js similarity index 84% rename from lib/priceCasterLib.js rename to lib/pricecaster.js index 7c7d86660..fb788655c 100644 --- a/lib/priceCasterLib.js +++ b/lib/pricecaster.js @@ -1,16 +1,14 @@ const algosdk = require('algosdk') const fs = require('fs') -const { exit } = require('process') -const tools = require('./tools/app-tools') +const tools = require('../tools/app-tools') -const approvalProgramFilename = 'contracts/price-keeper.teal' -const clearProgramFilename = 'contracts/clearstate.teal' +const approvalProgramFilename = 'teal/pricekeeper.teal' +const clearProgramFilename = 'teal/clearstate.teal' class PricecasterLib { - constructor (algodClient, ownerAddr = undefined, assetId = 0) { + constructor (algodClient, ownerAddr = undefined) { this.algodClient = algodClient this.ownerAddr = ownerAddr - this.assetId = assetId this.minFee = 1000 /** @@ -22,15 +20,6 @@ class PricecasterLib { this.appId = applicationId } - /** - * Set the wALGO asset id used in all the functions of this class. - * @param {number} assId asset id - * @returns {void} - */ - this.setAssetId = function (assId) { - this.assetId = assId - } - /** * Get minimum fee to pay for transactions. * @return {Number} minimum transaction fee @@ -126,8 +115,7 @@ class PricecasterLib { * @return {String} base64 string containing the compiled program */ this.compileApprovalProgram = async function (validatorAddress) { - let program = fs.readFileSync(approvalProgramFilename, 'utf8') - program = program.replace(/TMPL_VALIDATOR/g, validatorAddress) + const program = fs.readFileSync(approvalProgramFilename, 'utf8') return this.compileProgram(program) } @@ -146,11 +134,11 @@ class PricecasterLib { * @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, signCallback) { - const localInts = 2 - const localBytes = 3 - const globalInts = 0 - const globalBytes = 0 + this.createApp = async function (sender, validatorAddr, symbol, signCallback) { + const localInts = 0 + const localBytes = 0 + const globalInts = 2 + const globalBytes = 4 // declare onComplete as NoOp const onComplete = algosdk.OnApplicationComplete.NoOpOC @@ -161,14 +149,17 @@ class PricecasterLib { params.fee = this.minFee params.flatFee = true - const approvalProgramCompiled = await this.compileApprovalProgram(validatorAddr) + const approvalProgramCompiled = await this.compileApprovalProgram() const clearProgramCompiled = await this.compileClearProgram() + 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 + localInts, localBytes, globalInts, globalBytes, appArgs ) const txId = txApp.txID().toString() @@ -240,6 +231,37 @@ class PricecasterLib { 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 diff --git a/package.json b/package.json index 7f401ba55..52f12ccc9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Experimentation with pyth", "main": "index.js", "scripts": { - "test": "mocha" + "test": "mocha --timeout 60000" }, "author": "", "license": "ISC", diff --git a/teal/pricedata.teal.tmpl b/teal/pricekeeper.teal similarity index 83% rename from teal/pricedata.teal.tmpl rename to teal/pricekeeper.teal index 848332aa3..69a1eb15d 100644 --- a/teal/pricedata.teal.tmpl +++ b/teal/pricekeeper.teal @@ -1,15 +1,11 @@ #pragma version 5 // ================================================================================================ -// Pricecaster Program +// PriceKeeper Approval Program // ================================================================================================ // -// This contract has the following invariants at -// deployment stage: -// -// TMPL_VALIDATOR Unique data validator address that signs and sends the incoming message -// // App-globals: -// symbol : byte[] pair supported by this app. +// sym : byte[] Symbol to keep price for +// vaddr : byte[] Validator account // nonce : uint64 last sequence ID // price : byte[] current price // stdev : byte[] current confidence (standard deviation) @@ -43,7 +39,7 @@ int 0 txn ApplicationID == -bnz success +bnz handle_create // Handle app call: send price message txn OnCompletion @@ -51,9 +47,37 @@ int NoOp == bnz handle_call +// Handle deletion. +txn OnCompletion +int DeleteApplication +== +bnz success + // Fail otherwise err +handle_create: +// ----------------------------------------------------- +// Handle creation +// Arg 0: Validator address +// Arg 1: Symbol to keep price data +// ----------------------------------------------------- + +byte "vaddr" +txn ApplicationArgs 0 +app_global_put + +byte "sym" +txn ApplicationArgs 1 +dup +len +int 16 +== +assert +app_global_put + +b success + // ----------------------------------------------------- // Receive price message // ----------------------------------------------------- @@ -62,7 +86,8 @@ handle_call: // Verify if sender is the data validator txn Sender -addr PIQHXRVFDP4KSEZUPW6TB4UCDVJ5GJ3YYJIQWKWMEW2AUHJJCHPB4GPNCU +byte "vaddr" +app_global_get == assert @@ -141,6 +166,8 @@ load 0 extract 98 32 // Unpack pubkey X,Y components +byte "vaddr" +app_global_get ecdsa_pk_decompress Secp256k1 // Verify signature diff --git a/test/sc-test.js b/test/sc-test.js new file mode 100644 index 000000000..cd8c49e0d --- /dev/null +++ b/test/sc-test.js @@ -0,0 +1,50 @@ +const PricecasterLib = require('../lib/pricecaster') +const tools = require('../tools/app-tools') +const algosdk = require('algosdk') +// Test general configuration for Betanet + +const validatorAddr = 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU' +const validatorMnemo = 'assault approve result rare float sugar power float soul kind galaxy edit unusual pretty tone tilt net range pelican avoid unhappy amused recycle abstract master' +const otherAddr = 'DMTBK62XZ6KNI7L5E6TRBTPB4B3YNVB4WYGSWR42SEV4XKV4LYHGBW4O34' +const otherMnemo = 'old agree harbor cost pink fog chunk hope vital used rural soccer model acquire clown host friend bring marriage surge dirt surge slab absent punch' +const symbol = 'BTC/USD ' +const signatures = {} +signatures[validatorAddr] = algosdk.mnemonicToSecretKey(validatorMnemo) +signatures[otherAddr] = algosdk.mnemonicToSecretKey(otherMnemo) + +function signCallback (sender, tx) { + const txSigned = tx.signTxn(signatures[sender].sk) + return txSigned +} + +describe('Price-Keeper contract tests', function () { + let pclib + let algodClient + + before(async function () { + algodClient = new algosdk.Algodv2('', 'https://api.betanet.algoexplorer.io', '') + pclib = new PricecasterLib.PricecasterLib(algodClient) + + console.log('Clearing accounts of all previous apps...') + const appsTo = await tools.readCreatedApps(algodClient, validatorAddr) + for (let i = 0; i < appsTo.length; i++) { + console.log('Clearing ' + appsTo[i].id) + try { + const txId = await pclib.deleteApp(validatorAddr, signCallback, appsTo[i].id) + await pclib.waitForConfirmation(txId) + } catch (e) { + console.error('Could not delete application! Reason: ' + e) + } + } + + console.log('Creating new app...') + const txId = await pclib.createApp(validatorAddr, validatorAddr, symbol, signCallback) + const txResponse = await pclib.waitForTransactionResponse(txId) + const appId = pclib.appIdFromCreateAppResponse(txResponse) + pclib.setAppId(appId); + console.log('App Id: %d', appId) + }) + it('x', function () { + + }) +}) diff --git a/tools/app-tools.js b/tools/app-tools.js new file mode 100644 index 000000000..d674058b7 --- /dev/null +++ b/tools/app-tools.js @@ -0,0 +1,270 @@ +/************************************************************************* + * [2018] - [2020] Rand Labs Inc. + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Rand Labs Inc. + * The intellectual and technical concepts contained + * herein are proprietary to Rand Labs Inc. + */ +const sha512 = require('js-sha512') +const hibase32 = require('hi-base32') + +const ALGORAND_ADDRESS_SIZE = 58 + +function timeoutPromise (ms, promise) { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error('promise timeout')) + }, ms) + promise.then( + (res) => { + clearTimeout(timeoutId) + resolve(res) + }, + (err) => { + clearTimeout(timeoutId) + reject(err) + } + ) + }) +} + +function getInt64Bytes (x, len) { + if (!len) { + len = 8 + } + const bytes = new Uint8Array(len) + do { + len -= 1 + // eslint-disable-next-line no-bitwise + bytes[len] = x & (255) + // eslint-disable-next-line no-bitwise + x >>= 8 + } while (len) + return bytes +} + +function addressFromByteBuffer (addr) { + const bytes = Buffer.from(addr, 'base64') + + // compute checksum + const checksum = sha512.sha512_256.array(bytes).slice(28, 32) + + const c = new Uint8Array(bytes.length + checksum.length) + c.set(bytes) + c.set(checksum, bytes.length) + + const v = hibase32.encode(c) + + return v.toString().slice(0, ALGORAND_ADDRESS_SIZE) +} + +function printAppCallDeltaArray (deltaArray) { + for (let i = 0; i < deltaArray.length; i++) { + if (deltaArray[i].address) { + console.log('Local state change address: ' + deltaArray[i].address) + for (let j = 0; j < deltaArray[i].delta.length; j++) { + printAppCallDelta(deltaArray[i].delta[j]) + } + } else { + console.log('Global state change') + printAppCallDelta(deltaArray[i]) + } + } +} + +function printAppStateArray (stateArray) { + for (let n = 0; n < stateArray.length; n++) { + printAppState(stateArray[n]) + } +} + +function appValueState (stateValue) { + let text = '' + + if (stateValue.type == 1) { + const addr = addressFromByteBuffer(stateValue.bytes) + if (addr.length == ALGORAND_ADDRESS_SIZE) { + text += addr + } else { + text += stateValue.bytes + } + } else if (stateValue.type == 2) { + text = stateValue.uint + } else { + text += stateValue.bytes + } + + return text +} + +function appValueStateString (stateValue) { + let text = '' + + if (stateValue.type == 1) { + const addr = addressFromByteBuffer(stateValue.bytes) + if (addr.length == ALGORAND_ADDRESS_SIZE) { + text += addr + } else { + text += stateValue.bytes + } + } else if (stateValue.type == 2) { + text += stateValue.uint + } else { + text += stateValue.bytes + } + + return text +} + +function printAppState (state) { + let text = Buffer.from(state.key, 'base64').toString() + ': ' + + text += appValueStateString(state.value) + + console.log(text) +} + +async function printAppLocalState (algodClient, appId, accountAddr) { + const ret = await readAppLocalState(algodClient, appId, accountAddr) + if (ret) { + console.log('Application %d local state for account %s:', appId, accountAddr) + printAppStateArray(ret) + } +} + +async function printAppGlobalState (algodClient, appId, accountAddr) { + const ret = await readAppGlobalState(algodClient, appId, accountAddr) + if (ret) { + console.log('Application %d global state:', appId) + printAppStateArray(ret) + } +} + +async function readCreatedApps (algodClient, accountAddr) { + const accountInfoResponse = await algodClient.accountInformation(accountAddr).do() + return accountInfoResponse['created-apps'] +} + +async function readOptedInApps (algodClient, accountAddr) { + const accountInfoResponse = await algodClient.accountInformation(accountAddr).do() + return accountInfoResponse['apps-local-state'] +} + +// read global state of application +async function readAppGlobalState (algodClient, appId, accountAddr) { + const accountInfoResponse = await algodClient.accountInformation(accountAddr).do() + for (let i = 0; i < accountInfoResponse['created-apps'].length; i++) { + if (accountInfoResponse['created-apps'][i].id === appId) { + const globalState = accountInfoResponse['created-apps'][i].params['global-state'] + + return globalState + } + } +} + +async function readAppGlobalStateByKey (algodClient, appId, accountAddr, key) { + const accountInfoResponse = await algodClient.accountInformation(accountAddr).do() + for (let i = 0; i < accountInfoResponse['created-apps'].length; i++) { + if (accountInfoResponse['created-apps'][i].id === appId) { + // console.log("Application's global state:") + const stateArray = accountInfoResponse['created-apps'][i].params['global-state'] + for (let j = 0; j < stateArray.length; j++) { + const text = Buffer.from(stateArray[j].key, 'base64').toString() + + if (key === text) { + return appValueState(stateArray[j].value) + } + } + } + } +} + +// read local state of application from user account +async function readAppLocalState (algodClient, appId, accountAddr) { + const accountInfoResponse = await algodClient.accountInformation(accountAddr).do() + for (let i = 0; i < accountInfoResponse['apps-local-state'].length; i++) { + if (accountInfoResponse['apps-local-state'][i].id === appId) { + // console.log(accountAddr + " opted in, local state:") + + if (accountInfoResponse['apps-local-state'][i]['key-value']) { + return accountInfoResponse['apps-local-state'][i]['key-value'] + } + } + } +} + +async function readAppLocalStateByKey (algodClient, appId, accountAddr, key) { + const accountInfoResponse = await algodClient.accountInformation(accountAddr).do() + for (let i = 0; i < accountInfoResponse['apps-local-state'].length; i++) { + if (accountInfoResponse['apps-local-state'][i].id === appId) { + const stateArray = accountInfoResponse['apps-local-state'][i]['key-value'] + + if (!stateArray) { + return null + } + for (let j = 0; j < stateArray.length; j++) { + const text = Buffer.from(stateArray[j].key, 'base64').toString() + + if (key === text) { + return appValueState(stateArray[j].value) + } + } + // not found assume 0 + return 0 + } + } +} + +function uintArray8ToString (byteArray) { + return Array.from(byteArray, function (byte) { + // eslint-disable-next-line no-bitwise + return ('0' + (byte & 0xFF).toString(16)).slice(-2) + }).join('') +} + +/** + * Verify if transactionResponse has any information about a transaction local or global state change. + * @param {Object} transactionResponse object containing the transaction response of an application call + * @return {Boolean} returns true if there is a local or global delta meanining that + * the transaction made a change in the local or global state + */ +function anyAppCallDelta (transactionResponse) { + return (transactionResponse['global-state-delta'] || transactionResponse['local-state-delta']) +} + +/** + * Print to stdout the changes introduced by the transaction that generated the transactionResponse if any. + * @param {Object} transactionResponse object containing the transaction response of an application call + * @return {void} + */ +function printAppCallDelta (transactionResponse) { + if (transactionResponse['global-state-delta'] !== undefined) { + console.log('Global State updated:') + printAppCallDeltaArray(transactionResponse['global-state-delta']) + } + if (transactionResponse['local-state-delta'] !== undefined) { + console.log('Local State updated:') + printAppCallDeltaArray(transactionResponse['local-state-delta']) + } +} + +module.exports = { + timeoutPromise, + getInt64Bytes, + addressFromByteBuffer, + printAppStateArray, + printAppState, + printAppLocalState, + printAppGlobalState, + readCreatedApps, + readOptedInApps, + readAppGlobalState, + readAppGlobalStateByKey, + readAppLocalState, + readAppLocalStateByKey, + uintArray8ToString, + anyAppCallDelta, + printAppCallDelta +}