-
Notifications
You must be signed in to change notification settings - Fork 81
WIP Produce the simplest-possible getting started guide for the Bisq HTTP API #54
Changes from 10 commits
95a2f88
e778141
37f2b19
70ce251
7ee80ee
ae5b54a
24ffd06
eea7498
911f6ca
17a48d1
d73a565
b73db83
10be120
7d8e78c
7eb1466
56d3185
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
= Monitoring Bisq offers with Http API | ||
|
||
:sectlinks: | ||
:sectanchors: | ||
|
||
This guide walks you through the process of creating a bot that monitors available offers. | ||
|
||
== What you'll build | ||
|
||
You'll build a NodeJS-based script that connects to Bisq over an HTTP API to get offers and market prices and displays "interesting" offers on the console if any is found. | ||
|
||
== What you’ll need | ||
|
||
* About 15 minutes | ||
* A favorite text editor or IDE | ||
* NodeJS 6+ | ||
* Docker (to run from an image) or Git and Maven (to build from source) | ||
|
||
[NOTE] | ||
Bisq HTTP API is currently incubating. This means that things may be a bit rough around the edges | ||
and your feedback (see the link:#next-steps[next steps] section for details). | ||
|
||
== Run the API | ||
|
||
There are two alternative ways you can run an instance of the Bisq HTTP API, either from a Docker image or from source. | ||
|
||
=== Run the API using Docker | ||
|
||
The easiest way to run the API in headless mode is by using a Docker image: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could 'headless' be clarified here? Knowing virtually nothing about how this API works, my initial impression was that the API would query my local Bisq client for data, and that 'headless' simply meant 'no GUI' but that's clearly not the case. Basically I wasn't sure if the API needed my Bisq client to be running in the background. A quick 1 or 2 lines about what these images actually do / how the API works might help? Also, how does one know when the Docker image is done setting up? Is it when the Bisq ASCII art shows? I watched the terminal for a while until I realized that what I was seeing was actually streaming data, and API calls were already working. |
||
|
||
docker run -it --rm -p 8080:8080 -e BISQ_API_HOST=0.0.0.0 bisq/api | ||
|
||
=== Run the API from source | ||
|
||
For more hard-core developers that want to run from source: | ||
|
||
git clone https://github.com/mrosseel/bisq-api | ||
cd bisq-api | ||
mvn compile exec:java \ | ||
-Dexec.mainClass="network.bisq.api.app.ApiMain" \ | ||
-Dexec.args="--appName=http-api-monitor-offers" | ||
|
||
[NOTE] | ||
Since the API is in incubating phase we suggest to run it against different database directory then the default one. | ||
To do that, use `--appName` parameter. You can set it to whatever you like, just keep it different from `Bisq`. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
=== Verify the API is running | ||
|
||
In both cases the API should be running on port 8080. You can verify that by executing: | ||
|
||
curl http://localhost:8080/api/v1/version | ||
|
||
You should receive a response like the following: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
"application": "0.6.7", | ||
"network": 1, | ||
"p2PMessage": 10, | ||
"localDB": 1, | ||
"tradeProtocol": 1 | ||
} | ||
---- | ||
|
||
[NOTE] | ||
Complete interactive API documentation is available at http://localhost:8080/swagger. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd make this stand out a bit more. I don't know if it was just me, but I glazed over it the first few times I saw it. (I also wasn't aware of Swagger, so I just thought it was a cool pathname). Maybe take it out of a NOTE and make it something like " |
||
|
||
== API overview | ||
|
||
First we will look at the endpoints we need to fetch the data from and how they responses look like. | ||
|
||
=== List available offers | ||
|
||
Open offers are available at http://localhost:8080/api/v1/offers. | ||
|
||
The response should look like this: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
"offers": [OfferDetail], | ||
"total": integer | ||
} | ||
---- | ||
|
||
Where `offers` is an array with individual offers and `total` is number of all offers. The model for each individual `OfferDetail` is defined as follows: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure there's a lot of value in including the whole model here. As a reader, I feel like I'm supposed to do something with this, when in fact it's just for informational purposes. As an alternative, I'd deep-link the user to the swagger API docs that show this same information, and possibly do it in an INFO admonition so that the reader understands this is "just if they care / want to go deeper", and that it's not part of the critical path of the step-by-step guide. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is problematic cause the link looks like |
||
|
||
[source,json] | ||
---- | ||
{ | ||
acceptedBankIds [string] | ||
acceptedCountryCodes [string] | ||
amount integer($int64) | ||
arbitratorNodeAddresses [string] | ||
bankId string | ||
baseCurrencyCode string | ||
blockHeightAtOfferCreation integer($int64) | ||
buyerSecurityDeposit integer($int64) | ||
counterCurrencyCode string | ||
countryCode string | ||
currencyCode string | ||
date string($date-time) | ||
direction string Enum: [ BUY, SELL ] | ||
hashOfChallenge string | ||
id string | ||
isCurrencyForMakerFeeBtc boolean | ||
isPrivateOffer boolean | ||
lowerClosePrice integer($int64) | ||
makerFee integer($int64) | ||
makerPaymentAccountId string | ||
marketPriceMargin number($double) | ||
maxTradeLimit integer($int64) | ||
maxTradePeriod integer($int64) | ||
minAmount integer($int64) | ||
offerFeePaymentTxId string | ||
ownerNodeAddress string | ||
paymentMethodId string | ||
price integer($int64) | ||
protocolVersion integer($int32) | ||
sellerSecurityDeposit integer($int64) | ||
state string Enum: [ UNKNOWN, OFFER_FEE_PAID, AVAILABLE, NOT_AVAILABLE, REMOVED, MAKER_OFFLINE ] | ||
txFee integer($int64) | ||
upperClosePrice integer($int64) | ||
useAutoClose boolean | ||
useMarketBasedPrice boolean | ||
useReOpenAfterAutoClose boolean | ||
versionNr string | ||
} | ||
---- | ||
|
||
Now let's assume that we want to buy bitcoin, and let's define "interesting" offers as those that: | ||
|
||
. sell bitcoin at a discount 1% or more under the current market price, and | ||
. accept payment in Euros to a Polish SEPA account | ||
|
||
First we need to filter those offers using following static criteria: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
baseCurrencyCode: 'BTC', | ||
counterCurrencyCode: 'EUR', | ||
direction: 'SELL', | ||
paymentMethodId: 'SEPA' | ||
} | ||
---- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
(Very quickly written, but you get the idea) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've introduced |
||
|
||
Next we need to filter those offers by price. There are two types of offers: _market-based price offers_ and _fixed price offers_. They are distinguished by the `useMarketBasedPrice` attribute. In case of market-based price offers the filtering criteria is easy: the `marketPriceMargin` value must be above 0.1. In case of fixed price offers we have to fetch market price of BTC in EUR, then calculate whether the price is 1% or less, and finally we must filter offers which have a price _less_ than that calculated price. | ||
|
||
=== Get the market price | ||
|
||
In order to get the market price of BTC in EUR, execute the following query: | ||
|
||
curl http://localhost:8080/api/v1/currencies/prices?currencyCodes=EUR | ||
|
||
You should receive a response like the following: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
"prices": { | ||
"EUR": 7035.62 | ||
} | ||
} | ||
---- | ||
|
||
== Write the monitoring bot code | ||
|
||
Let's install some dependencies: | ||
|
||
npm install lodash http-as-promised | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be helpful to mention adding the I built the script as I read along, and ended up with errors because I didn't |
||
|
||
In general our script will look like this: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the sections that follow, I recommend that all source code snippets:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. both done |
||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=flow] | ||
---- | ||
|
||
So first 2 things to do (concurrently and asynchronously) is to fetch offers and market price of BTC from our API. Then we need to filter those offers and finally display the results. | ||
|
||
Getting offers is a simple call that returns promise with array of offers: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=getOffers] | ||
---- | ||
|
||
Similarly with `getMarketPrice`: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=getMarketPrice] | ||
---- | ||
|
||
Now our `filterOffers` function is ready to receive an array of results from the two functions described above: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=filterOffers] | ||
---- | ||
|
||
This function filters offers to match our criteria. It returns matching offers and maps them to a bit simpler structure that contains as little data as needed for `notify` function. We are using `lodash` library to simplify the filtering. | ||
|
||
The `getPriceFilter` function creates the actual filter function and looks like this: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=getPriceFilter] | ||
---- | ||
|
||
We are multiplying `marketPrice` by `10000` because that is the format in which the API returns the price. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
Here is a full script. | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=**;*] | ||
---- | ||
|
||
Now run | ||
|
||
node http-api-monitor-offers.js | ||
|
||
If there are any matching offers you should see something like this: | ||
|
||
5 interesting BTC offers from Bisq | ||
0.0625 BTC (-2%) | ||
0.01 BTC (-2%) | ||
0.01 BTC (-5%) | ||
0.033 BTC (-3%) | ||
0.02 BTC (-1.5%) | ||
0.25 BTC (-6%) | ||
|
||
If there are no matching offers, try fiddling with the value of `threshold`. Try setting it to -20, 0.01, etc. | ||
|
||
== Congratulations | ||
|
||
You are now able to monitor Bisq offers via HTTP API! | ||
|
||
== Next steps | ||
|
||
* Try adding a loop to keep the process repeating the search periodically (use _setInterval_) | ||
* Join our slack at https://bisq.slack.com and leave feedback on the API and this guide | ||
* Report https://github.com/mrosseel/bisq-api/issues[issues] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
const _ = require('lodash'); | ||
const $http = require('http-as-promised'); | ||
|
||
const threshold = 0.1; | ||
|
||
//tag::getPriceFilter[] | ||
function getPriceFilter(marketPrice) { | ||
const maxPrice = marketPrice * (1 - threshold) * 10000; | ||
return offer => { | ||
if (offer.useMarketBasedPrice) | ||
return offer.marketPriceMargin >= threshold; | ||
return offer.price < maxPrice; | ||
} | ||
} | ||
//end::getPriceFilter[] | ||
|
||
//tag::getMarketPrice[] | ||
function getMarketPrice() { | ||
return $http.get('http://localhost:8080/api/v1/currencies/prices?currencyCodes=EUR', {resolve: 'body', json: true}) | ||
.then(body => _.get(body, 'prices.EUR')) | ||
} | ||
//end::getMarketPrice[] | ||
|
||
//tag::getOffers[] | ||
function getOffers() { | ||
return $http.get('http://localhost:8080/api/v1/offers', {resolve: 'body', json: true}).then(body => body.offers); | ||
} | ||
//end::getOffers[] | ||
|
||
function notify(offers) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aside from the Just FYI. I guess it's because this is structured as a bottom-up walk-through. If it were reversed (i.e., top-down approach with full script first and then important parts clarified), it would be fine to pick and choose what to cover. |
||
if (!offers.length) { | ||
console.log('No interesting offers found'); | ||
return; | ||
} | ||
|
||
const text = _.map(offers, offer => `${offer.amount / 100000000} BTC (-${_.round(offer.margin * 100, 2)}%)`).join('\n'); | ||
console.info(text); | ||
} | ||
|
||
//tag::filterOffers[] | ||
function filterOffers([offers, marketPrice]) { | ||
return _(offers) | ||
.filter({ | ||
baseCurrencyCode: 'BTC', | ||
counterCurrencyCode: 'EUR', | ||
direction: 'SELL', | ||
paymentMethodId: 'SEPA' | ||
}) | ||
.filter(i => _.includes(i.acceptedCountryCodes, 'PL')) | ||
.filter(getPriceFilter(marketPrice)) | ||
.map(i => _.pick(i, 'baseCurrencyCode', 'counterCurrencyCode', 'direction', 'paymentMethodId', 'id', 'useMarketBasedPrice', 'price', 'marketPriceMargin', 'amount', 'minAmont')) | ||
.map(i => ({amount: i.amount, margin: i.useMarketBasedPrice ? i.marketPriceMargin : marketPrice / i.price})) | ||
.value(); | ||
} | ||
//end::filterOffers[] | ||
|
||
//tag::flow[] | ||
Promise.all([getOffers(), getMarketPrice()]) | ||
.then(filterOffers) | ||
.then(notify) | ||
.catch(e => console.error(e)); | ||
//end::flow[] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.