├── .DS_Store ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── examples └── readonly.py ├── luno_python ├── __init__.py ├── base_client.py ├── client.py └── error.py ├── renovate.json5 ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── test_client.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luno/luno-python/9d604d4dda32626993b2dbdd748cdee526b60937/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [ "3.12", "3.13" ] 17 | 18 | steps: 19 | 20 | - uses: actions/checkout@v4 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip setuptools wheel 30 | pip install .[test] 31 | 32 | - name: Test with pytest 33 | run: | 34 | pytest 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | *.pyc 3 | requirements.txt 4 | __pycache__/ 5 | .pytest_cache/ 6 | build/ 7 | dist/ 8 | *.egg-info/ 9 | .eggs/ 10 | .coverage 11 | .idea/ 12 | /env/ 13 | /.DS_Store 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Clone 4 | ```bash 5 | git clone https://github.com/luno/luno-python.git 6 | ``` 7 | ## Create Virtual env 8 | ```bash 9 | cd luno-python 10 | python -m venv env 11 | source env/bin/activate 12 | ``` 13 | 14 | ## Install Dependencies 15 | ```bash 16 | python -m pip install --upgrade pip setuptools wheel 17 | pip install -e '.[test]' 18 | ``` 19 | 20 | ## Run Tests 21 | ```bash 22 | pytest 23 | ``` -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Luno API [![Build Status](https://travis-ci.org/luno/luno-python.svg?branch=master)](https://travis-ci.org/luno/luno-python) 4 | 5 | This Python package provides a wrapper for the [Luno API](https://www.luno.com/api). 6 | 7 | ### Installation 8 | 9 | ``` 10 | pip install luno-python 11 | ``` 12 | 13 | ### Authentication 14 | 15 | Please visit the [Settings](https://www.luno.com/wallet/settings/api_keys) page 16 | to generate an API key. 17 | 18 | ### Example usage 19 | 20 | ```python 21 | from luno_python.client import Client 22 | 23 | c = Client(api_key_id='key_id', api_key_secret='key_secret') 24 | try: 25 | res = c.get_ticker(pair='XBTZAR') 26 | print res 27 | except Exception as e: 28 | print e 29 | ``` 30 | 31 | ### License 32 | 33 | [MIT](https://github.com/luno/luno-python/blob/master/LICENSE.txt) 34 | 35 | -------------------------------------------------------------------------------- /examples/readonly.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from luno_python.client import Client 5 | 6 | 7 | if __name__ == '__main__': 8 | c = Client(api_key_id=os.getenv('LUNO_API_KEY_ID'), 9 | api_key_secret=os.getenv('LUNO_API_KEY_SECRET')) 10 | 11 | res = c.get_tickers() 12 | print(res) 13 | time.sleep(0.5) 14 | 15 | res = c.get_ticker(pair='XBTZAR') 16 | print(res) 17 | time.sleep(0.5) 18 | 19 | res = c.get_order_book(pair='XBTZAR') 20 | print(res) 21 | time.sleep(0.5) 22 | 23 | since = int(time.time()*1000)-24*60*59*1000 24 | res = c.list_trades(pair='XBTZAR', since=since) 25 | print(res) 26 | time.sleep(0.5) 27 | 28 | res = c.get_candles(pair='XBTZAR', since=since, duration=300) 29 | print(res) 30 | time.sleep(0.5) 31 | 32 | res = c.get_balances() 33 | print(res) 34 | time.sleep(0.5) 35 | 36 | aid = '' 37 | if res['balance']: 38 | aid = res['balance'][0]['account_id'] 39 | 40 | if aid: 41 | res = c.list_transactions(id=aid, min_row=1, max_row=10) 42 | print(res) 43 | time.sleep(0.5) 44 | 45 | if aid: 46 | res = c.list_pending_transactions(id=aid) 47 | print(res) 48 | time.sleep(0.5) 49 | 50 | res = c.list_orders() 51 | print(res) 52 | time.sleep(0.5) 53 | 54 | res = c.list_user_trades(pair='XBTZAR') 55 | print(res) 56 | time.sleep(0.5) 57 | 58 | res = c.get_fee_info(pair='XBTZAR') 59 | print(res) 60 | time.sleep(0.5) 61 | 62 | res = c.get_funding_address(asset='XBT') 63 | print(res) 64 | time.sleep(0.5) 65 | 66 | res = c.list_withdrawals() 67 | print(res) 68 | time.sleep(0.5) 69 | 70 | wid = '' 71 | if res['withdrawals']: 72 | wid = res['withdrawals'][0]['id'] 73 | 74 | if wid: 75 | res = c.get_withdrawal(id=wid) 76 | print(res) 77 | time.sleep(0.5) 78 | -------------------------------------------------------------------------------- /luno_python/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.0.10' 2 | -------------------------------------------------------------------------------- /luno_python/base_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import platform 3 | import requests 4 | import six 5 | 6 | try: 7 | from json.decoder import JSONDecodeError 8 | except ImportError: 9 | JSONDecodeError = ValueError 10 | 11 | from . import VERSION 12 | from .error import APIError 13 | 14 | DEFAULT_BASE_URL = 'https://api.luno.com' 15 | DEFAULT_TIMEOUT = 10 16 | PYTHON_VERSION = platform.python_version() 17 | SYSTEM = platform.system() 18 | ARCH = platform.machine() 19 | 20 | 21 | class BaseClient: 22 | def __init__(self, base_url='', timeout=0, 23 | api_key_id='', api_key_secret=''): 24 | """ 25 | :type base_url: str 26 | :type timeout: float 27 | :type api_key_id: str 28 | :type api_key_secret: str 29 | """ 30 | self.set_auth(api_key_id, api_key_secret) 31 | self.set_base_url(base_url) 32 | self.set_timeout(timeout) 33 | 34 | self.session = requests.Session() 35 | 36 | def set_auth(self, api_key_id, api_key_secret): 37 | """Provides the client with an API key and secret. 38 | 39 | :type api_key_id: str 40 | :type api_key_secret: str 41 | """ 42 | self.api_key_id = api_key_id 43 | self.api_key_secret = api_key_secret 44 | 45 | def set_base_url(self, base_url): 46 | """Overrides the default base URL. For internal use. 47 | 48 | :type base_url: str 49 | """ 50 | if base_url == '': 51 | base_url = DEFAULT_BASE_URL 52 | self.base_url = base_url.rstrip('/') 53 | 54 | def set_timeout(self, timeout): 55 | """Sets the timeout, in seconds, for requests made by the client. 56 | 57 | :type timeout: float 58 | """ 59 | if timeout == 0: 60 | timeout = DEFAULT_TIMEOUT 61 | self.timeout = timeout 62 | 63 | def do(self, method, path, req=None, auth=False): 64 | """Performs an API request and returns the response. 65 | 66 | TODO: Handle 429s 67 | 68 | :type method: str 69 | :type path: str 70 | :type req: object 71 | :type auth: bool 72 | """ 73 | try: 74 | params = json.loads(json.dumps(req)) 75 | except Exception: 76 | params = None 77 | headers = {'User-Agent': self.make_user_agent()} 78 | args = dict(timeout=self.timeout, params=params, headers=headers) 79 | if auth: 80 | args['auth'] = (self.api_key_id, self.api_key_secret) 81 | url = self.make_url(path, params) 82 | res = self.session.request(method, url, **args) 83 | try: 84 | e = res.json() 85 | if 'error' in e and 'error_code' in e: 86 | raise APIError(e['error_code'], e['error']) 87 | return e 88 | except JSONDecodeError: 89 | raise Exception('luno: unknown API error (%s)' % res.status_code) 90 | 91 | def make_url(self, path, params): 92 | """ 93 | :type path: str 94 | :rtype: str 95 | """ 96 | if params: 97 | for k, v in six.iteritems(params): 98 | path = path.replace('{' + k + '}', str(v)) 99 | return self.base_url + '/' + path.lstrip('/') 100 | 101 | def make_user_agent(self): 102 | """ 103 | :rtype: str 104 | """ 105 | return "LunoPythonSDK/%s python/%s %s %s" % \ 106 | (VERSION, PYTHON_VERSION, SYSTEM, ARCH) 107 | -------------------------------------------------------------------------------- /luno_python/client.py: -------------------------------------------------------------------------------- 1 | from .base_client import BaseClient 2 | 3 | 4 | class Client(BaseClient): 5 | """ 6 | Python SDK for the Luno API. 7 | 8 | Example usage: 9 | 10 | from luno_python.client import Client 11 | 12 | 13 | c = Client(api_key_id='key_id', api_key_secret='key_secret') 14 | try: 15 | res = c.get_ticker(pair='XBTZAR') 16 | print res 17 | except Exception as e: 18 | print e 19 | """ 20 | 21 | def cancel_withdrawal(self, id): 22 | """Makes a call to DELETE /api/1/withdrawals/{id}. 23 | 24 | Cancels a withdrawal request. 25 | This can only be done if the request is still in state PENDING. 26 | 27 | Permissions required: Perm_W_Withdrawals 28 | 29 | :param id: ID of the withdrawal to cancel. 30 | :type id: int 31 | """ 32 | req = { 33 | 'id': id, 34 | } 35 | return self.do('DELETE', '/api/1/withdrawals/{id}', req=req, auth=True) 36 | 37 | def create_account(self, currency, name): 38 | """Makes a call to POST /api/1/accounts. 39 | 40 | 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. 41 | 42 | Permissions required: Perm_W_Addresses 43 | 44 | :param currency: The currency code for the Account you want to create. Please see the Currency section for a detailed list of currencies supported by the Luno platform. 45 | 46 | Users must be verified to trade currency in order to be able to create an Account. For more information on the verification process, please see How do I verify my identity?. 47 | 48 | Users have a limit of 10 accounts per currency. 49 | :type currency: str 50 | :param name: The label to use for this account 51 | :type name: str 52 | """ 53 | req = { 54 | 'currency': currency, 55 | 'name': name, 56 | } 57 | return self.do('POST', '/api/1/accounts', req=req, auth=True) 58 | 59 | def create_beneficiary(self, account_type, bank_account_number, bank_name, bank_recipient): 60 | """Makes a call to POST /api/1/beneficiaries. 61 | 62 | Create a new beneficiary. 63 | 64 | Permissions required: Perm_W_Beneficiaries 65 | 66 | :param account_type: Bank account type 67 | :type account_type: str 68 | :param bank_account_number: Beneficiary bank account number 69 | :type bank_account_number: str 70 | :param bank_name: Bank SWIFT code 71 | :type bank_name: str 72 | :param bank_recipient: The owner of the recipient account 73 | :type bank_recipient: str 74 | """ 75 | req = { 76 | 'account_type': account_type, 77 | 'bank_account_number': bank_account_number, 78 | 'bank_name': bank_name, 79 | 'bank_recipient': bank_recipient, 80 | } 81 | return self.do('POST', '/api/1/beneficiaries', req=req, auth=True) 82 | 83 | def create_funding_address(self, asset, account_id=None, name=None): 84 | """Makes a call to POST /api/1/funding_address. 85 | 86 | Allocates a new receive address to your account. There is a rate limit of 1 87 | address per hour, but bursts of up to 10 addresses are allowed. Only 1 88 | Ethereum receive address can be created. 89 | 90 | Permissions required: Perm_W_Addresses 91 | 92 | :param asset: Currency code of the asset. 93 | :type asset: str 94 | :param account_id: An optional account_id to assign the new Receive Address too 95 | :type account_id: int 96 | :param name: An optional name for the new Receive Address 97 | :type name: str 98 | """ 99 | req = { 100 | 'asset': asset, 101 | 'account_id': account_id, 102 | 'name': name, 103 | } 104 | return self.do('POST', '/api/1/funding_address', req=req, auth=True) 105 | 106 | def create_withdrawal(self, amount, type, beneficiary_id=None, external_id=None, fast=None, reference=None): 107 | """Makes a call to POST /api/1/withdrawals. 108 | 109 | Creates a new withdrawal request to the specified beneficiary. 110 | 111 | Permissions required: Perm_W_Withdrawals 112 | 113 | :param amount: Amount to withdraw. The currency withdrawn depends on the type setting. 114 | :type amount: float 115 | :param type: Withdrawal method. 116 | :type type: str 117 | :param beneficiary_id: The beneficiary ID of the bank account the withdrawal will be paid out to. 118 | This parameter is required if the user has set up multiple beneficiaries. 119 | The beneficiary ID can be found by selecting on the beneficiary name on the user’s Beneficiaries page. 120 | :type beneficiary_id: int 121 | :param external_id: Optional unique ID to associate with this withdrawal. 122 | Useful to prevent duplicate sends. 123 | This field supports all alphanumeric characters including "-" and "_". 124 | :type external_id: str 125 | :param fast: If true, it will be a fast withdrawal if possible. Fast withdrawals come with a fee. 126 | Currently fast withdrawals are only available for `type=ZAR_EFT`; for other types, an error is returned. 127 | Fast withdrawals are not possible for Bank of Baroda, Deutsche Bank, Merrill Lynch South Africa, UBS, Postbank and Tyme Bank. 128 | The fee to be charged is the same as when withdrawing from the UI. 129 | :type fast: bool 130 | :param reference: For internal use. 131 | Deprecated: We don't allow custom references and will remove this soon. 132 | :type reference: str 133 | """ 134 | req = { 135 | 'amount': amount, 136 | 'type': type, 137 | 'beneficiary_id': beneficiary_id, 138 | 'external_id': external_id, 139 | 'fast': fast, 140 | 'reference': reference, 141 | } 142 | return self.do('POST', '/api/1/withdrawals', req=req, auth=True) 143 | 144 | def delete_beneficiary(self, id): 145 | """Makes a call to DELETE /api/1/beneficiaries/{id}. 146 | 147 | Delete a beneficiary 148 | 149 | Permissions required: Perm_W_Beneficiaries 150 | 151 | :param id: ID of the Beneficiary to delete. 152 | :type id: int 153 | """ 154 | req = { 155 | 'id': id, 156 | } 157 | return self.do('DELETE', '/api/1/beneficiaries/{id}', req=req, auth=True) 158 | 159 | def get_balances(self, assets=None): 160 | """Makes a call to GET /api/1/balance. 161 | 162 | The list of all Accounts and their respective balances for the requesting user. 163 | 164 | Permissions required: Perm_R_Balance 165 | 166 | :param assets: Only return balances for wallets with these currencies (if not provided, 167 | all balances will be returned). To request balances for multiple currencies, 168 | pass the parameter multiple times, 169 | e.g. `assets=XBT&assets=ETH`. 170 | :type assets: list 171 | """ 172 | req = { 173 | 'assets': assets, 174 | } 175 | return self.do('GET', '/api/1/balance', req=req, auth=True) 176 | 177 | def get_candles(self, duration, pair, since): 178 | """Makes a call to GET /api/exchange/1/candles. 179 | 180 | Get candlestick market data from the specified time until now, from the oldest to the most recent. 181 | 182 | Permissions required: MP_None 183 | 184 | :param duration: Candle duration in seconds. 185 | For example, 300 corresponds to 5m candles. Currently supported 186 | durations are: 60 (1m), 300 (5m), 900 (15m), 1800 (30m), 3600 (1h), 187 | 10800 (3h), 14400 (4h), 28800 (8h), 86400 (24h), 259200 (3d), 604800 188 | (7d). 189 | :type duration: int 190 | :param pair: Currency pair 191 | :type pair: str 192 | :param since: Filter to candles starting on or after this timestamp (Unix milliseconds). 193 | Only up to 1000 of the earliest candles are returned. 194 | :type since: int 195 | """ 196 | req = { 197 | 'duration': duration, 198 | 'pair': pair, 199 | 'since': since, 200 | } 201 | return self.do('GET', '/api/exchange/1/candles', req=req, auth=True) 202 | 203 | def get_fee_info(self, pair): 204 | """Makes a call to GET /api/1/fee_info. 205 | 206 | Returns the fees and 30 day trading volume (as of midnight) for a given currency pair. For complete details, please see Fees & Features. 207 | 208 | Permissions required: Perm_R_Orders 209 | 210 | :param pair: Get fee information about this pair. 211 | :type pair: str 212 | """ 213 | req = { 214 | 'pair': pair, 215 | } 216 | return self.do('GET', '/api/1/fee_info', req=req, auth=True) 217 | 218 | def get_funding_address(self, asset, address=None): 219 | """Makes a call to GET /api/1/funding_address. 220 | 221 | Returns the default receive address associated with your account and the 222 | amount received via the address. Users can specify an optional address parameter to return information for a non-default receive address. 223 | In the response, total_received is the total confirmed amount received excluding unconfirmed transactions. 224 | total_unconfirmed is the total sum of unconfirmed receive transactions. 225 | 226 | Permissions required: Perm_R_Addresses 227 | 228 | :param asset: Currency code of the asset. 229 | :type asset: str 230 | :param address: Specific cryptocurrency address to retrieve. If not provided, the 231 | default address will be used. 232 | :type address: str 233 | """ 234 | req = { 235 | 'asset': asset, 236 | 'address': address, 237 | } 238 | return self.do('GET', '/api/1/funding_address', req=req, auth=True) 239 | 240 | def get_move(self, client_move_id=None, id=None): 241 | """Makes a call to GET /api/exchange/1/move. 242 | 243 | Get a specific move funds instruction by either id or 244 | client_move_id. If both are provided an API error will be 245 | returned. 246 | 247 | Permissions required: MP_None 248 | 249 | :param client_move_id: Get by the user defined ID. This is mutually exclusive with id and is required if id is 250 | not provided. 251 | :type client_move_id: str 252 | :param id: Get by the system ID. This is mutually exclusive with client_move_id and is required if 253 | client_move_id is not provided. 254 | :type id: str 255 | """ 256 | req = { 257 | 'client_move_id': client_move_id, 258 | 'id': id, 259 | } 260 | return self.do('GET', '/api/exchange/1/move', req=req, auth=True) 261 | 262 | def get_order(self, id): 263 | """Makes a call to GET /api/1/orders/{id}. 264 | 265 | Get an Order's details by its ID. 266 | 267 | Permissions required: Perm_R_Orders 268 | 269 | :param id: Order reference 270 | :type id: str 271 | """ 272 | req = { 273 | 'id': id, 274 | } 275 | return self.do('GET', '/api/1/orders/{id}', req=req, auth=True) 276 | 277 | def get_order_book(self, pair): 278 | """Makes a call to GET /api/1/orderbook_top. 279 | 280 | This request returns the best 100 `bids` and `asks`, for the currency pair specified, in the Order Book. 281 | 282 | `asks` are sorted by price ascending and `bids` are sorted by price descending. 283 | 284 | Multiple orders at the same price are aggregated. 285 | 286 | :param pair: Currency pair of the Orders to retrieve 287 | :type pair: str 288 | """ 289 | req = { 290 | 'pair': pair, 291 | } 292 | return self.do('GET', '/api/1/orderbook_top', req=req, auth=False) 293 | 294 | def get_order_book_full(self, pair): 295 | """Makes a call to GET /api/1/orderbook. 296 | 297 | This request returns all `bids` and `asks`, for the currency pair specified, in the Order Book. 298 | 299 | `asks` are sorted by price ascending and `bids` are sorted by price descending. 300 | 301 | Multiple orders at the same price are not aggregated. 302 | 303 | WARNING: This may return a large amount of data. 304 | Users are recommended to use the top 100 bids and asks 305 | or the Streaming API. 306 | 307 | :param pair: Currency pair of the Orders to retrieve 308 | :type pair: str 309 | """ 310 | req = { 311 | 'pair': pair, 312 | } 313 | return self.do('GET', '/api/1/orderbook', req=req, auth=False) 314 | 315 | def get_order_v2(self, id): 316 | """Makes a call to GET /api/exchange/2/orders/{id}. 317 | 318 | Get the details for an order. 319 | 320 | Permissions required: Perm_R_Orders 321 | 322 | :param id: Order reference 323 | :type id: str 324 | """ 325 | req = { 326 | 'id': id, 327 | } 328 | return self.do('GET', '/api/exchange/2/orders/{id}', req=req, auth=True) 329 | 330 | def get_order_v3(self, client_order_id=None, id=None): 331 | """Makes a call to GET /api/exchange/3/order. 332 | 333 | Get the details for an order by order reference or client order ID. 334 | Exactly one of the two parameters must be provided, otherwise an error is returned. 335 | Permissions required: Perm_R_Orders 336 | 337 | :param client_order_id: Client Order ID has the value that was passed in when the Order was posted. 338 | :type client_order_id: str 339 | :param id: Order reference 340 | :type id: str 341 | """ 342 | req = { 343 | 'client_order_id': client_order_id, 344 | 'id': id, 345 | } 346 | return self.do('GET', '/api/exchange/3/order', req=req, auth=True) 347 | 348 | def get_ticker(self, pair): 349 | """Makes a call to GET /api/1/ticker. 350 | 351 | Returns the latest ticker indicators for the specified currency pair. 352 | 353 | Please see the Currency list for the complete list of supported currency pairs. 354 | 355 | :param pair: Currency pair 356 | :type pair: str 357 | """ 358 | req = { 359 | 'pair': pair, 360 | } 361 | return self.do('GET', '/api/1/ticker', req=req, auth=False) 362 | 363 | def get_tickers(self, pair=None): 364 | """Makes a call to GET /api/1/tickers. 365 | 366 | Returns the latest ticker indicators from all active Luno exchanges. 367 | 368 | Please see the Currency list for the complete list of supported currency pairs. 369 | 370 | :param pair: Return tickers for multiple markets (if not provided, all tickers will be returned). 371 | To request tickers for multiple markets, pass the parameter multiple times, 372 | e.g. `pair=XBTZAR&pair=ETHZAR`. 373 | :type pair: list 374 | """ 375 | req = { 376 | 'pair': pair, 377 | } 378 | return self.do('GET', '/api/1/tickers', req=req, auth=False) 379 | 380 | def get_withdrawal(self, id): 381 | """Makes a call to GET /api/1/withdrawals/{id}. 382 | 383 | Returns the status of a particular withdrawal request. 384 | 385 | Permissions required: Perm_R_Withdrawals 386 | 387 | :param id: Withdrawal ID to retrieve. 388 | :type id: int 389 | """ 390 | req = { 391 | 'id': id, 392 | } 393 | return self.do('GET', '/api/1/withdrawals/{id}', req=req, auth=True) 394 | 395 | def list_beneficiaries(self, bank_recipient=None): 396 | """Makes a call to GET /api/1/beneficiaries. 397 | 398 | Returns a list of bank beneficiaries. 399 | 400 | Permissions required: Perm_R_Beneficiaries 401 | 402 | :param bank_recipient: :type bank_recipient: str 403 | """ 404 | req = { 405 | 'bank_recipient': bank_recipient, 406 | } 407 | return self.do('GET', '/api/1/beneficiaries', req=req, auth=True) 408 | 409 | def list_moves(self, before=None, limit=None): 410 | """Makes a call to GET /api/exchange/1/move/list_moves. 411 | 412 | Returns a list of the most recent moves ordered from newest to oldest. 413 | This endpoint will list up to 100 most recent moves by default. 414 | 415 | Permissions required: MP_None 416 | 417 | :param before: Filter to moves requested before this timestamp (Unix milliseconds) 418 | :type before: int 419 | :param limit: Limit to this many moves 420 | :type limit: int 421 | """ 422 | req = { 423 | 'before': before, 424 | 'limit': limit, 425 | } 426 | return self.do('GET', '/api/exchange/1/move/list_moves', req=req, auth=True) 427 | 428 | def list_orders(self, created_before=None, limit=None, pair=None, state=None): 429 | """Makes a call to GET /api/1/listorders. 430 | 431 | Returns a list of the most recently placed Orders. 432 | Users can specify an optional state=PENDING parameter to restrict the results to only open Orders. 433 | Users can also specify the market by using the optional currency pair parameter. 434 | 435 | Permissions required: Perm_R_Orders 436 | 437 | :param created_before: Filter to orders created before this timestamp (Unix milliseconds) 438 | :type created_before: int 439 | :param limit: Limit to this many orders 440 | :type limit: int 441 | :param pair: Filter to only orders of this currency pair 442 | :type pair: str 443 | :param state: Filter to only orders of this state 444 | :type state: str 445 | """ 446 | req = { 447 | 'created_before': created_before, 448 | 'limit': limit, 449 | 'pair': pair, 450 | 'state': state, 451 | } 452 | return self.do('GET', '/api/1/listorders', req=req, auth=True) 453 | 454 | def list_orders_v2(self, closed=None, created_before=None, limit=None, pair=None): 455 | """Makes a call to GET /api/exchange/2/listorders. 456 | 457 | Returns a list of the most recently placed orders ordered from newest to 458 | oldest. This endpoint will list up to 100 most recent open orders by 459 | default. 460 | 461 | Please note: This data is archived 100 days after an exchange order is completed. 462 | 463 | Permissions required: Perm_R_Orders 464 | 465 | :param closed: If true, will return closed orders instead of open orders. 466 | :type closed: bool 467 | :param created_before: Filter to orders created before this timestamp (Unix milliseconds) 468 | :type created_before: int 469 | :param limit: Limit to this many orders 470 | :type limit: int 471 | :param pair: Filter to only orders of this currency pair. 472 | :type pair: str 473 | """ 474 | req = { 475 | 'closed': closed, 476 | 'created_before': created_before, 477 | 'limit': limit, 478 | 'pair': pair, 479 | } 480 | return self.do('GET', '/api/exchange/2/listorders', req=req, auth=True) 481 | 482 | def list_pending_transactions(self, id): 483 | """Makes a call to GET /api/1/accounts/{id}/pending. 484 | 485 | Return a list of all transactions that have not completed for the Account. 486 | 487 | Pending transactions are not numbered, and may be reordered, deleted or updated at any time. 488 | 489 | Permissions required: Perm_R_Transactions 490 | 491 | :param id: Account ID 492 | :type id: int 493 | """ 494 | req = { 495 | 'id': id, 496 | } 497 | return self.do('GET', '/api/1/accounts/{id}/pending', req=req, auth=True) 498 | 499 | def list_trades(self, pair, since=None): 500 | """Makes a call to GET /api/1/trades. 501 | 502 | Returns a list of recent trades for the specified currency pair. At most 503 | 100 trades are returned per call and never trades older than 24h. The 504 | trades are sorted from newest to oldest. 505 | 506 | Please see the Currency list for the complete list of supported currency pairs. 507 | 508 | :param pair: Currency pair of the market to list the trades from 509 | :type pair: str 510 | :param since: Fetch trades executed after this time, specified as a Unix timestamp in 511 | milliseconds. An error will be returned if this is before 24h ago. Use 512 | this parameter to either restrict to a shorter window or to iterate over 513 | the trades in case you need more than the 100 most recent trades. 514 | :type since: int 515 | """ 516 | req = { 517 | 'pair': pair, 518 | 'since': since, 519 | } 520 | return self.do('GET', '/api/1/trades', req=req, auth=False) 521 | 522 | def list_transactions(self, id, max_row, min_row): 523 | """Makes a call to GET /api/1/accounts/{id}/transactions. 524 | 525 | Return a list of transaction entries from an account. 526 | 527 | Transaction entry rows are numbered sequentially starting from 1, where 1 is 528 | the oldest entry. The range of rows to return are specified with the 529 | min_row (inclusive) and max_row (exclusive) 530 | parameters. At most 1000 rows can be requested per call. 531 | 532 | If min_row or max_row is non-positive, the range 533 | wraps around the most recent row. For example, to fetch the 100 most recent 534 | rows, use min_row=-100 and max_row=0. 535 | 536 | Permissions required: Perm_R_Transactions 537 | 538 | :param id: Account ID - the unique identifier for the specific Account. 539 | :type id: int 540 | :param max_row: Maximum of the row range to return (exclusive) 541 | :type max_row: int 542 | :param min_row: Minimum of the row range to return (inclusive) 543 | :type min_row: int 544 | """ 545 | req = { 546 | 'id': id, 547 | 'max_row': max_row, 548 | 'min_row': min_row, 549 | } 550 | return self.do('GET', '/api/1/accounts/{id}/transactions', req=req, auth=True) 551 | 552 | def list_transfers(self, account_id, before=None, limit=None): 553 | """Makes a call to GET /api/exchange/1/transfers. 554 | 555 | Returns a list of the most recent confirmed transfers ordered from newest to 556 | oldest. 557 | This includes bank transfers, card payments, or on-chain transactions that 558 | have been reflected on your account available balance. 559 | 560 | Note that the Transfer `amount` is always a positive value and you should 561 | use the `inbound` flag to determine the direction of the transfer. 562 | 563 | If you need to paginate the results you can set the `before` parameter to 564 | the last returned transfer `created_at` field value and repeat the request 565 | until you have all the transfers you need. 566 | This endpoint will list up to 100 transfers at a time by default. 567 | 568 | Permissions required: Perm_R_Transfers 569 | 570 | :param account_id: Unique identifier of the account to list the transfers from. 571 | :type account_id: int 572 | :param before: Filter to transfers created before this timestamp (Unix milliseconds). 573 | The default value (0) will return the latest transfers on the account. 574 | :type before: int 575 | :param limit: Limit to this many transfers. 576 | :type limit: int 577 | """ 578 | req = { 579 | 'account_id': account_id, 580 | 'before': before, 581 | 'limit': limit, 582 | } 583 | return self.do('GET', '/api/exchange/1/transfers', req=req, auth=True) 584 | 585 | def list_user_trades(self, pair, after_seq=None, before=None, before_seq=None, limit=None, since=None, sort_desc=None): 586 | """Makes a call to GET /api/1/listtrades. 587 | 588 | Returns a list of the recent Trades for a given currency pair for this user, sorted by oldest first. 589 | If before is specified, then Trades are returned sorted by most-recent first. 590 | 591 | type in the response indicates the type of Order that was placed to participate in the trade. 592 | Possible types: BID, ASK. 593 | 594 | If is_buy in the response is true, then the Order which completed the trade (market taker) was a Bid Order. 595 | 596 | Results of this query may lag behind the latest data. 597 | 598 | Permissions required: Perm_R_Orders 599 | 600 | :param pair: Filter to trades of this currency pair. 601 | :type pair: str 602 | :param after_seq: Filter to trades from (including) this sequence number. 603 | Default behaviour is not to include this filter. 604 | :type after_seq: int 605 | :param before: Filter to trades before this timestamp (Unix milliseconds). 606 | :type before: int 607 | :param before_seq: Filter to trades before (excluding) this sequence number. 608 | Default behaviour is not to include this filter. 609 | :type before_seq: int 610 | :param limit: Limit to this number of trades (default 100). 611 | :type limit: int 612 | :param since: Filter to trades on or after this timestamp (Unix milliseconds). 613 | :type since: int 614 | :param sort_desc: If set to true, sorts trades in descending order, otherwise ascending 615 | order will be assumed. 616 | :type sort_desc: bool 617 | """ 618 | req = { 619 | 'pair': pair, 620 | 'after_seq': after_seq, 621 | 'before': before, 622 | 'before_seq': before_seq, 623 | 'limit': limit, 624 | 'since': since, 625 | 'sort_desc': sort_desc, 626 | } 627 | return self.do('GET', '/api/1/listtrades', req=req, auth=True) 628 | 629 | def list_withdrawals(self, before_id=None, limit=None): 630 | """Makes a call to GET /api/1/withdrawals. 631 | 632 | Returns a list of withdrawal requests. 633 | 634 | Permissions required: Perm_R_Withdrawals 635 | 636 | :param before_id: Filter to withdrawals requested on or before the withdrawal with this ID. 637 | Can be used for pagination. 638 | :type before_id: int 639 | :param limit: Limit to this many withdrawals 640 | :type limit: int 641 | """ 642 | req = { 643 | 'before_id': before_id, 644 | 'limit': limit, 645 | } 646 | return self.do('GET', '/api/1/withdrawals', req=req, auth=True) 647 | 648 | def markets(self, pair=None): 649 | """Makes a call to GET /api/exchange/1/markets. 650 | 651 | List all supported markets parameter information like price scale, min and 652 | max order volumes and market ID. 653 | 654 | :param pair: List of market pairs to return. Requesting only the required pairs will improve response times. 655 | :type pair: list 656 | """ 657 | req = { 658 | 'pair': pair, 659 | } 660 | return self.do('GET', '/api/exchange/1/markets', req=req, auth=False) 661 | 662 | def move(self, amount, credit_account_id, debit_account_id, client_move_id=None): 663 | """Makes a call to POST /api/exchange/1/move. 664 | 665 | Move funds between two of your transactional accounts with the same currency 666 | The funds may not be moved by the time the request returns. The GET method 667 | can be used to poll for the move's status. 668 | 669 | Note: moves will show as transactions, but not as transfers. 670 | 671 | Permissions required: MP_None_Write 672 | 673 | :param amount: Amount to transfer. Must be positive. 674 | :type amount: float 675 | :param credit_account_id: The account to credit the funds to. 676 | :type credit_account_id: int 677 | :param debit_account_id: The account to debit the funds from. 678 | :type debit_account_id: int 679 | :param client_move_id: Client move ID. 680 | May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). Maximum length: 255. 681 | It will be available in read endpoints, so you can use it to avoid duplicate moves between the same accounts. 682 | Values must be unique across all your successful calls of this endpoint; trying to create a move request 683 | with the same `client_move_id` as one of your past move requests will result in a HTTP 409 Conflict response. 684 | :type client_move_id: str 685 | """ 686 | req = { 687 | 'amount': amount, 688 | 'credit_account_id': credit_account_id, 689 | 'debit_account_id': debit_account_id, 690 | 'client_move_id': client_move_id, 691 | } 692 | return self.do('POST', '/api/exchange/1/move', req=req, auth=True) 693 | 694 | def post_limit_order(self, pair, price, type, volume, base_account_id=None, client_order_id=None, counter_account_id=None, post_only=None, stop_direction=None, stop_price=None, time_in_force=None, timestamp=None, ttl=None): 695 | """Makes a call to POST /api/1/postorder. 696 | 697 | Warning! Orders cannot be reversed once they have executed. 698 | Please ensure your program has been thoroughly tested before submitting Orders. 699 | 700 | If no base_account_id or counter_account_id are specified, 701 | your default base currency or counter currency account will be used. 702 | You can find your Account IDs by calling the Balances API. 703 | 704 | Permissions required: Perm_W_Orders 705 | 706 | :param pair: The currency pair to trade. 707 | :type pair: str 708 | :param price: Limit price as a decimal string in units of ZAR/BTC. 709 | :type price: float 710 | :param type: BID for a bid (buy) limit order
711 | ASK for an ask (sell) limit order 712 | :type type: str 713 | :param volume: Amount of cryptocurrency to buy or sell as a decimal string in units of the currency. 714 | :type volume: float 715 | :param base_account_id: The base currency Account to use in the trade. 716 | :type base_account_id: int 717 | :param client_order_id: Client order ID. 718 | May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). Maximum length: 255. 719 | It will be available in read endpoints, so you can use it to reconcile Luno with your internal system. 720 | Values must be unique across all your successful order creation endpoint calls; trying to create an order 721 | with the same `client_order_id` as one of your past orders will result in a HTTP 409 Conflict response. 722 | :type client_order_id: str 723 | :param counter_account_id: The counter currency Account to use in the trade. 724 | :type counter_account_id: int 725 | :param post_only: Post-only Orders will be cancelled if they would otherwise have traded 726 | immediately. 727 | For example, if there's a bid at ZAR 100,000 and you place a post-only ask at ZAR 100,000, 728 | your order will be cancelled instead of trading. 729 | If the best bid is ZAR 100,000 and you place a post-only ask at ZAR 101,000, 730 | your order won't trade but will go into the order book. 731 | :type post_only: bool 732 | :param stop_direction: Side of the trigger price to activate the order. This should be set if `stop_price` is also 733 | set. 734 | 735 | `RELATIVE_LAST_TRADE` will automatically infer the direction based on the last 736 | trade price and the stop price. If last trade price is less than stop price then stop 737 | direction is ABOVE otherwise is BELOW. 738 | :type stop_direction: str 739 | :param stop_price: Trigger trade price to activate this order as a decimal string. If this 740 | is set then this is treated as a Stop Limit Order and `stop_direction` 741 | is expected to be set too. 742 | :type stop_price: float 743 | :param time_in_force: GTC Good 'Til Cancelled. The order remains open until it is filled or cancelled by the user.
744 | IOC Immediate Or Cancel. The part of the order that cannot be filled immediately will be cancelled. Cannot be post-only.
745 | FOK Fill Or Kill. If the order cannot be filled immediately and completely it will be cancelled before any trade. Cannot be post-only. 746 | :type time_in_force: str 747 | :param timestamp: Unix timestamp in milliseconds of when the request was created and sent. 748 | :type timestamp: int 749 | :param ttl: Specifies the number of milliseconds after timestamp the request is valid for. 750 | If `timestamp` is not specified, `ttl` will not be used. 751 | :type ttl: int 752 | """ 753 | req = { 754 | 'pair': pair, 755 | 'price': price, 756 | 'type': type, 757 | 'volume': volume, 758 | 'base_account_id': base_account_id, 759 | 'client_order_id': client_order_id, 760 | 'counter_account_id': counter_account_id, 761 | 'post_only': post_only, 762 | 'stop_direction': stop_direction, 763 | 'stop_price': stop_price, 764 | 'time_in_force': time_in_force, 765 | 'timestamp': timestamp, 766 | 'ttl': ttl, 767 | } 768 | return self.do('POST', '/api/1/postorder', req=req, auth=True) 769 | 770 | def post_market_order(self, pair, type, base_account_id=None, base_volume=None, client_order_id=None, counter_account_id=None, counter_volume=None, timestamp=None, ttl=None): 771 | """Makes a call to POST /api/1/marketorder. 772 | 773 | A Market Order executes immediately, and either buys as much of the asset that can be bought for a set amount of fiat currency, or sells a set amount of the asset for as much as possible. 774 | 775 | Warning! Orders cannot be reversed once they have executed. 776 | Please ensure your program has been thoroughly tested before submitting Orders. 777 | 778 | If no base_account_id or counter_account_id are specified, the default base currency or counter currency account will be used. 779 | Users can find their account IDs by calling the Balances request. 780 | 781 | Permissions required: Perm_W_Orders 782 | 783 | :param pair: The currency pair to trade. 784 | :type pair: str 785 | :param type: BUY to buy an asset
786 | SELL to sell an asset 787 | :type type: str 788 | :param base_account_id: The base currency account to use in the trade. 789 | :type base_account_id: int 790 | :param base_volume: For a SELL order: amount of the base currency to use (e.g. how much BTC to sell for EUR in the BTC/EUR market) 791 | :type base_volume: float 792 | :param client_order_id: Client order ID. 793 | May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). Maximum length: 255. 794 | It will be available in read endpoints, so you can use it to reconcile Luno with your internal system. 795 | Values must be unique across all your successful order creation endpoint calls; trying to create an order 796 | with the same `client_order_id` as one of your past orders will result in a HTTP 409 Conflict response. 797 | :type client_order_id: str 798 | :param counter_account_id: The counter currency account to use in the trade. 799 | :type counter_account_id: int 800 | :param counter_volume: For a BUY order: amount of the counter currency to use (e.g. how much EUR to use to buy BTC in the BTC/EUR market) 801 | :type counter_volume: float 802 | :param timestamp: Unix timestamp in milliseconds of when the request was created and sent. 803 | :type timestamp: int 804 | :param ttl: Specifies the number of milliseconds after timestamp the request is valid for. 805 | If `timestamp` is not specified, `ttl` will not be used. 806 | :type ttl: int 807 | """ 808 | req = { 809 | 'pair': pair, 810 | 'type': type, 811 | 'base_account_id': base_account_id, 812 | 'base_volume': base_volume, 813 | 'client_order_id': client_order_id, 814 | 'counter_account_id': counter_account_id, 815 | 'counter_volume': counter_volume, 816 | 'timestamp': timestamp, 817 | 'ttl': ttl, 818 | } 819 | return self.do('POST', '/api/1/marketorder', req=req, auth=True) 820 | 821 | def send(self, address, amount, currency, account_id=None, description=None, destination_tag=None, external_id=None, forex_notice_self_declaration=None, has_destination_tag=None, is_drb=None, is_forex_send=None, memo=None, message=None): 822 | """Makes a call to POST /api/1/send. 823 | 824 | Send assets from an Account. Please note that the asset type sent must match the receive address of the same cryptocurrency of the same type - Bitcoin to Bitcoin, Ethereum to Ethereum, etc. 825 | 826 | Sends can be made to cryptocurrency receive addresses. 827 | 828 | Note: This is currently unavailable to users who are verified in countries with money travel rules. 829 | 830 | Permissions required: Perm_W_Send 831 | 832 | :param address: Destination address or email address. 833 | 834 | Note: 835 | 840 | :type address: str 841 | :param amount: Amount to send as a decimal string. 842 | :type amount: float 843 | :param currency: Currency to send. 844 | :type currency: str 845 | :param account_id: Optional source account. In case of multiple accounts for a single currency, the source account that will provide the funds for the transaction may be specified. If omitted, the default account will be used. 846 | :type account_id: int 847 | :param description: User description for the transaction to record on the account statement. 848 | :type description: str 849 | :param destination_tag: Optional XRP destination tag. Note that HasDestinationTag must be true if this value is provided. 850 | :type destination_tag: int 851 | :param external_id: Optional unique ID to associate with this withdrawal. 852 | Useful to prevent duplicate sends in case of failure. 853 | This supports all alphanumeric characters, as well as "-" and "_". 854 | :type external_id: str 855 | :param forex_notice_self_declaration: Only required for Foreign Exchange Notification under the Malaysia FEN rules. ForexNoticeSelfDeclaration must be true if the user has exceeded his/her annual investment limit in foreign currency assets. 856 | :type forex_notice_self_declaration: bool 857 | :param has_destination_tag: Optional boolean flag indicating that a XRP destination tag is provided (even if zero). 858 | :type has_destination_tag: bool 859 | :param is_drb: Only required for Foreign Exchange Notification under the Malaysia FEN rules. IsDRB must be true if the user has Domestic Ringgit Borrowing (DRB). 860 | :type is_drb: bool 861 | :param is_forex_send: Only required for Foreign Exchange Notification under the Malaysia FEN rules. IsForexSend must be true if sending to an address hosted outside of Malaysia. 862 | :type is_forex_send: bool 863 | :param memo: Optional memo string used to provide account information for ATOM, etc. where it holds "account" information 864 | for a generic address. 865 | :type memo: str 866 | :param message: Message to send to the recipient. 867 | This is only relevant when sending to an email address. 868 | :type message: str 869 | """ 870 | req = { 871 | 'address': address, 872 | 'amount': amount, 873 | 'currency': currency, 874 | 'account_id': account_id, 875 | 'description': description, 876 | 'destination_tag': destination_tag, 877 | 'external_id': external_id, 878 | 'forex_notice_self_declaration': forex_notice_self_declaration, 879 | 'has_destination_tag': has_destination_tag, 880 | 'is_drb': is_drb, 881 | 'is_forex_send': is_forex_send, 882 | 'memo': memo, 883 | 'message': message, 884 | } 885 | return self.do('POST', '/api/1/send', req=req, auth=True) 886 | 887 | def send_fee(self, address, amount, currency): 888 | """Makes a call to GET /api/1/send_fee. 889 | 890 | Calculate fees involved with a crypto send request. 891 | 892 | Send address can be to a cryptocurrency receive address, or the email address of another Luno platform user. 893 | 894 | Permissions required: MP_None 895 | 896 | :param address: Destination address or email address. 897 | 898 | Note: 899 | 904 | :type address: str 905 | :param amount: Amount to send as a decimal string. 906 | :type amount: float 907 | :param currency: Currency to send. 908 | :type currency: str 909 | """ 910 | req = { 911 | 'address': address, 912 | 'amount': amount, 913 | 'currency': currency, 914 | } 915 | return self.do('GET', '/api/1/send_fee', req=req, auth=True) 916 | 917 | def stop_order(self, order_id): 918 | """Makes a call to POST /api/1/stoporder. 919 | 920 | Request to cancel an Order. 921 | 922 | Note!: Once an Order has been completed, it can not be reversed. 923 | The return value from this request will indicate if the Stop request was successful or not. 924 | 925 | Permissions required: Perm_W_Orders 926 | 927 | :param order_id: The Order identifier as a string. 928 | :type order_id: str 929 | """ 930 | req = { 931 | 'order_id': order_id, 932 | } 933 | return self.do('POST', '/api/1/stoporder', req=req, auth=True) 934 | 935 | def update_account_name(self, id, name): 936 | """Makes a call to PUT /api/1/accounts/{id}/name. 937 | 938 | Update the name of an account with a given ID. 939 | 940 | Permissions required: Perm_W_Addresses 941 | 942 | :param id: Account ID - the unique identifier for the specific Account. 943 | :type id: int 944 | :param name: The label to use for this account 945 | :type name: str 946 | """ 947 | req = { 948 | 'id': id, 949 | 'name': name, 950 | } 951 | return self.do('PUT', '/api/1/accounts/{id}/name', req=req, auth=True) 952 | 953 | def validate(self, address, currency, address_name=None, beneficiary_name=None, country=None, date_of_birth=None, destination_tag=None, has_destination_tag=None, institution_name=None, is_legal_entity=None, is_private_wallet=None, is_self_send=None, memo=None, nationality=None, physical_address=None, wallet_name=None): 954 | """Makes a call to POST /api/1/address/validate. 955 | 956 | Validate receive addresses, to which a customer wishes to make cryptocurrency sends, are verified under covering 957 | regulatory requirements for the customer such as travel rules. 958 | 959 | Permissions required: Perm_W_Send 960 | 961 | :param address: Destination address or email address. 962 | 963 | Note: 964 | 969 | :type address: str 970 | :param currency: Currency is the currency associated with the address. 971 | :type currency: str 972 | :param address_name: AddressName is the optional name under which to store the address as in the address book. 973 | :type address_name: str 974 | :param beneficiary_name: BeneficiaryName is the name of the beneficial owner if is it is a private address 975 | :type beneficiary_name: str 976 | :param country: Country is the ISO 3166-1 country code of the beneficial owner of the address 977 | :type country: str 978 | :param date_of_birth: DateOfBirth is the date of birth of the (non-institutional) beneficial owner of the address in the form "YYYY-MM-DD" 979 | :type date_of_birth: str 980 | :param destination_tag: Optional XRP destination tag. Note that HasDestinationTag must be true if this value is provided. 981 | :type destination_tag: int 982 | :param has_destination_tag: Optional boolean flag indicating that a XRP destination tag is provided (even if zero). 983 | :type has_destination_tag: bool 984 | :param institution_name: InstitutionName is the name of the beneficial owner if is it is a legal entities address 985 | :type institution_name: str 986 | :param is_legal_entity: IsLegalEntity indicates if the address is for a legal entity and not a private beneficiary. 987 | If this field is true then the fields BeneficiaryName, Nationality & DateOfBirth should be empty but the 988 | fields InstitutionName and Country should be populated. 989 | If this field is false and IsSelfSend is false (or empty) then the field InstitutionName should be empty but the 990 | fields BeneficiaryName, Nationality & DateOfBirth and Country should be populated. 991 | :type is_legal_entity: bool 992 | :param is_private_wallet: IsPrivateWallet indicates if the address is for private wallet and not held at an exchange. 993 | :type is_private_wallet: bool 994 | :param is_self_send: IsSelfSend to indicate that the address belongs to the customer. 995 | If this field is true then the remaining omitempty fields should not 996 | be populated. 997 | :type is_self_send: bool 998 | :param memo: Optional memo string used to provide account information for ATOM, etc. where it holds "account" information 999 | for a generic address. 1000 | :type memo: str 1001 | :param nationality: Nationality ISO 3166-1 country code of the nationality of the (non-institutional) beneficial owner of the address 1002 | :type nationality: str 1003 | :param physical_address: PhysicalAddress is the legal physical address of the beneficial owner of the crypto address 1004 | :type physical_address: str 1005 | :param wallet_name: PrivateWalletName is the name of the private wallet 1006 | :type wallet_name: str 1007 | """ 1008 | req = { 1009 | 'address': address, 1010 | 'currency': currency, 1011 | 'address_name': address_name, 1012 | 'beneficiary_name': beneficiary_name, 1013 | 'country': country, 1014 | 'date_of_birth': date_of_birth, 1015 | 'destination_tag': destination_tag, 1016 | 'has_destination_tag': has_destination_tag, 1017 | 'institution_name': institution_name, 1018 | 'is_legal_entity': is_legal_entity, 1019 | 'is_private_wallet': is_private_wallet, 1020 | 'is_self_send': is_self_send, 1021 | 'memo': memo, 1022 | 'nationality': nationality, 1023 | 'physical_address': physical_address, 1024 | 'wallet_name': wallet_name, 1025 | } 1026 | return self.do('POST', '/api/1/address/validate', req=req, auth=True) 1027 | 1028 | 1029 | # vi: ft=python 1030 | -------------------------------------------------------------------------------- /luno_python/error.py: -------------------------------------------------------------------------------- 1 | class APIError(Exception): 2 | def __init__(self, code, message): 3 | self.code = code 4 | self.message = message 5 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:recommended', 5 | ], 6 | branchConcurrentLimit: 3, 7 | labels: [ 8 | 'Bot::Renovate', 9 | ], 10 | semanticCommits: 'disabled', 11 | commitMessagePrefix: 'renovate:', 12 | reviewers: [ 13 | 'echarrod', 14 | 'adamhicks', 15 | 'NeilLuno', 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | addopts=tests/ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | from luno_python import VERSION 4 | 5 | setup( 6 | name='luno-python', 7 | version=VERSION, 8 | packages=find_packages(exclude=['tests']), 9 | description='A Luno API client for Python', 10 | author='Neil Garb', 11 | author_email='neil@luno.com', 12 | install_requires=['requests>=2.18.4', 'six>=1.11.0'], 13 | license='MIT', 14 | url='https://github.com/luno/luno-python', 15 | download_url='https://github.com/luno/luno-python/tarball/%s' % (VERSION, ), 16 | keywords='Luno API Bitcoin Ethereum', 17 | test_suite='tests', 18 | setup_requires=['pytest-runner'], 19 | extras_require={ 20 | "test": ["pytest", "pytest-cov", "requests_mock"] 21 | }, 22 | ) 23 | 24 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luno/luno-python/9d604d4dda32626993b2dbdd748cdee526b60937/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | import requests_mock 4 | 5 | try: 6 | from json.decoder import JSONDecodeError 7 | except ImportError: 8 | JSONDecodeError = ValueError 9 | 10 | from luno_python.client import Client 11 | from luno_python.error import APIError 12 | 13 | 14 | def test_client(): 15 | c = Client() 16 | c.set_auth('api_key_id', 'api_key_secret') 17 | c.set_base_url('base_url') 18 | c.set_timeout(10) 19 | 20 | assert c.api_key_id == 'api_key_id' 21 | assert c.api_key_secret == 'api_key_secret' 22 | assert c.base_url == 'base_url' 23 | assert c.timeout == 10 24 | 25 | 26 | def test_client_do_basic(): 27 | c = Client() 28 | c.set_base_url('mock://test/') 29 | 30 | adapter = requests_mock.Adapter() 31 | c.session.mount('mock', adapter) 32 | 33 | adapter.register_uri('GET', 'mock://test/', text='ok') 34 | with pytest.raises(Exception): 35 | res = c.do('GET', '/') 36 | 37 | adapter.register_uri('GET', 'mock://test/', text='{"key":"value"}') 38 | res = c.do('GET', '/') 39 | assert res['key'] == 'value' 40 | 41 | adapter.register_uri('GET', 'mock://test/', text='{}', status_code=400) 42 | res = c.do('GET', '/') # no exception, because no error present 43 | 44 | adapter.register_uri('GET', 'mock://test/', 45 | text='{"error_code":"code","error":"message"}', 46 | status_code=400) 47 | with pytest.raises(APIError) as e: 48 | res = c.do('GET', '/') 49 | assert e.value.code == 'code' 50 | assert e.value.message == 'message' 51 | --------------------------------------------------------------------------------