1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
use std::collections::HashMap;
use std::future::Future;
use std::string::ToString;

use reqwest::Client;
use rust_decimal::Decimal;
use serde::de::DeserializeOwned;

use crate::{
    accounts, beneficiaries, credentials, market, orders, quotes, trades, transactions, urls,
};

const API_BASE: &str = "https://api.mybitx.com/api/1/";

/// The top level client for interacting with the Luno API.
pub struct LunoClient {
    pub(crate) credentials: credentials::Credentials,
    pub(crate) http: Client,
    pub(crate) url_maker: urls::UrlMaker,
}

impl LunoClient {
    pub fn new(key: String, secret: String) -> LunoClient {
        let credentials = credentials::Credentials::new(key, secret);
        let http = Client::new();
        let url_maker = urls::UrlMaker::new(API_BASE.to_owned());

        LunoClient {
            credentials,
            url_maker,
            http,
        }
    }

    pub(crate) async fn get<T>(&self, url: reqwest::Url) -> Result<T, reqwest::Error>
    where
        T: DeserializeOwned,
    {
        self.http
            .get(url)
            .basic_auth(
                self.credentials.key.to_owned(),
                Some(self.credentials.secret.to_owned()),
            )
            .send()
            .await?
            .json::<T>()
            .await
    }

    pub(crate) async fn put<T>(&self, url: reqwest::Url) -> Result<T, reqwest::Error>
    where
        T: DeserializeOwned,
    {
        self.http
            .put(url)
            .basic_auth(
                self.credentials.key.to_owned(),
                Some(self.credentials.secret.to_owned()),
            )
            .send()
            .await?
            .json::<T>()
            .await
    }

    pub(crate) async fn delete<T>(&self, url: reqwest::Url) -> Result<T, reqwest::Error>
    where
        T: DeserializeOwned,
    {
        self.http
            .delete(url)
            .basic_auth(
                self.credentials.key.to_owned(),
                Some(self.credentials.secret.to_owned()),
            )
            .send()
            .await?
            .json::<T>()
            .await
    }

    /// Returns the latest ticker indicators.
    pub fn get_ticker(
        &self,
        pair: market::TradingPair,
    ) -> impl Future<Output = Result<market::Ticker, reqwest::Error>> + '_ {
        let url = self.url_maker.ticker(pair);
        self.get(url)
    }

    /// Returns the latest ticker indicators from all active Luno exchanges.
    pub fn list_tickers(
        &self,
    ) -> impl Future<Output = Result<market::TickerList, reqwest::Error>> + '_ {
        let url = self.url_maker.tickers();
        self.get(url)
    }

    /// Returns a list of the top 100 bids and asks in the order book.
    /// Ask orders are sorted by price ascending.
    /// Bid orders are sorted by price descending. Orders of the same price are aggregated.
    pub fn get_orderbook_top(
        &self,
        pair: market::TradingPair,
    ) -> impl Future<Output = Result<market::Orderbook, reqwest::Error>> + '_ {
        let url = self.url_maker.orderbook_top(pair);
        self.get(url)
    }

    /// Returns a list of all bids and asks in the order book.
    /// Ask orders are sorted by price ascending. Bid orders are sorted by price descending.
    /// Multiple orders at the same price are not aggregated.
    ///
    /// Warning: This may return a large amount of data. Generally you should rather use `get_orderbook_top` or the Streaming API.
    pub fn get_orderbook(
        &self,
        pair: market::TradingPair,
    ) -> impl Future<Output = Result<market::Orderbook, reqwest::Error>> + '_ {
        let url = self.url_maker.orderbook(pair);
        self.get(url)
    }

    /// Returns a list of the most recent trades that happened in the last 24h.
    /// At most 100 results are returned per call.
    pub fn list_trades(
        &self,
        pair: market::TradingPair,
    ) -> impl Future<Output = Result<market::TradeList, reqwest::Error>> + '_ {
        let url = self.url_maker.trades(pair);
        self.get(url)
    }

    /// This request creates an account for the specified currency.
    /// Please note that the balances for the Account will be displayed based on the `asset` value,
    /// which is the currency the account is based on.
    ///
    /// Permissions required: `Perm_W_Addresses`.
    pub async fn create_account(
        &self,
        currency: market::Currency,
        name: &str,
    ) -> Result<accounts::Account, reqwest::Error> {
        let url = self.url_maker.accounts();
        let mut params = HashMap::new();
        params.insert("currency", currency.to_string());
        params.insert("name", name.to_string());

        self.http
            .post(url)
            .basic_auth(
                self.credentials.key.to_owned(),
                Some(self.credentials.secret.to_owned()),
            )
            .form(&params)
            .send()
            .await?
            .json::<accounts::Account>()
            .await
    }

    /// Update the name of an account with a given ID,
    ///
    /// `Perm_W_Addresses`
    pub fn update_account_name(
        &self,
        account_id: &str,
        name: &str,
    ) -> impl Future<Output = Result<accounts::UpdateAccountNameResponse, reqwest::Error>> + '_
    {
        let url = self.url_maker.account_name(account_id, name);
        self.put(url)
    }

    /// The list of all accounts and their respective balances for the requesting user.
    ///
    /// Permissions required: `Perm_R_Balance`.
    pub fn list_balances(
        &self,
    ) -> impl Future<Output = Result<accounts::BalanceList, reqwest::Error>> + '_ {
        let url = self.url_maker.balance();
        self.get(url)
    }

    /// Return a list of transaction entries from an account.
    ///
    /// Transaction entry rows are numbered sequentially starting from 1, where 1 is the oldest entry.
    /// The range of rows to return are specified with the `min_row` (inclusive) and `max_row` (exclusive) parameters.
    /// At most 1000 rows can be requested per call.
    ///
    /// If `min_row` or `max_row` is non-positive, the range wraps around the most recent row.
    /// For example, to fetch the 100 most recent rows, use `min_row=-100` and `max_row=0`.
    ///
    /// Permissions required: `Perm_R_Transactions`.
    pub fn list_transactions(
        &self,
        account_id: &str,
        min_row: i64,
        max_row: i64,
    ) -> impl Future<Output = Result<transactions::TransactionList, reqwest::Error>> + '_ {
        let url = self.url_maker.transactions(account_id, min_row, max_row);
        self.get(url)
    }

    /// Return a list of all transactions that have not completed for the account.
    ///
    /// Pending transactions are not numbered, and may be reordered, deleted or updated at any time.
    ///
    /// Permissions required: `Perm_R_Transactions`.
    pub fn list_pending_transactions(
        &self,
        account_id: &str,
    ) -> impl Future<Output = Result<transactions::PendingTransactionList, reqwest::Error>> + '_
    {
        let url = self.url_maker.pending_transactions(account_id);
        self.get(url)
    }

    /// Returns a list of bank beneficiaries
    ///
    /// Permissions required: Perm_R_Beneficiaries
    pub fn list_beneficiaries(
        &self,
    ) -> impl Future<Output = Result<beneficiaries::ListBeneficiariesResponse, reqwest::Error>> + '_
    {
        let url = self.url_maker.beneficiaries();
        self.get(url)
    }

    /// Get a list of the most recently placed orders.
    /// Note that `list_orders()` returns a `ListOrdersBuilder`
    /// that allows you chain pair and state filters onto your
    /// request.
    pub fn list_orders(&self) -> orders::ListOrdersBuilder {
        orders::ListOrdersBuilder {
            luno_client: self,
            url: self.url_maker.list_orders(),
            limit: None,
            created_before: None,
            pair: None,
            state: None,
        }
    }

    /// Create a new trade order.
    ///
    /// Warning! Orders cannot be reversed once they have executed.
    /// Please ensure your program has been thoroughly tested before submitting orders.
    ///
    /// If no `base_account_id` or `counter_account_id` are specified, your default base currency or counter currency account will be used.
    /// You can find your account IDs by calling `list_balances()`.
    pub fn limit_order(
        &self,
        pair: market::TradingPair,
        order_type: orders::LimitOrderType,
        volume: Decimal,
        price: Decimal,
    ) -> orders::PostLimitOrderBuilder {
        let mut params = HashMap::new();
        params.insert("pair", pair.to_string());
        params.insert("type", order_type.to_string());
        params.insert("volume", volume.to_string());
        params.insert("price", price.to_string());
        orders::PostLimitOrderBuilder {
            luno_client: self,
            url: self.url_maker.post_order(),
            params,
        }
    }

    /// Create a new market order.
    ///
    /// A market order executes immediately, and either buys as much cryptocurrency that can be bought for
    /// a set amount of fiat currency, or sells a set amount of cryptocurrency for as much fiat as possible.
    ///
    /// Warning! Orders cannot be reversed once they have executed.
    /// Please ensure your program has been thoroughly tested before submitting orders.
    ///
    /// If no base_account_id or counter_account_id are specified, your default base currency or counter currency account will be used.
    /// You can find your account IDs by calling the `list_balances()`.
    pub fn market_order(
        &self,
        pair: market::TradingPair,
        order_type: orders::MarketOrderType,
        volume: Decimal,
    ) -> orders::PostMarketOrderBuilder {
        let mut params = HashMap::new();
        params.insert("pair", pair.to_string());
        params.insert("type", order_type.to_string());
        match order_type {
            orders::MarketOrderType::BUY => params.insert("counter_volume", volume.to_string()),
            orders::MarketOrderType::SELL => params.insert("base_volume", volume.to_string()),
        };
        orders::PostMarketOrderBuilder {
            luno_client: self,
            url: self.url_maker.market_order(),
            params,
        }
    }

    /// Request to stop an order.
    pub async fn stop_order(
        &self,
        order_id: &str,
    ) -> Result<orders::StopOrderResponse, reqwest::Error> {
        let url = self.url_maker.stop_order();
        let mut params = HashMap::new();
        params.insert("order_id", order_id.to_string());

        self.http
            .post(url)
            .basic_auth(
                self.credentials.key.to_owned(),
                Some(self.credentials.secret.to_owned()),
            )
            .form(&params)
            .send()
            .await?
            .json::<orders::StopOrderResponse>()
            .await
    }

    /// Get an order by its ID.
    pub fn get_order(
        &self,
        order_id: &str,
    ) -> impl Future<Output = Result<orders::Order, reqwest::Error>> + '_ {
        let url = self.url_maker.orders(order_id);
        self.get(url)
    }

    /// Returns a list of your recent trades for a given pair, sorted by oldest first. If `before` is specified, then the trades are returned sorted by most recent first.
    ///
    /// `type` in the response indicates the type of order that you placed in order to participate in the trade. Possible types: `BID`, `ASK`.
    ///
    /// If `is_buy` in the response is true, then the order which completed the trade (market taker) was a bid order.
    ///
    /// Results of this query may lag behind the latest data.
    pub fn list_own_trades(&self, pair: market::TradingPair) -> trades::ListTradesBuilder {
        trades::ListTradesBuilder {
            luno_client: self,
            url: self.url_maker.list_trades(pair),
            since: None,
            before: None,
            after_seq: None,
            before_seq: None,
            sort_desc: None,
            limit: None,
        }
    }

    /// Returns the fees and 30 day trading volume (as of midnight) for a given currency pair.
    /// For complete details, please see [Fees & Features](https://www.luno.com/en/countries).
    pub fn get_fee_info(
        &self,
        pair: market::TradingPair,
    ) -> impl Future<Output = Result<trades::FeeInfo, reqwest::Error>> + '_ {
        let url = self.url_maker.fee_info(pair);
        self.get(url)
    }

    /// Creates a new quote to buy or sell a particular amount of a base currency for a counter currency.
    ///
    /// Users can specify either the exact amount to pay or the exact amount to receive.
    ///
    /// For example, to buy exactly 0.1 Bitcoin using ZAR, you would create a quote to BUY 0.1 XBTZAR.
    /// The returned quote includes the appropriate ZAR amount. To buy Bitcoin using exactly ZAR 100, create a quote to SELL 100 ZARXBT.
    /// The returned quote specifies the Bitcoin as the counter amount returned.
    ///
    /// An error is returned if the Account is not verified for the currency pair, or if the Account would have insufficient balance to ever exercise the quote.
    ///
    /// Permissions required: `Perm_W_Orders`
    pub fn quote(
        &self,
        order_type: orders::MarketOrderType,
        base_amount: Decimal,
        pair: market::TradingPair,
    ) -> quotes::CreateQuoteBuilder {
        let mut params = HashMap::new();
        params.insert("type", order_type.to_string());
        params.insert("base_amount", base_amount.to_string());
        params.insert("pair", pair.to_string());
        quotes::CreateQuoteBuilder {
            luno_client: self,
            url: self.url_maker.quotes(),
            params,
        }
    }

    /// Get the latest status of a quote by its id.
    ///
    /// Permissions required: `Perm_R_Orders`
    pub fn get_quote(
        &self,
        id: &str,
    ) -> impl Future<Output = Result<quotes::Quote, reqwest::Error>> + '_ {
        let url = self.url_maker.quote_action(id);
        self.get(url)
    }

    /// Exercise a quote to perform the Trade.
    /// If there is sufficient balance available in the Account, it will be debited and the counter amount credited.
    ///
    /// An error is returned if the quote has expired or if the Account has insufficient available balance.
    ///
    /// Permissions required: `Perm_W_Orders`
    pub fn exercise_quote(
        &self,
        id: &str,
    ) -> impl Future<Output = Result<quotes::Quote, reqwest::Error>> + '_ {
        let url = self.url_maker.quote_action(id);
        self.put(url)
    }

    /// Discard a Quote.
    /// Once a Quote has been discarded, it cannot be exercised even if it has not expired.
    ///
    /// Permissions required: `Perm_W_Orders`
    pub fn discard_quote(
        &self,
        id: &str,
    ) -> impl Future<Output = Result<quotes::Quote, reqwest::Error>> + '_ {
        let url = self.url_maker.quote_action(id);
        self.delete(url)
    }
}