From 8addb23a498ddc6a4f1796b1fb39e8b8f251c97c Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 17 Sep 2021 09:33:58 -0400 Subject: [PATCH] test that offers filtered when reserved funds spent or duplicated introduce envoy config for running tests with alice and bob test offer removal when reserved output spent using monero-javascript add instructions to run tests in readme update protobuf definitions --- README.md | 23 +++- config/envoy.test.yaml | 104 ++++++++++++++++ config/envoy.yaml | 8 +- config/pb.proto | 4 +- package-lock.json | 243 ++++++++++++++++++++++++++++++++++++++ package.json | 3 + src/HavenoDaemon.test.tsx | 175 +++++++++++++++++++++------ src/HavenoDaemon.tsx | 4 +- src/protobuf/pb_pb.d.ts | 12 ++ src/protobuf/pb_pb.js | 121 +++++++++++++++++-- 10 files changed, 647 insertions(+), 50 deletions(-) create mode 100644 config/envoy.test.yaml diff --git a/README.md b/README.md index a6866ab3..e113a7d8 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ A proof of concept to fetch and render data from Haveno's daemon in ReactJS. -This application is a basic [create-react-app](https://github.com/facebook/create-react-app) with typescript using [grpc-web](https://github.com/grpc/grpc-web) and a proxy ([envoy](https://www.envoyproxy.io/)) for Haveno's gRPC daemon. +This application is a lightly modified [create-react-app](https://github.com/facebook/create-react-app) with typescript using [envoy proxy](https://www.envoyproxy.io/) and [grpc-web](https://github.com/grpc/grpc-web) to use Haveno's gRPC API. -## How to Run in a Browser +## Run in a Browser -1. [Run a local Haveno test network](https://github.com/woodser/haveno#running-a-local-haveno-test-network) except replace `./haveno-desktop` with `./haveno-daemon` when starting Alice at port 9999. +1. [Run a local Haveno test network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) except replace `./haveno-desktop` with `./haveno-daemon` when starting Alice at port 9999. 2. `git clone https://github.com/haveno-dex/haveno-ui-poc` 4. Start envoy with the config in ./config/envoy.yaml
Example: `docker run --rm -it -v ~/git/haveno-ui-poc/config/envoy.yaml:/envoy.yaml -p 8080:8080 envoyproxy/envoy-dev:8a2143613d43d17d1eb35a24b4a4a4c432215606 -c /envoy.yaml` @@ -18,8 +18,25 @@ This application is a basic [create-react-app](https://github.com/facebook/creat

+## Run Tests + +Running the [top-level API tests](./src/HavenoDaemon.test.tsx) is a great way to develop and test Haveno end-to-end. + +[`HavenoDaemon`](./src/HavenoDaemon.tsx) provides the interface to the Haveno daemon's gRPC API. + +1. [Run a local Haveno test network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) except replace `./haveno-desktop` with `./haveno-daemon` when starting Alice at port 9999 and Bob at port 10000. +2. `git clone https://github.com/haveno-dex/haveno-ui-poc` +4. Start envoy with the test config in ./config/envoy.test.yaml.
+ Example: `docker run --rm -it -v ~/git/haveno-ui-poc/config/envoy.test.yaml:/envoy.test.yaml -p 8080:8080 -p 8081:8081 envoyproxy/envoy-dev:8a2143613d43d17d1eb35a24b4a4a4c432215606 -c /envoy.test.yaml` +5. `npm install` +6. Modify test config as needed in [HavenoDaemon.test.tsx](./src/HavenoDaemon.test.tsx). +7. `npm test` +8. Run all tests: `a` + ## How to Update the Protobuf Client +If the protobuf definitions in haveno-dex/haveno are updated, the typescript imports must be regenerated: + 1. Copy grpc.proto and pb.proto from Haveno's [protobuf definitions](https://github.com/haveno-dex/haveno/tree/master/proto/src/main/proto) to ./config. 2. Install protobuf for your system, e.g. on mac: `brew install protobuf` 3. `./bin/build_protobuf.sh` \ No newline at end of file diff --git a/config/envoy.test.yaml b/config/envoy.test.yaml new file mode 100644 index 00000000..7c946c87 --- /dev/null +++ b/config/envoy.test.yaml @@ -0,0 +1,104 @@ +# envoy configuration to test with alice and bob trader instances + +admin: + access_log_path: /tmp/admin_access.log + address: + socket_address: { address: 0.0.0.0, port_value: 9901 } + +static_resources: + listeners: + - name: alice_listener + address: + socket_address: { address: 0.0.0.0, port_value: 8080 } + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: { prefix: "/" } + route: + cluster: alice_service + timeout: 0s + max_stream_duration: + grpc_timeout_header_max: 0s + cors: + allow_origin_string_match: + - prefix: "*" + allow_methods: GET, PUT, DELETE, POST, OPTIONS + allow_headers: password,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 + http_filters: + - name: envoy.filters.http.grpc_web + - name: envoy.filters.http.cors + - name: envoy.filters.http.router + - name: bob_listener + address: + socket_address: { address: 0.0.0.0, port_value: 8081 } + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: { prefix: "/" } + route: + cluster: bob_service + timeout: 0s + max_stream_duration: + grpc_timeout_header_max: 0s + cors: + allow_origin_string_match: + - prefix: "*" + allow_methods: GET, PUT, DELETE, POST, OPTIONS + allow_headers: password,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 + http_filters: + - name: envoy.filters.http.grpc_web + - name: envoy.filters.http.cors + - name: envoy.filters.http.router + clusters: + - name: alice_service + connect_timeout: 0.25s + type: logical_dns + http2_protocol_options: {} + lb_policy: round_robin + load_assignment: + cluster_name: cluster_0 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: host.docker.internal + port_value: 9999 + - name: bob_service + connect_timeout: 0.25s + type: logical_dns + http2_protocol_options: {} + lb_policy: round_robin + load_assignment: + cluster_name: cluster_0 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: host.docker.internal + port_value: 10000 \ No newline at end of file diff --git a/config/envoy.yaml b/config/envoy.yaml index 1d91375a..306c2a4a 100644 --- a/config/envoy.yaml +++ b/config/envoy.yaml @@ -1,3 +1,5 @@ +# envoy configuration to run alice ui + admin: access_log_path: /tmp/admin_access.log address: @@ -5,7 +7,7 @@ admin: static_resources: listeners: - - name: listener_0 + - name: alice_listener address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: @@ -23,7 +25,7 @@ static_resources: routes: - match: { prefix: "/" } route: - cluster: haveno_service + cluster: alice_service timeout: 0s max_stream_duration: grpc_timeout_header_max: 0s @@ -39,7 +41,7 @@ static_resources: - name: envoy.filters.http.cors - name: envoy.filters.http.router clusters: - - name: haveno_service + - name: alice_service connect_timeout: 0.25s type: logical_dns http2_protocol_options: {} diff --git a/config/pb.proto b/config/pb.proto index be245fcf..2ae1fa46 100644 --- a/config/pb.proto +++ b/config/pb.proto @@ -177,7 +177,8 @@ message SignOfferRequest { string reserve_tx_hash = 8; string reserve_tx_hex = 9; string reserve_tx_key = 10; - string payout_address = 11; + repeated string reserve_tx_key_images = 11; + string payout_address = 12; } message SignOfferResponse { @@ -940,6 +941,7 @@ message OfferPayload { NodeAddress arbitrator_node_address = 1001; string arbitrator_signature = 1002; + repeated string reserve_tx_key_images = 1003; } message AccountAgeWitness { diff --git a/package-lock.json b/package-lock.json index a735f03a..bfdd1582 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,9 @@ "react-scripts": "4.0.3", "typescript": "^4.2.4", "web-vitals": "^1.1.1" + }, + "devDependencies": { + "monero-javascript": "^0.5.5" } }, "node_modules/@babel/code-frame": { @@ -5774,6 +5777,12 @@ "node": "*" } }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "dev": true + }, "node_modules/crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -9415,6 +9424,12 @@ "node": ">=4.0.0" } }, + "node_modules/html5-fs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/html5-fs/-/html5-fs-0.1.1.tgz", + "integrity": "sha1-jxRcucLoOLt9D6kx4wraBUtHzX0=", + "dev": true + }, "node_modules/htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -13170,6 +13185,58 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/monero-javascript": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/monero-javascript/-/monero-javascript-0.5.5.tgz", + "integrity": "sha512-Fpm0kNxEwvQkHh46l4DEOQk/225og2ujQadeIFqQ1pk9DguIwL2wvLs2xiAgbA3lN+9hEWBdjl1N+UktEIE3Ag==", + "dev": true, + "dependencies": { + "ajv": "^6.12.6", + "async": "2.6.1", + "crypto-js": "^4.0.0", + "html5-fs": "0.1.1", + "net": "^1.0.2", + "promise-throttle": "^1.1.2", + "request": "^2.88.0", + "request-promise": "^4.2.6", + "serialize-javascript": "^3.1.0", + "text-encoding": "^0.7.0", + "tls": "0.0.1", + "uuid": "3.3.2", + "web-worker": "^1.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/monero-javascript/node_modules/async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/monero-javascript/node_modules/serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/monero-javascript/node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -13280,6 +13347,12 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha1-0XV+yaf7I3HYPPR1XOPifhCCk4g=", + "dev": true + }, "node_modules/next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -15630,6 +15703,12 @@ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" }, + "node_modules/promise-throttle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-throttle/-/promise-throttle-1.1.2.tgz", + "integrity": "sha512-dij7vjyXNewuuN/gyr+TX2KRjw48mbV5FEtgyXaIoJjGYAKT0au23/voNvy9eS4UNJjx2KUdEcO5Yyfc1h7vWQ==", + "dev": true + }, "node_modules/prompts": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", @@ -16491,6 +16570,25 @@ "node": ">= 6" } }, + "node_modules/request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, "node_modules/request-promise-core": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", @@ -16534,6 +16632,19 @@ "node": ">=0.8" } }, + "node_modules/request-promise/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/request/node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -18808,6 +18919,13 @@ "node": ">=8" } }, + "node_modules/text-encoding": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", + "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", + "deprecated": "no longer maintained", + "dev": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -18870,6 +18988,12 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "node_modules/tls": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tls/-/tls-0.0.1.tgz", + "integrity": "sha1-CrK/WWjXHfL4wOFRXSSiJAuYqsg=", + "dev": true + }, "node_modules/tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -19876,6 +20000,12 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-1.1.1.tgz", "integrity": "sha512-jYOaqu01Ny1NvMwJ3dBJDUOJ2PGWknZWH4AUnvFOscvbdHMERIKT2TlgiAey5rVyfOePG7so2JcXXZdSnBvioQ==" }, + "node_modules/web-worker": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.1.0.tgz", + "integrity": "sha512-BsAzCx5k71qqjPXD5+nBEiLaH/5glNV1OASF2kB+13qkajkScd70mo0E4U0GyhD9nUGgWmSq8dVUqlIOdfICUg==", + "dev": true + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -25970,6 +26100,12 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "dev": true + }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -28813,6 +28949,12 @@ } } }, + "html5-fs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/html5-fs/-/html5-fs-0.1.1.tgz", + "integrity": "sha1-jxRcucLoOLt9D6kx4wraBUtHzX0=", + "dev": true + }, "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -31619,6 +31761,53 @@ "minimist": "^1.2.5" } }, + "monero-javascript": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/monero-javascript/-/monero-javascript-0.5.5.tgz", + "integrity": "sha512-Fpm0kNxEwvQkHh46l4DEOQk/225og2ujQadeIFqQ1pk9DguIwL2wvLs2xiAgbA3lN+9hEWBdjl1N+UktEIE3Ag==", + "dev": true, + "requires": { + "ajv": "^6.12.6", + "async": "2.6.1", + "crypto-js": "^4.0.0", + "html5-fs": "0.1.1", + "net": "^1.0.2", + "promise-throttle": "^1.1.2", + "request": "^2.88.0", + "request-promise": "^4.2.6", + "serialize-javascript": "^3.1.0", + "text-encoding": "^0.7.0", + "tls": "0.0.1", + "uuid": "3.3.2", + "web-worker": "^1.0.0" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + } + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -31713,6 +31902,12 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha1-0XV+yaf7I3HYPPR1XOPifhCCk4g=", + "dev": true + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -33591,6 +33786,12 @@ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" }, + "promise-throttle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-throttle/-/promise-throttle-1.1.2.tgz", + "integrity": "sha512-dij7vjyXNewuuN/gyr+TX2KRjw48mbV5FEtgyXaIoJjGYAKT0au23/voNvy9eS4UNJjx2KUdEcO5Yyfc1h7vWQ==", + "dev": true + }, "prompts": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", @@ -34290,6 +34491,30 @@ } } }, + "request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, "request-promise-core": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", @@ -36104,6 +36329,12 @@ "minimatch": "^3.0.4" } }, + "text-encoding": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", + "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -36165,6 +36396,12 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tls": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tls/-/tls-0.0.1.tgz", + "integrity": "sha1-CrK/WWjXHfL4wOFRXSSiJAuYqsg=", + "dev": true + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -36977,6 +37214,12 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-1.1.1.tgz", "integrity": "sha512-jYOaqu01Ny1NvMwJ3dBJDUOJ2PGWknZWH4AUnvFOscvbdHMERIKT2TlgiAey5rVyfOePG7so2JcXXZdSnBvioQ==" }, + "web-worker": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.1.0.tgz", + "integrity": "sha512-BsAzCx5k71qqjPXD5+nBEiLaH/5glNV1OASF2kB+13qkajkScd70mo0E4U0GyhD9nUGgWmSq8dVUqlIOdfICUg==", + "dev": true + }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", diff --git a/package.json b/package.json index 8e5e2927..907e1c6c 100644 --- a/package.json +++ b/package.json @@ -41,5 +41,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "monero-javascript": "^0.5.5" } } diff --git a/src/HavenoDaemon.test.tsx b/src/HavenoDaemon.test.tsx index 28f0d0b6..e9b58ec7 100644 --- a/src/HavenoDaemon.test.tsx +++ b/src/HavenoDaemon.test.tsx @@ -1,20 +1,50 @@ +// import haveno types import {HavenoDaemon} from "./HavenoDaemon"; -import {XmrBalanceInfo, OfferInfo} from './protobuf/grpc_pb'; +import {XmrBalanceInfo, OfferInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb import {PaymentAccount} from './protobuf/pb_pb'; -const HAVENO_UI_VERSION = "1.6.2"; -const HAVENO_DAEMON_URL = "http://localhost:8080"; -const HAVENO_DAEMON_PASSWORD = "apitest"; +// import monero-javascript +const monerojs = require("monero-javascript"); // TODO (woodser): support typescript and `npm install @types/monero-javascript` in monero-javascript +const MoneroDaemonRpc = monerojs.MoneroDaemonRpc; +const MoneroWalletRpc = monerojs.MoneroWalletRpc; -const daemon = new HavenoDaemon(HAVENO_DAEMON_URL, HAVENO_DAEMON_PASSWORD); +// alice config +const havenoVersion = "1.6.2"; +const aliceDaemonUrl = "http://localhost:8080"; +const aliceDaemonPassword = "apitest"; +const alice: HavenoDaemon = new HavenoDaemon(aliceDaemonUrl, aliceDaemonPassword); +const aliceWalletUrl = "http://127.0.0.1:51743"; // alice's internal haveno wallet for direct testing // TODO (woodser): make configurable rather than randomly generated +const aliceWalletUsername = "rpc_user"; +const aliceWalletPassword = "abc123"; +const aliceWallet = new MoneroWalletRpc(aliceWalletUrl, aliceWalletUsername, aliceWalletPassword); +const aliceWalletSyncPeriod = 5000; -test("Can get the version", async () => { - let version = await daemon.getVersion(); - expect(version).toEqual(HAVENO_UI_VERSION); +// bob config +const bobDaemonUrl = "http://localhost:8081"; +const bobDaemonPassword = "apitest"; +const bob: HavenoDaemon = new HavenoDaemon(bobDaemonUrl, bobDaemonPassword); + +// monero daemon config +const moneroDaemonUrl = "http://localhost:38081" +const moneroDaemonUsername = "superuser"; +const moneroDaemonPassword = "abctesting123"; +let monerod: any; + +beforeAll(async () => { + await monerojs.LibraryUtils.setWorkerDistPath("./node_modules/monero-javascript/src/main/js/common/MoneroWebWorker.js"); // TODO (woodser): remove this when update to monero-javascript-v0.5.6 which correctly detects node environment + monerod = await monerojs.connectToDaemonRpc(moneroDaemonUrl, moneroDaemonUsername, moneroDaemonPassword); + //for (let offer of await alice.getMyOffers("BUY")) await alice.removeOffer(offer.getId()); + //for (let offer of await alice.getMyOffers("SELL")) await alice.removeOffer(offer.getId()); + //for (let frozenOutput of await aliceWallet.getOutputs({isFrozen: true})) await aliceWallet.thawOutput(frozenOutput.getKeyImage().getHex()); }); -test("Can get the user's balances", async () => { - let balances: XmrBalanceInfo = await daemon.getBalances(); +test("Can get the version", async () => { + let version = await alice.getVersion(); + expect(version).toEqual(havenoVersion); +}); + +test("Can get balances", async () => { + let balances: XmrBalanceInfo = await alice.getBalances(); expect(balances.getUnlockedBalance()); expect(balances.getLockedBalance()); expect(balances.getReservedOfferBalance()); @@ -22,28 +52,28 @@ test("Can get the user's balances", async () => { }); test("Can get offers", async () => { - let offers: OfferInfo[] = await daemon.getOffers("BUY"); + let offers: OfferInfo[] = await alice.getOffers("BUY"); for (let offer of offers) { testOffer(offer); } }); -test("Can get the user's created offers", async () => { - let offers: OfferInfo[] = await daemon.getMyOffers("SELL"); +test("Can get my offers", async () => { + let offers: OfferInfo[] = await alice.getMyOffers("SELL"); for (let offer of offers) { testOffer(offer); } }); test("Can get payment accounts", async () => { - let paymentAccounts: PaymentAccount[] = await daemon.getPaymentAccounts(); + let paymentAccounts: PaymentAccount[] = await alice.getPaymentAccounts(); for (let paymentAccount of paymentAccounts) { testPaymentAccount(paymentAccount); } }); -test("Can create crypto payment account", async () => { - let ethPaymentAccount: PaymentAccount = await daemon.createCryptoPaymentAccount( +test("Can create a crypto payment account", async () => { + let ethPaymentAccount: PaymentAccount = await alice.createCryptoPaymentAccount( "my eth account", "eth", "0xdBdAb835Acd6fC84cF5F9aDD3c0B5a1E25fbd99f", @@ -52,10 +82,89 @@ test("Can create crypto payment account", async () => { }); test("Can post and remove an offer", async () => { + + // get unlocked balance before reserving funds for offer + let unlockedBalanceBefore: bigint = BigInt((await alice.getBalances()).getUnlockedBalance()); + + // post offer + let offer: OfferInfo = await postOffer(); + + // cancel offer + await alice.removeOffer(offer.getId()); + + // offer is removed from my offers + if (getOffer(await alice.getMyOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was found in my offers after removal"); + + // reserved balance released + expect(unlockedBalanceBefore).toEqual(BigInt((await alice.getBalances()).getUnlockedBalance())); +}); + +jest.setTimeout(15000); +test("Invalidates offers when reserved funds are spent", async () => { + + // get frozen key images before posting offer + let frozenKeyImagesBefore = []; + for (let frozenOutput of await aliceWallet.getOutputs({isFrozen: true})) frozenKeyImagesBefore.push(frozenOutput.getKeyImage().getHex()); + + // post offer + await wait(1000); + let offer: OfferInfo = await postOffer(); + + // get key images reserved by offer + let reservedKeyImages = []; + let frozenKeyImagesAfter = []; + for (let frozenOutput of await aliceWallet.getOutputs({isFrozen: true})) frozenKeyImagesAfter.push(frozenOutput.getKeyImage().getHex()); + for (let frozenKeyImageAfter of frozenKeyImagesAfter) { + if (!frozenKeyImagesBefore.includes(frozenKeyImageAfter)) reservedKeyImages.push(frozenKeyImageAfter); + } + + // offer is available to peers + await wait(3000); + if (!getOffer(await bob.getOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posting"); + + // spend one of offer's reserved outputs + if (!reservedKeyImages.length) throw new Error("No reserved key images detected"); + await aliceWallet.thawOutput(reservedKeyImages[0]); + let tx = await aliceWallet.sweepOutput({keyImage: reservedKeyImages[0], address: await aliceWallet.getPrimaryAddress(), relay: false}); + await monerod.submitTxHex(tx.getFullHex(), true); + + // wait for spend to be seen + await wait(aliceWalletSyncPeriod * 2); // TODO (woodser): need place for common test utilities + + // offer is removed from peer offers + if (getOffer(await bob.getOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was found in peer's offers after reserved funds spent"); + + // offer is removed from my offers + if (getOffer(await alice.getMyOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was found in my offers after reserved funds spent"); + + // offer is automatically cancelled + try { + await alice.removeOffer(offer.getId()); + throw new Error("cannot remove invalidated offer"); + } catch (err) { + if (err.message === "cannot remove invalidated offer") throw new Error(err.message); + } + + // flush tx from pool + await monerod.flushTxPool(tx.getHash()); +}); + +test("Can complete a trade", async () => { + + // wait for alice and bob to have unlocked balance for trade + let tradeAmount: bigint = BigInt("250000000000"); + await waitForUnlockedBalance(tradeAmount, alice, bob); + + // TODO: finish this test +}); + +// ------------------------------- HELPERS ------------------------------------ + +async function postOffer() { // test requires ethereum payment account let ethPaymentAccount: PaymentAccount | undefined; - for (let paymentAccount of await daemon.getPaymentAccounts()) { + for (let paymentAccount of await alice.getPaymentAccounts()) { if (paymentAccount.getSelectedTradeCurrency()?.getCode() === "ETH") { ethPaymentAccount = paymentAccount; break; @@ -64,9 +173,10 @@ test("Can post and remove an offer", async () => { if (!ethPaymentAccount) throw new Error("Test requires ethereum payment account to post offer"); // get unlocked balance before reserving offer - let unlockedBalanceBefore: bigint = BigInt((await daemon.getBalances()).getUnlockedBalance()); + let unlockedBalanceBefore: bigint = BigInt((await alice.getBalances()).getUnlockedBalance()); // post offer + // TODO: don't define variables, just document in comments let amount: bigint = BigInt("250000000000"); let minAmount: bigint = BigInt("150000000000"); let price: number = 12.378981; // TODO: price is optional? price string gets converted to long? @@ -75,7 +185,7 @@ test("Can post and remove an offer", async () => { let buyerSecurityDeposit: number = 0.15; // 15% let triggerPrice: number = 12; // TODO: fails if there is decimal, gets converted to long? let paymentAccountId: string = ethPaymentAccount.getId(); - let offer: OfferInfo = await daemon.postOffer("eth", + let offer: OfferInfo = await alice.postOffer("eth", "buy", // buy xmr for eth price, useMarketBasedPrice, @@ -88,24 +198,19 @@ test("Can post and remove an offer", async () => { testOffer(offer); // unlocked balance has decreased - let unlockedBalanceAfter: bigint = BigInt((await daemon.getBalances()).getUnlockedBalance()); + let unlockedBalanceAfter: bigint = BigInt((await alice.getBalances()).getUnlockedBalance()); expect(unlockedBalanceAfter).toBeLessThan(unlockedBalanceBefore); // offer is included in my offers only - if (!getOffer(await daemon.getMyOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in my offers"); - if (getOffer(await daemon.getOffers("buy"), offer.getId())) throw new Error("My offer " + offer.getId() + " should not appear in available offers"); + if (!getOffer(await alice.getMyOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in my offers"); + if (getOffer(await alice.getOffers("buy"), offer.getId())) throw new Error("My offer " + offer.getId() + " should not appear in available offers"); - // cancel the offer - await daemon.cancelOffer(offer.getId()); - - // offer is removed from my offers - if (getOffer(await daemon.getOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was found in my offers after removal"); - - // reserved balance restored - expect(unlockedBalanceBefore).toEqual(BigInt((await daemon.getBalances()).getUnlockedBalance())); -}); + return offer; +} -// ------------------------------- HELPERS ------------------------------------ +async function waitForUnlockedBalance(amount: bigint, ...clients: HavenoDaemon[]) { + throw new Error("waitForUnlockedFunds() not implemented"); // TODO: implement +} function getOffer(offers: OfferInfo[], id: string): OfferInfo | undefined { return offers.find(offer => offer.getId() === id); @@ -113,10 +218,14 @@ function getOffer(offers: OfferInfo[], id: string): OfferInfo | undefined { function testPaymentAccount(paymentAccount: PaymentAccount) { expect(paymentAccount.getId()).toHaveLength; - // TODO: test rest of offer + // TODO: test rest of payment account } function testOffer(offer: OfferInfo) { expect(offer.getId()).toHaveLength; // TODO: test rest of offer +} + +async function wait(durationMs: number) { + return new Promise(function(resolve) { setTimeout(resolve, durationMs); }); } \ No newline at end of file diff --git a/src/HavenoDaemon.tsx b/src/HavenoDaemon.tsx index f0eec6f8..d502f980 100644 --- a/src/HavenoDaemon.tsx +++ b/src/HavenoDaemon.tsx @@ -192,11 +192,11 @@ class HavenoDaemon { } /** - * Remove a posted offer, unreserving its funds. + * Remove a posted offer, releasing its reserved funds. * * @param {string} id - the offer id to cancel */ - async cancelOffer(id: string): Promise { + async removeOffer(id: string): Promise { let that = this; return new Promise(function(resolve, reject) { that._offersClient.cancelOffer(new CancelOfferRequest().setId(id), {password: that._password}, function(err: grpcWeb.Error) { diff --git a/src/protobuf/pb_pb.d.ts b/src/protobuf/pb_pb.d.ts index d28bf844..fbb8493e 100644 --- a/src/protobuf/pb_pb.d.ts +++ b/src/protobuf/pb_pb.d.ts @@ -788,6 +788,11 @@ export class SignOfferRequest extends jspb.Message { getReserveTxKey(): string; setReserveTxKey(value: string): SignOfferRequest; + getReserveTxKeyImagesList(): Array; + setReserveTxKeyImagesList(value: Array): SignOfferRequest; + clearReserveTxKeyImagesList(): SignOfferRequest; + addReserveTxKeyImages(value: string, index?: number): SignOfferRequest; + getPayoutAddress(): string; setPayoutAddress(value: string): SignOfferRequest; @@ -811,6 +816,7 @@ export namespace SignOfferRequest { reserveTxHash: string, reserveTxHex: string, reserveTxKey: string, + reserveTxKeyImagesList: Array, payoutAddress: string, } } @@ -4138,6 +4144,11 @@ export class OfferPayload extends jspb.Message { getArbitratorSignature(): string; setArbitratorSignature(value: string): OfferPayload; + getReserveTxKeyImagesList(): Array; + setReserveTxKeyImagesList(value: Array): OfferPayload; + clearReserveTxKeyImagesList(): OfferPayload; + addReserveTxKeyImages(value: string, index?: number): OfferPayload; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): OfferPayload.AsObject; static toObject(includeInstance: boolean, msg: OfferPayload): OfferPayload.AsObject; @@ -4186,6 +4197,7 @@ export namespace OfferPayload { protocolVersion: number, arbitratorNodeAddress?: NodeAddress.AsObject, arbitratorSignature: string, + reserveTxKeyImagesList: Array, } export enum Direction { diff --git a/src/protobuf/pb_pb.js b/src/protobuf/pb_pb.js index f92046f8..92c8fd79 100644 --- a/src/protobuf/pb_pb.js +++ b/src/protobuf/pb_pb.js @@ -537,7 +537,7 @@ if (goog.DEBUG && !COMPILED) { * @constructor */ proto.io.bisq.protobuffer.SignOfferRequest = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); + jspb.Message.initialize(this, opt_data, 0, -1, proto.io.bisq.protobuffer.SignOfferRequest.repeatedFields_, null); }; goog.inherits(proto.io.bisq.protobuffer.SignOfferRequest, jspb.Message); if (goog.DEBUG && !COMPILED) { @@ -11148,6 +11148,13 @@ proto.io.bisq.protobuffer.GetInventoryResponse.prototype.clearInventoryMap = fun +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.io.bisq.protobuffer.SignOfferRequest.repeatedFields_ = [11]; + if (jspb.Message.GENERATE_TO_OBJECT) { @@ -11189,7 +11196,8 @@ proto.io.bisq.protobuffer.SignOfferRequest.toObject = function(includeInstance, reserveTxHash: jspb.Message.getFieldWithDefault(msg, 8, ""), reserveTxHex: jspb.Message.getFieldWithDefault(msg, 9, ""), reserveTxKey: jspb.Message.getFieldWithDefault(msg, 10, ""), - payoutAddress: jspb.Message.getFieldWithDefault(msg, 11, "") + reserveTxKeyImagesList: (f = jspb.Message.getRepeatedField(msg, 11)) == null ? undefined : f, + payoutAddress: jspb.Message.getFieldWithDefault(msg, 12, "") }; if (includeInstance) { @@ -11270,6 +11278,10 @@ proto.io.bisq.protobuffer.SignOfferRequest.deserializeBinaryFromReader = functio msg.setReserveTxKey(value); break; case 11: + var value = /** @type {string} */ (reader.readString()); + msg.addReserveTxKeyImages(value); + break; + case 12: var value = /** @type {string} */ (reader.readString()); msg.setPayoutAddress(value); break; @@ -11375,10 +11387,17 @@ proto.io.bisq.protobuffer.SignOfferRequest.serializeBinaryToWriter = function(me f ); } + f = message.getReserveTxKeyImagesList(); + if (f.length > 0) { + writer.writeRepeatedString( + 11, + f + ); + } f = message.getPayoutAddress(); if (f.length > 0) { writer.writeString( - 11, + 12, f ); } @@ -11623,11 +11642,48 @@ proto.io.bisq.protobuffer.SignOfferRequest.prototype.setReserveTxKey = function( /** - * optional string payout_address = 11; + * repeated string reserve_tx_key_images = 11; + * @return {!Array} + */ +proto.io.bisq.protobuffer.SignOfferRequest.prototype.getReserveTxKeyImagesList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 11)); +}; + + +/** + * @param {!Array} value + * @return {!proto.io.bisq.protobuffer.SignOfferRequest} returns this + */ +proto.io.bisq.protobuffer.SignOfferRequest.prototype.setReserveTxKeyImagesList = function(value) { + return jspb.Message.setField(this, 11, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.io.bisq.protobuffer.SignOfferRequest} returns this + */ +proto.io.bisq.protobuffer.SignOfferRequest.prototype.addReserveTxKeyImages = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 11, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.io.bisq.protobuffer.SignOfferRequest} returns this + */ +proto.io.bisq.protobuffer.SignOfferRequest.prototype.clearReserveTxKeyImagesList = function() { + return this.setReserveTxKeyImagesList([]); +}; + + +/** + * optional string payout_address = 12; * @return {string} */ proto.io.bisq.protobuffer.SignOfferRequest.prototype.getPayoutAddress = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 11, "")); + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 12, "")); }; @@ -11636,7 +11692,7 @@ proto.io.bisq.protobuffer.SignOfferRequest.prototype.getPayoutAddress = function * @return {!proto.io.bisq.protobuffer.SignOfferRequest} returns this */ proto.io.bisq.protobuffer.SignOfferRequest.prototype.setPayoutAddress = function(value) { - return jspb.Message.setProto3StringField(this, 11, value); + return jspb.Message.setProto3StringField(this, 12, value); }; @@ -36401,7 +36457,7 @@ proto.io.bisq.protobuffer.MailboxStoragePayload.prototype.clearExtraDataMap = fu * @private {!Array} * @const */ -proto.io.bisq.protobuffer.OfferPayload.repeatedFields_ = [17,19]; +proto.io.bisq.protobuffer.OfferPayload.repeatedFields_ = [17,19,1003]; @@ -36471,7 +36527,8 @@ proto.io.bisq.protobuffer.OfferPayload.toObject = function(includeInstance, msg) extraDataMap: (f = msg.getExtraDataMap()) ? f.toObject(includeInstance, undefined) : [], protocolVersion: jspb.Message.getFieldWithDefault(msg, 36, 0), arbitratorNodeAddress: (f = msg.getArbitratorNodeAddress()) && proto.io.bisq.protobuffer.NodeAddress.toObject(includeInstance, f), - arbitratorSignature: jspb.Message.getFieldWithDefault(msg, 1002, "") + arbitratorSignature: jspb.Message.getFieldWithDefault(msg, 1002, ""), + reserveTxKeyImagesList: (f = jspb.Message.getRepeatedField(msg, 1003)) == null ? undefined : f }; if (includeInstance) { @@ -36665,6 +36722,10 @@ proto.io.bisq.protobuffer.OfferPayload.deserializeBinaryFromReader = function(ms var value = /** @type {string} */ (reader.readString()); msg.setArbitratorSignature(value); break; + case 1003: + var value = /** @type {string} */ (reader.readString()); + msg.addReserveTxKeyImages(value); + break; default: reader.skipField(); break; @@ -36960,6 +37021,13 @@ proto.io.bisq.protobuffer.OfferPayload.serializeBinaryToWriter = function(messag f ); } + f = message.getReserveTxKeyImagesList(); + if (f.length > 0) { + writer.writeRepeatedString( + 1003, + f + ); + } }; @@ -37755,6 +37823,43 @@ proto.io.bisq.protobuffer.OfferPayload.prototype.setArbitratorSignature = functi }; +/** + * repeated string reserve_tx_key_images = 1003; + * @return {!Array} + */ +proto.io.bisq.protobuffer.OfferPayload.prototype.getReserveTxKeyImagesList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1003)); +}; + + +/** + * @param {!Array} value + * @return {!proto.io.bisq.protobuffer.OfferPayload} returns this + */ +proto.io.bisq.protobuffer.OfferPayload.prototype.setReserveTxKeyImagesList = function(value) { + return jspb.Message.setField(this, 1003, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.io.bisq.protobuffer.OfferPayload} returns this + */ +proto.io.bisq.protobuffer.OfferPayload.prototype.addReserveTxKeyImages = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 1003, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.io.bisq.protobuffer.OfferPayload} returns this + */ +proto.io.bisq.protobuffer.OfferPayload.prototype.clearReserveTxKeyImagesList = function() { + return this.setReserveTxKeyImagesList([]); +}; + +