├── .vscode └── settings.json ├── .env.example ├── .editorconfig ├── env.js ├── init.js ├── enclave.js ├── LICENSE.md ├── input.js ├── package.json ├── index.js ├── .gitignore ├── README.md └── sawtooth-client.js /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | } 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 2 | PUBLIC_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 3 | REST_API_URL=http://localhost:8008 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in 2 | # this file, please see the EditorConfig documentation: 3 | # http://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | // Import PRIVATE_KEY, PUBLIC_KEY, VALIDATOR_URL from .env file 2 | const dotenv = require('dotenv') 3 | 4 | const { leafHash } = require('./sawtooth-client') 5 | 6 | dotenv.config() 7 | 8 | const env = { 9 | privateKey: process.env.PRIVATE_KEY || '', 10 | publicKey: process.env.PUBLIC_KEY || '', 11 | restApiUrl: process.env.REST_API_URL || 'http://localhost:8008', 12 | familyName: 'intkey', 13 | familyPrefix: leafHash('intkey', 6), 14 | familyVersion: '1.0' 15 | } 16 | 17 | module.exports = env 18 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | const { createContext, CryptoFactory } = require('sawtooth-sdk/signing') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const env = require('./env') 6 | const context = createContext('secp256k1') 7 | const privateKey = context.newRandomPrivateKey() 8 | const signer = new CryptoFactory(context).newSigner(privateKey) 9 | 10 | const output = `PRIVATE_KEY=${privateKey.asHex()}\nPUBLIC_KEY=${signer.getPublicKey().asHex()}\nREST_API_URL=http://localhost:8008` 11 | 12 | fs.writeFile(path.resolve(__dirname, './.env'), output, (err) => { 13 | if (err) { 14 | return console.log(err) 15 | } 16 | }) 17 | 18 | console.log('\nGenerated .env file with public/private keys and REST API URL\n') 19 | console.log(output, '\n') 20 | -------------------------------------------------------------------------------- /enclave.js: -------------------------------------------------------------------------------- 1 | const { createHash, randomBytes } = require('crypto') 2 | const secp256k1 = require('secp256k1/elliptic') 3 | 4 | const createPrivateKey = () => { 5 | let privateKey 6 | do { 7 | privateKey = randomBytes(32) 8 | } while (!secp256k1.privateKeyVerify(privateKey)) 9 | return privateKey 10 | } 11 | 12 | const EnclaveFactory = (privateKeyArg) => { 13 | const privateKey = privateKeyArg || createPrivateKey() 14 | const publicKey = secp256k1.publicKeyCreate(privateKey) 15 | return { 16 | privateKey, 17 | publicKey, 18 | sign(data) { 19 | const dataHash = createHash('sha256').update(data).digest() 20 | const result = secp256k1.sign(dataHash, privateKey) 21 | return result.signature 22 | }, 23 | verify: secp256k1.verify 24 | } 25 | } 26 | 27 | module.exports = { 28 | EnclaveFactory 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | ### Copyright (c) 2018 Dane Petersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /input.js: -------------------------------------------------------------------------------- 1 | const input = { 2 | payloadIsValid: (payload) => { 3 | if (valueIsValid(payload.Value) && verbIsValid(payload.Verb) && nameIsValid(payload.Name)) return true 4 | else return false 5 | }, 6 | submitPayload: async (payload, transactor) => { 7 | try { 8 | // Format the Sawtooth transaction 9 | const txn = payload 10 | console.log(`Submitting transaction to Sawtooth REST API`) 11 | // Wait for the response from the validator receiving the transaction 12 | const txnRes = await transactor.post(txn) 13 | // Log only a few key items from the response, because it's a lot of info 14 | console.log({ 15 | status: txnRes.status, 16 | statusText: txnRes.statusText 17 | }) 18 | return txnRes 19 | } catch (err) { 20 | console.log('Error submitting transaction to Sawtooth REST API: ', err) 21 | console.log('Transaction: ', txn) 22 | } 23 | } 24 | } 25 | 26 | const isInteger = (value) => { 27 | if (isNaN(value)) { 28 | return false 29 | } 30 | var x = parseFloat(value) 31 | return (x | 0) === x 32 | } 33 | 34 | const verbIsValid = (verb) => { 35 | const trimmed = verb.trim() 36 | if (trimmed === 'inc' || trimmed === 'dec' || trimmed === 'set') return true 37 | else return false 38 | } 39 | 40 | const nameIsValid = (name) => { 41 | if (name.toString().length <= 20) return true 42 | else return false 43 | } 44 | 45 | const valueIsValid = (value) => { 46 | if ((isInteger(value)) && (value >= 0) && (value < Math.pow(2, 32) - 1)) return true 47 | else return false 48 | } 49 | 50 | module.exports = input 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intkey-client-js", 3 | "description": "An intkey client/transactor using the Hyperledger Sawtooth Javascript SDK", 4 | "version": "0.4.0", 5 | "homepage": "https://github.com/thegreatsunra/intkey-client-js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/thegreatsunra/intkey-client-js.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/thegreatsunra/intkey-client-js/issues" 12 | }, 13 | "author": "Dane Petersen ", 14 | "license": "MIT", 15 | "contributors": [ 16 | { 17 | "name": "Jake Ingman", 18 | "email": "jingman@gmail.com", 19 | "url": "https://github.com/jingman" 20 | }, 21 | { 22 | "name": "Dane Petersen", 23 | "email": "thegreatsunra@gmail.com", 24 | "url": "https://github.com/thegreatsunra" 25 | } 26 | ], 27 | "engines": { 28 | "node": ">= 8.1.0", 29 | "npm": ">= 5.0.3" 30 | }, 31 | "dependencies": { 32 | "axios": "^0.18.0", 33 | "cbor": "^4.0.0", 34 | "dotenv": "^5.0.1", 35 | "request": "^2.85.0", 36 | "sawtooth-sdk": "^1.0.1", 37 | "yargs": "^11.0.0" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^4.19.1", 41 | "eslint-config-standard": "^11.0.0", 42 | "eslint-plugin-import": "^2.9.0", 43 | "eslint-plugin-node": "^6.0.1", 44 | "eslint-plugin-promise": "^3.7.0", 45 | "eslint-plugin-standard": "^3.0.1" 46 | }, 47 | "main": "index.js", 48 | "scripts": { 49 | "init": "node init.js", 50 | "lint": "eslint --ext .js *.config* src --fix", 51 | "test": "echo \"Error: no test specified\" && exit 1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { EnclaveFactory } = require('./enclave') 2 | const { SawtoothClientFactory } = require('./sawtooth-client') 3 | const argv = require('yargs') 4 | .usage('Usage: node $0 --name [string] --verb [set,inc,dec] --value [integer]') 5 | .choices('verb', ['set', 'inc', 'dec']) 6 | .number('value') 7 | .string(['verb', 'name']) 8 | .describe('name', 'unique identifier for the entry') 9 | .describe('verb', 'action to take on the entry') 10 | .describe('value', 'value to pass to the entry') 11 | .example('node index.js --name foo --verb set --value 42', 'If `foo` is undefined, create it and set its value to 42') 12 | .example('node index.js --name foo --verb inc --value 13', 'If `foo` is defined, increment it by 13') 13 | .example('node index.js --name foo --verb dec --value 7', 'If `foo` is defined, decrement it by 7 (but not below 0)') 14 | .wrap(null) 15 | .demandOption(['name', 'verb', 'value']) 16 | .help('h') 17 | .alias('h', 'help') 18 | .argv 19 | 20 | const env = require('./env') 21 | const input = require('./input') 22 | 23 | const enclave = EnclaveFactory(Buffer.from(env.privateKey, 'hex')) 24 | 25 | const intkeyClient = SawtoothClientFactory({ 26 | enclave: enclave, 27 | restApiUrl: env.restApiUrl 28 | }) 29 | 30 | const intkeyTransactor = intkeyClient.newTransactor({ 31 | familyName: env.familyName, 32 | familyVersion: env.familyVersion 33 | }) 34 | 35 | const newPayload = { 36 | Verb: argv.verb, 37 | Name: argv.name, 38 | Value: argv.value 39 | } 40 | 41 | if (input.payloadIsValid(newPayload)) { 42 | input.submitPayload(newPayload, intkeyTransactor) 43 | } else { 44 | console.log(`Oops! Your payload failed validation and was not submitted.`) 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.gitkeep 2 | 3 | ### Node ### 4 | 5 | # Logs 6 | logs 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Optional npm cache directory 12 | .npm 13 | 14 | # Dependency directories 15 | /node_modules 16 | /jspm_packages 17 | /bower_components 18 | 19 | # Yarn Integrity file 20 | .yarn-integrity 21 | 22 | # Optional eslint cache 23 | .eslintcache 24 | 25 | # dotenv environment variables file(s) 26 | .env 27 | .env.* 28 | !.env.example 29 | 30 | #Build generated 31 | dist/ 32 | build/ 33 | 34 | # Serverless generated files 35 | .serverless/ 36 | 37 | 38 | ### SublimeText ### 39 | # cache files for sublime text 40 | *.tmlanguage.cache 41 | *.tmPreferences.cache 42 | *.stTheme.cache 43 | 44 | # workspace files are user-specific 45 | *.sublime-workspace 46 | 47 | # project files should be checked into the repository, unless a significant 48 | # proportion of contributors will probably not be using SublimeText 49 | # *.sublime-project 50 | 51 | 52 | ### VisualStudioCode ### 53 | .vscode/* 54 | !.vscode/settings.json 55 | !.vscode/tasks.json 56 | !.vscode/launch.json 57 | !.vscode/extensions.json 58 | 59 | 60 | ### Vim ### 61 | *.sw[a-p] 62 | 63 | 64 | ### WebStorm/IntelliJ ### 65 | /.idea 66 | modules.xml 67 | *.ipr 68 | 69 | 70 | ### System Files ### 71 | *.DS_Store 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | ehthumbs.db 76 | ehthumbs_vista.db 77 | 78 | # Folder config file 79 | Desktop.ini 80 | 81 | # Recycle Bin used on file shares 82 | $RECYCLE.BIN/ 83 | 84 | # Thumbnails 85 | ._* 86 | 87 | # Files that might appear in the root of a volume 88 | .DocumentRevisions-V100 89 | .fseventsd 90 | .Spotlight-V100 91 | .TemporaryItems 92 | .Trashes 93 | .VolumeIcon.icns 94 | .com.apple.timemachine.donotpresent 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # intkey-client-js 2 | 3 | > An intkey client/transactor using the Hyperledger Sawtooth Javascript SDK 4 | 5 | ## Getting started 6 | 7 | ```bash 8 | git clone git@github.com:thegreatsunra/intkey-client-js.git 9 | 10 | cd intkey-client-js 11 | 12 | npm install 13 | 14 | ## Generate public/private keys and a placeholder Sawtooth REST API URL 15 | node init.js 16 | ``` 17 | 18 | ### Configuring the Sawtooth REST API URL 19 | 20 | By default, the intkey client will attempt to connect to a Sawtooth REST API at `http://localhost:8008`. 21 | 22 | To connect to a REST API at a different URL: 23 | 24 | 1. Edit the `.env` file, which was created above by running `node init.js` 25 | 1. In `.env`, change the value of `REST_API_URL` to the location of your Sawtooth REST API 26 | 1. The next time you run `node index.js` the URL you specified in `.env` will be used automatically 27 | 28 | If you're using [sawtooth-rest-api-proxy](https://github.com/thegreatsunra/sawtooth-rest-api-proxy) you should use `https://`, set the host to the public domain address of your server, and use port `8888`. 29 | 30 | For example: 31 | 32 | ```bash 33 | REST_API_URL=https://my-sawtooth-api-proxy.my-domain.tld:8888 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```bash 39 | cd intkey-client-js 40 | 41 | # If `foo` is undefined, create it and set its value to 42 42 | node index.js --name foo --verb set --value 42 43 | 44 | # If `foo` is defined, increment it by 13 45 | node index.js --name foo --verb inc --value 13 46 | 47 | # If `foo` is defined, decrement it by 7 (but not below 0) 48 | node index.js --name foo --verb dec --value 7 49 | 50 | # Show help 51 | node index.js --help 52 | 53 | # OPTIONS 54 | # --name unique identifier for the entry [string] [required] 55 | # --verb action to take on the entry [required] [choices: "set", "inc", "dec"] 56 | # --value value to pass to the entry [integer] [required] 57 | ``` 58 | 59 | ## Notes 60 | 61 | Need an intkey transaction processor? Try [intkey-tp-js](https://github.com/thegreatsunra/intkey-client-js). 62 | 63 | ## License 64 | 65 | The MIT License (MIT) 66 | 67 | Copyright (c) 2018 Dane Petersen 68 | -------------------------------------------------------------------------------- /sawtooth-client.js: -------------------------------------------------------------------------------- 1 | const { randomBytes, createHash } = require('crypto') 2 | const axios = require('axios') 3 | const cbor = require('cbor') 4 | const protobuf = require('sawtooth-sdk/protobuf') 5 | 6 | const leafHash = (input, length) => { 7 | return createHash('sha512').update(input).digest('hex').toLowerCase().slice(0, length) 8 | } 9 | 10 | const SawtoothClientFactory = (factoryOptions) => { 11 | return { 12 | async get(url) { 13 | try { 14 | const res = await axios({ 15 | method: 'get', 16 | baseURL: factoryOptions.restApiUrl, 17 | url 18 | }) 19 | return res 20 | } catch (err) { 21 | console.log('error', err) 22 | } 23 | }, 24 | newTransactor(transactorOptions) { 25 | const _familyNamespace = transactorOptions.familyNamespace || leafHash(transactorOptions.familyName, 6) 26 | const _familyVersion = transactorOptions.familyVersion || '1.0' 27 | const _familyEncoder = transactorOptions.familyEncoder || cbor.encode 28 | return { 29 | async post(payload, txnOptions) { 30 | 31 | // Encode the payload 32 | const payloadBytes = _familyEncoder(payload) 33 | 34 | // Encode a transaction header 35 | const transactionHeaderBytes = protobuf.TransactionHeader.encode({ 36 | familyName: transactorOptions.familyName, 37 | familyVersion: _familyVersion, 38 | inputs: [_familyNamespace], 39 | outputs: [_familyNamespace], 40 | signerPublicKey: factoryOptions.enclave.publicKey.toString('hex'), 41 | batcherPublicKey: factoryOptions.enclave.publicKey.toString('hex'), 42 | dependencies: [], 43 | nonce: randomBytes(32).toString('hex'), 44 | payloadSha512: createHash('sha512').update(payloadBytes).digest('hex'), 45 | ...txnOptions // overwrite above defaults with passed options 46 | }).finish() 47 | 48 | // Sign the txn header. This signature will also be the txn address 49 | const txnSignature = factoryOptions.enclave.sign(transactionHeaderBytes).toString('hex') 50 | 51 | // Create the transaction 52 | const transaction = protobuf.Transaction.create({ 53 | header: transactionHeaderBytes, 54 | headerSignature: txnSignature, 55 | payload: payloadBytes 56 | }) 57 | 58 | // Batch the transactions and encode a batch header 59 | const transactions = [transaction] 60 | const batchHeaderBytes = protobuf.BatchHeader.encode({ 61 | signerPublicKey: factoryOptions.enclave.publicKey.toString('hex'), 62 | transactionIds: transactions.map((txn) => txn.headerSignature), 63 | }).finish() 64 | 65 | // Sign the batch header and create the batch 66 | const batchSignature = factoryOptions.enclave.sign(batchHeaderBytes).toString('hex') 67 | const batch = protobuf.Batch.create({ 68 | header: batchHeaderBytes, 69 | headerSignature: batchSignature, 70 | transactions: transactions 71 | }) 72 | 73 | // Batch the batches into a batch list 74 | const batchListBytes = protobuf.BatchList.encode({ 75 | batches: [batch] 76 | }).finish() 77 | 78 | // Post the batch list 79 | try { 80 | const res = await axios({ 81 | method: 'post', 82 | baseURL: factoryOptions.restApiUrl, 83 | url: '/batches', 84 | headers: { 'Content-Type': 'application/octet-stream' }, 85 | data: batchListBytes 86 | }) 87 | return res 88 | } catch (err) { 89 | console.log('error', err) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | module.exports = { 98 | leafHash, 99 | SawtoothClientFactory 100 | } 101 | --------------------------------------------------------------------------------