Added new contract templates (#31)

This commit is contained in:
Chalith Desaman
2024-03-15 17:16:24 +05:30
committed by GitHub
parent 5838ea7fcc
commit 1face50d32
46 changed files with 3860 additions and 73 deletions

View File

@@ -0,0 +1,6 @@
{
"contract": {
"bin_path": "/usr/bin/node",
"bin_args": "index.js"
}
}

View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Install unzip, jq if not installed, because it's required for the upgrader.
if ! command -v unzip &>/dev/null; then
echo "Installing unzip"
apt-get update
apt-get install -y unzip
fi
if ! command -v jq &>/dev/null; then
echo "Installing unzip"
apt-get update
apt-get install -y jq
fi

View File

@@ -0,0 +1,14 @@
{
"name": "_projname_",
"version": "1.0.0",
"scripts": {
"build": "npx ncc build src/contract.js -o dist",
"build:prod": "npx ncc build src/contract.js --minify -o dist",
"start": "npm run build && hpdevkit deploy dist"
},
"dependencies": {
"bson": "6.4.0",
"hotpocket-nodejs-contract": "^0.7.3",
"@vercel/ncc": "0.34.0"
}
}

View File

@@ -0,0 +1,193 @@
const fs = require('fs');
const bson = require('bson');
const child_process = require('child_process');
const BUNDLE = "bundle.zip";
const HP_CFG_OVERRIDE = "hp.cfg.override";
const CONTRACT_CFG = "contract.config";
const INSTALL_SCRIPT = "install.sh"
const PATH_CFG = "../patch.cfg"
const BACKUP_PATH_CFG = "../patch.cfg.bk"
const HP_POST_EXEC_SCRIPT = "post_exec.sh";
const BACKUP = "backup";
const POST_EXEC_ERR_FILE = "post_exec.err"
export class _projname_ {
sendOutput; // This function must be wired up by the caller.
postExecErrors = {};
// This function will be called in each contract execution.
async handleContractExecution() {
// Read and clear the error file.
if (fs.existsSync(POST_EXEC_ERR_FILE)) {
this.postExecErrors = fs.readFileSync(POST_EXEC_ERR_FILE);
// Clear the file after reading.
fs.rmSync(POST_EXEC_ERR_FILE);
}
}
// This function will be called per each user.
async handleUserExecution(user) {
// Handle if there are errors for the user.
if (this.postExecErrors[user.publicKey]) {
if (this.postExecErrors[user.publicKey] !== "success") {
console.error(`Found post execution errors!`);
const error = this.postExecErrors[user.publicKey];
delete this.postExecErrors[user.publicKey];
await this.sendOutput(user, {
type: "upgradeResult",
status: "error",
error: error
});
}
}
}
async #upgradeContract(bundleContent, user, ctx) {
// Backup all the files first.
const backup = `${BACKUP}-${ctx.timestamp}`
child_process.execSync(`mkdir -p ../${backup} && cp -r ./* ../${backup}/ && mv ../${backup} ./${backup}`);
console.log('Contract binaries backed up!');
try {
fs.writeFileSync(BUNDLE, bundleContent, { mode: 0o644 });
// Install unzip if not exist.
child_process.execSync(`/usr/bin/unzip -o ${BUNDLE} && rm -f ${BUNDLE}`);
console.log('New contract binaries extracted!');
let hpCfg = {};
if (fs.existsSync(HP_CFG_OVERRIDE)) {
hpCfg = JSON.parse(fs.readFileSync(HP_CFG_OVERRIDE).toString());
child_process.execSync(`rm ${HP_CFG_OVERRIDE}`);
}
if (hpCfg.contract) {
let contractCfg = {};
if (fs.existsSync(CONTRACT_CFG)) {
contractCfg = JSON.parse(fs.readFileSync(CONTRACT_CFG).toString());
}
contractCfg = { ...contractCfg, ...hpCfg.contract };
fs.writeFileSync(CONTRACT_CFG, JSON.stringify(contractCfg, null, 2), { mode: 0o644 });
console.log('New contract configurations persisted!');
}
// mesh section. (only known_peers section handled currently)
if (hpCfg.mesh?.known_peers) {
if (hpCfg.mesh.known_peers.length > 0) {
ctx.updatePeers(hpCfg.mesh.known_peers);
console.log('Peer list updated!');
}
}
const command = `#!/bin/bash
# Backup patch config.
cp ${PATH_CFG} ${BACKUP_PATH_CFG}
function print_err() {
local error=$1
log=$(jq . ${POST_EXEC_ERR_FILE})
for key in $(jq -c 'keys[]' <<<$log); do
log=$(jq ".$key = \"$error\"" <<<$log)
done
echo $log >${POST_EXEC_ERR_FILE}
}
function rollback() {
# Restore patch.cfg if backup exists
[ -f ${BACKUP_PATH_CFG} ] && mv ${BACKUP_PATH_CFG} ${PATH_CFG}
return 0
}
function upgrade() {
[ -f "${CONTRACT_CFG}" ] && jq -s '.[0] * .[1]' ${PATH_CFG} ${CONTRACT_CFG} > ../tmp.cfg && mv ../tmp.cfg ${PATH_CFG}
if [ -f "${INSTALL_SCRIPT}" ]; then
echo "${INSTALL_SCRIPT} found. Executing..."
chmod +x ${INSTALL_SCRIPT}
./${INSTALL_SCRIPT}
installcode=$?
rm ${INSTALL_SCRIPT}
if [ "$installcode" -eq "0" ]; then
echo "${INSTALL_SCRIPT} executed successfully."
return 0
else
echo "${INSTALL_SCRIPT} ended with exit code:$installcode"
print_err "InstallScriptFailed"
return 1
fi
fi
}
upgrade
upgradecode=$?
if [ "$upgradecode" -eq "0" ]; then
# We have upgraded the contract successfully.
echo "Upgrade successful."
else
echo "Upgrade failed. Rolling back."
rollback
fi
finalcode=$?
exit $finalcode`;
// Create file to write post execution errors.
this.postExecErrors[user.publicKey] = 'success';
fs.writeFileSync(POST_EXEC_ERR_FILE, JSON.stringify(this.postExecErrors, null, 2), { mode: 0o644 });
console.log('Generated error log file!');
fs.writeFileSync(HP_POST_EXEC_SCRIPT, command, { mode: 0o777 });
console.log('Generated post execution script!');
await user.send(bson.serialize({
type: "upgradeResult",
status: "ok"
}));
}
catch (e) {
console.error(e);
child_process.execSync(`cp -r ${backup}/* ./ && rm -r ${backup}`);
await user.send(bson.serialize({
type: "upgradeResult",
status: "error",
error: e
}));
}
}
// This function will be called per each user input.
async handleRequest(user, msg, isReadOnly, ctx) {
// This sample application defines simple file operations.
// It's up to the application to decide the structure and contents of messages.
if (msg.type == "upgrade") {
if (isReadOnly) {
await this.sendOutput(user, {
type: "upgradeResult",
status: "error",
error: 'Contract upgrade is not supported in readonly mode'
});
return;
}
await this.#upgradeContract(msg.content.buffer, user, ctx);
}
}
}

View File

@@ -0,0 +1,53 @@
const HotPocket = require('hotpocket-nodejs-contract');
const bson = require('bson');
const { _projname_ } = require('./_projname_');
// HotPocket smart contract is defined as a function which takes the HotPocket contract context as an argument.
// This function gets invoked every consensus round and whenever a user sends a out-of-concensus read-request.
async function contract(ctx) {
// Create our application logic component.
// This pattern allows us to test the application logic independently of HotPocket.
const app = new _projname_();
// Wire-up output emissions from the application before we pass user inputs to it.
app.sendOutput = async (user, output) => {
await user.send(output)
}
// In 'readonly' mode, nothing our contract does will get persisted on the ledger. The benefit is
// readonly messages gets processed much faster due to not being subjected to consensus.
// We should only use readonly mode for returning/replying data for the requesting user.
//
// In consensus mode (NOT read-only), we can do anything like persisting to data storage and/or
// sending data to any connected user at the time. Everything will get subjected to consensus so
// there is a time-penalty.
const isReadOnly = ctx.readonly;
// This function is executed per each contract round.
await app.handleContractExecution();
// Process user inputs.
// Loop through list of users who have sent us inputs.
for (const user of ctx.users.list()) {
// This function is executed per each user.
await app.handleUserExecution(user);
// Loop through inputs sent by each user.
for (const input of user.inputs) {
// Read the data buffer sent by user (this can be any kind of data like string, json or binary data).
const buf = await ctx.users.read(input);
// Let's assume all data buffers for this contract are binary.
// In real-world apps, we need to gracefully filter out invalid data formats for our contract.
const msg = bson.deserialize(buf);
// Pass the JSON message to our application logic component.
await app.handleRequest(user, msg, isReadOnly, ctx);
}
}
}
const hpc = new HotPocket.Contract();
hpc.init(contract);