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

@@ -24,6 +24,7 @@ FROM ubuntu:focal as runner
COPY --from=builder /build/docker-extracted/usr/bin/docker /usr/bin/
COPY --from=builder /build/scripts/cluster.sh /usr/bin/cluster
COPY --from=builder /build/scripts/codegen.sh /usr/bin/codegen
COPY --from=builder /build/scripts/templates.sh /usr/bin/templates
COPY --from=builder /build/jq /usr/bin/jq
COPY code-templates /code-templates

View File

@@ -1,6 +1,6 @@
{
"name": "_projname_",
"dependencies": {
"hotpocket-js-client": "0.5.4"
"hotpocket-js-client": "^0.5.6"
}
}

View File

@@ -2,11 +2,12 @@
"name": "_projname_",
"version": "1.0.0",
"scripts": {
"build": "npx ncc build src/_projname_.js -o dist",
"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": {
"hotpocket-nodejs-contract": "0.5.10",
"hotpocket-nodejs-contract": "^0.7.3",
"@vercel/ncc": "0.34.0"
}
}

View File

@@ -30,7 +30,7 @@ async function clientApp() {
console.log('My public key is: ' + pkhex);
const ip = process.argv[2] || 'localhost';
const port = process.argv[3] || '8080';
const port = process.argv[3] || '8081';
const client = await HotPocket.createClient(
['wss://' + ip + ':' + port],
userKeyPair,

View File

@@ -0,0 +1,7 @@
{
"name": "_projname_",
"dependencies": {
"bson": "6.4.0",
"hotpocket-js-client": "^0.5.6"
}
}

View File

@@ -0,0 +1,163 @@
const fs = require('fs');
const readline = require('readline');
const bson = require('bson');
const path = require("path");
const HotPocket = require('hotpocket-js-client');
async function clientApp() {
const keyFile = 'user.key';
// Re-generate a user key pair for the client.
if (process.argv[2] == 'generatekeys' || !fs.existsSync(keyFile)) {
const newKeyPair = await HotPocket.generateKeys();
const saveData = Buffer.from(newKeyPair.privateKey).toString('hex');
fs.writeFileSync(keyFile, saveData);
console.log('New key pair generated.');
if (process.argv[2] == 'generatekeys') {
const pkhex = Buffer.from(newKeyPair.publicKey).toString('hex');
console.log('My public key is: ' + pkhex);
return;
}
}
// Generate the key pair using saved private key data.
const savedPrivateKeyHex = fs.readFileSync(keyFile).toString();
const userKeyPair = await HotPocket.generateKeys(savedPrivateKeyHex);
const pkhex = Buffer.from(userKeyPair.publicKey).toString('hex');
console.log('My public key is: ' + pkhex);
const ip = process.argv[2] || 'localhost';
const port = process.argv[3] || '8081';
const client = await HotPocket.createClient(
['wss://' + ip + ':' + port],
userKeyPair,
{ protocol: HotPocket.protocols.bson }
);
// Establish HotPocket connection.
if (!await client.connect()) {
console.log('Connection failed.');
return;
}
console.log('HotPocket Connected.');
// start listening for stdin
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// On ctrl + c we should close HP connection gracefully.
rl.on('SIGINT', () => {
console.log('SIGINT received...');
rl.close();
client.close();
});
// This will get fired if HP server disconnects unexpectedly.
client.on(HotPocket.events.disconnect, () => {
console.log('Disconnected');
rl.close();
});
// This will get fired when contract sends an output.
client.on(HotPocket.events.contractOutput, (r) => {
r.outputs.forEach(output => {
handleOutput(output, r.ledgerSeqNo);
});
});
const handleOutput = (output, ledgerSeqNo) => {
// If bson.deserialize error occurred it'll be caught by this try catch.
try {
const result = bson.deserialize(output);
if (result.type == "uploadResult") {
if (result.status == "ok")
console.log(`(ledger:${ledgerSeqNo})>> ${result.status}`);
else
console.log(`(ledger:${ledgerSeqNo})>> Upload failed. reason: ${result.status}`);
}
else if (result.type == "deleteResult") {
if (result.status == "ok")
console.log(`(ledger:${ledgerSeqNo})>> ${result.status}`);
else
console.log(`(ledger:${ledgerSeqNo})>> Delete failed. reason: ${result.status}`);
}
else if (result.type == "downloadResult") {
if (result.status == "ok")
console.log(`(ledger:${ledgerSeqNo})>> ${result.content}`);
else
console.log(`(ledger:${ledgerSeqNo})>> Download failed. reason: ${result.status}`);
}
else {
console.log("Unknown contract output.");
}
}
catch (e) {
console.log(e)
}
}
console.log("Ready to accept inputs.");
const input_pump = () => {
rl.question('', async (inp) => {
let input;
if (inp.startsWith("download ")) {
const fileName = inp.substr(9);
// Read only inputs can be sent as read requests, So it'll be quick.
const output = await client.submitContractReadRequest(bson.serialize({
type: "download",
fileName: fileName,
}));
handleOutput(output, (await client.getStatus()).ledgerSeqNo);
}
else if (inp.startsWith("upload ")) {
const filePath = inp.substr(7);
const fileName = path.basename(filePath);
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath);
const sizeKB = Math.round(fileContent.length / 1024);
console.log("Uploading file " + fileName + " (" + sizeKB + " KB)");
input = await client.submitContractInput(bson.serialize({
type: "upload",
fileName: fileName,
content: fileContent
}));
}
else
console.log("File not found");
}
else if (inp.startsWith("delete ")) {
const fileName = inp.substr(7);
input = await client.submitContractInput(bson.serialize({
type: "delete",
fileName: fileName,
}));
}
else {
console.log("Invalid command. [upload <local path>], [delete <file name>] or [download <file name>] expected.")
}
if (input) {
const submission = await input.submissionStatus;
if (submission.status != "accepted")
console.log("Submission failed. reason: " + submission.reason);
}
input_pump();
})
}
input_pump();
}
clientApp();

View File

@@ -0,0 +1,7 @@
{
"name": "_projname_",
"dependencies": {
"bson": "6.4.0",
"hotpocket-js-client": "^0.5.6"
}
}

View File

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

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,98 @@
const fs = require('fs');
const bson = require('bson');
export class _projname_ {
sendOutput; // This function must be wired up by the caller.
async handleRequest(user, msg, isReadOnly) {
// This sample application defines simple file operations.
// It's up to the application to decide the structure and contents of messages.
if (msg.type == "upload") {
// Check already exist.
if (fs.existsSync(msg.fileName)) {
await user.send(bson.serialize({
type: "uploadResult",
status: "already_exists",
fileName: msg.fileName
}));
}
// Error is too large.
else if (msg.content.length > 10 * 1024 * 1024) { // 10MB
await user.send(bson.serialize({
type: "uploadResult",
status: "too_large",
fileName: msg.fileName
}));
}
else {
// Do not write in read only mode.
if (!isReadOnly) {
// Save the file.
fs.writeFileSync(msg.fileName, msg.content.buffer);
await user.send(bson.serialize({
type: "uploadResult",
status: "ok",
fileName: msg.fileName
}));
}
else {
await this.sendOutput(user, {
type: "uploadResult",
status: "error",
error: 'Write is not supported in readonly mode'
})
}
}
}
else if (msg.type == "delete") {
// Delete if exist.
if (fs.existsSync(msg.fileName)) {
// Do not delete in read only mode.
if (!isReadOnly) {
fs.unlinkSync(msg.fileName);
await user.send(bson.serialize({
type: "deleteResult",
status: "ok",
fileName: msg.fileName
}));
}
else {
await this.sendOutput(user, {
type: "deleteResult",
status: "error",
error: 'Delete is not supported in readonly mode'
})
}
}
else {
await user.send(bson.serialize({
type: "deleteResult",
status: "not_found",
fileName: msg.fileName
}));
}
}
// Send file if exist.
else if (msg.type == "download") {
if (fs.existsSync(msg.fileName)) {
const fileContent = fs.readFileSync(msg.fileName);
await user.send(bson.serialize({
type: "downloadResult",
status: "ok",
fileName: msg.fileName,
content: fileContent
}));
}
else {
await user.send(bson.serialize({
type: "downloadResult",
status: "not_found",
fileName: msg.fileName
}));
}
}
}
}

View File

@@ -0,0 +1,48 @@
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;
// Process user inputs.
// Loop through list of users who have sent us inputs.
for (const user of ctx.users.list()) {
// 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);
}
}
}
const hpc = new HotPocket.Contract();
hpc.init(contract);

View File

@@ -0,0 +1,89 @@
const readline = require('readline');
const HotPocket = require('hotpocket-js-client');
async function clientApp() {
const userKeyPair = await HotPocket.generateKeys();
const client = await HotPocket.createClient(['wss://localhost:8081'], userKeyPair);
// Establish HotPocket connection.
if (!await client.connect()) {
console.log('Connection failed.');
return;
}
console.log('HotPocket Connected.');
// start listening for stdin
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// On ctrl + c we should close HP connection gracefully.
rl.on('SIGINT', () => {
console.log('SIGINT received...');
rl.close();
client.close();
});
// This will get fired if HP server disconnects unexpectedly.
client.on(HotPocket.events.disconnect, () => {
console.log('Disconnected');
rl.close();
});
// This will get fired when contract sends an output.
client.on(HotPocket.events.contractOutput, (r) => {
r.outputs.forEach(output => {
handleOutput(output, r.ledgerSeqNo);
});
});
const handleOutput = (output, ledgerSeqNo) => {
if (output.type == "makePaymentResult") {
if (output.status == "ok")
console.log(`(ledger:${ledgerSeqNo})>> `, output.data);
else
console.log(`(ledger:${ledgerSeqNo})>> Payment failed. reason: `, output.error ?? output.status);
}
else {
console.log("Unknown contract output.", output);
}
}
console.log("Ready to accept inputs.");
const input_pump = () => {
rl.question('', async (inp) => {
let input;
if (inp.startsWith("makePayment ")) {
const params = inp.split(' ');
if (params.length == 3) {
input = await client.submitContractInput(JSON.stringify({
type: "makePayment",
sender: params[1],
receiver: params[2]
}));
}
else {
console.log("Invalid command. [makePayment <sender-address> <receiver-address>]");
}
}
else {
console.log("Invalid command. [makePayment <sender-address> <receiver-address>]");
}
if (input) {
const submission = await input.submissionStatus;
if (submission.status != "accepted")
console.log("Submission failed. reason: " + submission.reason);
}
input_pump();
})
}
input_pump();
}
clientApp();

View File

@@ -1,6 +1,6 @@
{
"name": "_projname_",
"dependencies": {
"hotpocket-js-client": "0.5.4"
"hotpocket-js-client": "^0.5.6"
}
}

View File

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

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": {
"hotpocket-nodejs-contract": "^0.7.3",
"everpocket-nodejs-contract": "^0.1.3",
"@vercel/ncc": "0.34.0"
}
}

View File

@@ -0,0 +1,72 @@
const evp = require('everpocket-nodejs-contract');
export class _projname_ {
sendOutput; // This function must be wired up by the caller.
voteContext;
hpContext;
// This function will be called in each contract execution.
async handleContractExecution(ctx) {
this.voteContext = new evp.VoteContext(ctx);
this.hpContext = new evp.HotPocketContext(ctx, { voteContext: this.voteContext });
if (!ctx.readonly) {
// Listen to incoming unl messages and feed them to elector.
ctx.unl.onMessage((node, msg) => {
this.voteContext.feedUnlMessage(node, msg);
});
}
}
async handleRequest(user, message, isReadOnly) {
// This sample application defines two simple messages. 'get' and 'set'.
// It's up to the application to decide the structure and contents of messages.
if (message.type == 'makePayment') {
if (isReadOnly) {
await this.sendOutput(user, {
type: 'makePaymentResult',
status: 'error',
error: 'Submit multisig not supported in readonly mode'
})
return;
}
const sender = message.sender;
const receiver = message.receiver;
try {
const xrplContext = new evp.XrplContext(this.hpContext, sender);
await xrplContext.init();
const tx = await xrplContext.xrplAcc.prepareMakePayment(receiver, "1", "XRP")
console.log("----------- Multi-Signing Transaction");
const res = await xrplContext.multiSignAndSubmitTransaction(tx);
console.log("Transaction submitted");
await this.sendOutput(user, {
type: 'makePaymentResult',
status: 'ok',
data: res
})
}
catch (e) {
console.error(e);
await this.sendOutput(user, {
type: 'makePaymentResult',
status: 'error',
error: e
})
}
}
else {
await this.sendOutput(user, {
status: 'error',
error: 'Unknown message type'
})
}
}
}

View File

@@ -0,0 +1,50 @@
const HotPocket = require('hotpocket-nodejs-contract');
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(ctx);
// Process user inputs.
// Loop through list of users who have sent us inputs.
for (const user of ctx.users.list()) {
// 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 JSON.
// In real-world apps, we need to gracefully filter out invalid data formats for our contract.
const message = JSON.parse(buf);
// Pass the JSON message to our application logic component.
await app.handleRequest(user, message, isReadOnly);
}
}
}
const hpc = new HotPocket.Contract();
hpc.init(contract);

View File

@@ -0,0 +1,78 @@
const readline = require('readline');
const HotPocket = require('hotpocket-js-client');
async function clientApp() {
const userKeyPair = await HotPocket.generateKeys();
const client = await HotPocket.createClient(['wss://localhost:8081'], userKeyPair);
// Establish HotPocket connection.
if (!await client.connect()) {
console.log('Connection failed.');
return;
}
console.log('HotPocket Connected.');
// start listening for stdin
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// On ctrl + c we should close HP connection gracefully.
rl.on('SIGINT', () => {
console.log('SIGINT received...');
rl.close();
client.close();
});
// This will get fired if HP server disconnects unexpectedly.
client.on(HotPocket.events.disconnect, () => {
console.log('Disconnected');
rl.close();
});
// This will get fired when contract sends an output.
client.on(HotPocket.events.contractOutput, (r) => {
r.outputs.forEach(output => {
if (output.type == "rndResult") {
if (output.status == "ok")
console.log(`(ledger:${r.ledgerSeqNo})>> ${JSON.stringify(output)}`);
else
console.log(`(ledger:${r.ledgerSeqNo})>> Rnd failed. reason: ${output.error}`);
}
else {
console.log("Unknown contract output.");
}
});
});
console.log("Ready to accept inputs.");
const input_pump = () => {
rl.question('', async (inp) => {
let input;
if (inp.startsWith("rnd")) {
input = await client.submitContractInput(JSON.stringify({
type: "rnd"
}));
}
else {
console.log("Invalid command. [rnd] expected.")
}
if (input) {
const submission = await input.submissionStatus;
if (submission.status != "accepted")
console.log("Submission failed. reason: " + submission.reason);
}
input_pump();
})
}
input_pump();
}
clientApp();

View File

@@ -0,0 +1,6 @@
{
"name": "_projname_",
"dependencies": {
"hotpocket-js-client": "^0.5.6"
}
}

View File

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

View File

@@ -0,0 +1,13 @@
{
"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": {
"hotpocket-nodejs-contract": "^0.7.3",
"@vercel/ncc": "0.34.0"
}
}

View File

@@ -0,0 +1,65 @@
export class _projname_ {
sendOutput; // This function must be wired up by the caller.
async handleRequest(user, message, unl, timeout, isReadOnly) {
// This sample application defines a simple messages. 'rnd' and 'set'.
// It's up to the application to decide the structure and contents of messages.
if (message.type == 'rnd') {
// NPL messages are not supported in readonly mode.
if (!isReadOnly) {
// Start listening to incoming NPL messages before we send ours.
const promise = new Promise((resolve, reject) => {
let t = setTimeout(() => {
reject(`NPL messages aren't received withing ${timeout} ms.`);
}, timeout);
let nplRes = [];
unl.onMessage((node, msg) => {
nplRes.push({ pubkey: node.publicKey, number: Number(msg.toString()) });
// Resolve once all are received
if (nplRes.length === unl.count()) {
clearTimeout(t);
resolve(nplRes.sort((a, b) => a.number - b.number));
}
});
});
await unl.send(Math.random().toString());
try {
const receipt = await promise;
await this.sendOutput(user, {
type: 'rndResult',
status: "ok",
nplMessages: receipt,
rnd: receipt[0].number
});
}
catch (e) {
await this.sendOutput(user, {
type: "rndResult",
status: "error",
error: e.toString()
});
}
}
else {
await this.sendOutput(user, {
type: "rndResult",
status: "error",
error: 'NPL messages are not supported in readonly mode'
});
}
}
else {
await this.sendOutput(user, {
type: 'error',
error: 'Unknown message type'
})
}
}
}

View File

@@ -0,0 +1,53 @@
const HotPocket = require('hotpocket-nodejs-contract');
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;
// Access the HotPocket config.
const hpconfig = await ctx.getConfig();
// Wait only for half of roundtime for random number generation.
const timeout = Math.ceil(hpconfig.consensus.roundtime);
// Process user inputs.
// Loop through list of users who have sent us inputs.
for (const user of ctx.users.list()) {
// 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 JSON.
// In real-world apps, we need to gracefully filter out invalid data formats for our contract.
const message = JSON.parse(buf);
// Pass the JSON message to our application logic component.
await app.handleRequest(user, message, ctx.unl, timeout, isReadOnly);
}
}
}
const hpc = new HotPocket.Contract();
hpc.init(contract);

View File

@@ -0,0 +1,115 @@
const readline = require('readline');
const HotPocket = require('hotpocket-js-client');
async function clientApp() {
const userKeyPair = await HotPocket.generateKeys();
const client = await HotPocket.createClient(['wss://localhost:8081'], userKeyPair);
// Establish HotPocket connection.
if (!await client.connect()) {
console.log('Connection failed.');
return;
}
console.log('HotPocket Connected.');
// start listening for stdin
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// On ctrl + c we should close HP connection gracefully.
rl.on('SIGINT', () => {
console.log('SIGINT received...');
rl.close();
client.close();
});
// This will get fired if HP server disconnects unexpectedly.
client.on(HotPocket.events.disconnect, () => {
console.log('Disconnected');
rl.close();
});
// This will get fired when contract sends an output.
client.on(HotPocket.events.contractOutput, (r) => {
r.outputs.forEach(output => {
handleOutput(output, r.ledgerSeqNo);
});
});
const handleOutput = (output, ledgerSeqNo) => {
if (output.type == "statResult") {
console.log(`(ledger:${ledgerSeqNo})>> ${output.data}`);
}
else if (output.type == "dataResult") {
console.log(`(ledger:${ledgerSeqNo})>> ${output.data}`);
}
else if (output.type == "error") {
console.log(`(ledger:${ledgerSeqNo})>> Error: ${output.error}`);
}
else {
console.log("Unknown contract output.");
}
}
console.log("Ready to accept inputs.");
const input_pump = () => {
rl.question('', async (inp) => {
let input;
if (inp.startsWith("stat")) {
// Get contract status by read request and contract input.
// This output means
// Read request - contract is deployed and excitable.
// Contract input - node are in sync.
if (inp.startsWith("stat contract")) {
input = await client.submitContractInput(JSON.stringify({
type: "stat"
}));
}
else if (inp.startsWith("stat read")) {
const output = await client.submitContractReadRequest(JSON.stringify({
type: "stat"
}));
handleOutput(output, (await client.getStatus()).ledgerSeqNo);
}
else {
const res = await client.getStatus();
console.log(res);
}
}
else if (inp.startsWith("set ")) {
input = await client.submitContractInput(JSON.stringify({
type: "set",
data: inp.substr(4)
}));
}
else if (inp.startsWith("get")) {
// Read only inputs can be sent as read requests, So it'll be quick.
const output = await client.submitContractReadRequest(JSON.stringify({
type: "get"
}));
handleOutput(output, (await client.getStatus()).ledgerSeqNo);
}
else {
console.log("Invalid command. [set <data>] or [get] expected.")
}
if (input) {
const submission = await input.submissionStatus;
if (submission.status != "accepted")
console.log("Submission failed. reason: " + submission.reason);
}
input_pump();
})
}
input_pump();
}
clientApp();

View File

@@ -0,0 +1,6 @@
{
"name": "_projname_",
"dependencies": {
"hotpocket-js-client": "^0.5.6"
}
}

View File

@@ -1,2 +0,0 @@
node_modules
index.js

View File

@@ -1,41 +0,0 @@
{
"name": "_projname_",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "_projname_",
"version": "1.0.0",
"dependencies": {
"@vercel/ncc": "0.34.0",
"hotpocket-nodejs-contract": "0.5.6"
}
},
"node_modules/@vercel/ncc": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz",
"integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A==",
"bin": {
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/hotpocket-nodejs-contract": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/hotpocket-nodejs-contract/-/hotpocket-nodejs-contract-0.5.6.tgz",
"integrity": "sha512-Q52cE5lYGoVGvdUl3cDYixAh27QxzFF6JFaOm1+HoXU36dsKxZfDeJLGCfnDf4XcgEytM2mPE9STWUV+mdHqEg=="
}
},
"dependencies": {
"@vercel/ncc": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz",
"integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A=="
},
"hotpocket-nodejs-contract": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/hotpocket-nodejs-contract/-/hotpocket-nodejs-contract-0.5.6.tgz",
"integrity": "sha512-Q52cE5lYGoVGvdUl3cDYixAh27QxzFF6JFaOm1+HoXU36dsKxZfDeJLGCfnDf4XcgEytM2mPE9STWUV+mdHqEg=="
}
}
}

View File

@@ -7,7 +7,7 @@
"start": "npm run build && hpdevkit deploy dist"
},
"dependencies": {
"hotpocket-nodejs-contract": "0.5.10",
"hotpocket-nodejs-contract": "^0.7.3",
"@vercel/ncc": "0.34.0"
}
}

View File

@@ -8,16 +8,23 @@ export class _projname_ {
sendOutput; // This function must be wired up by the caller.
async handleRequest(user, message, isReadOnly) {
// This sample application defines two simple messages. 'get' and 'set'.
// It's up to the application to decide the structure and contents of messages.
if (message.type == 'get') {
if (message.type == 'stat') {
// Send response as the status.
await this.sendOutput(user, {
type: 'statResult',
data: 'Contract is online'
})
}
else if (message.type == 'get') {
// Retrieved previously saved data and return to the user.
const data = await this.getData();
await this.sendOutput(user, {
type: 'data_result',
type: 'dataResult',
data: data
})
}
@@ -26,6 +33,11 @@ export class _projname_ {
if (!isReadOnly) {
// Save the provided data into storage.
await this.setData(message.data);
await this.sendOutput(user, {
type: 'dataResult',
data: 'success'
})
}
else {
await this.sendOutput(user, {

View File

@@ -34,7 +34,7 @@ async function contract(ctx) {
const buf = await ctx.users.read(input);
// Let's assume all data buffers for this contract are JSON.
// In real-world apps, we need to gracefully fitler out invalid data formats for our contract.
// In real-world apps, we need to gracefully filter out invalid data formats for our contract.
const message = JSON.parse(buf);
// Pass the JSON message to our application logic component.

View File

@@ -0,0 +1,131 @@
const fs = require('fs');
const readline = require('readline');
const bson = require('bson');
const path = require("path");
const HotPocket = require('hotpocket-js-client');
async function clientApp() {
const keyFile = 'user.key';
// Re-generate a user key pair for the client.
if (process.argv[2] == 'generatekeys' || !fs.existsSync(keyFile)) {
const newKeyPair = await HotPocket.generateKeys();
const saveData = Buffer.from(newKeyPair.privateKey).toString('hex');
fs.writeFileSync(keyFile, saveData);
console.log('New key pair generated.');
if (process.argv[2] == 'generatekeys') {
const pkhex = Buffer.from(newKeyPair.publicKey).toString('hex');
console.log('My public key is: ' + pkhex);
return;
}
}
// Generate the key pair using saved private key data.
const savedPrivateKeyHex = fs.readFileSync(keyFile).toString();
const userKeyPair = await HotPocket.generateKeys(savedPrivateKeyHex);
const pkhex = Buffer.from(userKeyPair.publicKey).toString('hex');
console.log('My public key is: ' + pkhex);
const ip = process.argv[2] || 'localhost';
const port = process.argv[3] || '8081';
const client = await HotPocket.createClient(
['wss://' + ip + ':' + port],
userKeyPair,
{ protocol: HotPocket.protocols.bson }
);
// Establish HotPocket connection.
if (!await client.connect()) {
console.log('Connection failed.');
return;
}
console.log('HotPocket Connected.');
// start listening for stdin
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// On ctrl + c we should close HP connection gracefully.
rl.on('SIGINT', () => {
console.log('SIGINT received...');
rl.close();
client.close();
});
// This will get fired if HP server disconnects unexpectedly.
client.on(HotPocket.events.disconnect, () => {
console.log('Disconnected');
rl.close();
});
// This will get fired when contract sends an output.
client.on(HotPocket.events.contractOutput, (r) => {
r.outputs.forEach(output => {
handleOutput(output, r.ledgerSeqNo);
});
});
const handleOutput = (output, ledgerSeqNo) => {
// If bson.deserialize error occurred it'll be caught by this try catch.
try {
const result = bson.deserialize(output);
if (result.type == "upgradeResult") {
if (result.status == "ok")
console.log(`(ledger:${ledgerSeqNo})>> ${result.status}`);
else
console.log(`(ledger:${ledgerSeqNo})>> Upgrade failed. reason: `, result.error ?? result.status);
}
else {
console.log("Unknown contract output.");
}
}
catch (e) {
console.log(e)
}
}
console.log("Ready to accept inputs.");
const input_pump = () => {
rl.question('', async (inp) => {
let input;
if (inp.startsWith("upgrade ")) {
const filePath = inp.substr(8);
const fileName = path.basename(filePath);
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath);
const sizeKB = Math.round(fileContent.length / 1024);
console.log("Uploading file " + fileName + " (" + sizeKB + " KB)");
input = await client.submitContractInput(bson.serialize({
type: "upgrade",
content: fileContent
}));
}
else
console.log("File not found");
}
else {
console.log("Invalid command. [upgrade <local path>] expected.")
}
if (input) {
const submission = await input.submissionStatus;
if (submission.status != "accepted")
console.log("Submission failed. reason: " + submission.reason);
}
input_pump();
})
}
input_pump();
}
clientApp();

View File

@@ -0,0 +1,7 @@
{
"name": "_projname_",
"dependencies": {
"bson": "6.4.0",
"hotpocket-js-client": "^0.5.6"
}
}

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);

View File

@@ -8,6 +8,7 @@ network=$NETWORK
container_prefix=$CONTAINER_PREFIX
volume_mount=$VOLUME_MOUNT
bundle_mount=$BUNDLE_MOUNT
disparate_dir=$DISPARATE_DIR
hotpocket_image=$HOTPOCKET_IMAGE
config_overrides_file=$CONFIG_OVERRIDES_FILE
user_port_begin=$HP_USER_PORT_BEGIN
@@ -274,9 +275,13 @@ function attach_logs {
function sync_instance {
local node=$1
local contract_dir=$(contract_dir_mount_path $node)
rm -rf $contract_dir/ledger_fs/* $contract_dir/contract_fs/*
rm -rf $contract_dir/ledger_fs/* $contract_dir/contract_fs/*
mkdir -p $contract_dir/contract_fs/seed
cp -r $bundle_mount $contract_dir/contract_fs/seed/state
[ -d $contract_dir/contract_fs/seed/state/$disparate_dir ] && rm -r $contract_dir/contract_fs/seed/state/$disparate_dir
# Copy non-syncable files if exist.
[ -d "$bundle_mount/$disparate_dir/$i" ] && cp -r "$bundle_mount/$disparate_dir/$i/." "$contract_dir/contract_fs/seed/"
# Merge contract config overrides.
local cfg_file=$contract_dir/cfg/hp.cfg

View File

@@ -18,8 +18,8 @@ fi
[ ! -d $templates_dir/$platform ] && echo "Invalid platform '$platform' specified." && exit 1
[ ! -d $templates_dir/$platform/$apptype ] && echo "Invalid application type '$apptype' specified." && exit 1
if ! [[ "$projname" =~ ^[a-z][a-z0-9_-]*$ ]]; then
echo "Invalid project name. Must be lowercase. Must start with a letter. Can only contain letters, numbers, dash and underscore."
if ! [[ "$projname" =~ ^[a-z][a-z0-9_]*$ ]]; then
echo "Invalid project name. Must be lowercase. Must start with a letter. Can only contain letters, numbers, and underscore."
exit 1
fi

View File

@@ -0,0 +1,26 @@
#!/bin/bash
cmd=$1
templates_dir="/code-templates"
usage="Usage:
\nlist <platform>"
if [ "$cmd" = "list" ]; then
platform=$2
if [ ! -z $platform ]; then
platforms=("$templates_dir/$platform")
else
platforms="$templates_dir/*"
fi
for p in $platforms; do
[ ! -d "$p" ] && echo "There are no templates for platform: ${p##*/}." && exit 0
echo "$(tput bold)PLATFORM: ${p##*/}$(tput sgr0)"
for t in $(ls -1 "$p"); do
echo " $t"
done
done
fi
exit 0