diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..854213da
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,57 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+ paths-ignore:
+ - '**/README.md'
+
+jobs:
+ haveno-ts-e2e:
+ timeout-minutes: 50
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Stop Mono service
+ run: sudo killall mono
+
+ - name: Start containers
+ run: docker-compose -f "./docker/docker-compose.yml" up -d --build
+
+ - name: Install node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 16
+
+ - name: Install protoc-gen-grpc-web
+ run: |
+ curl -Lo protoc-gen-grpc-web https://github.com/grpc/grpc-web/releases/download/1.4.2/protoc-gen-grpc-web-1.4.2-linux-x86_64
+ sudo mv protoc-gen-grpc-web /usr/local/bin
+ sudo chmod a+x /usr/local/bin/protoc-gen-grpc-web
+ protoc-gen-grpc-web --version
+
+ - name: Install protoc
+ run: |
+ curl -Lo protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-linux-x86_64.zip
+ sudo unzip -q protoc.zip bin/protoc -d /usr/local
+ sudo chmod a+x /usr/local/bin/protoc
+ rm -rf protoc.zip
+ protoc --version
+
+ - name: Copy compiled haveno project from node2 container
+ run: |
+ sudo docker cp node2:/home/haveno/haveno ./../
+ sudo chmod 777 -R ./../haveno
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run tests
+ run: npm run test -- --baseCurrencyNetwork=XMR_LOCAL
+
+ - name: Stop containers
+ if: always()
+ run: docker-compose -f "./docker/docker-compose.yml" down
diff --git a/.gitignore b/.gitignore
index 49c7d40a..2f1131c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,4 @@ yarn-debug.log*
yarn-error.log*
# generated code
-/src/protobuf/**
\ No newline at end of file
+/src/protobuf/**
diff --git a/README.md b/README.md
index c49b9888..86d5798c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# Haveno TypeScript Library
+
+[](https://twitter.com/havenodex)
+[](https://matrix.to/#/#haveno:monero.social)
+
TypeScript library for using Haveno.
## Install
@@ -38,7 +42,7 @@ Running the [API tests](./src/HavenoClient.test.ts) is the best way to develop a
1. [Run a local Haveno test network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) and then shut down the arbitrator, user1, and user2 or run them as daemons, e.g. `make user1-daemon-local`. You may omit the arbitrator registration steps since it's done automatically in the tests.
2. Clone this project to the same parent directory as the haveno project: `git clone https://github.com/haveno-dex/haveno-ts`
-3. In a new terminal, start envoy with the config in haveno-ts/config/envoy.test.yaml (change absolute path for your system): `docker run --rm --add-host host.docker.internal:host-gateway -it -v ~/git/haveno-ts/config/envoy.test.yaml:/envoy.test.yaml -p 8079:8079 -p 8080:8080 -p 8081:8081 -p 8082:8082 -p 8083:8083 -p 8084:8084 -p 8085:8085 -p 8086:8086 envoyproxy/envoy-dev:8a2143613d43d17d1eb35a24b4a4a4c432215606 -c /envoy.test.yaml`
+3. In a new terminal, start envoy with the config in haveno-ts/config/envoy.test.yaml (change absolute path for your system): `docker run --rm --add-host host.docker.internal:host-gateway -it -v ~/git/haveno-ts/config/envoy.test.yaml:/envoy.test.yaml -p 8079:8079 -p 8080:8080 -p 8081:8081 -p 8082:8082 -p 8083:8083 -p 8084:8084 -p 8085:8085 -p 8086:8086 envoyproxy/envoy-dev:latest -c /envoy.test.yaml`
4. In a new terminal, start the funding wallet. This wallet will be funded automatically in order to fund the tests.
For example: `cd ~/git/haveno && make funding-wallet-local`.
5. Install protobuf compiler v3.19.1 or later for your system:
mac: `brew install protobuf`
@@ -47,4 +51,4 @@ Running the [API tests](./src/HavenoClient.test.ts) is the best way to develop a
6. Download `protoc-gen-grpc-web` plugin and make executable as [shown here](https://github.com/grpc/grpc-web#code-generator-plugin).
7. `cd haveno-ts`
8. `npm install`
-9. `npm run test -- --baseCurrencyNetwork=XMR_LOCAL` to run all tests or `npm run test -- --baseCurrencyNetwork=XMR_LOCAL -t "my test"` to run tests by name.
\ No newline at end of file
+9. `npm run test -- --baseCurrencyNetwork=XMR_LOCAL` to run all tests or `npm run test -- --baseCurrencyNetwork=XMR_LOCAL -t "my test"` to run tests by name.
diff --git a/config/envoy.test.yaml b/config/envoy.test.yaml
index 28c06c5e..1f8db111 100644
--- a/config/envoy.test.yaml
+++ b/config/envoy.test.yaml
@@ -37,10 +37,16 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
- - name: alice_listener
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ - name: user1_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
@@ -58,7 +64,7 @@ static_resources:
routes:
- match: { prefix: "/" }
route:
- cluster: alice_service
+ cluster: user1_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
@@ -70,10 +76,16 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
- - name: bob_listener
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ - name: user2_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8081 }
filter_chains:
@@ -91,7 +103,7 @@ static_resources:
routes:
- match: { prefix: "/" }
route:
- cluster: bob_service
+ cluster: user2_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
@@ -103,9 +115,15 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: haveno1_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8082 }
@@ -136,9 +154,15 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: haveno2_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8083 }
@@ -169,9 +193,15 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: haveno3_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8084 }
@@ -202,9 +232,15 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: haveno4_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8085 }
@@ -235,9 +271,15 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: haveno5_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8086 }
@@ -268,9 +310,15 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: arbitrator_service
connect_timeout: 0.25s
@@ -286,7 +334,7 @@ static_resources:
socket_address:
address: host.docker.internal
port_value: 9998
- - name: alice_service
+ - name: user1_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
@@ -300,7 +348,7 @@ static_resources:
socket_address:
address: host.docker.internal
port_value: 9999
- - name: bob_service
+ - name: user2_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
@@ -383,4 +431,4 @@ static_resources:
address:
socket_address:
address: host.docker.internal
- port_value: 10005
\ No newline at end of file
+ port_value: 10005
diff --git a/config/envoy.yaml b/config/envoy.yaml
index 306c2a4a..786018ad 100644
--- a/config/envoy.yaml
+++ b/config/envoy.yaml
@@ -1,4 +1,4 @@
-# envoy configuration to run alice ui
+# envoy configuration to run user1 ui
admin:
access_log_path: /tmp/admin_access.log
@@ -7,7 +7,7 @@ admin:
static_resources:
listeners:
- - name: alice_listener
+ - name: user1_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
@@ -25,7 +25,7 @@ static_resources:
routes:
- match: { prefix: "/" }
route:
- cluster: alice_service
+ cluster: user1_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
@@ -37,11 +37,17 @@ static_resources:
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- - name: envoy.filters.http.grpc_web
- - name: envoy.filters.http.cors
- - name: envoy.filters.http.router
+ - name: envoy.filters.http.grpc_web
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- - name: alice_service
+ - name: user1_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
@@ -54,4 +60,4 @@ static_resources:
address:
socket_address:
address: host.docker.internal
- port_value: 9999
\ No newline at end of file
+ port_value: 9999
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 00000000..64bffef1
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,173 @@
+version: '3.9'
+
+services:
+ envoy:
+ image: envoyproxy/envoy-dev:latest
+ container_name: envoy
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ ports:
+ - "8079:8079"
+ - "8080:8080"
+ - "8081:8081"
+ - "8082:8082"
+ - "8083:8083"
+ - "8084:8084"
+ - "8085:8085"
+ - "8086:8086"
+ volumes:
+ - ./../config/envoy.test.yaml:/config/envoy.test.yaml
+ command:
+ - -c
+ - ../config/envoy.test.yaml
+
+ node1:
+ build: haveno/
+ container_name: node1
+ entrypoint: [ "./.localnet/monerod" ]
+ command: [
+ "--testnet",
+ "--no-igd",
+ "--hide-my-port",
+ "--data-dir=./.localnet/xmr_local/node1",
+ "--p2p-bind-ip=127.0.0.1",
+ "--p2p-bind-port=48080",
+ "--rpc-bind-port=48081",
+ "--no-zmq",
+ "--add-exclusive-node=127.0.0.1:28080",
+ "--rpc-access-control-origins=http://localhost:8080",
+ "--fixed-difficulty=10",
+ "--non-interactive",
+ ]
+ network_mode: "host"
+
+ node2:
+ build: haveno/
+ container_name: node2
+ entrypoint: [ "./.localnet/monerod" ]
+ command: [
+ "--testnet",
+ "--no-igd",
+ "--hide-my-port",
+ "--data-dir=./.localnet/xmr_local/node2",
+ "--p2p-bind-ip=127.0.0.1",
+ "--rpc-bind-ip=0.0.0.0",
+ "--no-zmq",
+ "--confirm-external-bind",
+ "--add-exclusive-node=127.0.0.1:48080",
+ "--rpc-access-control-origins=http://localhost:8080",
+ "--fixed-difficulty=10",
+ "--non-interactive",
+ ]
+ network_mode: "host"
+ depends_on:
+ - node1
+
+ seed1:
+ build: haveno/
+ container_name: seed1
+ entrypoint: [ "./haveno-seednode" ]
+ command: [
+ "--baseCurrencyNetwork=XMR_LOCAL",
+ "--useLocalhostForP2P=true",
+ "--useDevPrivilegeKeys=true",
+ "--nodePort=2002",
+ "--appName=haveno-XMR_LOCAL_Seed1_2002",
+ ]
+ network_mode: "host"
+ depends_on:
+ - node2
+
+ seed2:
+ build: haveno/
+ container_name: seed2
+ entrypoint: [ "./haveno-seednode" ]
+ command: [
+ "--baseCurrencyNetwork=XMR_LOCAL",
+ "--useLocalhostForP2P=true",
+ "--useDevPrivilegeKeys=true",
+ "--nodePort=3002",
+ "--appName=haveno-XMR_LOCAL_Seed2_3002",
+ ]
+ network_mode: "host"
+ depends_on:
+ - node2
+
+ arbitrator:
+ build: haveno/
+ container_name: arbitrator
+ entrypoint: [ "./haveno-daemon" ]
+ command: [
+ "--baseCurrencyNetwork=XMR_LOCAL",
+ "--useLocalhostForP2P=true",
+ "--useDevPrivilegeKeys=true",
+ "--nodePort=4444",
+ "--appName=haveno-XMR_LOCAL_arbitrator",
+ "--apiPassword=apitest",
+ "--apiPort=9998",
+ "--walletRpcBindPort=38090",
+ "--passwordRequired=false",
+ ]
+ network_mode: "host"
+ depends_on:
+ - seed1
+
+ user1:
+ build: haveno/
+ container_name: user1
+ entrypoint: [ "./haveno-daemon" ]
+ command: [
+ "--baseCurrencyNetwork=XMR_LOCAL",
+ "--useLocalhostForP2P=true",
+ "--useDevPrivilegeKeys=true",
+ "--nodePort=5555",
+ "--appName=haveno_user1",
+ "--apiPassword=apitest",
+ "--apiPort=9999",
+ "--walletRpcBindPort=38091",
+ "--passwordRequired=false",
+ ]
+ network_mode: "host"
+ depends_on:
+ - seed1
+
+ user2:
+ build: haveno/
+ container_name: user2
+ entrypoint: [ "./haveno-daemon" ]
+ command: [
+ "--baseCurrencyNetwork=XMR_LOCAL",
+ "--useLocalhostForP2P=true",
+ "--useDevPrivilegeKeys=true",
+ "--nodePort=6666",
+ "--appName=haveno_user2",
+ "--apiPassword=apitest",
+ "--apiPort=10000",
+ "--walletRpcBindPort=38092",
+ "--passwordRequired=false",
+ ]
+ network_mode: "host"
+ depends_on:
+ - seed1
+
+ pricenode:
+ build: pricenode/
+ container_name: pricenode
+ entrypoint: [ "./haveno-pricenode" ]
+ network_mode: "host"
+
+ funding:
+ build: haveno/
+ container_name: funding_wallet
+ entrypoint: [ "./.localnet/monero-wallet-rpc" ]
+ command: [
+ "--testnet",
+ "--daemon-address=http://localhost:28081",
+ "--rpc-bind-port=28084",
+ "--rpc-login=rpc_user:abc123",
+ "--rpc-access-control-origins=http://localhost:8080",
+ "--wallet-dir=./.localnet",
+ ]
+ network_mode: "host"
+ depends_on:
+ - arbitrator
diff --git a/docker/haveno/Dockerfile b/docker/haveno/Dockerfile
new file mode 100644
index 00000000..87b46b71
--- /dev/null
+++ b/docker/haveno/Dockerfile
@@ -0,0 +1,21 @@
+FROM openjdk:11
+
+RUN set -ex && \
+ apt update && \
+ apt --no-install-recommends --yes install \
+ make \
+ git
+
+RUN set -ex && adduser --system --group --disabled-password haveno && \
+ mkdir -p /home/haveno && \
+ chown -R haveno:haveno /home/haveno
+
+USER haveno
+
+RUN set -ex && git clone https://github.com/haveno-dex/haveno.git /home/haveno/haveno
+
+WORKDIR /home/haveno/haveno
+
+RUN set -ex && make skip-tests
+
+ENTRYPOINT [ "./haveno-daemon" ]
diff --git a/docker/pricenode/Dockerfile b/docker/pricenode/Dockerfile
new file mode 100644
index 00000000..b6d433f2
--- /dev/null
+++ b/docker/pricenode/Dockerfile
@@ -0,0 +1,21 @@
+FROM gradle:jdk11
+
+RUN set -ex && \
+ apt update && \
+ apt --no-install-recommends --yes install \
+ make \
+ git
+
+RUN set -ex && adduser --system --group --disabled-password pricenode && \
+ mkdir -p /home/pricenode && \
+ chown -R pricenode:pricenode /home/pricenode
+
+USER pricenode
+
+RUN set -ex && git clone --recursive https://github.com/haveno-dex/haveno-pricenode.git /home/pricenode
+
+WORKDIR /home/pricenode
+
+RUN gradle clean build -x test
+
+ENTRYPOINT [ "./haveno-pricenode" ]
diff --git a/package.json b/package.json
index 5641768c..2b8cbd2b 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,6 @@
"files": ["dist/**/*"],
"scripts": {
"prepare": "scripts/build_protobuf.sh",
- "pretest": "scripts/build_protobuf.sh",
- "build": "./scripts/build_dist.sh",
"test": "jest",
"eslint": "eslint .",
"eslintfix": "eslint src/* --fix",
diff --git a/scripts/build_dist.sh b/scripts/build_dist.sh
deleted file mode 100755
index 542bcb60..00000000
--- a/scripts/build_dist.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-
-# run typescript compiler
-tsc
-
-# copy protobuf models to dist
-mkdir -p dist/protobuf || exit 1
-cp src/protobuf/grpc_pb.d.ts dist/protobuf/grpc_pb.d.ts || exit 1
-cp src/protobuf/grpc_pb.js dist/protobuf/grpc_pb.js || exit 1
-cp src/protobuf/pb_pb.d.ts dist/protobuf/pb_pb.d.ts || exit 1
-cp src/protobuf/pb_pb.js dist/protobuf/pb_pb.js || exit 1
\ No newline at end of file
diff --git a/scripts/build_protobuf.sh b/scripts/build_protobuf.sh
index a6325044..2e0b228f 100755
--- a/scripts/build_protobuf.sh
+++ b/scripts/build_protobuf.sh
@@ -4,3 +4,12 @@
mkdir -p ./src/protobuf
cd ./src/protobuf || exit 1
protoc -I=../../../haveno/proto/src/main/proto/ ../../../haveno/proto/src/main/proto/*.proto --js_out=import_style=commonjs,binary:./ --grpc-web_out=import_style=typescript,mode=grpcwebtext:./ || exit 1
+
+# run typescript compiler
+tsc
+
+# copy protobuf models to dist
+cp ./grpc_pb.d.ts ../../dist/protobuf/grpc_pb.d.ts || exit 1
+cp ./grpc_pb.js ../../dist/protobuf/grpc_pb.js || exit 1
+cp ./pb_pb.d.ts ../../dist/protobuf/pb_pb.d.ts || exit 1
+cp ./pb_pb.js ../../dist/protobuf/pb_pb.js || exit 1