├── tests ├── __init__.py └── test_client.py ├── luno_python ├── __init__.py ├── error.py ├── base_client.py └── client.py ├── setup.cfg ├── .gitignore ├── CLAUDE.md ├── renovate.json5 ├── .github └── workflows │ └── test.yml ├── pyproject.toml ├── README.md ├── LICENSE.txt ├── setup.py ├── CONTRIBUTING.md ├── examples └── readonly.py ├── .pre-commit-config.yaml └── AGENTS.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test package for luno-python.""" 2 | -------------------------------------------------------------------------------- /luno_python/__init__.py: -------------------------------------------------------------------------------- 1 | """Luno Python SDK.""" 2 | 3 | VERSION = "0.0.10" 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | addopts=tests/ 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | For compatibility with multiple agents, the instructions are located at @AGENTS.md (see [AGENTS.md](./AGENTS.md). 6 | -------------------------------------------------------------------------------- /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 | ], 16 | } 17 | -------------------------------------------------------------------------------- /luno_python/error.py: -------------------------------------------------------------------------------- 1 | """Luno API error classes.""" 2 | 3 | 4 | class APIError(Exception): 5 | """Exception raised for Luno API errors.""" 6 | 7 | def __init__(self, code, message): 8 | """Initialise APIError with code and message. 9 | 10 | :param code: Error code from the API 11 | :type code: str 12 | :param message: Error message from the API 13 | :type message: str 14 | """ 15 | self.code = code 16 | self.message = message 17 | -------------------------------------------------------------------------------- /.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@v5 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v6 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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.git 8 | | \.venv 9 | | env 10 | | build 11 | | dist 12 | | \.egg-info 13 | )/ 14 | ''' 15 | 16 | [tool.isort] 17 | profile = "black" 18 | line_length = 120 19 | skip_gitignore = true 20 | 21 | [tool.bandit] 22 | exclude_dirs = ["tests", "env", "build"] 23 | skips = [ 24 | "B101", # Skip assert_used check (common in tests) 25 | "B107", # Skip hardcoded_password_default (empty string defaults are acceptable for optional credentials) 26 | ] 27 | 28 | [tool.pytest.ini_options] 29 | testpaths = ["tests"] 30 | python_files = "test_*.py" 31 | python_classes = "Test*" 32 | python_functions = "test_*" 33 | addopts = "-v --cov=luno_python --cov-report=term-missing" 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup script for luno-python package.""" 2 | 3 | from setuptools import find_packages, setup 4 | 5 | from luno_python import VERSION 6 | 7 | setup( 8 | name="luno-python", 9 | version=VERSION, 10 | packages=find_packages(exclude=["tests"]), 11 | description="A Luno API client for Python", 12 | author="Neil Garb", 13 | author_email="neil@luno.com", 14 | install_requires=["requests>=2.18.4", "six>=1.11.0"], 15 | license="MIT", 16 | url="https://github.com/luno/luno-python", 17 | download_url=f"https://github.com/luno/luno-python/tarball/{VERSION}", 18 | keywords="Luno API Bitcoin Ethereum", 19 | test_suite="tests", 20 | setup_requires=["pytest-runner"], 21 | extras_require={ 22 | "test": ["pytest", "pytest-cov", "requests_mock"], 23 | "dev": [ 24 | "pytest", 25 | "pytest-cov", 26 | "requests_mock", 27 | "pre-commit", 28 | "black", 29 | "isort", 30 | "flake8", 31 | "flake8-docstrings", 32 | "flake8-bugbear", 33 | "bandit[toml]", 34 | ], 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Clone 4 | 5 | ```bash 6 | git clone https://github.com/luno/luno-python.git 7 | ``` 8 | 9 | ## Create Virtual env 10 | 11 | ```bash 12 | cd luno-python 13 | python -m venv env 14 | source env/bin/activate 15 | ``` 16 | 17 | ## Install Dependencies 18 | 19 | ```bash 20 | python -m pip install --upgrade pip setuptools wheel 21 | pip install -e '.[dev]' 22 | ``` 23 | 24 | This installs the package in editable mode with all development dependencies including testing tools and pre-commit hooks. 25 | 26 | ## Set Up Pre-commit Hooks 27 | 28 | This project uses [pre-commit](https://pre-commit.com/) to maintain code quality and consistency. The hooks run automatically before commits and pushes. 29 | 30 | ### Install the git hook scripts 31 | 32 | ```bash 33 | pre-commit install 34 | ``` 35 | 36 | This will run code formatting, linting, security checks, and tests on every commit. 37 | 38 | ### Run hooks manually 39 | 40 | To run all hooks on all files manually: 41 | 42 | ```bash 43 | pre-commit run --all-files 44 | ``` 45 | 46 | ### What the hooks do 47 | 48 | - **Code formatting**: Automatically formats code with `black` and sorts imports with `isort` 49 | - **Linting**: Checks code quality with `flake8` 50 | - **Security**: Scans for common security issues with `bandit` 51 | - **File checks**: Fixes trailing whitespace, ensures files end with newlines, validates YAML/JSON 52 | - **Tests**: Runs the full test suite (via `pytest`) 53 | 54 | ### Skip hooks (use sparingly) 55 | 56 | If you need to skip hooks for a specific commit: 57 | 58 | ```bash 59 | git commit --no-verify 60 | ``` 61 | 62 | ## Run Tests 63 | 64 | ```bash 65 | pytest 66 | ``` 67 | -------------------------------------------------------------------------------- /examples/readonly.py: -------------------------------------------------------------------------------- 1 | """Example script demonstrating read-only API calls to Luno.""" 2 | 3 | import os 4 | import time 5 | 6 | from luno_python.client import Client 7 | 8 | if __name__ == "__main__": 9 | c = Client(api_key_id=os.getenv("LUNO_API_KEY_ID"), 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 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Pre-commit hooks for luno-python 2 | # See https://pre-commit.com for more information 3 | repos: 4 | # General file checks 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.6.0 7 | hooks: 8 | - id: trailing-whitespace 9 | args: [--markdown-linebreak-ext=md] 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | - id: check-json 13 | - id: check-added-large-files 14 | args: ['--maxkb=500'] 15 | - id: check-merge-conflict 16 | - id: check-case-conflict 17 | - id: mixed-line-ending 18 | args: ['--fix=lf'] 19 | - id: detect-private-key 20 | 21 | # Python code formatting 22 | - repo: https://github.com/psf/black 23 | rev: 24.10.0 24 | hooks: 25 | - id: black 26 | language_version: python3 27 | args: ['--line-length=120'] 28 | 29 | # Import sorting 30 | - repo: https://github.com/pycqa/isort 31 | rev: 5.13.2 32 | hooks: 33 | - id: isort 34 | args: ['--profile=black', '--line-length=120'] 35 | 36 | # Linting 37 | - repo: https://github.com/pycqa/flake8 38 | rev: 7.1.1 39 | hooks: 40 | - id: flake8 41 | args: ['--max-line-length=120', '--extend-ignore=E203,W503'] 42 | additional_dependencies: [flake8-docstrings, flake8-bugbear] 43 | 44 | # Security checks 45 | - repo: https://github.com/PyCQA/bandit 46 | rev: 1.7.10 47 | hooks: 48 | - id: bandit 49 | args: ['-c', 'pyproject.toml'] 50 | additional_dependencies: ['bandit[toml]'] 51 | 52 | # Python upgrade syntax 53 | - repo: https://github.com/asottile/pyupgrade 54 | rev: v3.19.0 55 | hooks: 56 | - id: pyupgrade 57 | args: [--py37-plus] 58 | 59 | # Check for common Python bugs 60 | - repo: https://github.com/pre-commit/pygrep-hooks 61 | rev: v1.10.0 62 | hooks: 63 | - id: python-check-blanket-noqa 64 | - id: python-check-blanket-type-ignore 65 | - id: python-use-type-annotations 66 | 67 | # Tests 68 | - repo: local 69 | hooks: 70 | - id: pytest 71 | name: pytest 72 | entry: env/bin/pytest 73 | args: ['-v', '--override-ini=addopts='] 74 | language: system 75 | pass_filenames: false 76 | always_run: true 77 | -------------------------------------------------------------------------------- /luno_python/base_client.py: -------------------------------------------------------------------------------- 1 | """Base HTTP client for Luno API.""" 2 | 3 | import json 4 | import platform 5 | 6 | import requests 7 | 8 | try: 9 | from json.decoder import JSONDecodeError 10 | except ImportError: 11 | JSONDecodeError = ValueError 12 | 13 | from . import VERSION 14 | from .error import APIError 15 | 16 | DEFAULT_BASE_URL = "https://api.luno.com" 17 | DEFAULT_TIMEOUT = 10 18 | PYTHON_VERSION = platform.python_version() 19 | SYSTEM = platform.system() 20 | ARCH = platform.machine() 21 | 22 | 23 | class BaseClient: 24 | """Base HTTP client for making authenticated requests to the Luno API.""" 25 | 26 | def __init__(self, base_url="", timeout=0, api_key_id="", api_key_secret=""): 27 | """Initialise the base client. 28 | 29 | :type base_url: str 30 | :type timeout: float 31 | :type api_key_id: str 32 | :type api_key_secret: str 33 | """ 34 | self.set_auth(api_key_id, api_key_secret) 35 | self.set_base_url(base_url) 36 | self.set_timeout(timeout) 37 | 38 | self.session = requests.Session() 39 | 40 | def set_auth(self, api_key_id, api_key_secret): 41 | """Set the API key and secret for authentication. 42 | 43 | :type api_key_id: str 44 | :type api_key_secret: str 45 | """ 46 | self.api_key_id = api_key_id 47 | self.api_key_secret = api_key_secret 48 | 49 | def set_base_url(self, base_url): 50 | """Set the base URL for API requests. 51 | 52 | :type base_url: str 53 | """ 54 | if base_url == "": 55 | base_url = DEFAULT_BASE_URL 56 | self.base_url = base_url.rstrip("/") 57 | 58 | def set_timeout(self, timeout): 59 | """Set the timeout in seconds for API requests. 60 | 61 | :type timeout: float 62 | """ 63 | if timeout == 0: 64 | timeout = DEFAULT_TIMEOUT 65 | self.timeout = timeout 66 | 67 | def do(self, method, path, req=None, auth=False): 68 | """Perform an API request and return the response. 69 | 70 | TODO: Handle 429s. 71 | 72 | :type method: str 73 | :type path: str 74 | :type req: object 75 | :type auth: bool 76 | """ 77 | if req is None: 78 | params = None 79 | else: 80 | try: 81 | params = json.loads(json.dumps(req)) 82 | except TypeError as e: 83 | msg = "luno: request parameters must be JSON-serializable: %s" 84 | raise TypeError(msg % str(e)) from e 85 | headers = {"User-Agent": self.make_user_agent()} 86 | args = dict(timeout=self.timeout, params=params, headers=headers) 87 | if auth: 88 | args["auth"] = (self.api_key_id, self.api_key_secret) 89 | url = self.make_url(path, params) 90 | res = self.session.request(method, url, **args) 91 | try: 92 | e = res.json() 93 | if "error" in e and "error_code" in e: 94 | raise APIError(e["error_code"], e["error"]) 95 | return e 96 | except JSONDecodeError: 97 | raise Exception("luno: unknown API error (%s)" % res.status_code) 98 | 99 | def make_url(self, path, params): 100 | """Construct the full URL for an API request. 101 | 102 | :type path: str 103 | :rtype: str 104 | """ 105 | if params: 106 | for k, v in params.items(): 107 | path = path.replace("{" + k + "}", str(v)) 108 | return self.base_url + "/" + path.lstrip("/") 109 | 110 | def make_user_agent(self): 111 | """Generate the User-Agent string for API requests. 112 | 113 | :rtype: str 114 | """ 115 | return f"LunoPythonSDK/{VERSION} python/{PYTHON_VERSION} {SYSTEM} {ARCH}" 116 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS.md 2 | 3 | This file provides guidance to agents (e.g. Claude Code / GitHub CoPilot) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is the **Luno Python SDK**, a wrapper for the Luno API (cryptocurrency exchange platform). The package provides a Python client for interacting with Luno's REST API, supporting operations like account management, trading, fund transfers, and market data queries. 8 | 9 | ## Architecture Overview 10 | 11 | ### Core Structure 12 | 13 | - **`luno_python/base_client.py`**: Base HTTP client class (`BaseClient`) that handles: 14 | - HTTP requests using the `requests` library 15 | - Basic auth with API key and secret 16 | - Error handling and JSON parsing 17 | - User-Agent generation 18 | - URL construction with parameter substitution 19 | 20 | - **`luno_python/client.py`**: Main `Client` class extending `BaseClient`. Contains ~100+ API methods auto-generated from the Luno API specification. Each method: 21 | - Wraps a specific API endpoint 22 | - Constructs the request object with parameters 23 | - Calls `do()` to make the HTTP request 24 | - Returns the API response as a dictionary 25 | 26 | - **`luno_python/error.py`**: Custom `APIError` exception class for handling API errors 27 | 28 | - **`luno_python/__init__.py`**: Exports package version 29 | 30 | ### Request/Response Flow 31 | 32 | 1. User calls a method on `Client` (e.g., `get_ticker(pair='XBTZAR')`) 33 | 2. Method builds a request dict with parameters 34 | 3. Method calls `self.do(method, path, req, auth)` from `BaseClient` 35 | 4. `do()` makes HTTP request via `requests.Session` 36 | 5. `do()` parses JSON response and checks for API error codes 37 | 6. If error found, raises `APIError`; otherwise returns response dict 38 | 39 | ### API Design Patterns 40 | 41 | - **Authentication**: HTTP Basic Auth with `api_key_id` and `api_key_secret` 42 | - **Path parameters**: Substituted using `{param_name}` syntax in paths 43 | - **Query parameters**: Passed via `params` in GET requests 44 | - **Error handling**: API returns `{"error_code": "...", "error": "..."}` on errors 45 | 46 | ## Development Commands 47 | 48 | ### Setup 49 | 50 | ```bash 51 | # Create and activate virtual environment 52 | python -m venv env 53 | source env/bin/activate # On Windows: env\Scripts\activate 54 | 55 | # Install package in development mode with test dependencies 56 | pip install -e '.[test]' 57 | ``` 58 | 59 | ### Testing 60 | 61 | ```bash 62 | # Run all tests 63 | pytest 64 | 65 | # Run specific test file 66 | pytest tests/test_client.py 67 | 68 | # Run specific test 69 | pytest tests/test_client.py::test_client_do_basic 70 | 71 | # Run with verbose output 72 | pytest -v 73 | 74 | # Run with coverage 75 | pytest --cov=luno_python 76 | ``` 77 | 78 | ### Dependencies 79 | 80 | - **Runtime**: `requests>=2.18.4`, `six>=1.11.0` 81 | - **Test**: `pytest`, `pytest-cov`, `requests_mock` 82 | 83 | ## Testing Approach 84 | 85 | Tests use `requests_mock` to mock HTTP responses. The pattern: 86 | 87 | 1. Create a `Client` instance 88 | 2. Mount mock adapter to session: `adapter.register_uri(method, url, ...)` 89 | 3. Call client method and assert response 90 | 91 | Recent additions test the `get_balances()` method with `account_id` parameter for filtering results by account. 92 | 93 | ## Git Workflow 94 | 95 | When making changes: 96 | 97 | 1. Run `git pull` before creating a new branch 98 | 2. Branch naming: `{username}-{issue-number}-{description}` 99 | 3. Commit messages: Present tense, describe *why* not *what* 100 | 4. Example: `"client: Add account_id parameter to get_balances method"` 101 | 5. Push and create PR when ready 102 | 103 | ## Notes on Code Generation 104 | 105 | The `client.py` file contains ~100+ methods that are auto-generated from Luno's API specification. When modifying or adding methods: 106 | 107 | - Follow existing docstring format (includes HTTP method, path, permissions, parameter descriptions) 108 | - Each method constructs a `req` dict with parameters and calls `self.do()` 109 | - Type hints in docstrings use `:type param: type_name` format for Python 2 compatibility 110 | 111 | ## File Editing Requirements 112 | 113 | Always ensure files end with a newline character. This maintains consistency with Git diffs and repository standards. 114 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | """Tests for the Luno Python client.""" 2 | 3 | from decimal import Decimal 4 | 5 | import pytest 6 | import requests_mock 7 | 8 | try: 9 | from json.decoder import JSONDecodeError 10 | except ImportError: 11 | JSONDecodeError = ValueError 12 | 13 | from luno_python.client import Client 14 | from luno_python.error import APIError 15 | 16 | # Mock response fixtures 17 | MOCK_BALANCES_RESPONSE = { 18 | "balance": [ 19 | { 20 | "account_id": "12345678910", 21 | "asset": "XBT", 22 | "balance": "0.00", 23 | "reserved": "0.00", 24 | "unconfirmed": "0.00", 25 | }, 26 | { 27 | "account_id": "98765432100", 28 | "asset": "ETH", 29 | "balance": "1.50", 30 | "reserved": "0.10", 31 | "unconfirmed": "0.05", 32 | }, 33 | { 34 | "account_id": "55555555555", 35 | "asset": "ZAR", 36 | "balance": "1000.00", 37 | "reserved": "0.00", 38 | "unconfirmed": "0.00", 39 | }, 40 | ] 41 | } 42 | 43 | MOCK_BALANCES_RESPONSE_TWO_ACCOUNTS = { 44 | "balance": [ 45 | { 46 | "account_id": "12345678910", 47 | "asset": "XBT", 48 | "balance": "0.00", 49 | "reserved": "0.00", 50 | "unconfirmed": "0.00", 51 | }, 52 | { 53 | "account_id": "98765432100", 54 | "asset": "ETH", 55 | "balance": "1.50", 56 | "reserved": "0.10", 57 | "unconfirmed": "0.05", 58 | }, 59 | ] 60 | } 61 | 62 | MOCK_BALANCES_RESPONSE_INTEGER_IDS = { 63 | "balance": [ 64 | { 65 | "account_id": 12345678910, 66 | "asset": "XBT", 67 | "balance": "0.00", 68 | "reserved": "0.00", 69 | "unconfirmed": "0.00", 70 | }, 71 | { 72 | "account_id": 98765432100, 73 | "asset": "ETH", 74 | "balance": "1.50", 75 | "reserved": "0.10", 76 | "unconfirmed": "0.05", 77 | }, 78 | ] 79 | } 80 | 81 | MOCK_EMPTY_BALANCE_RESPONSE = {"balance": []} 82 | 83 | MOCK_MALFORMED_RESPONSE = {"some_other_key": "value"} 84 | 85 | 86 | def test_client(): 87 | """Test client initialization and configuration.""" 88 | c = Client() 89 | c.set_auth("api_key_id", "api_key_secret") 90 | c.set_base_url("base_url") 91 | c.set_timeout(10) 92 | 93 | assert c.api_key_id == "api_key_id" 94 | assert c.api_key_secret == "api_key_secret" 95 | assert c.base_url == "base_url" 96 | assert c.timeout == 10 97 | 98 | 99 | def test_client_do_basic(): 100 | """Test basic client do method functionality.""" 101 | c = Client() 102 | c.set_base_url("mock://test/") 103 | 104 | adapter = requests_mock.Adapter() 105 | c.session.mount("mock", adapter) 106 | 107 | adapter.register_uri("GET", "mock://test/", text="ok") 108 | with pytest.raises(Exception, match="unknown API error"): 109 | res = c.do("GET", "/") 110 | 111 | adapter.register_uri("GET", "mock://test/", text='{"key":"value"}') 112 | res = c.do("GET", "/") 113 | assert res["key"] == "value" 114 | 115 | adapter.register_uri("GET", "mock://test/", text="{}", status_code=400) 116 | res = c.do("GET", "/") # no exception, because no error present 117 | 118 | adapter.register_uri("GET", "mock://test/", text='{"error_code":"code","error":"message"}', status_code=400) 119 | with pytest.raises(APIError) as e: 120 | res = c.do("GET", "/") 121 | assert e.value.code == "code" 122 | assert e.value.message == "message" 123 | 124 | 125 | def test_get_balances_without_account_id(): 126 | """Test get_balances without account_id parameter (backward compatibility).""" 127 | c = Client() 128 | c.set_base_url("mock://test/") 129 | 130 | adapter = requests_mock.Adapter() 131 | c.session.mount("mock", adapter) 132 | 133 | adapter.register_uri("GET", "mock://test/api/1/balance", json=MOCK_BALANCES_RESPONSE) 134 | 135 | # Test without account_id - should return full response 136 | result = c.get_balances() 137 | assert result == MOCK_BALANCES_RESPONSE 138 | assert "balance" in result 139 | assert len(result["balance"]) == 3 140 | 141 | 142 | def test_get_balances_with_valid_account_id(): 143 | """Test get_balances with valid account_id parameter.""" 144 | c = Client() 145 | c.set_base_url("mock://test/") 146 | 147 | adapter = requests_mock.Adapter() 148 | c.session.mount("mock", adapter) 149 | 150 | adapter.register_uri("GET", "mock://test/api/1/balance", json=MOCK_BALANCES_RESPONSE) 151 | 152 | # Test with valid account_id - should return single account 153 | result = c.get_balances(account_id="12345678910") 154 | expected = { 155 | "account_id": "12345678910", 156 | "asset": "XBT", 157 | "balance": "0.00", 158 | "reserved": "0.00", 159 | "unconfirmed": "0.00", 160 | } 161 | assert result == expected 162 | 163 | # Test with another valid account_id 164 | result = c.get_balances(account_id="98765432100") 165 | expected = { 166 | "account_id": "98765432100", 167 | "asset": "ETH", 168 | "balance": "1.50", 169 | "reserved": "0.10", 170 | "unconfirmed": "0.05", 171 | } 172 | assert result == expected 173 | 174 | 175 | def test_get_balances_with_invalid_account_id(): 176 | """Test get_balances with invalid account_id parameter.""" 177 | c = Client() 178 | c.set_base_url("mock://test/") 179 | 180 | adapter = requests_mock.Adapter() 181 | c.session.mount("mock", adapter) 182 | 183 | adapter.register_uri("GET", "mock://test/api/1/balance", json=MOCK_BALANCES_RESPONSE_TWO_ACCOUNTS) 184 | 185 | # Test with invalid account_id - should return None 186 | result = c.get_balances(account_id="99999999999") 187 | assert result is None 188 | 189 | 190 | def test_get_balances_with_account_id_and_assets(): 191 | """Test get_balances with both account_id and assets parameters.""" 192 | c = Client() 193 | c.set_base_url("mock://test/") 194 | 195 | adapter = requests_mock.Adapter() 196 | c.session.mount("mock", adapter) 197 | 198 | adapter.register_uri("GET", "mock://test/api/1/balance", json=MOCK_BALANCES_RESPONSE_TWO_ACCOUNTS) 199 | 200 | # Test with both parameters 201 | result = c.get_balances(assets=["XBT"], account_id="12345678910") 202 | expected = { 203 | "account_id": "12345678910", 204 | "asset": "XBT", 205 | "balance": "0.00", 206 | "reserved": "0.00", 207 | "unconfirmed": "0.00", 208 | } 209 | assert result == expected 210 | 211 | 212 | def test_get_balances_with_account_id_type_conversion(): 213 | """Test get_balances with account_id type conversion (string vs int).""" 214 | c = Client() 215 | c.set_base_url("mock://test/") 216 | 217 | adapter = requests_mock.Adapter() 218 | c.session.mount("mock", adapter) 219 | 220 | adapter.register_uri("GET", "mock://test/api/1/balance", json=MOCK_BALANCES_RESPONSE_INTEGER_IDS) 221 | 222 | # Test with string account_id when API returns integer - should work due to type conversion 223 | result = c.get_balances(account_id="12345678910") 224 | expected = { 225 | "account_id": 12345678910, 226 | "asset": "XBT", 227 | "balance": "0.00", 228 | "reserved": "0.00", 229 | "unconfirmed": "0.00", 230 | } 231 | assert result == expected 232 | 233 | 234 | def test_get_balances_with_empty_balance_response(): 235 | """Test get_balances when API returns empty balance list.""" 236 | c = Client() 237 | c.set_base_url("mock://test/") 238 | 239 | adapter = requests_mock.Adapter() 240 | c.session.mount("mock", adapter) 241 | 242 | adapter.register_uri("GET", "mock://test/api/1/balance", json=MOCK_EMPTY_BALANCE_RESPONSE) 243 | 244 | # Test with account_id on empty response 245 | result = c.get_balances(account_id="12345678910") 246 | assert result is None 247 | 248 | # Test without account_id on empty response 249 | result = c.get_balances() 250 | assert result == MOCK_EMPTY_BALANCE_RESPONSE 251 | 252 | 253 | def test_get_balances_with_malformed_response(): 254 | """Test get_balances when API returns malformed response.""" 255 | c = Client() 256 | c.set_base_url("mock://test/") 257 | 258 | adapter = requests_mock.Adapter() 259 | c.session.mount("mock", adapter) 260 | 261 | adapter.register_uri("GET", "mock://test/api/1/balance", json=MOCK_MALFORMED_RESPONSE) 262 | 263 | # Test with account_id on malformed response 264 | result = c.get_balances(account_id="12345678910") 265 | assert result is None 266 | 267 | # Test without account_id on malformed response 268 | result = c.get_balances() 269 | assert result == MOCK_MALFORMED_RESPONSE 270 | 271 | 272 | def test_client_do_with_non_serializable_params(): 273 | """Test that non-JSON-serializable parameters raise a clear error.""" 274 | c = Client() 275 | c.set_base_url("mock://test/") 276 | 277 | # Test with Decimal (not JSON-serializable) 278 | with pytest.raises(TypeError) as exc_info: 279 | c.do("GET", "/", req={"amount": Decimal("10.5")}) 280 | assert "JSON-serializable" in str(exc_info.value) 281 | 282 | # Test with custom object (not JSON-serializable) 283 | class CustomObject: 284 | pass 285 | 286 | with pytest.raises(TypeError) as exc_info: 287 | c.do("GET", "/", req={"obj": CustomObject()}) 288 | assert "JSON-serializable" in str(exc_info.value) 289 | 290 | # Test with None request (should not raise) 291 | adapter = requests_mock.Adapter() 292 | c.session.mount("mock", adapter) 293 | adapter.register_uri("GET", "mock://test/", json={"result": "ok"}) 294 | result = c.do("GET", "/", req=None) 295 | assert result["result"] == "ok" 296 | -------------------------------------------------------------------------------- /luno_python/client.py: -------------------------------------------------------------------------------- 1 | """Luno API client implementation.""" 2 | 3 | from .base_client import BaseClient 4 | 5 | 6 | class Client(BaseClient): 7 | """ 8 | Python SDK for the Luno API. 9 | 10 | Example usage: 11 | 12 | from luno_python.client import Client 13 | 14 | 15 | c = Client(api_key_id='key_id', api_key_secret='key_secret') 16 | try: 17 | res = c.get_ticker(pair='XBTZAR') 18 | print res 19 | except Exception as e: 20 | print e 21 | """ 22 | 23 | def cancel_withdrawal(self, id): 24 | """Make a call to DELETE /api/1/withdrawals/{id}. 25 | 26 | Cancel a withdrawal request. 27 | This can only be done if the request is still in state PENDING. 28 | 29 | Permissions required: Perm_W_Withdrawals 30 | 31 | :param id: ID of the withdrawal to cancel. 32 | :type id: int 33 | """ 34 | req = { 35 | "id": id, 36 | } 37 | return self.do("DELETE", "/api/1/withdrawals/{id}", req=req, auth=True) 38 | 39 | def create_account(self, currency, name): 40 | """Make a call to POST /api/1/accounts. 41 | 42 | This request creates an Account for the specified currency. Please note that the balances for the Account will 43 | be displayed based on the asset value, which is the currency the Account is based on. 44 | 45 | Permissions required: Perm_W_Addresses 46 | 47 | :param currency: The currency code for the Account you want to create. Please see the Currency section for a 48 | detailed list of currencies supported by the Luno platform. 49 | 50 | Users must be verified to trade currency in order to be able to create an Account. For more 51 | information on the verification process, please see 52 | How do I verify my identity?. 53 | 54 | Users have a limit of 10 accounts per currency. 55 | :type currency: str 56 | :param name: The label to use for this account 57 | :type name: str 58 | """ 59 | req = { 60 | "currency": currency, 61 | "name": name, 62 | } 63 | return self.do("POST", "/api/1/accounts", req=req, auth=True) 64 | 65 | def create_beneficiary(self, account_type, bank_account_number, bank_name, bank_recipient): 66 | """Make a call to POST /api/1/beneficiaries. 67 | 68 | Create a new beneficiary. 69 | 70 | Permissions required: Perm_W_Beneficiaries 71 | 72 | :param account_type: Bank account type 73 | :type account_type: str 74 | :param bank_account_number: Beneficiary bank account number 75 | :type bank_account_number: str 76 | :param bank_name: Bank SWIFT code 77 | :type bank_name: str 78 | :param bank_recipient: The owner of the recipient account 79 | :type bank_recipient: str 80 | """ 81 | req = { 82 | "account_type": account_type, 83 | "bank_account_number": bank_account_number, 84 | "bank_name": bank_name, 85 | "bank_recipient": bank_recipient, 86 | } 87 | return self.do("POST", "/api/1/beneficiaries", req=req, auth=True) 88 | 89 | def create_funding_address(self, asset, account_id=None, name=None): 90 | """Make a call to POST /api/1/funding_address. 91 | 92 | Allocate a new receive address to your account. There is a rate limit of 1 93 | address per hour, but bursts of up to 10 addresses are allowed. Only 1 94 | Ethereum receive address can be created. 95 | 96 | Permissions required: Perm_W_Addresses 97 | 98 | :param asset: Currency code of the asset. 99 | :type asset: str 100 | :param account_id: An optional account_id to assign the new Receive Address too 101 | :type account_id: int 102 | :param name: An optional name for the new Receive Address 103 | :type name: str 104 | """ 105 | req = { 106 | "asset": asset, 107 | "account_id": account_id, 108 | "name": name, 109 | } 110 | return self.do("POST", "/api/1/funding_address", req=req, auth=True) 111 | 112 | def create_withdrawal(self, amount, type, beneficiary_id=None, external_id=None, fast=None, reference=None): 113 | """Make a call to POST /api/1/withdrawals. 114 | 115 | Create a new withdrawal request to the specified beneficiary. 116 | 117 | Permissions required: Perm_W_Withdrawals 118 | 119 | :param amount: Amount to withdraw. The currency withdrawn depends on the type setting. 120 | :type amount: float 121 | :param type: Withdrawal method. 122 | :type type: str 123 | :param beneficiary_id: The beneficiary ID of the bank account the withdrawal will be paid out to. 124 | This parameter is required if the user has set up multiple beneficiaries. 125 | The beneficiary ID can be found by selecting on the beneficiary name on the user's 126 | Beneficiaries page. 127 | :type beneficiary_id: int 128 | :param external_id: Optional unique ID to associate with this withdrawal. 129 | Useful to prevent duplicate sends. 130 | This field supports all alphanumeric characters including "-" and "_". 131 | :type external_id: str 132 | :param fast: If true, it will be a fast withdrawal if possible. Fast withdrawals come with a fee. 133 | Currently fast withdrawals are only available for `type=ZAR_EFT`; for other types, an error is 134 | returned. Fast withdrawals are not possible for Bank of Baroda, Deutsche Bank, Merrill Lynch South 135 | Africa, UBS, Postbank and Tyme Bank. 136 | The fee to be charged is the same as when withdrawing from the UI. 137 | :type fast: bool 138 | :param reference: For internal use. 139 | Deprecated: We don't allow custom references and will remove this soon. 140 | :type reference: str 141 | """ 142 | req = { 143 | "amount": amount, 144 | "type": type, 145 | "beneficiary_id": beneficiary_id, 146 | "external_id": external_id, 147 | "fast": fast, 148 | "reference": reference, 149 | } 150 | return self.do("POST", "/api/1/withdrawals", req=req, auth=True) 151 | 152 | def delete_beneficiary(self, id): 153 | """Make a call to DELETE /api/1/beneficiaries/{id}. 154 | 155 | Delete a beneficiary 156 | 157 | Permissions required: Perm_W_Beneficiaries 158 | 159 | :param id: ID of the Beneficiary to delete. 160 | :type id: int 161 | """ 162 | req = { 163 | "id": id, 164 | } 165 | return self.do("DELETE", "/api/1/beneficiaries/{id}", req=req, auth=True) 166 | 167 | def get_balances(self, assets=None, account_id=None): 168 | """Make a call to GET /api/1/balance. 169 | 170 | The list of all Accounts and their respective balances for the requesting user. 171 | 172 | Permissions required: Perm_R_Balance 173 | 174 | :param assets: Only return balances for wallets with these currencies (if not provided, 175 | all balances will be returned). To request balances for multiple currencies, 176 | pass the parameter multiple times, 177 | e.g. `assets=XBT&assets=ETH`. 178 | :type assets: list 179 | :param account_id: Only return balance for the account with this ID. If provided, 180 | returns a single account object instead of the full response. 181 | :type account_id: str 182 | """ 183 | req = { 184 | "assets": assets, 185 | } 186 | response = self.do("GET", "/api/1/balance", req=req, auth=True) 187 | 188 | # If account_id is specified, filter to return only that account 189 | if account_id is not None: 190 | if "balance" in response: 191 | for account in response["balance"]: 192 | if str(account.get("account_id")) == str(account_id): 193 | return account 194 | # If account_id not found, return None 195 | return None 196 | 197 | # Return full response if no account_id specified (backward compatibility) 198 | return response 199 | 200 | def get_candles(self, duration, pair, since): 201 | """Make a call to GET /api/exchange/1/candles. 202 | 203 | Get candlestick market data from the specified time until now, from the oldest to the most recent. 204 | 205 | Permissions required: MP_None 206 | 207 | :param duration: Candle duration in seconds. 208 | For example, 300 corresponds to 5m candles. Currently supported 209 | durations are: 60 (1m), 300 (5m), 900 (15m), 1800 (30m), 3600 (1h), 210 | 10800 (3h), 14400 (4h), 28800 (8h), 86400 (24h), 259200 (3d), 604800 211 | (7d). 212 | :type duration: int 213 | :param pair: Currency pair 214 | :type pair: str 215 | :param since: Filter to candles starting on or after this timestamp (Unix milliseconds). 216 | Only up to 1000 of the earliest candles are returned. 217 | :type since: int 218 | """ 219 | req = { 220 | "duration": duration, 221 | "pair": pair, 222 | "since": since, 223 | } 224 | return self.do("GET", "/api/exchange/1/candles", req=req, auth=True) 225 | 226 | def get_fee_info(self, pair): 227 | """Make a call to GET /api/1/fee_info. 228 | 229 | Return the fees and 30 day trading volume (as of midnight) for a given currency pair. For complete details, 230 | please see Fees & Features. 231 | 232 | Permissions required: Perm_R_Orders 233 | 234 | :param pair: Get fee information about this pair. 235 | :type pair: str 236 | """ 237 | req = { 238 | "pair": pair, 239 | } 240 | return self.do("GET", "/api/1/fee_info", req=req, auth=True) 241 | 242 | def get_funding_address(self, asset, address=None): 243 | """Make a call to GET /api/1/funding_address. 244 | 245 | Return the default receive address associated with your account and the 246 | amount received via the address. Users can specify an optional address parameter to return information for a 247 | non-default receive address. 248 | In the response, total_received is the total confirmed amount received excluding unconfirmed 249 | transactions. total_unconfirmed is the total sum of unconfirmed receive transactions. 250 | 251 | Permissions required: Perm_R_Addresses 252 | 253 | :param asset: Currency code of the asset. 254 | :type asset: str 255 | :param address: Specific cryptocurrency address to retrieve. If not provided, the 256 | default address will be used. 257 | :type address: str 258 | """ 259 | req = { 260 | "asset": asset, 261 | "address": address, 262 | } 263 | return self.do("GET", "/api/1/funding_address", req=req, auth=True) 264 | 265 | def get_move(self, client_move_id=None, id=None): 266 | """Make a call to GET /api/exchange/1/move. 267 | 268 | Get a specific move funds instruction by either id or 269 | client_move_id. If both are provided an API error will be 270 | returned. 271 | 272 | Permissions required: MP_None 273 | 274 | :param client_move_id: Get by the user defined ID. This is mutually exclusive with id and is 275 | required if id is not provided. 276 | :type client_move_id: str 277 | :param id: Get by the system ID. This is mutually exclusive with client_move_id and is required if 278 | client_move_id is not provided. 279 | :type id: str 280 | """ 281 | req = { 282 | "client_move_id": client_move_id, 283 | "id": id, 284 | } 285 | return self.do("GET", "/api/exchange/1/move", req=req, auth=True) 286 | 287 | def get_order(self, id): 288 | """Make a call to GET /api/1/orders/{id}. 289 | 290 | Get an Order's details by its ID. 291 | 292 | Permissions required: Perm_R_Orders 293 | 294 | :param id: Order reference 295 | :type id: str 296 | """ 297 | req = { 298 | "id": id, 299 | } 300 | return self.do("GET", "/api/1/orders/{id}", req=req, auth=True) 301 | 302 | def get_order_book(self, pair): 303 | """Make a call to GET /api/1/orderbook_top. 304 | 305 | This request returns the best 100 `bids` and `asks`, for the currency pair specified, in the Order Book. 306 | 307 | `asks` are sorted by price ascending and `bids` are sorted by price descending. 308 | 309 | Multiple orders at the same price are aggregated. 310 | 311 | :param pair: Currency pair of the Orders to retrieve 312 | :type pair: str 313 | """ 314 | req = { 315 | "pair": pair, 316 | } 317 | return self.do("GET", "/api/1/orderbook_top", req=req, auth=False) 318 | 319 | def get_order_book_full(self, pair): 320 | """Make a call to GET /api/1/orderbook. 321 | 322 | This request returns all `bids` and `asks`, for the currency pair specified, in the Order Book. 323 | 324 | `asks` are sorted by price ascending and `bids` are sorted by price descending. 325 | 326 | Multiple orders at the same price are not aggregated. 327 | 328 | WARNING: This may return a large amount of data. 329 | Users are recommended to use the top 100 bids and asks 330 | or the Streaming API. 331 | 332 | :param pair: Currency pair of the Orders to retrieve 333 | :type pair: str 334 | """ 335 | req = { 336 | "pair": pair, 337 | } 338 | return self.do("GET", "/api/1/orderbook", req=req, auth=False) 339 | 340 | def get_order_v2(self, id): 341 | """Make a call to GET /api/exchange/2/orders/{id}. 342 | 343 | Get the details for an order. 344 | 345 | Permissions required: Perm_R_Orders 346 | 347 | :param id: Order reference 348 | :type id: str 349 | """ 350 | req = { 351 | "id": id, 352 | } 353 | return self.do("GET", "/api/exchange/2/orders/{id}", req=req, auth=True) 354 | 355 | def get_order_v3(self, client_order_id=None, id=None): 356 | """Make a call to GET /api/exchange/3/order. 357 | 358 | Get the details for an order by order reference or client order ID. 359 | Exactly one of the two parameters must be provided, otherwise an error is returned. 360 | Permissions required: Perm_R_Orders 361 | 362 | :param client_order_id: Client Order ID has the value that was passed in when the Order was posted. 363 | :type client_order_id: str 364 | :param id: Order reference 365 | :type id: str 366 | """ 367 | req = { 368 | "client_order_id": client_order_id, 369 | "id": id, 370 | } 371 | return self.do("GET", "/api/exchange/3/order", req=req, auth=True) 372 | 373 | def get_ticker(self, pair): 374 | """Make a call to GET /api/1/ticker. 375 | 376 | Return the latest ticker indicators for the specified currency pair. 377 | 378 | Please see the Currency list for the complete list of supported currency pairs. 379 | 380 | :param pair: Currency pair 381 | :type pair: str 382 | """ 383 | req = { 384 | "pair": pair, 385 | } 386 | return self.do("GET", "/api/1/ticker", req=req, auth=False) 387 | 388 | def get_tickers(self, pair=None): 389 | """Make a call to GET /api/1/tickers. 390 | 391 | Return the latest ticker indicators from all active Luno exchanges. 392 | 393 | Please see the Currency list for the complete list of supported currency pairs. 394 | 395 | :param pair: Return tickers for multiple markets (if not provided, all tickers will be returned). 396 | To request tickers for multiple markets, pass the parameter multiple times, 397 | e.g. `pair=XBTZAR&pair=ETHZAR`. 398 | :type pair: list 399 | """ 400 | req = { 401 | "pair": pair, 402 | } 403 | return self.do("GET", "/api/1/tickers", req=req, auth=False) 404 | 405 | def get_withdrawal(self, id): 406 | """Make a call to GET /api/1/withdrawals/{id}. 407 | 408 | Return the status of a particular withdrawal request. 409 | 410 | Permissions required: Perm_R_Withdrawals 411 | 412 | :param id: Withdrawal ID to retrieve. 413 | :type id: int 414 | """ 415 | req = { 416 | "id": id, 417 | } 418 | return self.do("GET", "/api/1/withdrawals/{id}", req=req, auth=True) 419 | 420 | def list_beneficiaries(self, bank_recipient=None): 421 | """Make a call to GET /api/1/beneficiaries. 422 | 423 | Return a list of bank beneficiaries. 424 | 425 | Permissions required: Perm_R_Beneficiaries 426 | 427 | :param bank_recipient: :type bank_recipient: str 428 | """ 429 | req = { 430 | "bank_recipient": bank_recipient, 431 | } 432 | return self.do("GET", "/api/1/beneficiaries", req=req, auth=True) 433 | 434 | def list_moves(self, before=None, limit=None): 435 | """Make a call to GET /api/exchange/1/move/list_moves. 436 | 437 | Return a list of the most recent moves ordered from newest to oldest. 438 | This endpoint will list up to 100 most recent moves by default. 439 | 440 | Permissions required: MP_None 441 | 442 | :param before: Filter to moves requested before this timestamp (Unix milliseconds) 443 | :type before: int 444 | :param limit: Limit to this many moves 445 | :type limit: int 446 | """ 447 | req = { 448 | "before": before, 449 | "limit": limit, 450 | } 451 | return self.do("GET", "/api/exchange/1/move/list_moves", req=req, auth=True) 452 | 453 | def list_orders(self, created_before=None, limit=None, pair=None, state=None): 454 | """Make a call to GET /api/1/listorders. 455 | 456 | Return a list of the most recently placed Orders. 457 | Users can specify an optional state=PENDING parameter to restrict the results to only open Orders. 458 | Users can also specify the market by using the optional currency pair parameter. 459 | 460 | Permissions required: Perm_R_Orders 461 | 462 | :param created_before: Filter to orders created before this timestamp (Unix milliseconds) 463 | :type created_before: int 464 | :param limit: Limit to this many orders 465 | :type limit: int 466 | :param pair: Filter to only orders of this currency pair 467 | :type pair: str 468 | :param state: Filter to only orders of this state 469 | :type state: str 470 | """ 471 | req = { 472 | "created_before": created_before, 473 | "limit": limit, 474 | "pair": pair, 475 | "state": state, 476 | } 477 | return self.do("GET", "/api/1/listorders", req=req, auth=True) 478 | 479 | def list_orders_v2(self, closed=None, created_before=None, limit=None, pair=None): 480 | """Make a call to GET /api/exchange/2/listorders. 481 | 482 | Return a list of the most recently placed orders ordered from newest to 483 | oldest. This endpoint will list up to 100 most recent open orders by 484 | default. 485 | 486 | Please note: This data is archived 100 days after an exchange order is completed. 487 | 488 | Permissions required: Perm_R_Orders 489 | 490 | :param closed: If true, will return closed orders instead of open orders. 491 | :type closed: bool 492 | :param created_before: Filter to orders created before this timestamp (Unix milliseconds) 493 | :type created_before: int 494 | :param limit: Limit to this many orders 495 | :type limit: int 496 | :param pair: Filter to only orders of this currency pair. 497 | :type pair: str 498 | """ 499 | req = { 500 | "closed": closed, 501 | "created_before": created_before, 502 | "limit": limit, 503 | "pair": pair, 504 | } 505 | return self.do("GET", "/api/exchange/2/listorders", req=req, auth=True) 506 | 507 | def list_pending_transactions(self, id): 508 | """Make a call to GET /api/1/accounts/{id}/pending. 509 | 510 | Return a list of all transactions that have not completed for the Account. 511 | 512 | Pending transactions are not numbered, and may be reordered, deleted or updated at any time. 513 | 514 | Permissions required: Perm_R_Transactions 515 | 516 | :param id: Account ID 517 | :type id: int 518 | """ 519 | req = { 520 | "id": id, 521 | } 522 | return self.do("GET", "/api/1/accounts/{id}/pending", req=req, auth=True) 523 | 524 | def list_trades(self, pair, since=None): 525 | """Make a call to GET /api/1/trades. 526 | 527 | Return a list of recent trades for the specified currency pair. At most 528 | 100 trades are returned per call and never trades older than 24h. The 529 | trades are sorted from newest to oldest. 530 | 531 | Please see the Currency list for the complete list of supported currency pairs. 532 | 533 | :param pair: Currency pair of the market to list the trades from 534 | :type pair: str 535 | :param since: Fetch trades executed after this time, specified as a Unix timestamp in 536 | milliseconds. An error will be returned if this is before 24h ago. Use 537 | this parameter to either restrict to a shorter window or to iterate over 538 | the trades in case you need more than the 100 most recent trades. 539 | :type since: int 540 | """ 541 | req = { 542 | "pair": pair, 543 | "since": since, 544 | } 545 | return self.do("GET", "/api/1/trades", req=req, auth=False) 546 | 547 | def list_transactions(self, id, max_row, min_row): 548 | """Make a call to GET /api/1/accounts/{id}/transactions. 549 | 550 | Return a list of transaction entries from an account. 551 | 552 | Transaction entry rows are numbered sequentially starting from 1, where 1 is 553 | the oldest entry. The range of rows to return are specified with the 554 | min_row (inclusive) and max_row (exclusive) 555 | parameters. At most 1000 rows can be requested per call. 556 | 557 | If min_row or max_row is non-positive, the range 558 | wraps around the most recent row. For example, to fetch the 100 most recent 559 | rows, use min_row=-100 and max_row=0. 560 | 561 | Permissions required: Perm_R_Transactions 562 | 563 | :param id: Account ID - the unique identifier for the specific Account. 564 | :type id: int 565 | :param max_row: Maximum of the row range to return (exclusive) 566 | :type max_row: int 567 | :param min_row: Minimum of the row range to return (inclusive) 568 | :type min_row: int 569 | """ 570 | req = { 571 | "id": id, 572 | "max_row": max_row, 573 | "min_row": min_row, 574 | } 575 | return self.do("GET", "/api/1/accounts/{id}/transactions", req=req, auth=True) 576 | 577 | def list_transfers(self, account_id, before=None, limit=None): 578 | """Make a call to GET /api/exchange/1/transfers. 579 | 580 | Return a list of the most recent confirmed transfers ordered from newest to 581 | oldest. 582 | This includes bank transfers, card payments, or on-chain transactions that 583 | have been reflected on your account available balance. 584 | 585 | Note that the Transfer `amount` is always a positive value and you should 586 | use the `inbound` flag to determine the direction of the transfer. 587 | 588 | If you need to paginate the results you can set the `before` parameter to 589 | the last returned transfer `created_at` field value and repeat the request 590 | until you have all the transfers you need. 591 | This endpoint will list up to 100 transfers at a time by default. 592 | 593 | Permissions required: Perm_R_Transfers 594 | 595 | :param account_id: Unique identifier of the account to list the transfers from. 596 | :type account_id: int 597 | :param before: Filter to transfers created before this timestamp (Unix milliseconds). 598 | The default value (0) will return the latest transfers on the account. 599 | :type before: int 600 | :param limit: Limit to this many transfers. 601 | :type limit: int 602 | """ 603 | req = { 604 | "account_id": account_id, 605 | "before": before, 606 | "limit": limit, 607 | } 608 | return self.do("GET", "/api/exchange/1/transfers", req=req, auth=True) 609 | 610 | def list_user_trades( 611 | self, 612 | pair, 613 | after_seq=None, 614 | before=None, 615 | before_seq=None, 616 | limit=None, 617 | since=None, 618 | sort_desc=None, 619 | ): 620 | """Make a call to GET /api/1/listtrades. 621 | 622 | Return a list of the recent Trades for a given currency pair for this user, sorted by oldest first. 623 | If before is specified, then Trades are returned sorted by most-recent first. 624 | 625 | type in the response indicates the type of Order that was placed to participate in the trade. 626 | Possible types: BID, ASK. 627 | 628 | If is_buy in the response is true, then the Order which completed the trade (market taker) was a 629 | Bid Order. 630 | 631 | Results of this query may lag behind the latest data. 632 | 633 | Permissions required: Perm_R_Orders 634 | 635 | :param pair: Filter to trades of this currency pair. 636 | :type pair: str 637 | :param after_seq: Filter to trades from (including) this sequence number. 638 | Default behaviour is not to include this filter. 639 | :type after_seq: int 640 | :param before: Filter to trades before this timestamp (Unix milliseconds). 641 | :type before: int 642 | :param before_seq: Filter to trades before (excluding) this sequence number. 643 | Default behaviour is not to include this filter. 644 | :type before_seq: int 645 | :param limit: Limit to this number of trades (default 100). 646 | :type limit: int 647 | :param since: Filter to trades on or after this timestamp (Unix milliseconds). 648 | :type since: int 649 | :param sort_desc: If set to true, sorts trades in descending order, otherwise ascending 650 | order will be assumed. 651 | :type sort_desc: bool 652 | """ 653 | req = { 654 | "pair": pair, 655 | "after_seq": after_seq, 656 | "before": before, 657 | "before_seq": before_seq, 658 | "limit": limit, 659 | "since": since, 660 | "sort_desc": sort_desc, 661 | } 662 | return self.do("GET", "/api/1/listtrades", req=req, auth=True) 663 | 664 | def list_withdrawals(self, before_id=None, limit=None): 665 | """Make a call to GET /api/1/withdrawals. 666 | 667 | Return a list of withdrawal requests. 668 | 669 | Permissions required: Perm_R_Withdrawals 670 | 671 | :param before_id: Filter to withdrawals requested on or before the withdrawal with this ID. 672 | Can be used for pagination. 673 | :type before_id: int 674 | :param limit: Limit to this many withdrawals 675 | :type limit: int 676 | """ 677 | req = { 678 | "before_id": before_id, 679 | "limit": limit, 680 | } 681 | return self.do("GET", "/api/1/withdrawals", req=req, auth=True) 682 | 683 | def markets(self, pair=None): 684 | """Make a call to GET /api/exchange/1/markets. 685 | 686 | List all supported markets parameter information like price scale, min and 687 | max order volumes and market ID. 688 | 689 | :param pair: List of market pairs to return. Requesting only the required pairs will improve response times. 690 | :type pair: list 691 | """ 692 | req = { 693 | "pair": pair, 694 | } 695 | return self.do("GET", "/api/exchange/1/markets", req=req, auth=False) 696 | 697 | def move(self, amount, credit_account_id, debit_account_id, client_move_id=None): 698 | """Make a call to POST /api/exchange/1/move. 699 | 700 | Move funds between two of your transactional accounts with the same currency 701 | The funds may not be moved by the time the request returns. The GET method 702 | can be used to poll for the move's status. 703 | 704 | Note: moves will show as transactions, but not as transfers. 705 | 706 | Permissions required: MP_None_Write 707 | 708 | :param amount: Amount to transfer. Must be positive. 709 | :type amount: float 710 | :param credit_account_id: The account to credit the funds to. 711 | :type credit_account_id: int 712 | :param debit_account_id: The account to debit the funds from. 713 | :type debit_account_id: int 714 | :param client_move_id: Client move ID. 715 | May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). 716 | Maximum length: 255. It will be available in read endpoints, so you can use it to avoid 717 | duplicate moves between the same accounts. Values must be unique across all your 718 | successful calls of this endpoint; trying to create a move request with the same 719 | `client_move_id` as one of your past move requests will result in a HTTP 409 Conflict 720 | response. 721 | :type client_move_id: str 722 | """ 723 | req = { 724 | "amount": amount, 725 | "credit_account_id": credit_account_id, 726 | "debit_account_id": debit_account_id, 727 | "client_move_id": client_move_id, 728 | } 729 | return self.do("POST", "/api/exchange/1/move", req=req, auth=True) 730 | 731 | def post_limit_order( 732 | self, 733 | pair, 734 | price, 735 | type, 736 | volume, 737 | base_account_id=None, 738 | client_order_id=None, 739 | counter_account_id=None, 740 | post_only=None, 741 | stop_direction=None, 742 | stop_price=None, 743 | time_in_force=None, 744 | timestamp=None, 745 | ttl=None, 746 | ): 747 | """Make a call to POST /api/1/postorder. 748 | 749 | Warning! Orders cannot be reversed once they have executed. 750 | Please ensure your program has been thoroughly tested before submitting Orders. 751 | 752 | If no base_account_id or counter_account_id are specified, 753 | your default base currency or counter currency account will be used. 754 | You can find your Account IDs by calling the Balances API. 755 | 756 | Permissions required: Perm_W_Orders 757 | 758 | :param pair: The currency pair to trade. 759 | :type pair: str 760 | :param price: Limit price as a decimal string in units of ZAR/BTC. 761 | :type price: float 762 | :param type: BID for a bid (buy) limit order
763 | ASK for an ask (sell) limit order 764 | :type type: str 765 | :param volume: Amount of cryptocurrency to buy or sell as a decimal string in units of the currency. 766 | :type volume: float 767 | :param base_account_id: The base currency Account to use in the trade. 768 | :type base_account_id: int 769 | :param client_order_id: Client order ID. 770 | May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). 771 | Maximum length: 255. It will be available in read endpoints, so you can use it to 772 | reconcile Luno with your internal system. Values must be unique across all your 773 | successful order creation endpoint calls; trying to create an order with the same 774 | `client_order_id` as one of your past orders will result in a HTTP 409 Conflict 775 | response. 776 | :type client_order_id: str 777 | :param counter_account_id: The counter currency Account to use in the trade. 778 | :type counter_account_id: int 779 | :param post_only: Post-only Orders will be cancelled if they would otherwise have traded immediately. 780 | For example, if there's a bid at ZAR 100,000 and you place a post-only ask at ZAR 100,000, 781 | your order will be cancelled instead of trading. 782 | If the best bid is ZAR 100,000 and you place a post-only ask at ZAR 101,000, 783 | your order won't trade but will go into the order book. 784 | :type post_only: bool 785 | :param stop_direction: Side of the trigger price to activate the order. This should be set if `stop_price` is 786 | also set. 787 | 788 | `RELATIVE_LAST_TRADE` will automatically infer the direction based on the last trade 789 | price and the stop price. If last trade price is less than stop price then stop direction 790 | is ABOVE otherwise is BELOW. 791 | :type stop_direction: str 792 | :param stop_price: Trigger trade price to activate this order as a decimal string. If this 793 | is set then this is treated as a Stop Limit Order and `stop_direction` 794 | is expected to be set too. 795 | :type stop_price: float 796 | :param time_in_force: GTC Good 'Til Cancelled. The order remains open until it is filled or 797 | cancelled by the user.
798 | IOC Immediate Or Cancel. The part of the order that cannot be filled 799 | immediately will be cancelled. Cannot be post-only.
800 | FOK Fill Or Kill. If the order cannot be filled immediately and completely it 801 | will be cancelled before any trade. Cannot be post-only. 802 | :type time_in_force: str 803 | :param timestamp: Unix timestamp in milliseconds of when the request was created and sent. 804 | :type timestamp: int 805 | :param ttl: Specifies the number of milliseconds after timestamp the request is valid for. 806 | If `timestamp` is not specified, `ttl` will not be used. 807 | :type ttl: int 808 | """ 809 | req = { 810 | "pair": pair, 811 | "price": price, 812 | "type": type, 813 | "volume": volume, 814 | "base_account_id": base_account_id, 815 | "client_order_id": client_order_id, 816 | "counter_account_id": counter_account_id, 817 | "post_only": post_only, 818 | "stop_direction": stop_direction, 819 | "stop_price": stop_price, 820 | "time_in_force": time_in_force, 821 | "timestamp": timestamp, 822 | "ttl": ttl, 823 | } 824 | return self.do("POST", "/api/1/postorder", req=req, auth=True) 825 | 826 | def post_market_order( 827 | self, 828 | pair, 829 | type, 830 | base_account_id=None, 831 | base_volume=None, 832 | client_order_id=None, 833 | counter_account_id=None, 834 | counter_volume=None, 835 | timestamp=None, 836 | ttl=None, 837 | ): 838 | """Make a call to POST /api/1/marketorder. 839 | 840 | A Market Order executes immediately, and either buys as much of the asset that can be bought for a set amount of 841 | fiat currency, or sells a set amount of the asset for as much as possible. 842 | 843 | Warning! Orders cannot be reversed once they have executed. 844 | Please ensure your program has been thoroughly tested before submitting Orders. 845 | 846 | If no base_account_id or counter_account_id are specified, the default base 847 | currency or counter currency account will be used. Users can find their account IDs by calling the 848 | Balances request. 849 | 850 | Permissions required: Perm_W_Orders 851 | 852 | :param pair: The currency pair to trade. 853 | :type pair: str 854 | :param type: BUY to buy an asset
855 | SELL to sell an asset 856 | :type type: str 857 | :param base_account_id: The base currency account to use in the trade. 858 | :type base_account_id: int 859 | :param base_volume: For a SELL order: amount of the base currency to use (e.g. how much BTC to sell 860 | for EUR in the BTC/EUR market) 861 | :type base_volume: float 862 | :param client_order_id: Client order ID. 863 | May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). 864 | Maximum length: 255. It will be available in read endpoints, so you can use it to 865 | reconcile Luno with your internal system. Values must be unique across all your 866 | successful order creation endpoint calls; trying to create an order with the same 867 | `client_order_id` as one of your past orders will result in a HTTP 409 Conflict 868 | response. 869 | :type client_order_id: str 870 | :param counter_account_id: The counter currency account to use in the trade. 871 | :type counter_account_id: int 872 | :param counter_volume: For a BUY order: amount of the counter currency to use (e.g. how much EUR to 873 | use to buy BTC in the BTC/EUR market) 874 | :type counter_volume: float 875 | :param timestamp: Unix timestamp in milliseconds of when the request was created and sent. 876 | :type timestamp: int 877 | :param ttl: Specifies the number of milliseconds after timestamp the request is valid for. 878 | If `timestamp` is not specified, `ttl` will not be used. 879 | :type ttl: int 880 | """ 881 | req = { 882 | "pair": pair, 883 | "type": type, 884 | "base_account_id": base_account_id, 885 | "base_volume": base_volume, 886 | "client_order_id": client_order_id, 887 | "counter_account_id": counter_account_id, 888 | "counter_volume": counter_volume, 889 | "timestamp": timestamp, 890 | "ttl": ttl, 891 | } 892 | return self.do("POST", "/api/1/marketorder", req=req, auth=True) 893 | 894 | def send( 895 | self, 896 | address, 897 | amount, 898 | currency, 899 | account_id=None, 900 | description=None, 901 | destination_tag=None, 902 | external_id=None, 903 | forex_notice_self_declaration=None, 904 | has_destination_tag=None, 905 | is_drb=None, 906 | is_forex_send=None, 907 | memo=None, 908 | message=None, 909 | ): 910 | """Make a call to POST /api/1/send. 911 | 912 | Send assets from an Account. Please note that the asset type sent must match the receive address of the same 913 | cryptocurrency of the same type - Bitcoin to Bitcoin, Ethereum to Ethereum, etc. 914 | 915 | Sends can be made to cryptocurrency receive addresses. 916 | 917 | Note: This is currently unavailable to users who are verified in countries with money travel rules. 918 | 919 | Permissions required: Perm_W_Send 920 | 921 | :param address: Destination address or email address. 922 | 923 | Note: 924 | 930 | :type address: str 931 | :param amount: Amount to send as a decimal string. 932 | :type amount: float 933 | :param currency: Currency to send. 934 | :type currency: str 935 | :param account_id: Optional source account. In case of multiple accounts for a single currency, the source 936 | account that will provide the funds for the transaction may be specified. If omitted, the 937 | default account will be used. 938 | :type account_id: int 939 | :param description: User description for the transaction to record on the account statement. 940 | :type description: str 941 | :param destination_tag: Optional XRP destination tag. Note that HasDestinationTag must be true if this value is 942 | provided. 943 | :type destination_tag: int 944 | :param external_id: Optional unique ID to associate with this withdrawal. 945 | Useful to prevent duplicate sends in case of failure. 946 | This supports all alphanumeric characters, as well as "-" and "_". 947 | :type external_id: str 948 | :param forex_notice_self_declaration: Only required for Foreign Exchange Notification under the Malaysia FEN 949 | rules. ForexNoticeSelfDeclaration must be true if the user has exceeded 950 | his/her annual investment limit in foreign currency assets. 951 | :type forex_notice_self_declaration: bool 952 | :param has_destination_tag: Optional boolean flag indicating that a XRP destination tag is provided (even if 953 | zero). 954 | :type has_destination_tag: bool 955 | :param is_drb: Only required for Foreign Exchange Notification under the Malaysia FEN rules. IsDRB must be true 956 | if the user has Domestic Ringgit Borrowing (DRB). 957 | :type is_drb: bool 958 | :param is_forex_send: Only required for Foreign Exchange Notification under the Malaysia FEN rules. IsForexSend 959 | must be true if sending to an address hosted outside of Malaysia. 960 | :type is_forex_send: bool 961 | :param memo: Optional memo string used to provide account information for ATOM, etc. where it holds "account" 962 | information for a generic address. 963 | :type memo: str 964 | :param message: Message to send to the recipient. 965 | This is only relevant when sending to an email address. 966 | :type message: str 967 | """ 968 | req = { 969 | "address": address, 970 | "amount": amount, 971 | "currency": currency, 972 | "account_id": account_id, 973 | "description": description, 974 | "destination_tag": destination_tag, 975 | "external_id": external_id, 976 | "forex_notice_self_declaration": forex_notice_self_declaration, 977 | "has_destination_tag": has_destination_tag, 978 | "is_drb": is_drb, 979 | "is_forex_send": is_forex_send, 980 | "memo": memo, 981 | "message": message, 982 | } 983 | return self.do("POST", "/api/1/send", req=req, auth=True) 984 | 985 | def send_fee(self, address, amount, currency): 986 | """Make a call to GET /api/1/send_fee. 987 | 988 | Calculate fees involved with a crypto send request. 989 | 990 | Send address can be to a cryptocurrency receive address, or the email address of another Luno platform user. 991 | 992 | Permissions required: MP_None 993 | 994 | :param address: Destination address or email address. 995 | 996 | Note: 997 | 1003 | :type address: str 1004 | :param amount: Amount to send as a decimal string. 1005 | :type amount: float 1006 | :param currency: Currency to send. 1007 | :type currency: str 1008 | """ 1009 | req = { 1010 | "address": address, 1011 | "amount": amount, 1012 | "currency": currency, 1013 | } 1014 | return self.do("GET", "/api/1/send_fee", req=req, auth=True) 1015 | 1016 | def stop_order(self, order_id): 1017 | """Make a call to POST /api/1/stoporder. 1018 | 1019 | Request to cancel an Order. 1020 | 1021 | Note!: Once an Order has been completed, it can not be reversed. 1022 | The return value from this request will indicate if the Stop request was successful or not. 1023 | 1024 | Permissions required: Perm_W_Orders 1025 | 1026 | :param order_id: The Order identifier as a string. 1027 | :type order_id: str 1028 | """ 1029 | req = { 1030 | "order_id": order_id, 1031 | } 1032 | return self.do("POST", "/api/1/stoporder", req=req, auth=True) 1033 | 1034 | def update_account_name(self, id, name): 1035 | """Make a call to PUT /api/1/accounts/{id}/name. 1036 | 1037 | Update the name of an account with a given ID. 1038 | 1039 | Permissions required: Perm_W_Addresses 1040 | 1041 | :param id: Account ID - the unique identifier for the specific Account. 1042 | :type id: int 1043 | :param name: The label to use for this account 1044 | :type name: str 1045 | """ 1046 | req = { 1047 | "id": id, 1048 | "name": name, 1049 | } 1050 | return self.do("PUT", "/api/1/accounts/{id}/name", req=req, auth=True) 1051 | 1052 | def validate( 1053 | self, 1054 | address, 1055 | currency, 1056 | address_name=None, 1057 | beneficiary_name=None, 1058 | country=None, 1059 | date_of_birth=None, 1060 | destination_tag=None, 1061 | has_destination_tag=None, 1062 | institution_name=None, 1063 | is_legal_entity=None, 1064 | is_private_wallet=None, 1065 | is_self_send=None, 1066 | memo=None, 1067 | nationality=None, 1068 | physical_address=None, 1069 | wallet_name=None, 1070 | ): 1071 | """Make a call to POST /api/1/address/validate. 1072 | 1073 | Validate receive addresses, to which a customer wishes to make cryptocurrency sends, are verified under covering 1074 | regulatory requirements for the customer such as travel rules. 1075 | 1076 | Permissions required: Perm_W_Send 1077 | 1078 | :param address: Destination address or email address. 1079 | 1080 | Note: 1081 | 1087 | :type address: str 1088 | :param currency: Currency is the currency associated with the address. 1089 | :type currency: str 1090 | :param address_name: AddressName is the optional name under which to store the address as in the address book. 1091 | :type address_name: str 1092 | :param beneficiary_name: BeneficiaryName is the name of the beneficial owner if is it is a private address 1093 | :type beneficiary_name: str 1094 | :param country: Country is the ISO 3166-1 country code of the beneficial owner of the address 1095 | :type country: str 1096 | :param date_of_birth: DateOfBirth is the date of birth of the (non-institutional) beneficial owner of the 1097 | address in the form "YYYY-MM-DD" 1098 | :type date_of_birth: str 1099 | :param destination_tag: Optional XRP destination tag. Note that HasDestinationTag must be true if this value is 1100 | provided. 1101 | :type destination_tag: int 1102 | :param has_destination_tag: Optional boolean flag indicating that a XRP destination tag is provided (even if 1103 | zero). 1104 | :type has_destination_tag: bool 1105 | :param institution_name: InstitutionName is the name of the beneficial owner if is it is a legal entities 1106 | address 1107 | :type institution_name: str 1108 | :param is_legal_entity: IsLegalEntity indicates if the address is for a legal entity and not a private 1109 | beneficiary. If this field is true then the fields BeneficiaryName, Nationality & 1110 | DateOfBirth should be empty but the fields InstitutionName and Country should be 1111 | populated. If this field is false and IsSelfSend is false (or empty) then the field 1112 | InstitutionName should be empty but the fields BeneficiaryName, Nationality & 1113 | DateOfBirth and Country should be populated. 1114 | :type is_legal_entity: bool 1115 | :param is_private_wallet: IsPrivateWallet indicates if the address is for private wallet and not held at an 1116 | exchange. 1117 | :type is_private_wallet: bool 1118 | :param is_self_send: IsSelfSend to indicate that the address belongs to the customer. 1119 | If this field is true then the remaining omitempty fields should not 1120 | be populated. 1121 | :type is_self_send: bool 1122 | :param memo: Optional memo string used to provide account information for ATOM, etc. where it holds "account" 1123 | information for a generic address. 1124 | :type memo: str 1125 | :param nationality: Nationality ISO 3166-1 country code of the nationality of the (non-institutional) beneficial 1126 | owner of the address 1127 | :type nationality: str 1128 | :param physical_address: PhysicalAddress is the legal physical address of the beneficial owner of the crypto 1129 | address 1130 | :type physical_address: str 1131 | :param wallet_name: PrivateWalletName is the name of the private wallet 1132 | :type wallet_name: str 1133 | """ 1134 | req = { 1135 | "address": address, 1136 | "currency": currency, 1137 | "address_name": address_name, 1138 | "beneficiary_name": beneficiary_name, 1139 | "country": country, 1140 | "date_of_birth": date_of_birth, 1141 | "destination_tag": destination_tag, 1142 | "has_destination_tag": has_destination_tag, 1143 | "institution_name": institution_name, 1144 | "is_legal_entity": is_legal_entity, 1145 | "is_private_wallet": is_private_wallet, 1146 | "is_self_send": is_self_send, 1147 | "memo": memo, 1148 | "nationality": nationality, 1149 | "physical_address": physical_address, 1150 | "wallet_name": wallet_name, 1151 | } 1152 | return self.do("POST", "/api/1/address/validate", req=req, auth=True) 1153 | 1154 | 1155 | # vi: ft=python 1156 | --------------------------------------------------------------------------------