├── .gitignore ├── src ├── web │ ├── favicon.ico │ ├── index.html │ ├── index.js │ ├── controls │ │ ├── balance-ctrl.js │ │ ├── transfer-ctrl.js │ │ ├── common.js │ │ ├── selector-ctrl.js │ │ ├── address-ctrl.js │ │ ├── custom-deploy-ctrl.js │ │ └── main-ctrl.js │ ├── style.less │ └── rnode-actions.js ├── lib.js ├── rho │ ├── check-balance.js │ ├── bond.js │ └── transfer-funds.js ├── nodejs │ ├── repl.js │ ├── client-read.js │ ├── client.js │ └── client-insert-signed.js ├── eth │ ├── eth-wrapper.js │ └── eth-sign.js ├── rnode-sign.js ├── rchain-networks.js └── rnode-web.js ├── docs └── intellisense-vscode.png ├── data ├── genesis │ ├── wallets.txt │ └── bonds.txt ├── node.key.pem └── node.certificate.pem ├── tsconfig.json ├── .env ├── .github └── workflows │ └── github-pages.yml ├── LICENSE ├── docker-compose.yml ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | .parcel-cache/ 4 | .gen 5 | rnode-grpc-* 6 | -------------------------------------------------------------------------------- /src/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgrospic/rnode-client-js/HEAD/src/web/favicon.ico -------------------------------------------------------------------------------- /docs/intellisense-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgrospic/rnode-client-js/HEAD/docs/intellisense-vscode.png -------------------------------------------------------------------------------- /data/genesis/wallets.txt: -------------------------------------------------------------------------------- 1 | 11113y7AfYj7hShN49oAHHd3KiWxZRsodesdBi8QwSrPR5Veyh77S,100000000000,0 # Start network with this REV balances - only used on genesis 2 | -------------------------------------------------------------------------------- /data/genesis/bonds.txt: -------------------------------------------------------------------------------- 1 | 048c024adca3706ed6460a895ec7608b4217a1b1d080dd5259e41d9c55e21a875aa10b438e144b679ef202a350861011418fa6bee3128300d9b4cbdc4971a207fa 1234567890123 2 | -------------------------------------------------------------------------------- /data/node.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCC964NyCbeGDW3INKBd 3 | ML9dTLwZxaFw3N74g9VV5PPckw== 4 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "noImplicitAny": true, 5 | "alwaysStrict": true, 6 | "noImplicitThis": true, 7 | "noImplicitReturns": true, 8 | "noUnusedLocals": false 9 | }, 10 | "include": [ 11 | "src/web-ts/**/*", 12 | "rnode-grpc-gen/js/rnode-grpc-js.d.ts" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/lib.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | export const encodeBase16 = bytes => 3 | Array.from(bytes).map(x => (x & 0xff).toString(16).padStart(2, "0")).join('') 4 | 5 | export const decodeBase16 = hexStr => { 6 | const removed0x = hexStr.replace(/^0x/, '') 7 | const byte2hex = ([arr, bhi], x) => 8 | bhi ? [[...arr, parseInt(`${bhi}${x}`, 16)]] : [arr, x] 9 | const [resArr] = Array.from(removed0x).reduce(byte2hex, [[]]) 10 | return Uint8Array.from(resArr) 11 | } 12 | 13 | export const decodeAscii = (str = '') => 14 | Array.from(str).map(x => `${x}`.charCodeAt(0)) 15 | -------------------------------------------------------------------------------- /data/node.certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBXjCCAQKgAwIBAgIIRgmpOyKqJiIwDAYIKoZIzj0EAwIFADAzMTEwLwYDVQQD 3 | EyhlYmZmZDQxOWRlYTYwMjIwNzM0Y2NlYTg4NzVlODZkODdiYWMxMGE3MB4XDTE5 4 | MTExNjE0MTcwMVoXDTIwMTExNTE0MTcwMVowMzExMC8GA1UEAxMoZWJmZmQ0MTlk 5 | ZWE2MDIyMDczNGNjZWE4ODc1ZTg2ZDg3YmFjMTBhNzBZMBMGByqGSM49AgEGCCqG 6 | SM49AwEHA0IABOMsflowoPMdm4WV5E/sjWVUwQZ0TcKBJNqbMzBwFTHIeTtXfjkz 7 | +OkeMZa1gK7tNm+9XkTa2eaoCiGF8lsgXQkwDAYIKoZIzj0EAwIFAANIADBFAiBO 8 | 38RcQxpi0UZ+UlEJGbjiBNMkwOENmP0vKxF54+4skAIhAIHr7dMrvev5Fd/tESmi 9 | VMI7KJh06qdcafM0sx8MSYLr 10 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /src/rho/check-balance.js: -------------------------------------------------------------------------------- 1 | // Rholang code to check REVs balance 2 | // - intended for use with RNode exploratory deploy 3 | export const checkBalance_rho = addr => ` 4 | new return, rl(\`rho:registry:lookup\`), RevVaultCh, vaultCh in { 5 | rl!(\`rho:rchain:revVault\`, *RevVaultCh) | 6 | for (@(_, RevVault) <- RevVaultCh) { 7 | @RevVault!("findOrCreate", "${addr}", *vaultCh) | 8 | for (@maybeVault <- vaultCh) { 9 | match maybeVault { 10 | (true, vault) => @vault!("balance", *return) 11 | (false, err) => return!(err) 12 | } 13 | } 14 | } 15 | } 16 | ` 17 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # RNode configuration (used with Docker compose) 2 | # --------------------------------------------------------------------------------------------------- 3 | 4 | # RNODE_IMAGE=rchain/rnode 5 | RNODE_IMAGE=rchain/rnode:v0.12.3 6 | 7 | # My network 8 | # --------------------------------------------------------------------------------------------------- 9 | 10 | MY_NET_IP=127.0.0.1 11 | 12 | # Bootstrap 13 | # --------------------------------------------------------------------------------------------------- 14 | VALIDATOR_BOOT_PRIVATE=bb6f30056d1981b98e729cef72a82920e6242a4395e500bd24bd6c6e6a65c36c 15 | 16 | VALIDATOR_BOOT_ADDRESS="rnode://ebffd419dea60220734ccea8875e86d87bac10a7@boot?protocol=40400&discovery=40404" 17 | -------------------------------------------------------------------------------- /src/rho/bond.js: -------------------------------------------------------------------------------- 1 | // Rholang code to bond a validator 2 | export const posBond_rho = amount => ` 3 | new retCh, PoSCh, rl(\`rho:registry:lookup\`), stdout(\`rho:io:stdout\`) in { 4 | stdout!("About to lookup pos contract...") | 5 | 6 | rl!(\`rho:rchain:pos\`, *PoSCh) | 7 | 8 | for(@(_, PoS) <- PoSCh) { 9 | stdout!("About to bond...") | 10 | 11 | @PoS!("bond", ${amount}, *retCh) | 12 | for ( ret <- retCh) { 13 | stdout!("PoS return!") | 14 | match *ret { 15 | {(true, message)} => stdout!(("BOND_SUCCESS", "Successfully bonded!", message)) 16 | 17 | {(false, message)} => stdout!(("BOND_ERROR", message)) 18 | } 19 | } 20 | } 21 | } 22 | ` 23 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Publish gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | gh-pages: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out 13 | uses: actions/checkout@v3 14 | 15 | - name: Setup Node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '18.x' 19 | 20 | - name: Cache dependencies 21 | uses: actions/cache@v3 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-node- 27 | 28 | - run: npm install 29 | - run: npm run build 30 | 31 | - name: Push gh-pages 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./dist 36 | -------------------------------------------------------------------------------- /src/nodejs/repl.js: -------------------------------------------------------------------------------- 1 | // Reference to TypeScript definitions for IntelliSense in VSCode 2 | /// 3 | // @ts-check 4 | const grpc = require('@grpc/grpc-js') 5 | 6 | const { rnodeRepl } = require('@tgrospic/rnode-grpc-js') 7 | 8 | // Generated files with rnode-grpc-js tool 9 | const protoSchema = require('../../rnode-grpc-gen/js/pbjs_generated.json') 10 | // Import generated protobuf types (in global scope) 11 | require('../../rnode-grpc-gen/js/repl_pb') 12 | 13 | const { log } = console 14 | 15 | const sampleRholangCode = 'new out(`rho:io:stdout`) in { out!("Nodejs deploy test") }' 16 | 17 | const rnodeInternalUrl = 'localhost:40402' 18 | 19 | const options = { grpcLib: grpc, host: rnodeInternalUrl, protoSchema } 20 | 21 | const { Eval } = rnodeRepl(options) 22 | 23 | const main = async () => { 24 | // Examples of eval request to RNode 25 | 26 | const evalResult = await Eval({ program: sampleRholangCode }) 27 | log('EVAL', evalResult.output) 28 | } 29 | 30 | main() 31 | -------------------------------------------------------------------------------- /src/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | RChain web 10 | 17 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/web/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Web example with REV transfer and balance check 3 | import { pageLog, handleHashHref } from './controls/common' 4 | import { makeRNodeWeb } from '../rnode-web' 5 | import { makeRNodeActions } from './rnode-actions' 6 | import { startApp } from './controls/main-ctrl' 7 | 8 | // DOM global functions / dependencies 9 | const { log: logOrig, warn } = console 10 | const { fetch } = window 11 | 12 | // Prevents default redirect for link 13 | handleHashHref(document.body) 14 | 15 | // Page printer mirrors the console `log` 16 | const log = pageLog({log: logOrig, document}) 17 | 18 | // Make RNode web API client / wrapped around DOM fetch 19 | const rnodeWeb = makeRNodeWeb({fetch}) 20 | 21 | // Make application actions as a wrapper around RNode API 22 | const appNodeEff = makeRNodeActions(rnodeWeb, {log, warn}) 23 | 24 | // Application root element 25 | const appRoot = document.querySelector('#app') 26 | 27 | // Attach to window load event (to refresh on duplicated tab) 28 | window.addEventListener('load', ev => { 29 | // Start main app / supply effects 30 | startApp(appRoot, {...appNodeEff, log, warn}) 31 | }) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present Tomislav Grospic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/rho/transfer-funds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rholang code to transfer REVs 3 | * https://github.com/rchain/rchain/blob/3eca061/rholang/examples/vault_demo/3.transfer_funds.rho 4 | * 5 | * NOTE: Leading whitespaces are removed to fix strange bug in Trezor<->Metamask communication. 6 | * https://github.com/tgrospic/rnode-client-js/issues/22 7 | */ 8 | export const transferFunds_rho = (revAddrFrom, revAddrTo, amount) => ` 9 | new rl(\`rho:registry:lookup\`), RevVaultCh in { 10 | rl!(\`rho:rchain:revVault\`, *RevVaultCh) | 11 | for (@(_, RevVault) <- RevVaultCh) { 12 | new vaultCh, vaultTo, revVaultkeyCh, 13 | deployerId(\`rho:rchain:deployerId\`), 14 | deployId(\`rho:rchain:deployId\`) 15 | in { 16 | match ("${revAddrFrom}", "${revAddrTo}", ${amount}) { 17 | (revAddrFrom, revAddrTo, amount) => { 18 | @RevVault!("findOrCreate", revAddrFrom, *vaultCh) | 19 | @RevVault!("findOrCreate", revAddrTo, *vaultTo) | 20 | @RevVault!("deployerAuthKey", *deployerId, *revVaultkeyCh) | 21 | for (@vault <- vaultCh; key <- revVaultkeyCh; _ <- vaultTo) { 22 | match vault { 23 | (true, vault) => { 24 | new resultCh in { 25 | @vault!("transfer", revAddrTo, amount, *key, *resultCh) | 26 | for (@result <- resultCh) { 27 | match result { 28 | (true , _ ) => deployId!((true, "Transfer successful (not yet finalized).")) 29 | (false, err) => deployId!((false, err)) 30 | } 31 | } 32 | } 33 | } 34 | err => { 35 | deployId!((false, "REV vault cannot be found or created.")) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | ` 45 | -------------------------------------------------------------------------------- /src/eth/eth-wrapper.js: -------------------------------------------------------------------------------- 1 | // Metamask wrapper for Ethereum provider 2 | // https://metamask.github.io/metamask-docs/guide/ethereum-provider.html#methods-new-api 3 | // Updated by EIP-1193 (ethereum.request) 4 | // https://eips.ethereum.org/EIPS/eip-1193 5 | 6 | import { encodeBase16 } from "../lib" 7 | 8 | // Ethereum object injected by Metamask 9 | const eth_ = window.ethereum 10 | 11 | export const ethDetected = !!eth_ 12 | 13 | // https://docs.metamask.io/guide/ethereum-provider.html#properties 14 | if (ethDetected) eth_.autoRefreshOnNetworkChange = false 15 | 16 | // Send a request to Ethereum API (Metamask) 17 | const ethRequest = (method, args) => { 18 | if (!eth_) throw Error(`Ethereum (Metamask) not detected.`) 19 | 20 | return eth_.request({method, ...args}) 21 | } 22 | 23 | // Request an address selected in Metamask 24 | // - the first request will ask the user for permission 25 | export const ethereumAddress = async () => { 26 | const accounts = await ethRequest('eth_requestAccounts') 27 | 28 | if (!Array.isArray(accounts)) 29 | throw Error(`Ethereum RPC response is not a list of accounts (${accounts}).`) 30 | 31 | // Returns ETH address in hex format 32 | return accounts[0] 33 | } 34 | 35 | // Ethereum personal signature 36 | // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign 37 | export const ethereumSign = async (bytes, ethAddr) => { 38 | // Create args, fix arrays/buffers 39 | const msg = encodeBase16(bytes) 40 | const args = { params: [msg, ethAddr] } 41 | 42 | // Returns signature in hex format 43 | return await ethRequest('personal_sign', args) 44 | } 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.3' 2 | 3 | x-rnode: 4 | &default-rnode 5 | image: $RNODE_IMAGE 6 | user: root 7 | restart: always 8 | networks: 9 | - rchain-net 10 | 11 | services: 12 | 13 | boot: 14 | << : *default-rnode 15 | container_name: boot 16 | command: run -s --validator-private-key $VALIDATOR_BOOT_PRIVATE --allow-private-addresses --host boot 17 | --protocol-port 40400 --discovery-port 40404 18 | --wallets-file /data/genesis/wallets.txt --bonds-file /data/genesis/bonds.txt 19 | --tls-certificate-path /data/node.certificate.pem --tls-key-path /data/node.key.pem 20 | --approve-duration 10seconds --approve-interval 10seconds 21 | ports: 22 | - $MY_NET_IP:40401:40401 23 | - $MY_NET_IP:40402:40402 24 | - $MY_NET_IP:40403:40403 25 | - $MY_NET_IP:40405:40405 26 | # Ports exposed externally 27 | - $MY_NET_IP:40400:40400 28 | - $MY_NET_IP:40404:40404 29 | volumes: 30 | - ./data:/data 31 | 32 | read: 33 | << : *default-rnode 34 | container_name: read 35 | command: run -b $VALIDATOR_BOOT_ADDRESS --allow-private-addresses --host read --no-upnp 36 | --protocol-port 40410 --discovery-port 40414 37 | --approve-duration 10seconds --approve-interval 10seconds 38 | --fork-choice-check-if-stale-interval 30seconds --fork-choice-stale-threshold 30seconds 39 | ports: 40 | - $MY_NET_IP:40411:40401 41 | - $MY_NET_IP:40413:40403 42 | - $MY_NET_IP:40415:40405 43 | # Ports exposed externally 44 | - $MY_NET_IP:40410:40410 45 | - $MY_NET_IP:40414:40414 46 | 47 | networks: 48 | rchain-net: 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tgrospic/rnode-client-js", 3 | "version": "0.10.0", 4 | "description": "RNode sample client: nodejs & web", 5 | "author": "Tomislav Grospic", 6 | "license": "MIT", 7 | "scripts": { 8 | "start:nodejs": "node src/nodejs/client", 9 | "start:web": "parcel src/web/index.html", 10 | "build:web": "npm-run-all clean build", 11 | "build": "parcel build --public-url ./ src/web/index.html", 12 | "rnode-generate": "rnode-grpc --rnode-version dev", 13 | "rnode-generate-dev": "rnode-grpc --rnode-version dev --gen-dir rnode-grpc-dev", 14 | "clean": "rimraf dist .cache", 15 | "clean:all": "rimraf dist .cache rnode-grpc-gen node_modules", 16 | "gh-pages": "npm run build-web && git checkout gh-pages && rm web.* style.* && cp dist/* . && git add ." 17 | }, 18 | "dependencies": { 19 | "@grpc/grpc-js": "^1.4.4", 20 | "@tgrospic/rnode-grpc-js": "^0.11.0", 21 | "blakejs": "^1.1.1", 22 | "elliptic": "^6.5.4", 23 | "ethereumjs-util": "^7.1.3", 24 | "google-protobuf": "^3.19.1", 25 | "grpc-web": "^1.3.0", 26 | "mithril": "^2.0.4", 27 | "npm-run-all": "^4.1.5", 28 | "ramda": "^0.27.1" 29 | }, 30 | "devDependencies": { 31 | "@parcel/transformer-less": "^2.1.1", 32 | "@types/elliptic": "^6.4.14", 33 | "assert": "^2.0.0", 34 | "buffer": "^6.0.3", 35 | "events": "^3.3.0", 36 | "grpc-tools": "^1.11.2", 37 | "less": "^4.1.2", 38 | "parcel": "^2.1.1", 39 | "process": "^0.11.10", 40 | "protobufjs": "^6.11.2", 41 | "rimraf": "^3.0.2", 42 | "stream-browserify": "^3.0.0", 43 | "typescript": "^4.4.4" 44 | }, 45 | "browserslist": "> 0.25%, not dead", 46 | "engines": { 47 | "node": ">=8.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/eth/eth-sign.js: -------------------------------------------------------------------------------- 1 | import { ec } from 'elliptic' 2 | import * as ethUtil from 'ethereumjs-util' 3 | 4 | import { decodeAscii } from '../lib.js' 5 | import { deployDataProtobufSerialize } from '../rnode-sign.js' 6 | 7 | export const recoverPublicKeyEth = (data, sigHex) => { 8 | // Ethereum lib to recover public key from massage and signature 9 | const hashed = ethUtil.hashPersonalMessage(ethUtil.toBuffer([...data])) 10 | const sigBytes = ethUtil.toBuffer(sigHex) 11 | const {v, r, s} = ethUtil.fromRpcSig(sigBytes) 12 | // Public key without prefix 13 | const pubkeyRecover = ethUtil.ecrecover(hashed, v, r, s) 14 | 15 | return ethUtil.bufferToHex([4, ...pubkeyRecover]) 16 | } 17 | 18 | export const verifyDeployEth = deploySigned => { 19 | const { 20 | term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId, 21 | deployer, sig, // : Array[Byte] 22 | } = deploySigned 23 | 24 | // Serialize deploy data for signing 25 | const deploySerialized = deployDataProtobufSerialize({ 26 | term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId, 27 | }) 28 | 29 | // Create a hash of message with prefix 30 | // https://github.com/ethereumjs/ethereumjs-util/blob/4a8001c/src/signature.ts#L136 31 | const deployLen = deploySerialized.length 32 | const msgPrefix = `\x19Ethereum Signed Message:\n${deployLen}` 33 | const prefixBin = decodeAscii(msgPrefix) 34 | const msg = ethUtil.toBuffer([...prefixBin, ...deploySerialized]) 35 | const hashed = ethUtil.keccak256(msg) 36 | 37 | // Check deployer's signature 38 | const crypt = new ec('secp256k1') 39 | const key = crypt.keyFromPublic(deployer) 40 | const sigRS = { r: sig.slice(0, 32), s: sig.slice(32, 64) } 41 | const isValid = key.verify(hashed, sigRS) 42 | 43 | return isValid 44 | } 45 | -------------------------------------------------------------------------------- /src/nodejs/client-read.js: -------------------------------------------------------------------------------- 1 | // Reference to TypeScript definitions for IntelliSense in VSCode 2 | /// 3 | // @ts-check 4 | const grpc = require('@grpc/grpc-js') 5 | const { ec } = require('elliptic') 6 | 7 | const { rnodeDeploy, rhoParToJson } = require('@tgrospic/rnode-grpc-js') 8 | 9 | // Generated files with rnode-grpc-js tool 10 | const protoSchema = require('../../rnode-grpc-gen/js/pbjs_generated.json') 11 | // Import generated protobuf types (in global scope) 12 | require('../../rnode-grpc-gen/js/DeployServiceV1_pb') 13 | require('../../rnode-grpc-gen/js/ProposeServiceV1_pb') 14 | 15 | const { log, warn } = console 16 | const util = require('util') 17 | 18 | const sampleRholangCode = ` 19 | new return, out(\`rho:io:stdout\`), x in { 20 | out!("Nodejs exploretory deploy test") | 21 | 22 | // Return value from Rholang 23 | // NOTE: exploratory deploy uses first defined channel to return values 24 | return!(("Return value from exploretory deploy", [1], true, Set(42), {"my_key": "My value"}, *x)) 25 | } 26 | ` 27 | 28 | const rnodeExternalUrl = 'localhost:40411' 29 | // const rnodeExternalUrl = 'observer.testnet.rchain.coop:40401' 30 | 31 | const rnodeExample = async () => { 32 | // Get RNode service methods 33 | const options = host => ({ grpcLib: grpc, host, protoSchema }) 34 | 35 | const { exploratoryDeploy } = rnodeDeploy(options(rnodeExternalUrl)) 36 | 37 | // Get result from exploratory (read-only) deploy 38 | const { result: { postblockdataList, block } } = await exploratoryDeploy({ 39 | term: sampleRholangCode, 40 | }) 41 | log('BLOCK', util.inspect(block, {depth: 100, colors: true})) 42 | 43 | // Raw data (Par objects) returned from Rholang 44 | const pars = postblockdataList 45 | 46 | log('RAW_DATA', util.inspect(pars, {depth: 100, colors: true})) 47 | 48 | // Rholang term converted to JSON 49 | // NOTE: Only part of Rholang types are converted: 50 | // primitive types, List, Set, object (Map), Uri, ByteArray, unforgeable names. 51 | const json = pars.map(rhoParToJson) 52 | 53 | log('JSON', util.inspect(json, {depth: 100, colors: true})) 54 | } 55 | 56 | rnodeExample() 57 | -------------------------------------------------------------------------------- /src/web/controls/balance-ctrl.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import m from 'mithril' 3 | import * as R from 'ramda' 4 | import { labelStyle, showTokenDecimal, showNetworkError } from './common' 5 | 6 | const initSelected = (st, wallet) => { 7 | const {account} = st 8 | 9 | // Pre-select first account if not selected 10 | const selAccount = R.isNil(account) && !R.isNil(wallet) 11 | ? R.head(wallet) : account 12 | 13 | return {...st, account: selAccount} 14 | } 15 | 16 | export const balanceCtrl = (st, {wallet = [], node, onCheckBalance}) => { 17 | const {tokenName, tokenDecimal} = node 18 | 19 | const checkBalanceEv = async _ => { 20 | st.update(s => ({...s, dataBal: '...', dataError: ''})) 21 | 22 | const [bal, dataError] = await onCheckBalance(account.revAddr) 23 | .catch(ex => ['', ex.message]) 24 | 25 | const dataBal = typeof bal === 'number' 26 | ? bal === 0 ? `${bal}` : `${bal} (${showTokenDecimal(bal, tokenDecimal)} ${tokenName})` : '' 27 | st.update(s => ({...s, dataBal, dataError})) 28 | } 29 | 30 | const accountChangeEv = ev => { 31 | const account = R.find(R.propEq('revAddr', ev.target.value), wallet) 32 | st.set({account}) 33 | } 34 | 35 | const labelAddr = `${tokenName} address` 36 | const isWalletEmpty = R.isNil(wallet) || R.isEmpty(wallet) 37 | 38 | // Control state 39 | const {account, dataBal, dataError} = initSelected(st.view({}), wallet) 40 | 41 | return m('.ctrl.balance-ctrl', 42 | m('h2', `Check ${tokenName} balance`), 43 | isWalletEmpty ? m('b', `${tokenName} wallet is empty, add accounts to check balance.`) : [ 44 | m('', 'Sends exploratory deploy to selected read-only RNode.'), 45 | 46 | // REV address dropdown 47 | m('', labelStyle(account), labelAddr), 48 | m('select', {onchange: accountChangeEv}, 49 | wallet.map(({name, revAddr}) => 50 | m('option', {value: revAddr}, `${name}: ${revAddr}`) 51 | ), 52 | ), 53 | 54 | // Action button / results 55 | m(''), 56 | m('button', {onclick: checkBalanceEv, disabled: !account}, 'Check balance'), 57 | m('b', dataBal), 58 | m('b.warning', showNetworkError(dataError)), 59 | ] 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/web/style.less: -------------------------------------------------------------------------------- 1 | @color-red : #bf0808; 2 | @color-blue: #0649af; 3 | @color-grey: #a8a8a847; 4 | 5 | @color-bg-light-red: #b1a8a847; 6 | @color-bg-light-blue: #c2c7ce61; 7 | 8 | :root { 9 | font-size: .9rem; 10 | font-family: sans-serif; 11 | padding-bottom: 40px; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | } 17 | 18 | * + * { 19 | margin-top: 4px; 20 | margin-right: 5px; 21 | } 22 | 23 | hr { 24 | margin: 12px 0 5px; 25 | padding: 0; 26 | border-color: #ffffffb3; 27 | } 28 | 29 | h1 { 30 | margin: 8px 0 6px; 31 | } 32 | 33 | h2 { 34 | margin: 6px 0 4px; 35 | } 36 | 37 | h3 { 38 | margin: 4px 0 2px; 39 | } 40 | 41 | input, select, textarea { 42 | box-sizing: border-box; 43 | max-width: 100%; 44 | } 45 | 46 | input, select, button { 47 | padding: 4px; 48 | } 49 | 50 | input[type="text"] { 51 | width: 480px; 52 | border-color: #cfcfcf9c; 53 | border-width: 1px; 54 | } 55 | 56 | input[type="text"].addr-name { 57 | width: 180px; 58 | } 59 | 60 | button { 61 | min-width: 85px; 62 | } 63 | 64 | pre { 65 | margin: 0; 66 | padding: 10px 0px; 67 | border-bottom: solid 1px @color-grey; 68 | } 69 | 70 | table th { 71 | text-align: left; 72 | } 73 | 74 | a, a.visited, a.hover { 75 | color: @color-blue; 76 | } 77 | 78 | optgroup > option { 79 | color: #000; 80 | } 81 | 82 | .testnet-color { 83 | color: @color-blue; 84 | } 85 | 86 | .mainnet-color { 87 | color: @color-red; 88 | } 89 | 90 | .rev { 91 | font-size: 1.2rem; 92 | } 93 | 94 | .testnet { 95 | h2 { 96 | .testnet-color 97 | } 98 | 99 | .address-ctrl { 100 | background-color: @color-bg-light-blue; 101 | } 102 | 103 | .add-account:not([disabled]) { 104 | .testnet-color; 105 | font-weight: bold; 106 | } 107 | } 108 | 109 | .mainnet { 110 | h2 { 111 | .mainnet-color 112 | } 113 | 114 | .address-ctrl { 115 | background-color: @color-bg-light-red; 116 | } 117 | 118 | .add-account:not([disabled]) { 119 | .mainnet-color; 120 | font-weight: bold; 121 | } 122 | } 123 | 124 | .ctrl { 125 | padding-left: 10px; 126 | padding-right: 10px; 127 | } 128 | 129 | .selector-ctrl { 130 | padding-bottom: 5px; 131 | 132 | h2 { 133 | display: inline-block; 134 | padding-right: 10px;; 135 | } 136 | 137 | table { 138 | font-size: .8em; 139 | color: #3c3c3c; 140 | } 141 | } 142 | 143 | .selector-ctrl pre { 144 | display:inline-block; 145 | padding: 0px 0px; 146 | border: none; 147 | } 148 | 149 | .address-ctrl { 150 | padding-top: 15px; 151 | padding-bottom: 25px; 152 | background-color: @color-grey; 153 | 154 | .info { 155 | font-size: .9rem; 156 | } 157 | } 158 | 159 | 160 | .transfer-ctrl .rev-amount { 161 | width: 195px; 162 | } 163 | 164 | 165 | .custom-deploy-ctrl { 166 | .deploy-code { 167 | width: 730px; 168 | } 169 | 170 | .phlo-limit { 171 | width: 200px; 172 | } 173 | } 174 | 175 | .warning { 176 | margin: 0; 177 | padding: 5px 0; 178 | color: @color-red; 179 | font-weight: bold; 180 | } 181 | 182 | .wallet { 183 | width: 500px; 184 | } 185 | 186 | .account { 187 | font-weight: bold; 188 | text-decoration: underline; 189 | cursor: pointer; 190 | } 191 | 192 | // Hard Fork info 193 | .hf-info { 194 | padding: 15px 20px; 195 | background-color: #0d9a25; 196 | color: white; 197 | font-size: 1.5em; 198 | 199 | * + * { 200 | margin: 0; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/web/controls/transfer-ctrl.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import m from 'mithril' 3 | import * as R from 'ramda' 4 | import { labelStyle, showTokenDecimal, labelRev, showNetworkError } from './common' 5 | import { ethDetected } from '../../eth/eth-wrapper' 6 | 7 | const initSelected = (st, wallet) => { 8 | const {account, toAccount} = st 9 | 10 | // Pre-select first account if not selected 11 | 12 | const selAccount = R.isNil(account) && !R.isNil(wallet) 13 | ? R.head(wallet) : account 14 | 15 | const selToAccount = R.isNil(toAccount) && !R.isNil(wallet) 16 | ? R.head(wallet) : toAccount 17 | 18 | return {...st, account: selAccount, toAccount: selToAccount} 19 | } 20 | 21 | export const transferCtrl = (st, {wallet, node, onTransfer, warn}) => { 22 | const valEv = name => ev => { 23 | const val = ev.target.value 24 | st.update(s => ({...s, [name]: val})) 25 | } 26 | 27 | const send = async _ => { 28 | st.update(s => ({...s, status: '...', error: ''})) 29 | await onTransfer({fromAccount: account, toAccount, amount}) 30 | .then(x => { 31 | st.update(s => ({...s, status: x, error: ''})) 32 | }) 33 | .catch(ex => { 34 | st.update(s => ({...s, status: '', error: ex.message})) 35 | warn('Transfer error', ex) 36 | }) 37 | } 38 | 39 | const onSelectFrom = async ev => { 40 | const account = R.find(R.propEq('revAddr', ev.target.value), wallet) 41 | st.update(s => ({...s, account})) 42 | } 43 | 44 | const onSelectTo = async ev => { 45 | const toAccount = R.find(R.propEq('revAddr', ev.target.value), wallet) 46 | st.update(s => ({...s, toAccount})) 47 | } 48 | 49 | // Control state 50 | const {account, toAccount, amount, status, error} = initSelected(st.view({}), wallet) 51 | 52 | const {tokenName, tokenDecimal} = node 53 | const labelSource = `Source ${tokenName} address` 54 | const labelDestination = `Destination ${tokenName} address` 55 | const labelAmount = `Amount (in tiny ${tokenName} x10^${tokenDecimal})` 56 | const isWalletEmpty = R.isNil(wallet) || R.isEmpty(wallet) 57 | const canTransfer = account && toAccount && amount && (account || ethDetected) 58 | const amountPreview = showTokenDecimal(amount, tokenDecimal) 59 | 60 | return m('.ctrl.transfer-ctrl', 61 | m('h2', `Transfer ${tokenName} tokens`), 62 | isWalletEmpty ? m('b', `${tokenName} wallet is empty, add accounts to make transfers.`) : [ 63 | m('', 'Sends transfer deploy to selected validator RNode.'), 64 | 65 | // Source REV address dropdown 66 | m('', labelStyle(account), labelSource), 67 | m('select', {onchange: onSelectFrom}, 68 | wallet.map(({name, revAddr}) => 69 | m('option', {value: revAddr}, `${name}: ${revAddr}`) 70 | ), 71 | ), 72 | 73 | // Target REV address dropdown 74 | m(''), 75 | m('', labelStyle(toAccount), labelDestination), 76 | m('select', {onchange: onSelectTo}, 77 | wallet.map(({name, revAddr}) => 78 | m('option', {value: revAddr}, `${name}: ${revAddr}`) 79 | ), 80 | ), 81 | 82 | // REV amount 83 | m(''), 84 | m('', labelStyle(amount), labelAmount), 85 | m('input[type=number].rev-amount', { 86 | placeholder: labelAmount, value: amount, 87 | oninput: valEv('amount'), 88 | }), 89 | labelRev(amountPreview, tokenName), 90 | 91 | // Action button / result 92 | m(''), 93 | m('button', {onclick: send, disabled: !canTransfer}, 'Transfer'), 94 | status && m('b', status), 95 | error && m('b.warning', showNetworkError(error)), 96 | ] 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /src/rnode-sign.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import blake from 'blakejs' 3 | import { ec } from 'elliptic' 4 | import jspb from 'google-protobuf' 5 | 6 | export const signDeploy = (privateKey, deployObj) => { 7 | const { 8 | term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId, 9 | sigAlgorithm = 'secp256k1', 10 | } = deployObj 11 | 12 | // Serialize deploy data for signing 13 | const deploySerialized = deployDataProtobufSerialize({ 14 | term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId, 15 | }) 16 | 17 | // Signing key 18 | const crypt = new ec(sigAlgorithm) 19 | const key = getSignKey(crypt, privateKey) 20 | const deployer = Uint8Array.from(key.getPublic('array')) 21 | // Hash and sign serialized deploy 22 | const hashed = blake.blake2bHex(deploySerialized, void 666, 32) 23 | const sigArray = key.sign(hashed, {canonical: true}).toDER('array') 24 | const sig = Uint8Array.from(sigArray) 25 | 26 | // Return deploy object / ready for sending to RNode 27 | return { 28 | term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId, 29 | deployer, sig, sigAlgorithm, 30 | } 31 | } 32 | 33 | export const verifyDeploy = deployObj => { 34 | const { 35 | term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId, 36 | deployer, sig, sigAlgorithm, 37 | } = deployObj 38 | 39 | // Serialize deploy data for signing 40 | const deploySerialized = deployDataProtobufSerialize({ 41 | term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId, 42 | }) 43 | 44 | // Signing public key to verify 45 | const crypt = new ec(sigAlgorithm) 46 | const key = crypt.keyFromPublic(deployer) 47 | // Hash and verify signature 48 | const hashed = blake.blake2bHex(deploySerialized, void 666, 32) 49 | const isValid = key.verify(hashed, sig) 50 | 51 | return isValid 52 | } 53 | 54 | // Fix for ec.keyFromPrivate not accepting KeyPair 55 | // - detect KeyPair if it have `sign` function 56 | const getSignKey = (crypt, pk) => 57 | pk && pk.sign && pk.sign.constructor == Function ? pk : crypt.keyFromPrivate(pk) 58 | 59 | // Serialization of DeployDataProto object without generated JS code 60 | export const deployDataProtobufSerialize = deployData => { 61 | const { term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId } = deployData 62 | 63 | // Create binary stream writer 64 | const writer = new jspb.BinaryWriter() 65 | // Write fields (protobuf doesn't serialize default values) 66 | const writeString = (order, val) => val != "" && writer.writeString(order, val) 67 | const writeInt64 = (order, val) => val != 0 && writer.writeInt64(order, val) 68 | 69 | // https://github.com/rchain/rchain/blob/ebe4d476371/models/src/main/protobuf/CasperMessage.proto#L134-L149 70 | // message DeployDataProto { 71 | // bytes deployer = 1; //public key 72 | // string term = 2; //rholang source code to deploy (will be parsed into `Par`) 73 | // int64 timestamp = 3; //millisecond timestamp 74 | // bytes sig = 4; //signature of (hash(term) + timestamp) using private key 75 | // string sigAlgorithm = 5; //name of the algorithm used to sign 76 | // int64 phloPrice = 7; //phlo price 77 | // int64 phloLimit = 8; //phlo limit for the deployment 78 | // int64 validAfterBlockNumber = 10; 79 | // string shardId = 11;//shard ID to prevent replay of deploys between shards 80 | // } 81 | 82 | // Serialize fields 83 | writeString(2, term) 84 | writeInt64(3, timestamp) 85 | writeInt64(7, phloPrice) 86 | writeInt64(8, phloLimit) 87 | writeInt64(10, validAfterBlockNumber) 88 | writeString(11, shardId) 89 | 90 | return writer.getResultBuffer() 91 | } 92 | -------------------------------------------------------------------------------- /src/web/controls/common.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as R from 'ramda' 3 | import m from 'mithril' 4 | 5 | // Common styles 6 | 7 | const labelBaseStyle = { 'font-size': '.8rem', padding: '2px 0 0 0', transition: 'all 1s' } 8 | 9 | const styleShowHide = isVis => ({ opacity: isVis ? .65 : 0, height: isVis ? 'auto' : 0 }) 10 | 11 | export const labelStyle = isVis => ({ style: {...labelBaseStyle, ...styleShowHide(isVis) } }) 12 | 13 | export const showTokenDecimal = (amount, digits)=> { 14 | const d = digits // decimal places 15 | const amountNr = parseInt(amount) 16 | const amountStr = isNaN(amountNr) ? '' : `${amountNr}` 17 | const length = amountStr.length 18 | const trimZeroes = s => s.replace(/[\.]?[0]*$/ig, '') 19 | if (length === 0) return '' 20 | if (length <= d) { 21 | const padded = amountStr.padStart(d, '0') 22 | return trimZeroes(`0.${padded}`) 23 | } else if (length > d) { 24 | const prefix = amountStr.slice(0, -d) 25 | const suffix = amountStr.slice(-d) 26 | return trimZeroes(`${prefix}.${suffix}`) 27 | } 28 | } 29 | 30 | export const labelRev = (amount, tokenName) => 31 | amount && m('span.rev', amount, m('b', ` ${tokenName}`)) 32 | 33 | export const showNetworkError = errMessage => 34 | errMessage == 'Failed to fetch' 35 | ? `${errMessage}: select a running RNode from the above selector.` 36 | : errMessage 37 | 38 | // State cell 39 | export const mkCell = () => { 40 | let _stRef, _listener 41 | const ln = path => 42 | R.is(Function, path) ? path : R.lensPath(path.split('.').filter(p => !R.isEmpty(p))) 43 | const stCell = path => { 44 | const lens = ln(path) 45 | return { 46 | view: def => { 47 | const res = R.view(lens, _stRef) 48 | return R.isNil(res) ? def : res 49 | }, 50 | set: v => { 51 | _stRef = R.set(lens, v, _stRef) 52 | // Trigger event (render) 53 | _listener(_stRef) 54 | }, 55 | update: f => { 56 | const s = R.view(lens, _stRef) 57 | _stRef = R.set(lens, f(s), _stRef) 58 | // Trigger event (render) 59 | _listener(_stRef) 60 | }, 61 | // Compose lenses / make sub cells 62 | o: compPath => { 63 | const subLens = R.compose(lens, ln(compPath)) 64 | return stCell(subLens) 65 | }, 66 | } 67 | } 68 | return { 69 | ...stCell(''), 70 | // Set event (on-change) listener 71 | setListener: f => { _listener = f }, 72 | } 73 | } 74 | 75 | // Wraps Virtual DOM renderer to render state 76 | export const makeRenderer = (element, view) => (state, deps) => { 77 | const stateCell = mkCell() 78 | const render = () => { 79 | m.render(element, view(stateCell, deps)) 80 | } 81 | stateCell.setListener(render) 82 | stateCell.set(state) 83 | } 84 | 85 | export const pageLog = ({log, document}) => { 86 | // Page logger 87 | const logEL = document.querySelector('#log') 88 | const logWrap = (...args) => { 89 | const lines = Array.from(args).map(x => { 90 | const f = (_, v) => v && v.buffer instanceof ArrayBuffer 91 | ? Array.from(v).toString() : v 92 | return JSON.stringify(x, f, 2).replace(/\\n/g, '
') 93 | }) 94 | const el = document.createElement('pre') 95 | el.innerHTML = lines.join(' ') 96 | logEL.prepend(el) 97 | log(...args) 98 | } 99 | return logWrap 100 | } 101 | 102 | export const handleHashHref = pageBody => { 103 | // Prevents default redirect for link
104 | pageBody.addEventListener('click', ev => { 105 | const target = ev.target 106 | const isHrefHash = target 107 | && target.nodeName === 'A' 108 | && target.attributes['href'].value === '#' 109 | 110 | if (isHrefHash) ev.preventDefault() 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /src/nodejs/client.js: -------------------------------------------------------------------------------- 1 | // Reference to TypeScript definitions for IntelliSense in VSCode 2 | /// 3 | // @ts-check 4 | const grpc = require('@grpc/grpc-js') 5 | const { ec } = require('elliptic') 6 | 7 | const { rnodeDeploy, rnodePropose, signDeploy, verifyDeploy, rhoParToJson } = require('@tgrospic/rnode-grpc-js') 8 | 9 | // Generated files with rnode-grpc-js tool 10 | const protoSchema = require('../../rnode-grpc-gen/js/pbjs_generated.json') 11 | // Import generated protobuf types (in global scope) 12 | require('../../rnode-grpc-gen/js/DeployServiceV1_pb') 13 | require('../../rnode-grpc-gen/js/ProposeServiceV1_pb') 14 | 15 | const { log, warn } = console 16 | const util = require('util') 17 | 18 | const sampleRholangCode = ` 19 | new return(\`rho:rchain:deployId\`), out(\`rho:io:stdout\`), x in { 20 | out!("Nodejs deploy test") | 21 | 22 | // Return value from Rholang 23 | return!(("Return value from deploy", [1], true, Set(42), {"my_key": "My value"}, *x)) 24 | } 25 | ` 26 | 27 | const rnodeExternalUrl = 'localhost:40401' 28 | // const rnodeExternalUrl = 'node3.testnet.rchain.coop:40401' 29 | 30 | const rnodeInternalUrl = 'localhost:40402' 31 | 32 | const rnodeExample = async () => { 33 | // Get RNode service methods 34 | const options = host => ({ grpcLib: grpc, host, protoSchema }) 35 | 36 | const { 37 | getBlocks, 38 | lastFinalizedBlock, 39 | visualizeDag, 40 | deployStatus, 41 | doDeploy, 42 | listenForDataAtName, 43 | } = rnodeDeploy(options(rnodeExternalUrl)) 44 | 45 | const { propose } = rnodePropose(options(rnodeInternalUrl)) 46 | 47 | // Examples of requests to RNode 48 | 49 | const lastBlockObj = await lastFinalizedBlock() 50 | log('LAST BLOCK', lastBlockObj) 51 | 52 | 53 | const blocks = await getBlocks({ depth: 1 }) 54 | log('BLOCKS', blocks) 55 | 56 | 57 | const vdagObj = await visualizeDag({ depth: 20, showjustificationlines: true }) 58 | log('VDAG', vdagObj.map(x => x.content).join('')) 59 | 60 | 61 | // Sample deploy 62 | 63 | const secp256k1 = new ec('secp256k1') 64 | // const key = secp256k1.genKeyPair() 65 | const key = 'bb6f30056d1981b98e729cef72a82920e6242a4395e500bd24bd6c6e6a65c36c' 66 | 67 | const deployData = { 68 | term: sampleRholangCode, 69 | timestamp: Date.now(), 70 | phloprice: 1, 71 | phlolimit: 10e3, 72 | validafterblocknumber: lastBlockObj.blockinfo?.blockinfo.blocknumber || 0, 73 | shardid: 'root', 74 | } 75 | const deploy = signDeploy(key, deployData) 76 | log('SIGNED DEPLOY', deploy) 77 | 78 | const isValidDeploy = verifyDeploy(deploy) 79 | log('DEPLOY IS VALID', isValidDeploy) 80 | 81 | const { result } = await doDeploy(deploy) 82 | log('DEPLOY RESPONSE', result) 83 | 84 | // Create new block with deploy 85 | 86 | const { result: proposeRes } = await propose() 87 | log('PROPOSE RESPONSE', proposeRes) 88 | 89 | // Get result from deploy (deployStatus is valid from RNode version 0.13.x) 90 | 91 | const { deployexecstatus } = await deployStatus({ 92 | deployid: deploy.sig, 93 | }) 94 | 95 | // Raw data (Par objects) returned from Rholang 96 | const res = deployexecstatus?.processedwithsuccess?.deployresultList ?? [] 97 | 98 | log('RAW_DATA', util.inspect(res, {depth: 100, colors: true})) 99 | 100 | // Rholang term converted to JSON 101 | // NOTE: Only part of Rholang types are converted: 102 | // primitive types, List, Set, object (Map), Uri, ByteArray, unforgeable names. 103 | const json = res.map(rhoParToJson) 104 | 105 | log('JSON', util.inspect(json, {depth: 100, colors: true})) 106 | 107 | 108 | // Get result from deploy 109 | // NOTE: old way which will become obsolete in future versions of RNode) 110 | 111 | const { payload } = await listenForDataAtName({ 112 | depth: 5, 113 | name: { unforgeablesList: [{gDeployIdBody: { sig: deploy.sig }}] }, 114 | }) 115 | 116 | // Raw data (Par objects) returned from Rholang 117 | const res2 = payload?.blockinfoList?.at(0)?.postblockdataList ?? [] 118 | 119 | const json2 = res2.map(rhoParToJson) 120 | 121 | log('JSON', util.inspect(json2, {depth: 100, colors: true})) 122 | } 123 | 124 | rnodeExample() 125 | -------------------------------------------------------------------------------- /src/rchain-networks.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const defaultPorts = { grpc: 40401, http: 40403, httpAdmin: 40405 } 3 | const defaultPortsSSL = { grpc: 40401, https: 443, httpAdmin: 40405 } 4 | 5 | // Shard IDs 6 | const defaultShardId = 'root' 7 | const testNetShardId = 'testnet6' 8 | const mainNetShardId = '' // not used until HF2 9 | 10 | // Token name 11 | const tokenName = 'REV' 12 | // Number of decimal places for token display (balance, phlo limit, labels) 13 | const defautTokenDecimal = 8 14 | 15 | // Local network 16 | 17 | export const localNet = { 18 | title: 'Local network', 19 | name: 'localnet', 20 | tokenName, 21 | tokenDecimal: defautTokenDecimal, 22 | hosts: [ 23 | { domain: 'localhost', shardId: defaultShardId, ...defaultPorts }, 24 | { domain: 'localhost', shardId: defaultShardId, grpc: 40411, http: 40413, httpAdmin: 40415 }, 25 | { domain: 'localhost', shardId: defaultShardId, grpc: 40421, http: 40423, httpAdmin: 40425 }, 26 | { domain: 'localhost', shardId: defaultShardId, grpc: 40431, http: 40433, httpAdmin: 40435 }, 27 | { domain: 'localhost', shardId: defaultShardId, grpc: 40441, http: 40443, httpAdmin: 40445 }, 28 | { domain: 'localhost', shardId: defaultShardId, grpc: 40451, http: 40453, httpAdmin: 40455 }, 29 | ], 30 | readOnlys: [ 31 | { domain: 'localhost', shardId: defaultShardId, ...defaultPorts }, 32 | { domain: 'localhost', shardId: defaultShardId, grpc: 40411, http: 40413, httpAdmin: 40415 }, 33 | { domain: 'localhost', shardId: defaultShardId, grpc: 40421, http: 40423, httpAdmin: 40425 }, 34 | { domain: 'localhost', shardId: defaultShardId, grpc: 40431, http: 40433, httpAdmin: 40435 }, 35 | { domain: 'localhost', shardId: defaultShardId, grpc: 40441, http: 40443, httpAdmin: 40445 }, 36 | { domain: 'localhost', shardId: defaultShardId, grpc: 40451, http: 40453, httpAdmin: 40455 }, 37 | ] 38 | } 39 | 40 | // Test network 41 | 42 | const range = n => [...Array(n).keys()] 43 | 44 | const getTestNetUrls = n => { 45 | const instance = `node${n}` 46 | return { 47 | domain: `${instance}.testnet.rchain.coop`, 48 | instance, 49 | shardId: testNetShardId, 50 | ...defaultPortsSSL, 51 | } 52 | } 53 | 54 | const testnetHosts = range(5).map(getTestNetUrls) 55 | 56 | export const testNet = { 57 | title: 'RChain testing network', 58 | name: 'testnet', 59 | tokenName, 60 | tokenDecimal: defautTokenDecimal, 61 | hosts: testnetHosts, 62 | readOnlys: [ 63 | { domain: 'observer.testnet.rchain.coop', instance: 'observer', shardId: testNetShardId, ...defaultPortsSSL }, 64 | ], 65 | } 66 | 67 | // MAIN network 68 | 69 | const getMainNetUrls = n => ({ 70 | domain: `node${n}.root-shard.mainnet.rchain.coop`, 71 | shardId: mainNetShardId, 72 | ...defaultPortsSSL, 73 | }) 74 | 75 | const mainnetHosts = range(30).map(getMainNetUrls) 76 | 77 | export const mainNet = { 78 | title: 'RChain MAIN network', 79 | name: 'mainnet', 80 | tokenName, 81 | tokenDecimal: defautTokenDecimal, 82 | hosts: mainnetHosts, 83 | readOnlys: [ 84 | // Load balancer (not gRPC) server for us, asia and eu servers 85 | { domain: 'observer.services.mainnet.rchain.coop', shardId: mainNetShardId, https: 443 }, 86 | { domain: 'observer-us.services.mainnet.rchain.coop', shardId: mainNetShardId, ...defaultPortsSSL }, 87 | { domain: 'observer-asia.services.mainnet.rchain.coop', shardId: mainNetShardId, ...defaultPortsSSL }, 88 | { domain: 'observer-eu.services.mainnet.rchain.coop', shardId: mainNetShardId, ...defaultPortsSSL }, 89 | ], 90 | } 91 | 92 | export const getNodeUrls = ({name, tokenName, tokenDecimal, shardId, domain, grpc, http, https, httpAdmin, httpsAdmin, instance}) => { 93 | const scheme = !!https ? 'https' : !!http ? 'http' : '' 94 | const schemeAdmin = !!httpsAdmin ? 'https' : !!httpAdmin ? 'http' : '' 95 | const httpUrl = !!https || !!http ? `${scheme}://${domain}:${https || http}` : void 8 96 | const httpAdminUrl = !!httpsAdmin || !!httpAdmin ? `${schemeAdmin}://${domain}:${httpsAdmin || httpAdmin}` : void 8 97 | const grpcUrl = !!grpc ? `${domain}:${grpc}` : void 8 98 | 99 | return { 100 | network : name, 101 | tokenName, 102 | tokenDecimal, 103 | shardId, 104 | grpcUrl, 105 | httpUrl, 106 | httpAdminUrl, 107 | statusUrl : `${httpUrl}/api/status`, 108 | getBlocksUrl : `${httpUrl}/api/blocks`, 109 | // Testnet only 110 | logsUrl : instance && `http://${domain}:8181/logs/name:${instance}`, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/web/controls/selector-ctrl.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as R from 'ramda' 3 | import m from 'mithril' 4 | import { getNodeUrls } from '../../rchain-networks' 5 | 6 | export const selectorCtrl = (st, {nets, onDevMode}) => { 7 | const findValidatorByIndex = index => 8 | nets.flatMap(({hosts}) => hosts)[index] 9 | 10 | const findReadOnlyByIndex = (index, netName) => 11 | nets.filter(x => x.name === netName).flatMap(({readOnlys}) => readOnlys)[index] 12 | 13 | const onSelIdx = ev => { 14 | const sel = findValidatorByIndex(ev.target.selectedIndex) 15 | const read = sel.name === valNode.name ? readNode : findReadOnlyByIndex(0, sel.name) 16 | st.set({valNode: sel, readNode: read}) 17 | } 18 | 19 | const onSelReadIdx = ev => { 20 | const sel = findReadOnlyByIndex(ev.target.selectedIndex, valNode.name) 21 | st.set({valNode, readNode: sel}) 22 | } 23 | 24 | const onDevModeInput = ev => { 25 | const checked = ev.target?.checked || false 26 | onDevMode({enabled: checked}) 27 | } 28 | 29 | const getDdlText = ({name, domain, grpc, https, http}) => { 30 | const httpInfo = !!https ? `:${https}` : !!http ? `:${http}` : ' ' 31 | const isLocal = name === 'localnet' 32 | return isLocal ? `${domain}${httpInfo}` : domain 33 | } 34 | 35 | const isEqNode = (v1, v2) => 36 | R.eqBy(({domain, gprc, https, http}) => ({domain, gprc, https, http}), v1, v2) 37 | 38 | // Control state 39 | const {valNode, readNode, devMode} = st.view({}) 40 | 41 | const isLocal = valNode.name === 'localnet' 42 | const isTestnet = valNode.name === 'testnet' 43 | const isMainnet = valNode.name === 'mainnet' 44 | const valUrls = getNodeUrls(valNode) 45 | const readUrls = getNodeUrls(readNode) 46 | // Dev mode tooltip text 47 | const devModeText = `Development mode for locally accessible nodes` 48 | 49 | return m('.ctrl.selector-ctrl', 50 | // Validator selector 51 | m('h2', 'RChain Network selector'), 52 | // Dev mode switch 53 | m('label', {title: devModeText}, 54 | m('input[type=checkbox]', {checked: devMode, oninput: onDevModeInput}), 'dev mode'), 55 | m('h3', `${valNode.title} - validator node`), 56 | m('select', {onchange: onSelIdx}, 57 | nets.map(({title, hosts, name}) => 58 | m(`optgroup.${name}-color`, {label: title}, 59 | hosts.map(({name, domain, grpc, https, http}) => 60 | m(`option`, 61 | {title, selected: isEqNode(valNode, {domain, grpc, https, http})}, 62 | getDdlText({name, domain, grpc, https, http}) 63 | ) 64 | ), 65 | ), 66 | ), 67 | ), 68 | 69 | // Validator info 70 | m(''), 71 | m('span', 'Direct links'), 72 | m('a', {target: '_blank', href: valUrls.statusUrl}, 'status'), 73 | m('a', {target: '_blank', href: valUrls.getBlocksUrl}, 'blocks'), 74 | isTestnet && [ 75 | valUrls.logsUrl && m('a', {target: '_blank', href: valUrls.logsUrl}, 'logs'), 76 | valUrls.filesUrl && m('a', {target: '_blank', href: valUrls.filesUrl}, 'files'), 77 | ], 78 | m('table', 79 | valUrls.grpcUrl && m('tr', m('td', 'gRPC'), m('td', m('pre', valUrls.grpcUrl))), 80 | m('tr', m('td', 'HTTP'), m('td', m('pre', valUrls.httpUrl))), 81 | isLocal && m('tr', m('td', 'Admin'), m('td', m('pre', valUrls.httpAdminUrl))), 82 | ), 83 | isMainnet && [ 84 | m('p.warning', `You are connected to MAIN RChain network. Any deploy will use REAL ${valUrls.tokenName}s.`), 85 | ], 86 | 87 | // Read-only selector 88 | m('h3', `${readNode.title} - read-only node`), 89 | m('select', {onchange: onSelReadIdx}, 90 | nets.filter(x => x.name === valNode.name).map(({title, readOnlys}) => 91 | m('optgroup', {label: title}, 92 | readOnlys.map(({name, domain, grpc, https, http}) => 93 | m('option', 94 | {title, selected: isEqNode(readNode, {domain, grpc, https, http})}, 95 | getDdlText({name, domain, grpc, https, http}) 96 | ) 97 | ), 98 | ), 99 | ), 100 | ), 101 | 102 | // Read-only info 103 | m(''), 104 | m('span', 'Direct links'), 105 | m('a', {target: '_blank', href: readUrls.statusUrl}, 'status'), 106 | m('a', {target: '_blank', href: readUrls.getBlocksUrl}, 'blocks'), 107 | isTestnet && [ 108 | readUrls.logsUrl && m('a', {target: '_blank', href: readUrls.logsUrl}, 'logs'), 109 | readUrls.filesUrl && m('a', {target: '_blank', href: readUrls.filesUrl}, 'files'), 110 | ], 111 | m('table', 112 | readUrls.grpcUrl && m('tr', m('td', 'gRPC'), m('td', m('pre', readUrls.grpcUrl))), 113 | m('tr', m('td', 'HTTP'), m('td', m('pre', readUrls.httpUrl))), 114 | isLocal && m('tr', m('td', 'Admin'), m('td', m('pre', readUrls.httpAdminUrl))), 115 | ), 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RNode JS client examples 2 | 3 | See also recording of code walk-thru sessions: 4 | - [2020\-07\-28 RChain Education](https://youtu.be/5JEtt53EacI?t=1043) 5 | - [2020\-08\-25 RChain Education](https://www.youtube.com/watch?v=2EUd2vOiJX8) 6 | 7 | ## Web (HTTP) 8 | 9 | In the browser connection to RNode can be done with **RNode Web API**. It's also possible to use gRPC with the proxy. 10 | Web API has only defined schema in Scala source, for the new info please check [RChain issue 2974](https://github.com/rchain/rchain/issues/2974). 11 | 12 |
13 | Quick info to run Web example with two nodes 14 | 15 | ```sh 16 | # Run nodes and web page example 17 | npm install && docker-compose up -d && npm run start:web 18 | 19 | # Logs from all nodes 20 | docker-compose logs -f 21 | ``` 22 |
23 | 24 | Web example is published from `gh-pages` branch on this url [https://tgrospic.github.io/rnode-client-js](https://tgrospic.github.io/rnode-client-js). 25 | 26 | ## Nodejs (gRPC) 27 | 28 | This repo contains examples how to use [**@tgrospic/rnode-grpc-js**](https://github.com/tgrospic/rnode-grpc-js) helper library to generate **RNode gRPC API** for **Nodejs**. 29 | 30 | ## Example of RNode connection to Metamask (with hardware wallet) 31 | 32 | RNode has support for Ethereum type of signatures so Metamask can be used for signing deploys e.g. making transfers of REVs. In Web example, button to add selected Metamask account should be visible next to REV import textbox. 33 | 34 | Helper functions are in [eth-wrapper.js](src/eth/eth-wrapper.js) which contains the code for communication with Metamask, getting selected ETH address and sending deploys for signing. 35 | In [eth-sign.js](src/eth/eth-sign.js) are functions to verify deploy signature and to extract public key. 36 | This is all that is needed for communication with Metamask and also for connected hardware wallets (Ledger). How to use these functions and send deploys to RNode is in [rnode-web.js](src/rnode-web.js). 37 | 38 | Changes on the web page are only saved in memory so it will be lost after refreshing the page. 39 | RChain networks available for selection are in [rchain-networks.js](src/rchain-networks.js) file. 40 | 41 | ## Install 42 | 43 | Install project dependencies (in `./node_modules` folder). 44 | 45 | ```sh 46 | # This is enough for HTTP connection to RNode and to run Web example 47 | npm install 48 | ``` 49 | 50 | ### Install (gRPC only) 51 | 52 | Generate JS bindings (default in `./rnode-grpc-gen`). 53 | 54 | ```sh 55 | # Defined as script command in package.json 56 | npm run rnode-generate 57 | 58 | # Or call executable script directly from npm bin folder 59 | # - which is in the PATH when npm scripts are executed 60 | node_modules/.bin/rnode-grpc 61 | ``` 62 | 63 | ## Run **Web example** ([`src/web`](src/web)) 64 | 65 | This will start local Nodejs dev server in watch mode [http://localhost:1234](http://localhost:1234). 66 | 67 | Test page contains a list of nodes to select, check balance, send transfers and deploys. 68 | 69 | ```sh 70 | # Run web example 71 | npm run start:web 72 | ``` 73 | 74 | ## Run **Nodejs example** ([`src/nodejs/client.js`](src/nodejs/client.js)) 75 | 76 | In `src/nodejs/client.js` script is an example of how to connect to RNode from Nodejs. 77 | 78 | ```sh 79 | # Run nodejs example / sample requests to RChain testnet 80 | npm run start:nodejs 81 | ``` 82 | 83 | ## Run RNode with Docker 84 | 85 | In the project is [Docker compose](docker-compose.yml) configuration to run local RChain network. 86 | 87 | ```sh 88 | # Starts validator and read-only RNode in daemon mode 89 | docker-compose up -d 90 | 91 | # Logs from all nodes 92 | docker-compose logs -f 93 | ``` 94 | 95 | ## Build static page (offline mode) 96 | 97 | Web site can be compiled to static page which can be opened with double click on `index.html` in `./dist` directory where the page is built. This is exactly what we need for offline wallet. :) 98 | 99 | Because it's a static page it can be published directly on Github via `gh-pages` branch which contains compiled files from `./dist` directory. It is visible on this url [https://tgrospic.github.io/rnode-client-js](https://tgrospic.github.io/rnode-client-js). If you fork this repo you can do this with you own version of app. 100 | With [GitHub pages action](.github/workflows/github-pages.yml) any commit to _master_ branch will rebuild and publish the page. Locally the page can be generated with _build_ command. 101 | 102 | ```sh 103 | # Compile static web site (to ./dist folder) 104 | npm run build:web 105 | ``` 106 | 107 | ## TypeScript definitions (gRPC API) 108 | 109 | `rnode-grpc-js` library also generates a TypeScript definition file that can be referenced in your code and can provide IntelliSense support in VSCode. 110 | 111 | ```typescript 112 | /// 113 | ``` 114 | 115 | ![](docs/intellisense-vscode.png) 116 | -------------------------------------------------------------------------------- /src/web/controls/address-ctrl.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as R from 'ramda' 3 | import m from 'mithril' 4 | import { 5 | getAddrFromPrivateKey, getAddrFromPublicKey, getAddrFromEth, 6 | newRevAddress, verifyRevAddr, 7 | } from '@tgrospic/rnode-grpc-js' 8 | import { labelStyle } from './common' 9 | import { ethereumAddress, ethDetected } from '../../eth/eth-wrapper' 10 | 11 | export const addressCtrl = (st, {wallet, node, onAddAccount}) => { 12 | const updateAddress = text => { 13 | const val = text.replace(/^0x/, '').trim() 14 | // Account from private key, public key, ETH or REV address 15 | const fromPriv = getAddrFromPrivateKey(val) 16 | const fromPub = getAddrFromPublicKey(val) 17 | const fromEth = getAddrFromEth(val) 18 | const isRev = verifyRevAddr(val) 19 | // Render 20 | if (isRev) { 21 | st.set({text, revAddr: text}) 22 | } else if (!!fromPriv) { 23 | st.set({text, privKey: val, ...fromPriv}) 24 | } else if (!!fromPub) { 25 | st.set({text, pubKey: val, ...fromPub}) 26 | } else if (!!fromEth) { 27 | st.set({text, privKey: '', pubKey: '', ethAddr: val, revAddr: fromEth}) 28 | } else 29 | st.set({text}) 30 | } 31 | 32 | const addAccount = async _ => { 33 | const account = {name, privKey, pubKey, ethAddr, revAddr} 34 | await onAddAccount(account) 35 | clear() 36 | } 37 | 38 | const clear = _ => { 39 | st.set({text: ''}) 40 | } 41 | 42 | const fillMetamaskAccountEv = async _ => { 43 | const ethAddr = await ethereumAddress() 44 | updateAddress(ethAddr) 45 | } 46 | 47 | const addrKeyPressEv = ev => { 48 | const text = ev.target.value 49 | updateAddress(text) 50 | } 51 | 52 | const nameKeyPressEv = ev => { 53 | const nameVal = ev.target.value 54 | st.update(s => ({...s, name: nameVal})) 55 | } 56 | 57 | const newRevAddrEv = _ => { 58 | const {privKey} = newRevAddress() 59 | updateAddress(privKey) 60 | } 61 | 62 | const updateEv = revAddr => _ => { 63 | const acc = wallet.find(R.propEq('revAddr', revAddr)) 64 | st.set(acc) 65 | } 66 | 67 | // Control state 68 | const {text, privKey, pubKey, ethAddr, revAddr, name} = st.view({}) 69 | 70 | const tokenName = node.tokenName 71 | const description = m('span.info', 72 | `Any address used on this page must be first added as an account and assign a name. All accounts are then shown in dropdown menus to select as send or receive address.`, 73 | m('br'), 74 | `Entered information is not stored anywhere except on the page. After exit or refresh the page, all information is lost.` 75 | ) 76 | const labelSource = `${tokenName} address / ETH address / Public key / Private key` 77 | const metamaskTitle = 'Copy ETH address from selected Metamask account' 78 | const newAccountTitle = `Generate new private key (public key, ETH, ${tokenName})` 79 | const saveTitle = 'Save account with assigned name' 80 | const closeTitle = 'Cancel edit of account' 81 | const namePlaceholder = 'Friendly name for account' 82 | const addDisabled = !name || !name.trim() 83 | const isEdit = !!revAddr 84 | 85 | return m('.ctrl.address-ctrl', 86 | m('h2', `${tokenName} wallet (import ${tokenName} address, ETH address, public/private key, Metamask)`), 87 | description, 88 | 89 | // Input textbox 90 | m('', labelStyle(text), labelSource), 91 | m('input[type=text]', { 92 | autocomplete: 'nono', placeholder: labelSource, 93 | value: text, oninput: addrKeyPressEv 94 | }), 95 | 96 | // New accounts 97 | ethDetected && m('button', {title: metamaskTitle, disabled: isEdit, onclick: fillMetamaskAccountEv}, 'Metamask account'), 98 | m('button', {title: newAccountTitle, disabled: isEdit, onclick: newRevAddrEv}, 'New account'), 99 | 100 | // Edit wallet item 101 | isEdit && m('.address-gen', 102 | m('table', 103 | privKey && m('tr', m('td', 'Private key'), m('td', privKey)), 104 | pubKey && m('tr', m('td', 'Public key'), m('td', pubKey)), 105 | ethAddr && m('tr', m('td', 'ETH'), m('td', ethAddr)), 106 | m('tr', m('td', tokenName), m('td', m('b', revAddr))), 107 | ), 108 | // Action buttons 109 | m('input[type=text].addr-name', {placeholder: namePlaceholder, value: name, oninput: nameKeyPressEv}), 110 | m('button.add-account', {title: saveTitle, onclick: addAccount, disabled: addDisabled}, 'Save account'), 111 | m('button', {title: closeTitle, onclick: clear}, 'Close'), 112 | ), 113 | 114 | // Wallet display 115 | wallet && !!wallet.length && m('table.wallet', 116 | m('thead', 117 | m('th', 'Account'), 118 | m('th', tokenName), 119 | m('th', 'ETH'), 120 | m('th', 'PUBLIC'), 121 | m('th', 'PRIVATE'), 122 | ), 123 | wallet.map(({name, privKey = '', pubKey = '', ethAddr = '', revAddr}) => { 124 | const rev = revAddr.slice(0, 10) 125 | const eth = ethAddr.slice(0, 10) 126 | const pub = pubKey.slice(0, 10) 127 | const priv = privKey.slice(0, 5) 128 | return m('tr', 129 | m('td.account', {onclick: updateEv(revAddr)}, name), 130 | m('td', rev), 131 | m('td', eth), 132 | m('td', pub), 133 | m('td', 134 | priv ? m('span', {title: 'Private key saved with this account'}, '✓') : '' 135 | ), 136 | ) 137 | }) 138 | ), 139 | ) 140 | } 141 | -------------------------------------------------------------------------------- /src/web/controls/custom-deploy-ctrl.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import m from 'mithril' 3 | import * as R from 'ramda' 4 | import { labelStyle, showTokenDecimal, labelRev, showNetworkError } from './common' 5 | 6 | const sampleReturnCode = `new return(\`rho:rchain:deployId\`) in { 7 | return!((42, true, "Hello from blockchain!")) 8 | }` 9 | 10 | const sampleInsertToRegistry = `new return(\`rho:rchain:deployId\`), 11 | insertArbitrary(\`rho:registry:insertArbitrary\`) 12 | in { 13 | new uriCh, valueCh in { 14 | insertArbitrary!("My value", *uriCh) | 15 | for (@uri <- uriCh) { 16 | return!(("URI", uri)) 17 | } 18 | } 19 | }` 20 | 21 | const sampleRegistryLookup = `new return(\`rho:rchain:deployId\`), 22 | lookup(\`rho:registry:lookup\`) 23 | in { 24 | new valueCh in { 25 | // Fill in registry URI: \`rho:id:11fhnau8j3...h4459w9bpus6oi\` 26 | lookup!( , *valueCh) | 27 | for (@value <- valueCh) { 28 | return!(("Value from registry", value)) 29 | } 30 | } 31 | }` 32 | 33 | const samples = [ 34 | ['return data', sampleReturnCode], 35 | ['insert to registry', sampleInsertToRegistry], 36 | ['registry lookup', sampleRegistryLookup], 37 | ] 38 | 39 | /** 40 | * @param { import("@tgrospic/rnode-grpc-js").RevAddress[] } wallet 41 | */ 42 | const initSelected = (st, wallet /** @param any[] */) => { 43 | const {selRevAddr, phloLimit = 500e3} = st 44 | 45 | // Pre-select first account if not selected 46 | const initRevAddr = R.isNil(selRevAddr) && !R.isNil(wallet) && !!wallet.length 47 | ? R.head(wallet).revAddr : selRevAddr 48 | 49 | return {...st, selRevAddr: initRevAddr, phloLimit} 50 | } 51 | 52 | export const customDeployCtrl = (st, {wallet = [], node, onSendDeploy, onPropose, warn}) => { 53 | const onSendDeployEv = code => async _ => { 54 | st.update(s => ({...s, status: '...', dataError: ''})) 55 | 56 | const account = R.find(R.propEq('revAddr', selRevAddr), wallet) 57 | const [status, dataError] = await onSendDeploy({code, account, phloLimit}) 58 | .then(x => [x, '']) 59 | .catch(ex => { 60 | warn('DEPLOY ERROR', ex) 61 | return ['', ex.message] 62 | }) 63 | 64 | st.update(s => ({...s, status, dataError})) 65 | } 66 | 67 | const onProposeEv = async _ => { 68 | st.update(s => ({...s, proposeStatus: '...', proposeError: ''})) 69 | 70 | const [proposeStatus, proposeError] = await onPropose(node) 71 | .then(x => [x, '']) 72 | .catch(ex => ['', ex.message]) 73 | 74 | st.update(s => ({...s, proposeStatus, proposeError})) 75 | } 76 | 77 | const accountChangeEv = ev => { 78 | const { revAddr } = wallet[ev.target.selectedIndex] 79 | st.update(s => ({...s, selRevAddr: revAddr})) 80 | } 81 | 82 | const updateCodeEv = code => _ => { 83 | st.update(s => ({...s, code})) 84 | } 85 | 86 | // Field update by name 87 | const valEv = name => ev => { 88 | const val = ev.target.value 89 | st.update(s => ({...s, [name]: val})) 90 | } 91 | 92 | // Control state 93 | const {selRevAddr, code, phloLimit, status, dataError, proposeStatus, proposeError} 94 | = initSelected(st.view({}), wallet) 95 | 96 | const tokenName = node.tokenName 97 | const labelAddr = 'Signing account' 98 | const labelCode = 'Rholang code' 99 | const labelPhloLimit = `Phlo limit (in tiny ${tokenName} x10^${node.tokenDecimal})` 100 | const isWalletEmpty = R.isNil(wallet) || R.isEmpty(wallet) 101 | const showPropose = node.network === 'localnet' 102 | const canDeploy = (code || '').trim() !== '' && !!selRevAddr 103 | const phloLimitPreview = showTokenDecimal(phloLimit, node.tokenDecimal) 104 | 105 | return m('.ctrl.custom-deploy-ctrl', 106 | m('h2', 'Custom deploy'), 107 | isWalletEmpty ? m('b', `${node.tokenName} wallet is empty, add accounts to make deploys.`) : [ 108 | m('span', 'Send deploy to selected validator RNode.'), 109 | 110 | // Rholang examples 111 | m('', 112 | m('span', 'Sample code: '), 113 | samples.map(([title, code]) => 114 | m('a', {onclick: updateCodeEv(code), href: '#'}, title), 115 | ) 116 | ), 117 | 118 | // REV address dropdown 119 | m('', labelStyle(!!selRevAddr), labelAddr), 120 | m('select', {onchange: accountChangeEv}, 121 | wallet.map(({name, revAddr}) => 122 | m('option', `${name}: ${revAddr}`) 123 | ), 124 | ), 125 | 126 | // Rholang code (editor) 127 | m('', labelStyle(code), labelCode), 128 | m('textarea.deploy-code', {value: code, rows: 13, placeholder: 'Rholang code', oninput: valEv('code')}), 129 | 130 | // Phlo limit 131 | m('', labelStyle(true), labelPhloLimit), 132 | m('input[type=number].phlo-limit', { 133 | value: phloLimit, placeholder: labelPhloLimit, oninput: valEv('phloLimit') 134 | }), 135 | labelRev(phloLimitPreview, tokenName), 136 | 137 | // Action buttons / results 138 | m(''), 139 | m('button', {onclick: onSendDeployEv(code), disabled: !canDeploy}, 'Deploy Rholang code'), 140 | status && m('b', status), 141 | dataError && m('b.warning', showNetworkError(dataError)), 142 | 143 | m(''), 144 | showPropose && m('button', {onclick: onProposeEv}, 'Propose'), 145 | showPropose && proposeStatus && m('b', proposeStatus), 146 | showPropose && proposeError && m('b.warning', showNetworkError(proposeError)), 147 | ] 148 | ) 149 | } 150 | -------------------------------------------------------------------------------- /src/web/rnode-actions.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as R from 'ramda' 3 | import { checkBalance_rho } from '../rho/check-balance' 4 | import { transferFunds_rho } from '../rho/transfer-funds' 5 | 6 | export const makeRNodeActions = (rnodeWeb, {log, warn}) => { 7 | const { rnodeHttp, sendDeploy, getDataForDeploy, propose } = rnodeWeb 8 | 9 | // App actions to process communication with RNode 10 | return { 11 | appCheckBalance: appCheckBalance({rnodeHttp}), 12 | appTransfer : appTransfer({sendDeploy, getDataForDeploy, propose, log, warn}), 13 | appSendDeploy : appSendDeploy({sendDeploy, getDataForDeploy, log}), 14 | appPropose : appPropose({propose, log}), 15 | } 16 | } 17 | 18 | const appCheckBalance = ({rnodeHttp}) => async ({node, revAddr}) => { 19 | const deployCode = checkBalance_rho(revAddr) 20 | const {expr: [e]} = await rnodeHttp(node.httpUrl, 'explore-deploy', deployCode) 21 | const dataBal = e && e.ExprInt && e.ExprInt.data 22 | const dataError = e && e.ExprString && e.ExprString.data 23 | return [dataBal, dataError] 24 | } 25 | 26 | const appTransfer = effects => async ({node, fromAccount, toAccount, amount, setStatus}) => { 27 | const {sendDeploy, getDataForDeploy, propose, log, warn} = effects 28 | 29 | log('TRANSFER', {amount, from: fromAccount.name, to: toAccount.name, shardId: node.shardId, node: node.httpUrl}) 30 | 31 | setStatus(`Deploying ...`) 32 | 33 | // Send deploy 34 | const code = transferFunds_rho(fromAccount.revAddr, toAccount.revAddr, amount) 35 | const {signature} = await sendDeploy(node, fromAccount, code) 36 | log('DEPLOY ID (signature)', signature) 37 | 38 | if (node.network === 'localnet') { 39 | // Propose on local network, don't wait for result 40 | propose(node).catch(ex => warn(ex)) 41 | } 42 | 43 | // Progress dots 44 | const mkProgress = i => () => { 45 | i = i > 60 ? 0 : i + 3 46 | return `Checking result ${R.repeat('.', i).join('')}` 47 | } 48 | const progressStep = mkProgress(0) 49 | const updateProgress = _ => setStatus(progressStep()) 50 | updateProgress() 51 | 52 | // Try to get result from next proposed block 53 | const {data, cost} = await getDataForDeploy(node, signature, updateProgress) 54 | // Extract data from response object 55 | const args = data ? rhoExprToJS(data.expr) : void 0 56 | const costTxt = R.isNil(cost) ? 'failed to retrive' : cost 57 | const [success, message] = args || [false, 'deploy found in the block but failed to get confirmation data'] 58 | 59 | if (!success) throw Error(`Transfer error: ${message}. // cost: ${costTxt}`) 60 | return `✓ ${message} // cost: ${costTxt}` 61 | } 62 | 63 | const appSendDeploy = effects => async ({node, code, account, phloLimit, setStatus}) => { 64 | const {sendDeploy, getDataForDeploy, log} = effects 65 | 66 | log('SENDING DEPLOY', {account: account.name, phloLimit, shardId: node.shardId, node: node.httpUrl, code}) 67 | 68 | setStatus(`Deploying ...`) 69 | 70 | const {signature} = await sendDeploy(node, account, code, phloLimit) 71 | log('DEPLOY ID (signature)', signature) 72 | 73 | // Progress dots 74 | const mkProgress = i => () => { 75 | i = i > 60 ? 0 : i + 3 76 | return `Checking result ${R.repeat('.', i).join('')}` 77 | } 78 | const progressStep = mkProgress(0) 79 | const updateProgress = _ => setStatus(progressStep()) 80 | updateProgress() 81 | 82 | // Try to get result from next proposed block 83 | const {data, cost} = await getDataForDeploy(node, signature, updateProgress) 84 | // Extract data from response object 85 | const args = data ? rhoExprToJS(data.expr) : void 0 86 | 87 | log('DEPLOY RETURN DATA', {args, cost, rawData: data}) 88 | 89 | const costTxt = R.isNil(cost) ? 'failed to retrive' : cost 90 | const [success, message] = R.isNil(args) 91 | ? [false, 'deploy found in the block but data is not sent on `rho:rchain:deployId` channel'] 92 | : [true, R.is(Array, args) ? args.join(', ') : args] 93 | 94 | if (!success) throw Error(`Deploy error: ${message}. // cost: ${costTxt}`) 95 | return `✓ (${message}) // cost: ${costTxt}` 96 | } 97 | 98 | const appPropose = ({propose, log}) => async ({httpAdminUrl}) => { 99 | const resp = await propose({httpAdminUrl}) 100 | 101 | log('Propose result', resp) 102 | 103 | return resp 104 | } 105 | 106 | // Converts RhoExpr response from RNode WebAPI 107 | // https://github.com/rchain/rchain/blob/b7331ae05/node/src/main/scala/coop/rchain/node/api/WebApi.scala#L128-L147 108 | // - return!("One argument") // monadic 109 | // - return!((true, A, B)) // monadic as tuple 110 | // - return!(true, A, B) // polyadic 111 | // new return(`rho:rchain:deployId`) in { 112 | // return!((true, "Hello from blockchain!")) 113 | // } 114 | // TODO: make it stack safe 115 | const rhoExprToJS = input => { 116 | const loop = rhoExpr => convert(rhoExpr)(converters) 117 | const converters = R.toPairs(converterMapping(loop)) 118 | return loop(input) 119 | } 120 | 121 | const convert = rhoExpr => R.pipe( 122 | R.map(matchTypeConverter(rhoExpr)), 123 | R.find(x => !R.isNil(x)), 124 | // Return the whole object if unknown type 125 | x => R.isNil(x) ? [R.identity, rhoExpr] : x, 126 | ([f, d]) => f(d) 127 | ) 128 | 129 | const matchTypeConverter = rhoExpr => ([type, f]) => { 130 | const d = R.path([type, 'data'], rhoExpr) 131 | return R.isNil(d) ? void 666 : [f, d] 132 | } 133 | 134 | const converterMapping = loop => ({ 135 | "ExprInt": R.identity, 136 | "ExprBool": R.identity, 137 | "ExprString": R.identity, 138 | "ExprBytes": R.identity, 139 | "ExprUri": R.identity, 140 | "UnforgDeploy": R.identity, 141 | "UnforgDeployer": R.identity, 142 | "UnforgPrivate": R.identity, 143 | "ExprUnforg": loop, 144 | "ExprPar": R.map(loop), 145 | "ExprTuple": R.map(loop), 146 | "ExprList": R.map(loop), 147 | "ExprSet": R.map(loop), 148 | "ExprMap": R.mapObjIndexed(loop), 149 | }) 150 | -------------------------------------------------------------------------------- /src/nodejs/client-insert-signed.js: -------------------------------------------------------------------------------- 1 | // Reference to TypeScript definitions for IntelliSense in VSCode 2 | /// 3 | // @ts-check 4 | const grpc = require('@grpc/grpc-js') 5 | const { ec } = require('elliptic') 6 | const { blake2bHex } = require('blakejs') 7 | 8 | const { rnodeService, rnodeProtobuf, signDeploy, verifyDeploy } = require('@tgrospic/rnode-grpc-js') 9 | 10 | const protoSchema = require('../../rnode-grpc-gen/js/pbjs_generated.json') 11 | require('../../rnode-grpc-gen/js/DeployServiceV1_pb') 12 | require('../../rnode-grpc-gen/js/ProposeServiceV1_pb') 13 | 14 | const { log, warn } = console 15 | 16 | const rnodeUrl = 'localhost:40402' 17 | // const rnodeUrl = 'node3.testnet.rchain.coop:40401' 18 | 19 | const secp256k1 = new ec('secp256k1') 20 | 21 | const rnodeExample = async args => { 22 | // RNode client options 23 | const options = host => ({ grpcLib: grpc, host, protoSchema }) 24 | // RNode API methods 25 | const { getBlocks, previewPrivateNames, doDeploy, propose } = rnodeService(options(rnodeUrl)) 26 | 27 | // Protobuf serializer for Par (Rholang AST). 28 | const { Par } = rnodeProtobuf({ protoSchema }) 29 | 30 | /* 31 | * NOTE: This example is only valid until RNode v0.12.x version. 32 | * ============================================================= 33 | * 34 | * To insert data to the registry, with the signature, tuple `(nonce, data)` 35 | * must be signed by the client. 36 | * 37 | * - `nonce`: number increasing with each insert 38 | * - `data` : data we wish to store in the registry 39 | * (unforgeable name when storing a contract) 40 | * 41 | * Signing this tuple on the client side gives a proof to Rholang that owner 42 | * of the private/public key has permission to insert/update data associated for this key, 43 | * from which registry address is generated. 44 | * 45 | * So in Rholang we supply this information to `rho:registry:insertSigned:secp256k1`. 46 | * 47 | * - public key (used for checking the signature) 48 | * - `(nonce, data)` 49 | * - signature of `(nonce, data)` 50 | * 51 | * When storing a contract in the registry we first need to generate the same unforgeable name 52 | * which Rholang will generate when deploying the contract. For this we need to call RNode with 53 | * _timestamp_ and _public key_ used later in deploy data. 54 | * 55 | * Note: we also need timestamp to create deploy, so in this example 56 | * the same number is used for both purposes. 57 | */ 58 | 59 | // Private/public key used to sign a deploy. 60 | const deployPrivateKey = secp256k1.keyFromPrivate('') 61 | const deployPublicKey = Uint8Array.from(deployPrivateKey.getPublic('array')) 62 | 63 | // Timestamp used in deploy data. 64 | const timestamp = Date.now() 65 | 66 | // Unforgeable name on which contract in the registry will be registered. 67 | const { payload: { idsList: [ unforgName ] } } = await previewPrivateNames({ 68 | timestamp, user: deployPublicKey, nameqty: 1 69 | }) 70 | 71 | // Build a tuple with Rholang AST (source for signing). 72 | // (nonce, unforgName) 73 | const nonce = timestamp // increase nonce with each insert/update 74 | const dataToSign = Par.serialize({ 75 | exprsList: [{ 76 | eTupleBody: { 77 | psList: [{ 78 | // `nonce`: number increasing with each insert 79 | exprsList: [{ gInt: nonce }] 80 | }, { 81 | // `data` : channel (unforgeable name) of the contract 82 | bundlesList: [{ 83 | body: { 84 | unforgeablesList: [{ 85 | gPrivateBody: { id: unforgName } 86 | }] 87 | }, 88 | // Unforgeable name is write only `bundle+{*unforgName}` 89 | writeflag: true, 90 | readflag: false 91 | }] 92 | // Instead of unforgeable name we can store any process. 93 | // exprsList: [{ gString: "My data in registry!" }] 94 | }] 95 | } 96 | }] 97 | }) 98 | 99 | // Private/public key used for signing registry access (it can be the same as deploy key). 100 | const privateKey = secp256k1.keyFromPrivate('') 101 | const publicKeyHex = privateKey.getPublic('hex') 102 | // log("PRIV KEY", secp256k1.genKeyPair().getPrivate('hex')) // generate new key 103 | 104 | // Sign `(nonce, unforgName)` 105 | const hashed = blake2bHex(dataToSign, void 666, 32) 106 | const signatureHex = privateKey.sign(hashed, {canonical: true}).toDER('hex') 107 | 108 | const contract = ` 109 | new MyContract, rs(\`rho:registry:insertSigned:secp256k1\`), uriOut, out(\`rho:io:stdout\`) 110 | in { 111 | contract MyContract(ret) = { 112 | ret!("Hello Arthur!") 113 | } | 114 | 115 | rs!( 116 | "${publicKeyHex}".hexToBytes(), 117 | (${nonce}, bundle+{*MyContract}), 118 | "${signatureHex}".hexToBytes(), 119 | *uriOut 120 | ) | 121 | 122 | for(@uri <- uriOut) { 123 | out!(("Registered", uri)) 124 | } 125 | } 126 | ` 127 | // Get latest block number from RNode 128 | const [ block ] = await getBlocks({ depth: 1 }) 129 | const lastBlockNr = block.blockinfo.blocknumber 130 | 131 | const deployData = { 132 | term: contract, 133 | timestamp, 134 | phloprice: 1, 135 | phlolimit: 150e3, 136 | validafterblocknumber: lastBlockNr, 137 | shardid: 'root', 138 | } 139 | 140 | const deploy = signDeploy(deployPrivateKey, deployData) 141 | log('SIGNED DEPLOY', deploy) 142 | 143 | const isValidDeploy = verifyDeploy(deploy) 144 | log('DEPLOY IS VALID', isValidDeploy) 145 | 146 | const { result } = await doDeploy(deploy) 147 | log('DEPLOY RESPONSE', result) 148 | 149 | if (rnodeUrl.match(/localhost/)) { 150 | const { result: proposeRes } = await propose() 151 | log('PROPOSE RESPONSE', proposeRes) 152 | } 153 | 154 | } 155 | 156 | rnodeExample(process.argv) 157 | -------------------------------------------------------------------------------- /src/web/controls/main-ctrl.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as R from 'ramda' 3 | import m from 'mithril' 4 | import { localNet, testNet, mainNet, getNodeUrls } from '../../rchain-networks' 5 | import { ethDetected } from '../../eth/eth-wrapper' 6 | import { makeRenderer } from './common' 7 | 8 | // Controls 9 | import { selectorCtrl } from './selector-ctrl' 10 | import { addressCtrl } from './address-ctrl' 11 | import { balanceCtrl } from './balance-ctrl' 12 | import { transferCtrl } from './transfer-ctrl' 13 | import { customDeployCtrl } from './custom-deploy-ctrl' 14 | import { newRevAddress } from '@tgrospic/rnode-grpc-js' 15 | 16 | /* 17 | This will display the test page to select local, testnet, and mainnet validators 18 | and make REV transfers and check balance. 19 | */ 20 | 21 | const repoUrl = 'https://github.com/tgrospic/rnode-client-js' 22 | 23 | const mainCtrl = (st, effects) => { 24 | const { appCheckBalance, appTransfer, appSendDeploy, appPropose, log, warn } = effects 25 | 26 | const onCheckBalance = node => revAddr => appCheckBalance({node, revAddr}) 27 | 28 | const onTransfer = (node, setStatus) => ({fromAccount, toAccount, amount}) => 29 | appTransfer({node, fromAccount, toAccount, amount, setStatus}) 30 | 31 | const onSendDeploy = (node, setStatus) => ({code, account, phloLimit}) => 32 | appSendDeploy({node, code, account, phloLimit, setStatus}) 33 | 34 | const onPropose = node => () => appPropose(node) 35 | 36 | const appendUpdateLens = pred => R.lens(R.find(pred), (x, xs) => { 37 | const idx = R.findIndex(pred, xs) 38 | // @ts-ignore - `R.update` types doesn't have defined curried variant 39 | const apply = idx === -1 ? R.append : R.update(idx) 40 | // @ts-ignore 41 | return apply(x, xs) 42 | }) 43 | 44 | const onSaveAccount = account => 45 | st.o('wallet') 46 | .o(appendUpdateLens(R.propEq('revAddr', account.revAddr))) 47 | .set(account) 48 | 49 | // State lenses for each control 50 | const selSt = st.o('sel') 51 | const addressSt = st.o('address') 52 | const balanceSt = st.o('balance') 53 | const transferSt = st.o('transfer') 54 | const customDeploySt = st.o('customDeploy') 55 | 56 | const {nets, netsDev, netsTestMain, sel, wallet, devMode} = st.view() 57 | const valNodeUrls = getNodeUrls(sel.valNode) 58 | const readNodeUrls = getNodeUrls(sel.readNode) 59 | 60 | const setTransferStatus = transferSt.o('status').set 61 | const setDeployStatus = customDeploySt.o('status').set 62 | 63 | const onDevMode = ({enabled}) => { 64 | // Show local network in dev mode 65 | const nets = enabled ? netsDev : netsTestMain 66 | const net = nets[0] 67 | const sel = {valNode: net.hosts[0], readNode: net.readOnlys[0]} 68 | st.update(s => ({...s, nets, sel, devMode: enabled})) 69 | } 70 | 71 | // TEMP: Hard Fork 1 info 72 | const startMs = new Date('2021-07-18 15:00').getTime() 73 | const endMs = new Date('2021-07-25 01:00').getTime() 74 | const nowMs = Date.now() 75 | const leftMs = endMs - nowMs 76 | const hfMsgOn = leftMs > 0 77 | // Make smaller with time 78 | const pos = leftMs / (endMs - startMs) 79 | const zoom = (1 - .5) * pos + .5 80 | // TEMP: Hard Fork 1 info 81 | 82 | // App render 83 | return m(`.${sel.valNode.name}`, 84 | m('.ctrl', 85 | 'Demo client for RNode ', 86 | m('a', {href: repoUrl, target: '_blank'}, repoUrl), 87 | m('h1', 'RNode client testing page'), 88 | ), 89 | 90 | hfMsgOn && m('.hf-info', { style: `zoom: ${zoom}` }, 91 | m.trust('Main net is back online after the Hard Fork 1. '), 92 | m.trust('See
RCHIP#42 for more info.'), 93 | ), 94 | 95 | // Selector control 96 | m('hr'), 97 | selectorCtrl(selSt, {nets, onDevMode}), 98 | 99 | // REV wallet control 100 | addressCtrl(addressSt, {wallet, node: valNodeUrls, onAddAccount: onSaveAccount}), 101 | 102 | // Check balance control 103 | balanceCtrl(balanceSt, {wallet, node: valNodeUrls, onCheckBalance: onCheckBalance(readNodeUrls)}), 104 | m('hr'), 105 | 106 | // Transfer REV control 107 | transferCtrl(transferSt, { 108 | wallet, node: valNodeUrls, onTransfer: onTransfer(valNodeUrls, setTransferStatus), warn, 109 | }), 110 | 111 | // Custom deploy control 112 | m('hr'), 113 | customDeployCtrl(customDeploySt, { 114 | wallet, node: valNodeUrls, 115 | onSendDeploy: onSendDeploy(valNodeUrls, setDeployStatus), 116 | onPropose: onPropose(valNodeUrls), 117 | warn, 118 | }), 119 | ) 120 | } 121 | 122 | // Initialize all networks for display in UI 123 | const prepareNets = nets => 124 | nets.map(({title, name, tokenName, tokenDecimal: tokenDecimal, hosts, readOnlys}) => ({ 125 | title, name, tokenName, 126 | hosts: hosts.map(x => ({...x, title, name, tokenName, tokenDecimal})), 127 | readOnlys: readOnlys.map(x => ({...x, title, name})), 128 | })) 129 | 130 | const devMode = false 131 | const netsDev = prepareNets([localNet]) 132 | const netsTestMain = prepareNets([testNet, mainNet]) 133 | const nets = devMode ? netsDev : netsTestMain 134 | const initNet = nets[0] 135 | 136 | // Initial application state 137 | const initialState = { 138 | // All networks 139 | netsDev, 140 | netsTestMain, 141 | // Validators to choose 142 | nets, 143 | // Selected validator 144 | sel: { valNode: initNet.hosts[0], readNode: initNet.readOnlys[0] }, 145 | // Initial wallet 146 | wallet: [], // [{name: 'My REV account', ...newRevAddress()}], 147 | // Dev mode (show local networks) 148 | devMode, 149 | } 150 | 151 | export const startApp = (element, effects) => { 152 | const { warn } = effects 153 | 154 | // App renderer / creates state cell that is passed to controls 155 | const r = makeRenderer(element, mainCtrl) 156 | 157 | // Start app / the big bang! 158 | r(initialState, effects) 159 | 160 | warn('ETH detected', ethDetected) 161 | } 162 | -------------------------------------------------------------------------------- /src/rnode-web.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as R from 'ramda' 3 | import { ec } from 'elliptic' 4 | 5 | import { encodeBase16, decodeBase16 } from './lib.js' 6 | import { verifyDeployEth, recoverPublicKeyEth } from './eth/eth-sign.js' 7 | import { ethDetected, ethereumAddress, ethereumSign } from './eth/eth-wrapper.js' 8 | import { signDeploy, verifyDeploy, deployDataProtobufSerialize } from './rnode-sign' 9 | 10 | export const makeRNodeWeb = effects => { 11 | // Dependency DOM fetch function 12 | const { fetch } = effects 13 | 14 | // Basic wrapper around DOM `fetch` method 15 | const rnodeHttp = makeRNodeHttpInternal(fetch) 16 | 17 | // RNode HTTP API methods 18 | return { 19 | rnodeHttp, 20 | sendDeploy : sendDeploy(rnodeHttp), 21 | getDataForDeploy: getDataForDeploy(rnodeHttp), 22 | propose : propose(rnodeHttp), 23 | } 24 | } 25 | 26 | // Helper function to create JSON request to RNode Web API 27 | const makeRNodeHttpInternal = domFetch => async (httpUrl, apiMethod, data) => { 28 | // Prepare fetch options 29 | const postMethods = ['prepare-deploy', 'deploy', 'data-at-name', 'explore-deploy', 'propose'] 30 | const isPost = !!data && R.includes(apiMethod, postMethods) 31 | const httpMethod = isPost ? 'POST' : 'GET' 32 | const url = method => `${httpUrl}/api/${method}` 33 | const body = typeof data === 'string' ? data : JSON.stringify(data) 34 | // Make JSON request 35 | const opt = { method: httpMethod, body } 36 | const resp = await domFetch(url(apiMethod), opt) 37 | const result = await resp.json() 38 | // Add status if server error 39 | if (!resp.ok) { 40 | const ex = Error(result); 41 | // @ts-ignore 42 | ex.status = resp.status 43 | throw ex 44 | } 45 | 46 | return result 47 | } 48 | 49 | // Creates deploy, signing and sending to RNode 50 | const sendDeploy = rnodeHttp => async (node, account, code, phloLimit) => { 51 | // Check if deploy can be signed 52 | if (!account.privKey) { 53 | const ethAddr = account.ethAddr 54 | if (ethDetected && !!ethAddr) { 55 | // If Metamask is detected check ETH address 56 | const ethAddr = await ethereumAddress() 57 | if (ethAddr.replace(/^0x/, '') !== account.ethAddr) 58 | throw Error('Selected account is not the same as Metamask account.') 59 | } else { 60 | throw Error(`Selected account doesn't have private key and cannot be used for signing.`) 61 | } 62 | } 63 | 64 | // Get the latest block number 65 | const [{ blockNumber }] = await rnodeHttp(node.httpUrl, 'blocks/1') 66 | 67 | // Create a deploy 68 | const phloLimitNum = !!phloLimit || phloLimit == 0 ? phloLimit : 500e3 69 | const deployData = { 70 | term: code, 71 | phloLimit: phloLimitNum, phloPrice: 1, 72 | validAfterBlockNumber: blockNumber, 73 | timestamp: Date.now(), 74 | shardId: node.shardId, 75 | } 76 | 77 | const deploy = !!account.privKey 78 | ? signPrivKey(deployData, account.privKey) 79 | : await signMetamask(deployData) 80 | 81 | // Send deploy / result is deploy signature (ID) 82 | await rnodeHttp(node.httpUrl, 'deploy', deploy) 83 | 84 | return deploy 85 | } 86 | 87 | // Singleton timeout handle to ensure only one execution 88 | let GET_DATA_TIMEOUT_HANDLE 89 | 90 | // Listen for data on `deploy signature` 91 | const getDataForDeploy = rnodeHttp => async ({httpUrl}, deployId, onProgress) => { 92 | GET_DATA_TIMEOUT_HANDLE && clearTimeout(GET_DATA_TIMEOUT_HANDLE) 93 | 94 | const getData = (resolve, reject) => async () => { 95 | const getDataUnsafe = async () => { 96 | // Fetch deploy by signature (deployId) 97 | const deploy = await fetchDeploy(rnodeHttp)({httpUrl}, deployId) 98 | if (deploy) { 99 | // Deploy found (added to a block) 100 | const args = { depth: 1, name: { UnforgDeploy: { data: deployId } } } 101 | // Request for data at deploy signature (deployId) 102 | const { exprs } = await rnodeHttp(httpUrl, 'data-at-name', args) 103 | // Extract cost from deploy info 104 | const { cost } = deploy 105 | // Check deploy errors 106 | const {errored, systemDeployError} = deploy 107 | if (errored) { 108 | throw Error(`Deploy error when executing Rholang code.`) 109 | } else if (!!systemDeployError) { 110 | throw Error(`${systemDeployError} (system error).`) 111 | } 112 | // Return data with cost (assumes data in one block) 113 | resolve({data: exprs[0], cost}) 114 | } else { 115 | // Retry 116 | const cancel = await onProgress() 117 | if (!cancel) { 118 | GET_DATA_TIMEOUT_HANDLE && clearTimeout(GET_DATA_TIMEOUT_HANDLE) 119 | GET_DATA_TIMEOUT_HANDLE = setTimeout(getData(resolve, reject), 7500) 120 | } 121 | } 122 | } 123 | try { await getDataUnsafe() } 124 | catch (ex) { reject(ex) } 125 | } 126 | return await new Promise((resolve, reject) => { 127 | getData(resolve, reject)() 128 | }) 129 | } 130 | 131 | const fetchDeploy = rnodeHttp => async ({httpUrl}, deployId) => { 132 | // Request a block with the deploy 133 | const block = await rnodeHttp(httpUrl, `deploy/${deployId}`) 134 | .catch(ex => { 135 | // Handle response code 400 / deploy not found 136 | if (ex.status !== 400) throw ex 137 | }) 138 | if (block) { 139 | const {deploys} = await rnodeHttp(httpUrl, `block/${block.blockHash}`) 140 | const deploy = deploys.find(({sig}) => sig === deployId) 141 | if (!deploy) // This should not be possible if block is returned 142 | throw Error(`Deploy is not found in the block (${block.blockHash}).`) 143 | // Return deploy 144 | return deploy 145 | } 146 | } 147 | 148 | // Helper function to propose via HTTP 149 | const propose = rnodeHttp => ({httpAdminUrl}) => rnodeHttp(httpAdminUrl, 'propose', {}) 150 | 151 | // Creates deploy signature with Metamask 152 | const signMetamask = async deployData => { 153 | // Serialize and sign with Metamask extension 154 | // - this will open a popup for user to confirm/review 155 | const data = deployDataProtobufSerialize(deployData) 156 | const ethAddr = await ethereumAddress() 157 | const sigHex = await ethereumSign(data, ethAddr) 158 | // Extract public key from signed message and signature 159 | const pubKeyHex = recoverPublicKeyEth(data, sigHex) 160 | // Create deploy object for signature verification 161 | const deploy = { 162 | ...deployData, 163 | sig: decodeBase16(sigHex), 164 | deployer: decodeBase16(pubKeyHex), 165 | sigAlgorithm: 'secp256k1:eth', 166 | } 167 | // Verify signature signed with Metamask 168 | const isValidDeploy = verifyDeployEth(deploy) 169 | if (!isValidDeploy) throw Error('Metamask signature verification failed.') 170 | 171 | return toWebDeploy(deploy) 172 | } 173 | 174 | // Creates deploy signature with plain private key 175 | const signPrivKey = (deployData, privateKey) => { 176 | // Create signing key 177 | const secp256k1 = new ec('secp256k1') 178 | const key = secp256k1.keyFromPrivate(privateKey) 179 | const deploy = signDeploy(key, deployData) 180 | // Verify deploy signature 181 | const isValidDeploy = verifyDeploy(deploy) 182 | if (!isValidDeploy) throw Error('Deploy signature verification failed.') 183 | 184 | return toWebDeploy(deploy) 185 | } 186 | 187 | // Converts JS object from protobuf spec. to Web API spec. 188 | const toWebDeploy = deployData => { 189 | const { 190 | term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId, 191 | deployer, sig, sigAlgorithm, 192 | } = deployData 193 | 194 | const result = { 195 | data: { term, timestamp, phloPrice, phloLimit, validAfterBlockNumber, shardId }, 196 | sigAlgorithm, 197 | signature: encodeBase16(sig), 198 | deployer: encodeBase16(deployer), 199 | } 200 | return result 201 | } 202 | --------------------------------------------------------------------------------