From 09e72ef8bb74795aecb1d5f9eb55d93155061f0b Mon Sep 17 00:00:00 2001 From: Ravin Perera <33562092+ravinsp@users.noreply.github.com> Date: Thu, 9 Jul 2020 14:22:43 +0530 Subject: [PATCH] Added nodejs client library examples. (#100) --- examples/echo_contract/.gitignore | 2 +- examples/echo_contract/contract.js | 12 +- examples/file_contract/.gitignore | 1 + examples/file_contract/contract.js | 86 ++++++-- examples/file_contract/package-lock.json | 39 ++++ examples/file_contract/package.json | 5 + examples/hpclient/file-client.js | 237 +++++++++++----------- examples/hpclient/hp-client-lib.js | 243 +++++++++++++++++++++++ examples/hpclient/text-client.js | 217 ++++++-------------- 9 files changed, 543 insertions(+), 299 deletions(-) create mode 100644 examples/file_contract/.gitignore create mode 100644 examples/file_contract/package-lock.json create mode 100644 examples/file_contract/package.json create mode 100644 examples/hpclient/hp-client-lib.js diff --git a/examples/echo_contract/.gitignore b/examples/echo_contract/.gitignore index 394522f4..b512c09d 100644 --- a/examples/echo_contract/.gitignore +++ b/examples/echo_contract/.gitignore @@ -1 +1 @@ -node_modules/** \ No newline at end of file +node_modules \ No newline at end of file diff --git a/examples/echo_contract/contract.js b/examples/echo_contract/contract.js index 3bfe710e..35ad8ed0 100644 --- a/examples/echo_contract/contract.js +++ b/examples/echo_contract/contract.js @@ -4,18 +4,18 @@ process.on('uncaughtException', (err) => { const fs = require('fs') //console.log("===Sample contract started==="); -let hpargs = JSON.parse(fs.readFileSync(0, 'utf8')); +const hpargs = JSON.parse(fs.readFileSync(0, 'utf8')); //console.log(hpargs); // We just save execution args as an example state file change. if (!hpargs.readonly) fs.appendFileSync("exects.txt", "ts:" + hpargs.ts + "\n"); -Object.keys(hpargs.usrfd).forEach(function (key, index) { - let userfds = hpargs.usrfd[key]; +Object.keys(hpargs.usrfd).forEach(function (key) { + const userfds = hpargs.usrfd[key]; if (userfds[0] != -1) { - let userinput = fs.readFileSync(userfds[0], 'utf8'); + const userinput = fs.readFileSync(userfds[0], 'utf8'); // Append user input to a state file if not in read only mode. if (!hpargs.readonly) @@ -31,14 +31,14 @@ Object.keys(hpargs.usrfd).forEach(function (key, index) { if (!hpargs.readonly) { if (hpargs.nplfd[0] != -1) { - let nplinput = fs.readFileSync(hpargs.nplfd[0], 'utf8'); + const nplinput = fs.readFileSync(hpargs.nplfd[0], 'utf8'); console.log("Input received from peers:"); console.log(nplinput); fs.writeSync(hpargs.nplfd[1], "Echoing: " + nplinput); } if (hpargs.hpfd[0] != -1) { - let hpinput = fs.readFileSync(hpargs.hpfd[0], 'utf8'); + const hpinput = fs.readFileSync(hpargs.hpfd[0], 'utf8'); console.log("Input received from hp:"); console.log(hpinput); fs.writeSync(hpargs.hpfd[1], "Echoing: " + hpinput); diff --git a/examples/file_contract/.gitignore b/examples/file_contract/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/examples/file_contract/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/examples/file_contract/contract.js b/examples/file_contract/contract.js index 0e4d7e7e..a6acaa2e 100644 --- a/examples/file_contract/contract.js +++ b/examples/file_contract/contract.js @@ -1,27 +1,81 @@ -process.on('uncaughtException', (err) => { - console.error('There was an uncaught error', err) -}) -const fs = require('fs') +const fs = require('fs'); +const bson = require('bson'); //console.log("===File contract started==="); -//console.log("Contract args received from hp: " + input); -let hpargs = JSON.parse(fs.readFileSync(0, 'utf8')); +const hpargs = JSON.parse(fs.readFileSync(0, 'utf8')); +//console.log("Contract args received from hp: " + hpargs); -// We just save execution args as an example state file change. -fs.appendFileSync("exects.txt", "ts:" + hpargs.ts + "\n"); - -Object.keys(hpargs.usrfd).forEach(function (key, index) { - let userfds = hpargs.usrfd[key]; +Object.keys(hpargs.usrfd).forEach(function (key) { + const userfds = hpargs.usrfd[key]; if (userfds[0] != -1) { - let fileContent = fs.readFileSync(userfds[0]); + const input = fs.readFileSync(userfds[0]); + const msg = bson.deserialize(input); - // Save the content into a new file. - var fileName = new Date().getTime().toString(); - fs.writeFileSync(fileName, fileContent); - fs.writeSync(userfds[1], "Saved file (len: " + fileContent.length / 1024 + " KB)"); + if (msg.type == "upload") { + if (fs.existsSync(msg.fileName)) { + fs.writeSync(userfds[1], bson.serialize({ + type: "uploadResult", + status: "already_exists", + fileName: msg.fileName + })); + } + else if (msg.content.length > 10 * 1024 * 1024) { // 10MB + fs.writeSync(userfds[1], bson.serialize({ + type: "uploadResult", + status: "too_large", + fileName: msg.fileName + })); + } + else { + + // Save the file. + fs.writeFileSync(msg.fileName, msg.content.buffer); + + fs.writeSync(userfds[1], bson.serialize({ + type: "uploadResult", + status: "ok", + fileName: msg.fileName + })); + } + } + else if (msg.type == "delete") { + if (fs.existsSync(msg.fileName)) { + fs.unlinkSync(msg.fileName); + fs.writeSync(userfds[1], bson.serialize({ + type: "deleteResult", + status: "ok", + fileName: msg.fileName + })); + } + else { + fs.writeSync(userfds[1], bson.serialize({ + type: "deleteResult", + status: "not_found", + fileName: msg.fileName + })); + } + } + else if (msg.type == "download") { + if (fs.existsSync(msg.fileName)) { + const fileContent = fs.readFileSync(msg.fileName); + fs.writeSync(userfds[1], bson.serialize({ + type: "downloadResult", + status: "ok", + fileName: msg.fileName, + content: fileContent + })); + } + else { + fs.writeSync(userfds[1], bson.serialize({ + type: "downloadResult", + status: "not_found", + fileName: msg.fileName + })); + } + } } }); diff --git a/examples/file_contract/package-lock.json b/examples/file_contract/package-lock.json new file mode 100644 index 00000000..9b14263f --- /dev/null +++ b/examples/file_contract/package-lock.json @@ -0,0 +1,39 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "bson": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.0.4.tgz", + "integrity": "sha512-Ioi3TD0/1V3aI8+hPfC56TetYmzfq2H07jJa9A1lKTxWsFtHtYdLMGMXjtGEg9v0f72NSM07diRQEUNYhLupIA==", + "requires": { + "buffer": "^5.1.0", + "long": "^4.0.0" + } + }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } + } +} diff --git a/examples/file_contract/package.json b/examples/file_contract/package.json new file mode 100644 index 00000000..6c7801db --- /dev/null +++ b/examples/file_contract/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "bson": "4.0.4" + } +} diff --git a/examples/hpclient/file-client.js b/examples/hpclient/file-client.js index fb7a364d..529c3917 100644 --- a/examples/hpclient/file-client.js +++ b/examples/hpclient/file-client.js @@ -1,141 +1,146 @@ -const fs = require('fs') -const ws_api = require('ws'); -const sodium = require('libsodium-wrappers') -const readline = require('readline') +const fs = require('fs'); +const readline = require('readline'); +const sodium = require('libsodium-wrappers'); +const { exit } = require('process'); +const { HotPocketClient, HotPocketProtocols, HotPocketEvents } = require('./hp-client-lib'); +const bson = require('bson'); +var path = require("path"); -// sodium has a trigger when it's ready, we will wait and execute from there -sodium.ready.then(main).catch((e) => { console.log(e) }) +async function main() { -function main() { + await sodium.ready; - var keys = sodium.crypto_sign_keypair() - - - // check for client keys - if (!fs.existsSync('.hp_client_keys')) { + let keys = {}; + const key_file = '.hp_client_keys'; + if (!fs.existsSync(key_file)) { + keys = sodium.crypto_sign_keypair(); keys.privateKey = sodium.to_hex(keys.privateKey) keys.publicKey = sodium.to_hex(keys.publicKey) - fs.writeFileSync('.hp_client_keys', JSON.stringify(keys)) + fs.writeFileSync(key_file, JSON.stringify(keys)) } else { - keys = JSON.parse(fs.readFileSync('.hp_client_keys')) + keys = JSON.parse(fs.readFileSync(key_file)) keys.privateKey = Uint8Array.from(Buffer.from(keys.privateKey, 'hex')) keys.publicKey = Uint8Array.from(Buffer.from(keys.publicKey, 'hex')) } + const pkhex = 'ed' + Buffer.from(keys.publicKey).toString('hex'); + console.log('My public key is: ' + pkhex); - var server = 'wss://localhost:8080' - + let server = 'wss://localhost:8080' if (process.argv.length == 3) server = 'wss://localhost:' + process.argv[2] - if (process.argv.length == 4) server = 'wss://' + process.argv[2] + ':' + process.argv[3] + const hpc = new HotPocketClient(server, HotPocketProtocols.BSON, keys); - var ws = new ws_api(server, { - rejectUnauthorized: false + // Establish HotPocket connection. + if (!await hpc.connect()) { + console.log('Connection failed.'); + exit; + } + console.log('HotPocket Connected.'); + + // This will get fired if HP server disconnects unexpectedly. + hpc.on(HotPocketEvents.disconnect, () => { + console.log('Server diconnected'); + exit; }) - // if the console ctrl + c's us we should close ws gracefully - process.once('SIGINT', function (code) { - console.log('SIGINT received...'); - ws.close() - }); - - function create_input_container(inp) { - - let inp_container = { - nonce: (new Date()).getTime().toString(), - input: inp.toString('hex'), - max_lcl_seqno: 9999999 + // This will get fired when contract sends an output. + hpc.on(HotPocketEvents.contractOutput, (output) => { + const result = bson.deserialize(output); + if (result.type == "uploadResult") { + if (result.status == "ok") + console.log("File " + result.fileName + " uploaded successfully."); + else + console.log("File " + result.fileName + " upload failed. reason: " + result.status); } - let inp_container_bytes = JSON.stringify(inp_container); - let sig_bytes = sodium.crypto_sign_detached(inp_container_bytes, keys.privateKey); - - let signed_inp_container = { - type: "contract_input", - input_container: inp_container_bytes.toString('hex'), - sig: Buffer.from(sig_bytes).toString('hex') - } - - return JSON.stringify(signed_inp_container); - } - - function create_status_request() { - let statreq = { type: 'stat' } - return JSON.stringify(statreq); - } - - function handle_public_challange(m) { - let pkhex = 'ed' + Buffer.from(keys.publicKey).toString('hex'); - console.log('My public key is: ' + pkhex); - - // sign the challenge and send back the response - var sigbytes = sodium.crypto_sign_detached(m.challenge, keys.privateKey); - var response = { - type: 'handshake_response', - challenge: m.challenge, - sig: Buffer.from(sigbytes).toString('hex'), - pubkey: pkhex, - protocol: 'json' - } - - ws.send(JSON.stringify(response)) - - // start listening for stdin - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - console.log("Ready to accept inputs.") - - // Capture user input from the console. - var input_pump = () => { - rl.question('', (inp) => { - - let msgtosend = ""; - - if (inp == "stat") - msgtosend = create_status_request(); - else { - var fileContent = fs.readFileSync(inp); - msgtosend = create_input_container(fileContent); - - console.log("Sending file (len: " + fileContent.length / 1024 + " KB)"); - } - - ws.send(msgtosend) - - input_pump() - }) - } - input_pump(); - } - - ws.on('message', (data) => { - - try { - m = JSON.parse(data) - } catch (e) { - console.log("Exception: " + data); - return - } - - if (m.type == 'handshake_challenge') { - handle_public_challange(m); - } - else if (m.type == 'contract_output') { - console.log("Contract says: " + Buffer.from(m.content, 'hex').toString()); - } - else if (m.type == 'contract_input_status') { - if (m.status != "accepted") - console.log("Input status: " + m.status); + else if (result.type == "deleteResult") { + if (result.status == "ok") + console.log("File " + result.fileName + " deleted successfully."); + else + console.log("File " + result.fileName + " delete failed. reason: " + result.status); } else { - console.log(m); + console.log("Unknown contract output."); } + }) + // This will get fired when contract sends a read response. + hpc.on(HotPocketEvents.contractReadResponse, (response) => { + const result = bson.deserialize(response); + if (result.type == "downloadResult") { + if (result.status == "ok") { + fs.writeFileSync(result.fileName, result.content.buffer); + console.log("File " + result.fileName + " downloaded to current directory."); + } + else { + console.log("File " + result.fileName + " download failed. reason: " + result.status); + } + } + else { + console.log("Unknown read request result."); + } + }) + + // On ctrl + c we should close HP connection gracefully. + process.once('SIGINT', function () { + console.log('SIGINT received...'); + hpc.close(); }); - ws.on('close', () => { - console.log('Server disconnected.'); + // start listening for stdin + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout }); + console.log("Ready to accept inputs."); + + const input_pump = () => { + rl.question('', async (inp) => { + + if (inp.startsWith("upload ")) { + + const filePath = inp.substr(7); + const fileName = path.basename(filePath) + const fileContent = fs.readFileSync(filePath); + const sizeKB = Math.round(fileContent.length / 1024); + console.log("Uploading file " + fileName + " (" + sizeKB + " KB)"); + + const submissionStatus = await hpc.sendContractInput(bson.serialize({ + type: "upload", + fileName: fileName, + content: fileContent + }), null, 100); + + if (submissionStatus && submissionStatus != "ok") + console.log("Upload failed. reason: " + submissionStatus); + } + else if (inp.startsWith("delete ")) { + + const fileName = inp.substr(7); + const submissionStatus = await hpc.sendContractInput(bson.serialize({ + type: "delete", + fileName: fileName + })); + + if (submissionStatus && submissionStatus != "ok") + console.log("Delete failed. reason: " + submissionStatus); + } + else if (inp.startsWith("download ")) { + + const fileName = inp.substr(9); + hpc.sendContractReadRequest(bson.serialize({ + type: "download", + fileName: fileName + })); + } + else { + console.log("Invalid command. [upload | delete | download ] expected.") + } + + input_pump(); + }) + } + input_pump(); } + +main(); \ No newline at end of file diff --git a/examples/hpclient/hp-client-lib.js b/examples/hpclient/hp-client-lib.js new file mode 100644 index 00000000..8c573c48 --- /dev/null +++ b/examples/hpclient/hp-client-lib.js @@ -0,0 +1,243 @@ +const ws_api = require('ws'); +const sodium = require('libsodium-wrappers'); +const EventEmitter = require('events'); +const bson = require('bson'); + +const protocols = { + JSON: "json", + BSON: "bson" +} +Object.freeze(protocols); + +const events = { + disconnect: "disconnect", + contractOutput: "contractOutput", + contractReadResponse: "contractReadResponse" +} +Object.freeze(events); + +function HotPocketClient(server, protocol, keys) { + + if (protocol != protocols.JSON && protocol != protocols.BSON) + throw new Error("Protocol: 'json' or 'bson' expected."); + + let ws = null; + const msgHelper = new MessageHelper(keys, protocol); + const emitter = new EventEmitter(); + + let handshakeResolver = null; + let statResponseResolver = null; + let contractInputResolvers = {}; + + this.connect = function () { + return new Promise(resolve => { + + handshakeResolver = resolve; + + ws = new ws_api(server, { + rejectUnauthorized: false + }) + + ws.on('close', () => { + + // If there are any ongoing resolvers resolve them with error output. + + handshakeResolver && handshakeResolver(false); + handshakeResolver = null; + + statResponseResolver && statResponseResolver(null); + statResponseResolver = null; + + Object.values(contractInputResolvers).forEach(resolver => resolver(null)); + contractInputResolvers = {}; + + emitter.emit(events.disconnect); + }); + + ws.on('message', (msg) => { + try { + // Use JSON if we are still in handshake phase. + m = handshakeResolver ? JSON.parse(msg) : msgHelper.deserializeMessage(msg); + } catch (e) { + console.log("Exception deserializing: " + received_msg); + return; + } + + if (m.type == 'handshake_challenge') { + // sign the challenge and send back the response + const response = msgHelper.createHandshakeResponse(m.challenge); + ws.send(JSON.stringify(response)); + + setTimeout(() => { + // If we are still connected, report handshaking as successful. + // (If websocket disconnects, handshakeResolver will be null) + handshakeResolver && handshakeResolver(true); + handshakeResolver = null; + }, 100); + } + else if (m.type == 'contract_read_response') { + const decoded = msgHelper.binaryDecode(m.content); + emitter.emit(events.contractReadResponse, decoded); + } + else if (m.type == 'contract_input_status') { + const sigKey = (typeof m.input_sig === "string") ? m.input_sig : m.input_sig.toString("hex"); + const resolver = contractInputResolvers[sigKey]; + if (resolver) { + if (m.status == "accepted") + resolver("ok"); + else + resolver(m.reason); + delete contractInputResolvers[sigKey]; + } + } + else if (m.type == 'contract_output') { + const decoded = msgHelper.binaryDecode(m.content); + emitter.emit(events.contractOutput, decoded); + } + else if (m.type == "stat_response") { + statResponseResolver && statResponseResolver({ + lcl: m.lcl, + lclSeqNo: m.lcl_seqno + }); + statResponseResolver = null; + } + else { + console.log("Received unrecognized message: type:" + m.type); + } + }); + }); + }; + + this.on = function (event, listener) { + emitter.on(event, listener); + } + + this.close = function () { + Promise.resolve().then + return new Promise(resolve => { + try { + ws.removeAllListeners("close"); + ws.on("close", resolve); + ws.close(); + } catch (error) { + resolve(); + } + }) + } + + this.getStatus = function () { + const msg = msgHelper.createStatusRequest(); + const p = new Promise(resolve => { + statResponseResolver = resolve; + }); + + ws.send(msgHelper.serializeObject(msg)); + return p; + } + + this.sendContractInput = async function (input, nonce = null, maxLclOffset = null) { + + if (!maxLclOffset) + maxLclOffset = 10; + + if (!nonce) + nonce = (new Date()).getTime().toString(); + + // Acquire the current lcl and add the specified offset. + const stat = await this.getStatus(); + if (!stat) + return new Promise(resolve => resolve(null)); + const maxLclSeqNo = stat.lclSeqNo + maxLclOffset; + + const msg = msgHelper.createContractInput(input, nonce, maxLclSeqNo); + const sigKey = (typeof msg.sig === "string") ? msg.sig : msg.sig.toString("hex"); + const p = new Promise(resolve => { + contractInputResolvers[sigKey] = resolve; + }); + + ws.send(msgHelper.serializeObject(msg)); + return p; + } + + this.sendContractReadRequest = function (request) { + const msg = msgHelper.createReadRequest(request); + ws.send(msgHelper.serializeObject(msg)); + } +} + +function MessageHelper(keys, protocol) { + + this.binaryEncode = function (data) { + const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data); + return protocol == protocols.JSON ? buffer.toString("hex") : buffer; + } + + this.binaryDecode = function (content) { + return (protocol == protocols.JSON) ? Buffer.from(content, "hex") : content.buffer; + } + + this.serializeObject = function (obj) { + return protocol == protocols.JSON ? Buffer.from(JSON.stringify(obj)) : bson.serialize(obj); + } + + this.deserializeMessage = function (m) { + return protocol == protocols.JSON ? JSON.parse(m) : bson.deserialize(m); + } + + this.createHandshakeResponse = function (challenge) { + // For handshake response encoding we Hot Pocket always use json. + // Handshake response will specify the protocol to use for subsequent messages. + const sigBytes = sodium.crypto_sign_detached(challenge, keys.privateKey); + return { + type: "handshake_response", + challenge: challenge, + sig: Buffer.from(sigBytes).toString("hex"), + pubkey: "ed" + Buffer.from(keys.publicKey).toString("hex"), + protocol: protocol + } + } + + this.createContractInput = function (input, nonce, maxLclSeqNo) { + + if (input.length == 0) + return null; + + const inpContainer = { + input: this.binaryEncode(input), + nonce: nonce, + max_lcl_seqno: maxLclSeqNo + } + + const inpContainerBytes = this.serializeObject(inpContainer); + const sigBytes = sodium.crypto_sign_detached(Buffer.from(inpContainerBytes), keys.privateKey); + + const signedInpContainer = { + type: "contract_input", + input_container: this.binaryEncode(inpContainerBytes), + sig: this.binaryEncode(sigBytes) + } + + return signedInpContainer; + } + + this.createReadRequest = function (request) { + + if (request.length == 0) + return null; + + return { + type: "contract_read_request", + content: this.binaryEncode(request) + } + } + + this.createStatusRequest = function () { + return { type: 'stat' }; + } +} + +module.exports = { + HotPocketClient, + HotPocketProtocols: protocols, + HotPocketEvents: events +}; \ No newline at end of file diff --git a/examples/hpclient/text-client.js b/examples/hpclient/text-client.js index 748915a5..40e5a5cb 100644 --- a/examples/hpclient/text-client.js +++ b/examples/hpclient/text-client.js @@ -1,37 +1,12 @@ -// Usage: -// node text-client.js [json|bson] -// node text-client.js [json|bson] [] -// node text-client.js [json|bson] [] [] - -const fs = require('fs') -const ws_api = require('ws'); -const sodium = require('libsodium-wrappers') -const readline = require('readline') -const bson = require('bson'); +const fs = require('fs'); +const readline = require('readline'); +const sodium = require('libsodium-wrappers'); const { exit } = require('process'); +const { HotPocketClient, HotPocketProtocols, HotPocketEvents } = require('./hp-client-lib'); -function main() { +async function main() { - // We use json protocol for messages until handshake completion. - let is_json = true; - - if (process.argv.length < 3) { - console.log("Not enough arguments. 'protocol: [json|bson] required") - return; - } - const protocol = process.argv[2]; - if (protocol != 'json' && protocol != 'bson') { - console.log("Not enough arguments. 'protocol: [json|bson] required") - return; - } - - let server = 'wss://localhost:8080' - if (process.argv.length == 4) server = 'wss://localhost:' + process.argv[3] - if (process.argv.length == 5) server = 'wss://' + process.argv[3] + ':' + process.argv[4] - - const ws = new ws_api(server, { - rejectUnauthorized: false - }) + await sodium.ready; let keys = {}; const key_file = '.hp_client_keys'; @@ -49,140 +24,62 @@ function main() { const pkhex = 'ed' + Buffer.from(keys.publicKey).toString('hex'); console.log('My public key is: ' + pkhex); - // if the console ctrl + c's us we should close ws gracefully + let server = 'wss://localhost:8080' + if (process.argv.length == 3) server = 'wss://localhost:' + process.argv[2] + if (process.argv.length == 4) server = 'wss://' + process.argv[2] + ':' + process.argv[3] + const hpc = new HotPocketClient(server, HotPocketProtocols.JSON, keys); + + // Establish HotPocket connection. + if (!await hpc.connect()) { + console.log('Connection failed.'); + exit; + } + console.log('HotPocket Connected.'); + + // This will get fired if HP server disconnects unexpectedly. + hpc.on(HotPocketEvents.disconnect, () => { + console.log('Server diconnected'); + exit; + }) + + // This will get fired when contract sends an output. + hpc.on(HotPocketEvents.contractOutput, (output) => { + console.log("Contract output>> " + Buffer.from(output, "hex")); + }) + + // This will get fired when contract sends a read response. + hpc.on(HotPocketEvents.contractReadResponse, (response) => { + console.log("Contract read response>> " + Buffer.from(response, "hex")); + }) + + // On ctrl + c we should close HP connection gracefully. process.once('SIGINT', function () { console.log('SIGINT received...'); - ws.close(); + hpc.close(); }); - function encode_buffer(buffer) { - return is_json ? buffer.toString('hex') : buffer; - } - - function serialize_object(obj) { - return is_json ? Buffer.from(JSON.stringify(obj)) : bson.serialize(obj); - } - - function deserialize_message(m) { - return is_json ? JSON.parse(m) : bson.deserialize(m); - } - - function create_handshake_response(challenge) { - const sig_bytes = sodium.crypto_sign_detached(challenge, keys.privateKey); - return { - type: 'handshake_response', - challenge: challenge, - sig: encode_buffer(Buffer.from(sig_bytes)), - pubkey: pkhex, - protocol: protocol - } - } - - function create_input_container(inp) { - - if (inp.length == 0) - return null; - - const inp_container = { - nonce: (new Date()).getTime().toString(), - input: encode_buffer(Buffer.from(inp)), - max_lcl_seqno: 999999999 - } - - const inp_container_bytes = serialize_object(inp_container); - const sig_bytes = Buffer.from(sodium.crypto_sign_detached(inp_container_bytes, keys.privateKey)); - - const signed_inp_container = { - type: "contract_input", - input_container: encode_buffer(inp_container_bytes), - sig: encode_buffer(sig_bytes) - } - - return signed_inp_container; - } - - function create_read_request_container(inp) { - - if (inp.length == 0) - return null; - - return { - type: "contract_read_request", - content: encode_buffer(Buffer.from(inp)) - } - } - - function create_status_request() { - return { type: 'stat' }; - } - - function handle_handshake_challange(m) { - - // sign the challenge and send back the response - const response = create_handshake_response(m.challenge); - ws.send(serialize_object(response)); - is_json = (protocol == 'json'); - - // start listening for stdin - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - console.log("Ready to accept inputs."); - - // Capture user input from the console. - const input_pump = () => { - rl.question('', (inp) => { - - let msg; - if (inp == "stat") - msg = create_status_request(); - else if (inp.startsWith("read ")) - msg = create_read_request_container(inp.substr(5)); - else - msg = create_input_container(inp); - - if (msg != null) - ws.send(serialize_object(msg)) - - input_pump(); - }) - } - input_pump(); - } - - ws.on('message', (received_msg) => { - - try { - m = deserialize_message(received_msg); - } catch (e) { - console.log("Exception deserializing: " + received_msg); - return; - } - - if (m.type == 'handshake_challenge') { - handle_handshake_challange(m); - } - else if (m.type == 'contract_output' || m.type == 'contract_read_response') { - const contract_reply = is_json ? Buffer.from(m.content, 'hex').toString() : m.content.toString(); - console.log(contract_reply); - } - else if (m.type == 'contract_input_status') { - if (m.status != "accepted") - console.log("Input status: " + m.status + " | reason: " + m.reason); - } - else { - console.log(m); - } - + // start listening for stdin + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout }); + console.log("Ready to accept inputs."); - ws.on('close', () => { - console.log('Server disconnected.'); - exit(); - }); + const input_pump = () => { + rl.question('', async (inp) => { + + if (inp.startsWith("read ")) + hpc.sendContractReadRequest(inp.substr(5)) + else { + const submissionStatus = await hpc.sendContractInput(inp); + if (submissionStatus && submissionStatus != "ok") + console.log("Input submission failed. reason: " + submissionStatus); + } + + input_pump(); + }) + } + input_pump(); } -// sodium has a trigger when it's ready, we will wait and execute from there -sodium.ready.then(main).catch((e) => { console.log(e) }) \ No newline at end of file +main(); \ No newline at end of file