├── .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 | 
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 |
--------------------------------------------------------------------------------