From b4252fa9d37fac48ed19eea12425fd509d3a36f1 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 12 Jun 2019 01:01:48 +0100 Subject: [PATCH] ZcashClient.sync() Currently synchronous, but that's fine for a demo. --- README.md | 21 ++++++- demo-www/index.html | 1 + demo-www/index.js | 16 +++++- envoy/envoy.Dockerfile | 3 + envoy/envoy.yaml | 45 +++++++++++++++ zcash-client-sdk-js/src/index.js | 99 +++++++++++++++++++++++++++++++- 6 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 envoy/envoy.Dockerfile create mode 100644 envoy/envoy.yaml diff --git a/README.md b/README.md index 61702b2..f40dd3b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,26 @@ $ ./build.sh ``` -## Running +## Running the backend + +Web browsers currently cannot talk directly to gRPC servers, so it is necessary to run a +proxy as part of the backend. The `envoy/` subdirectory contains a Dockerfile and config +file for an Envoy proxy that listens on `localhost:8081` and will route requests to a +`lightwalletd` frontend listening on `localhost:9067`. + +See [the `lightwalletd` documentation](https://github.com/zcash-hackworks/lightwalletd) +for details on how to set up a local `lightwalletd` testnet instance. Note that when +starting the frontend, you may need to use `--bind-addr 0.0.0.0:9067` so that the Docker +container can access it. + +To build and run the Envoy proxy: + +```sh +$ docker build -t lightwalletd/envoy -f envoy/envoy.Dockerfile envoy +$ docker run -d -p 8081:8081 --network=host lightwalletd/envoy +``` + +## Running the demo ```sh $ cd demo-www diff --git a/demo-www/index.html b/demo-www/index.html index d88c5d5..79fe9df 100644 --- a/demo-www/index.html +++ b/demo-www/index.html @@ -15,6 +15,7 @@

That's your Zcash address!

You have no TAZ. Go here to get some!

+

Syncing...

diff --git a/demo-www/index.js b/demo-www/index.js index af97a11..f428828 100644 --- a/demo-www/index.js +++ b/demo-www/index.js @@ -3,8 +3,9 @@ import { ZcashClient } from 'zcash-client-sdk' const address = document.getElementById('zcash-client-address') const balance = document.getElementById('zcash-client-balance') const noBalance = document.getElementById('zcash-client-no-balance') +const syncStatus = document.getElementById('zcash-client-sync-status') -var zcashClient = new ZcashClient({ +var zcashClient = new ZcashClient('http://localhost:8081', { setAddress: (newAddress) => { address.textContent = newAddress }, @@ -15,11 +16,24 @@ var zcashClient = new ZcashClient({ } else { noBalance.style.display = '' } + }, + updateSyncStatus: (syncedHeight, latestHeight) => { + if (syncedHeight === latestHeight) { + syncStatus.textContent = `Synced! Latest height: ${latestHeight}` + } else { + syncStatus.textContent = `Syncing (${syncedHeight} / ${latestHeight})...` + } } +}, { + height: 500000, + hash: '004fada8d4dbc5e80b13522d2c6bd0116113c9b7197f0c6be69bc7a62f2824cd', + sapling_tree: '01b733e839b5f844287a6a491409a991ec70277f39a50c99163ed378d23a829a0700100001916db36dfb9a0cf26115ed050b264546c0fa23459433c31fd72f63d188202f2400011f5f4e3bd18da479f48d674dbab64454f6995b113fa21c9d8853a9e764fb3e1f01df9d2c233ca60360e3c2bb73caf5839a1be634c8b99aea22d02abda2e747d9100001970d41722c078288101acd0a75612acfb4c434f2a55aab09fb4e812accc2ba7301485150f0deac7774dcd0fe32043bde9ba2b6bbfff787ad074339af68e88ee70101601324f1421e00a43ef57f197faf385ee4cac65aab58048016ecbd94e022973701e1b17f4bd9d1b6ca1107f619ac6d27b53dd3350d5be09b08935923cbed97906c0000000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39' }) zcashClient.load(() => { // Loading complete, show the wallet document.getElementById('zcash-client-loading').remove() document.getElementById('zcash-client-content').style.display = '' + + zcashClient.sync() }) diff --git a/envoy/envoy.Dockerfile b/envoy/envoy.Dockerfile new file mode 100644 index 0000000..8222da0 --- /dev/null +++ b/envoy/envoy.Dockerfile @@ -0,0 +1,3 @@ +FROM envoyproxy/envoy:latest +COPY ./envoy.yaml /etc/envoy/envoy.yaml +CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml diff --git a/envoy/envoy.yaml b/envoy/envoy.yaml new file mode 100644 index 0000000..a9c793b --- /dev/null +++ b/envoy/envoy.yaml @@ -0,0 +1,45 @@ +admin: + access_log_path: /tmp/admin_access.log + address: + socket_address: { address: 0.0.0.0, port_value: 9901 } + +static_resources: + listeners: + - name: listener_0 + address: + socket_address: { address: 0.0.0.0, port_value: 8081 } + filter_chains: + - filters: + - name: envoy.http_connection_manager + config: + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: { prefix: "/" } + route: + cluster: lightwalletd_frontend + max_grpc_timeout: 0s + cors: + allow_origin: + - "*" + allow_methods: GET, PUT, DELETE, POST, OPTIONS + allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout + max_age: "1728000" + expose_headers: custom-header-1,grpc-status,grpc-message + enabled: true + http_filters: + - name: envoy.grpc_web + - name: envoy.cors + - name: envoy.router + clusters: + - name: lightwalletd_frontend + connect_timeout: 0.25s + type: logical_dns + http2_protocol_options: {} + lb_policy: round_robin + hosts: [{ socket_address: { address: localhost, port_value: 9067 }}] diff --git a/zcash-client-sdk-js/src/index.js b/zcash-client-sdk-js/src/index.js index 85f3d54..88b4686 100644 --- a/zcash-client-sdk-js/src/index.js +++ b/zcash-client-sdk-js/src/index.js @@ -1,17 +1,114 @@ import { Client } from 'zcash-client-backend-wasm' +const { BlockID, BlockRange, ChainSpec } = require('./service_pb.js') +const { CompactTxStreamerClient } = require('./service_grpc_web_pb.js') +const grpc = {} +grpc.web = require('grpc-web') + const COIN = 100000000 +const CHAIN_REFRESH_INTERVAL = 60 * 1000 +const BATCH_SIZE = 1000 + export class ZcashClient { - constructor (uiHandlers) { + constructor (lightwalletdURL, uiHandlers, checkpoint) { + this.lightwalletd = new CompactTxStreamerClient(lightwalletdURL) this.client = Client.new() this.uiHandlers = uiHandlers + + if (!this.client.set_initial_block(checkpoint.height, checkpoint.hash, checkpoint.sapling_tree)) { + console.error('Invalid checkpoint data') + } } updateUI () { this.uiHandlers.updateBalance(this.client.balance() / COIN) } + sync () { + var self = this + + var chainSpec = new ChainSpec() + + self.lightwalletd.getLatestBlock(chainSpec, {}, (err, latestBlock) => { + if (err) { + console.error('Error fetching latest block') + console.error(`Error code: ${err.code} "${err.message}"`) + return + } + + var startHeight = self.client.last_scanned_height() + var latestHeight = latestBlock.getHeight() + if (startHeight === latestHeight) { + console.log('No new blocks') + window.setTimeout(() => { self.sync() }, CHAIN_REFRESH_INTERVAL) + return + } + + var endHeight + if (latestHeight - startHeight < BATCH_SIZE) { + endHeight = latestHeight + } else { + endHeight = startHeight + BATCH_SIZE - 1 + } + console.debug(`Latest block: ${latestHeight}`) + console.debug(`Requesting blocks in range [${startHeight}, ${endHeight}]`) + + var blockStart = new BlockID() + blockStart.setHeight(startHeight) + var blockEnd = new BlockID() + blockEnd.setHeight(endHeight) + var blockRange = new BlockRange() + blockRange.setStart(blockStart) + blockRange.setEnd(blockEnd) + + var stream = self.lightwalletd.getBlockRange(blockRange, {}) + stream.on('data', (block) => { + // Scan the block + if (!self.client.scan_block(block.serializeBinary())) { + console.error('Failed to scan block') + } + }) + stream.on('status', (status) => { + if (status.metadata) { + console.debug('Received metadata') + console.debug(status.metadata) + } + if (status.code !== grpc.web.StatusCode.OK) { + console.error(`Error code: ${status.code} "${status.details}"`) + } + + // Perform end-of-stream updates here, because we don't always get the + // 'end' event for some reason, but we do always get the 'status' event. + + var syncedHeight = self.client.last_scanned_height() + if (endHeight !== syncedHeight) { + console.error('Block stream finished before expected end height') + } + + // Update UI for current chain status + console.log(`Scanned to height: ${syncedHeight}`) + self.updateUI() + self.uiHandlers.updateSyncStatus(syncedHeight, latestHeight) + + // Queue up the next sync + if (syncedHeight === latestHeight) { + console.log('Finished syncing!') + window.setTimeout(() => { self.sync() }, CHAIN_REFRESH_INTERVAL) + } else { + self.sync() + } + }) + stream.on('error', (err) => { + console.error('Error while streaming blocks') + console.error(`Error code: ${err.code} "${err.message}"`) + }) + stream.on('end', () => { + console.debug('Block stream end signal received') + }) + }) + } + load (onFinished) { var self = this