-
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 8 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,327 @@ | ||
= Monitoring Bisq offers with Http API | ||
|
||
This guide walks you through the process of creating a bot that monitors available offers and sends email notifications. | ||
|
||
== 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 sends email notification whenever "interesting" offers are detected. | ||
|
||
== 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) | ||
* A GMail account | ||
|
||
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.
|
||
== 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="io.bisq.api.app.ApiMain" \ | ||
-Dexec.args="--appName=bisqmon" | ||
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 |
||
|
||
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 " |
||
|
||
== 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 | ||
} | ||
} | ||
---- | ||
|
||
== The JavaScript part | ||
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.
"The JavaScript part" sounds boring or like some random detail, whereas "Writing the bot" sounds like what I came here to do. 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. |
||
|
||
Let's install some dependencies: | ||
|
||
npm install lodash http-as-promised nodemailer | ||
|
||
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 |
||
|
||
[source,javascript] | ||
---- | ||
Promise.all([getOffers(), getMarketPrice()]) | ||
.then(filterOffers) | ||
.then(notify) | ||
.catch(e => console.error(e)); | ||
---- | ||
|
||
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 send notification. | ||
|
||
Getting offers is a simple call that returns promise with array of offers: | ||
|
||
[source,javascript] | ||
---- | ||
function getOffers() { | ||
return $http.get('http://localhost:8080/api/v1/offers', {resolve: 'body', json: true}) | ||
.then(body => body.offers); | ||
} | ||
---- | ||
|
||
Similarly with `getMarketPrice`: | ||
|
||
[source,javascript] | ||
---- | ||
function getMarketPrice() { | ||
return $http.get('http://localhost:8080/api/v1/currencies/prices?currencyCodes=EUR', {resolve: 'body', json: true}) | ||
.then(body => _.get(body, 'prices.EUR')) | ||
} | ||
---- | ||
|
||
Now our `filterOffers` function is ready to receive an array of results from the two functions described above: | ||
|
||
[source,javascript] | ||
---- | ||
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 => ({amount: i.amount, margin: i.useMarketBasedPrice ? i.marketPriceMargin : marketPrice / i.price})) | ||
.value(); | ||
} | ||
---- | ||
|
||
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: | ||
|
||
[source,javascript] | ||
---- | ||
function getPriceFilter(marketPrice) { | ||
const maxPrice = marketPrice * (1 - threshold) * 10000; | ||
return offer => { | ||
if (offer.useMarketBasedPrice) | ||
return offer.marketPriceMargin >= threshold; | ||
return offer.price < maxPrice; | ||
} | ||
} | ||
---- | ||
|
||
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. You must substitute `EMAIL_ACCOUNT_USERNAME`, `EMAIL_ACCOUNT_PASSWORD`, `EMAIL_FROM_ADDRESS` and `EMAIL_TO_ADDRESS` | ||
with appropriate values. | ||
|
||
[source,javascript] | ||
---- | ||
const _ = require('lodash'); | ||
const $http = require('http-as-promised'); | ||
const nodemailer = require('nodemailer'); | ||
|
||
const threshold = 0.1; | ||
|
||
const EMAIL_ACCOUNT_USERNAME = 'email@gmail.com'; | ||
const EMAIL_ACCOUNT_PASSWORD = 'secret'; | ||
const EMAIL_FROM_ADDRESS = 'from@gmail.com'; | ||
const EMAIL_TO_ADDRESS = 'to@gmail.com'; | ||
|
||
function getPriceFilter(marketPrice) { | ||
const maxPrice = marketPrice * (1 - threshold) * 10000; | ||
return offer => { | ||
if (offer.useMarketBasedPrice) | ||
return offer.marketPriceMargin >= threshold; | ||
return offer.price < maxPrice; | ||
} | ||
} | ||
|
||
function getMarketPrice() { | ||
return $http.get('http://localhost:8080/api/v1/currencies/prices?currencyCodes=EUR', {resolve: 'body', json: true}) | ||
.then(body => _.get(body, 'prices.EUR')) | ||
} | ||
|
||
function getOffers() { | ||
return $http.get('http://localhost:8080/api/v1/offers', {resolve: 'body', json: true}).then(body => body.offers); | ||
} | ||
|
||
function notify(offers) { | ||
if (!offers.length) { | ||
console.log('No interesting offers found'); | ||
return; | ||
} | ||
|
||
const transporter = nodemailer.createTransport({ | ||
service: 'gmail', | ||
auth: { | ||
user: EMAIL_ACCOUNT_USERNAME, | ||
pass: EMAIL_ACCOUNT_PASSWORD | ||
} | ||
}); | ||
|
||
const text = _.map(offers, offer => `${offer.amount / 100000000} BTC (-${_.round(offer.margin * 100, 2)}%)`).join('\n'); | ||
|
||
const mailOptions = { | ||
from: EMAIL_FROM_ADDRESS, | ||
to: EMAIL_TO_ADDRESS, | ||
subject: `${offers.length} interesting BTC offers from Bisq`, | ||
text: text | ||
}; | ||
|
||
transporter.sendMail(mailOptions, function (error) { | ||
if (error) { | ||
console.log(error); | ||
} else { | ||
console.log(`Notification about ${offers.length} offers sent`); | ||
} | ||
}); | ||
} | ||
|
||
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(); | ||
} | ||
|
||
Promise.all([getOffers(), getMarketPrice()]) | ||
.then(filterOffers) | ||
.then(notify) | ||
.catch(e => console.error(e)); | ||
---- | ||
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 |
||
|
||
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.
|
||
If there are any matching offers you should receive an mail 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.
|
||
|
||
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%) | ||
|
||
[NOTE] | ||
If you would like to use something else that gmail then you will need a bit different mail transport configuration. For reference look at https://nodemailer.com/smtp/. | ||
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.
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
* *_User Docs_* | ||
** <<intro#, Introduction>> — What Bisq is, why it exists and how it works | ||
** <<getting-started#, Getting Started>> — Go from zero to trading in 15 minutes | ||
** <<http-api#, Getting Started - HTTP API>> — How to access Bisq programatically | ||
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.
|
||
|
||
* *_Contributor Docs_* | ||
** <<contributor-checklist#, New contributor checklist>> | ||
|
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.
I'd like to drop the email notifications. It makes sense for a script you'd actually want to run in real life, but here in a "let's build the simplest possible bot that could actually work for the purposes of getting our heads around the Bisq HTTP API" guide, all the stuff around GMail and email libraries and usernames and passwords and the code required to bootstrap it all is just noise and makes the whole thing longer and more complicated than it needs to be. As I walked through this guide myself, I punted when I got to this point; I just didn't want to deal. Because: (a) I never use my GMail account, (b) it's set up for 2FA anyway so a simple username / password won't work, (c) I don't want to set up a test account just for the purposes of this guide that told me it would take 15 minutes and is now in fact going to suck up more of my time than I want it to.
As an alternative, just print interesting offers (i.e. those that match the filter) to the command line whenever they show up, or if for some reason that is not idiomatic, then to a file that the user can tail.