Extending the Haveno API
This guide is for contributors who add or change calls in Haveno's API. It covers how the API is built, how to run its end-to-end tests, and how to add a new call.
Just want to use the API?
To build a program on top of Haveno, see Haveno API. The calls are documented in the API reference.
How it works
The API spans two repositories: haveno, which serves it from the daemon, and haveno-ts, the TypeScript client and its end-to-end tests. A call passes through these layers:
| Layer | Location | Role |
|---|---|---|
| Protobuf definitions | proto/.../grpc.proto |
Services, requests, and replies |
| gRPC services | daemon/.../grpc/ |
Convert between protobuf and core types, handle errors, set rate limits |
| Core API | core/.../api/CoreApi.java |
Single entry point to the core |
| Core services | core/.../api/ |
Implement each call against wallets, offers, trades, and the network |
| Client | src/HavenoClient.ts |
Typed method for each call |
| Tests | src/HavenoClient.test.ts |
End-to-end tests against live daemons |
Envoy translates the browser-compatible gRPC-Web used by haveno-ts into gRPC. The daemon checks the API password and rate limits before a call reaches its service.
For example, getOffers is served by GrpcOffersService.getOffers, which calls CoreApi.getOffers, then CoreOffersService.getOffers.
Set up a test network
The tests run against a local Monero testnet and Haveno network on your machine.
Requirements
- Haveno build dependencies
- Node.js
- The Envoy proxy, e.g.
brew install envoy - The protobuf compiler
protocv29.3, with theprotoc-gen-jsandprotoc-gen-grpc-webplugins installed as shown here
Start the network
-
Clone both repositories into the same parent directory, since haveno-ts builds against
../haveno, and build Haveno: -
From
haveno, start each of the following in its own terminal: -
From
haveno-ts, start Envoy in another terminal: -
From
haveno-ts, install dependencies. This also generates the client's protobuf code from../haveno/proto:
The tests start the arbitrator, user1, and user2 daemons if they are not already running, and handle mining, funding, and arbitrator registration. To follow a daemon's output, or to debug it, start it yourself first with make arbitrator-daemon-local, make user1-daemon-local, or make user2-daemon-local. The tests connect to running daemons instead of starting new ones.
Windows
Run the commands from Git Bash, or MSYS2 MINGW64 for the make targets. Envoy runs in WSL2, or download Envoy v1.28.0 and add it to your PATH. Install the JavaScript plugin with npm install --global protoc-gen-js. See the haveno-ts test notes for details.
Docker
To work on haveno-ts alone, start the whole network with docker compose up from haveno-ts/docker instead. It builds Haveno's master branch, so it does not include your local changes to Haveno.
Run the tests
From haveno-ts, run a test by name:
Omit -t to run the full suite. To run the subset that runs in continuous integration, select the tests tagged CI:
The daemons log errors as the tests exercise invalid inputs, so errors in their output are expected.
Tip
Daemons started by the tests run the current build of Haveno. A daemon you started yourself keeps running the old code, so restart it after rebuilding.
Add an API call
-
Define the call. Add the
rpcand its request and reply messages togrpc.proto. Give new fields new numbers, and never renumber or reuse an existing field, so existing clients keep working. -
Implement it in the core. Add the logic to the matching
Core*Serviceincore/.../api/, and expose it throughCoreApi. -
Serve it from the daemon. Override the generated method in the matching
Grpc*Service, following its existing methods:@Override public void getOffers(GetOffersRequest req, StreamObserver<GetOffersReply> responseObserver) { try { List<OfferInfo> result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode()) .stream().map(OfferInfo::toOfferInfo) .collect(Collectors.toList()); var reply = GetOffersReply.newBuilder() .addAllOffers(result.stream().map(OfferInfo::toProtoMessage).collect(Collectors.toList())) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } }Then add a rate limit for the call in the service's
rateMeteringInterceptor(). A call without one is not rate limited. -
Rebuild Haveno from
haveno: -
Regenerate the client's protobuf code from
haveno-ts: -
Add a client method to
HavenoClient.ts. Its doc comment becomes the API reference, so describe each parameter and the return value:/** * Get available offers to buy or sell XMR. * * @param {string} assetCode - traded asset code * @param {OfferDirection|undefined} direction - "buy" or "sell" (default all) * @return {OfferInfo[]} the available offers */ async getOffers(assetCode: string, direction?: OfferDirection): Promise<OfferInfo[]> { try { ... } catch (e: any) { throw new HavenoError(e.message, e.code); } } -
Add tests to
HavenoClient.test.ts, following the existing tests. IncludeCIin a test's name tags, e.g."Can get offers (Test, CI)", to run it in continuous integration. -
Run the tests, first your new test by name and then the full suite, as in Run the tests.
-
Optionally add it to the CLI. Haveno CLI commands are defined in
cli/, inMethod.java,GrpcClient.java, andCliMain.java. -
Open pull requests to haveno and haveno-ts. The haveno-ts tests build Haveno's
masterbranch, so they pass once the haveno pull request is merged.
Further reading
- API reference: complete method documentation
- Haveno API: using the API from a program
- Build and run Haveno: building Haveno and running a local network by hand
- Contributor guidelines: how to submit changes
Questions?
For help, join the Matrix room.