2643 lines
119 KiB
Python
2643 lines
119 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
|
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
|
|
|
from ccxt.async_support.base.exchange import Exchange
|
|
from ccxt.abstract.wavesexchange import ImplicitAPI
|
|
import asyncio
|
|
import json
|
|
from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import AccountSuspended
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import BadSymbol
|
|
from ccxt.base.errors import InsufficientFunds
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import OrderNotFound
|
|
from ccxt.base.errors import DuplicateOrderId
|
|
from ccxt.base.errors import ExchangeNotAvailable
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class wavesexchange(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(wavesexchange, self).describe(), {
|
|
'id': 'wavesexchange',
|
|
'name': 'Waves.Exchange',
|
|
'countries': ['CH'], # Switzerland
|
|
'certified': False,
|
|
'pro': False,
|
|
'dex': True,
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': True,
|
|
'margin': False,
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'addMargin': False,
|
|
'borrowCrossMargin': False,
|
|
'borrowIsolatedMargin': False,
|
|
'borrowMargin': False,
|
|
'cancelOrder': True,
|
|
'closeAllPositions': False,
|
|
'closePosition': False,
|
|
'createMarketOrder': True,
|
|
'createOrder': True,
|
|
'createReduceOnlyOrder': False,
|
|
'createStopLimitOrder': False,
|
|
'createStopMarketOrder': False,
|
|
'createStopOrder': False,
|
|
'fetchAllGreeks': False,
|
|
'fetchBalance': True,
|
|
'fetchBorrowInterest': False,
|
|
'fetchBorrowRate': False,
|
|
'fetchBorrowRateHistories': False,
|
|
'fetchBorrowRateHistory': False,
|
|
'fetchBorrowRates': False,
|
|
'fetchBorrowRatesPerSymbol': False,
|
|
'fetchClosedOrders': True,
|
|
'fetchCrossBorrowRate': False,
|
|
'fetchCrossBorrowRates': False,
|
|
'fetchCurrencies': False,
|
|
'fetchDepositAddress': True,
|
|
'fetchDepositAddresses': None,
|
|
'fetchDepositAddressesByNetwork': None,
|
|
'fetchDepositWithdrawFee': 'emulated',
|
|
'fetchDepositWithdrawFees': True,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingInterval': False,
|
|
'fetchFundingIntervals': False,
|
|
'fetchFundingRate': False,
|
|
'fetchFundingRateHistory': False,
|
|
'fetchFundingRates': False,
|
|
'fetchGreeks': False,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchIsolatedBorrowRate': False,
|
|
'fetchIsolatedBorrowRates': False,
|
|
'fetchIsolatedPositions': False,
|
|
'fetchLeverage': False,
|
|
'fetchLeverages': False,
|
|
'fetchLeverageTiers': False,
|
|
'fetchLiquidations': False,
|
|
'fetchLongShortRatio': False,
|
|
'fetchLongShortRatioHistory': False,
|
|
'fetchMarginAdjustmentHistory': False,
|
|
'fetchMarginMode': False,
|
|
'fetchMarginModes': False,
|
|
'fetchMarketLeverageTiers': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMarkPrice': False,
|
|
'fetchMarkPrices': False,
|
|
'fetchMyLiquidations': False,
|
|
'fetchMySettlementHistory': False,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenInterest': False,
|
|
'fetchOpenInterestHistory': False,
|
|
'fetchOpenInterests': False,
|
|
'fetchOpenOrders': True,
|
|
'fetchOption': False,
|
|
'fetchOptionChain': False,
|
|
'fetchOrder': True,
|
|
'fetchOrderBook': True,
|
|
'fetchOrders': True,
|
|
'fetchPosition': False,
|
|
'fetchPositionHistory': False,
|
|
'fetchPositionMode': False,
|
|
'fetchPositions': False,
|
|
'fetchPositionsForSymbol': False,
|
|
'fetchPositionsHistory': False,
|
|
'fetchPositionsRisk': False,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchSettlementHistory': False,
|
|
'fetchTicker': True,
|
|
'fetchTickers': True,
|
|
'fetchTrades': True,
|
|
'fetchTransfer': False,
|
|
'fetchTransfers': False,
|
|
'fetchUnderlyingAssets': False,
|
|
'fetchVolatilityHistory': False,
|
|
'reduceMargin': False,
|
|
'repayCrossMargin': False,
|
|
'repayIsolatedMargin': False,
|
|
'sandbox': True,
|
|
'setLeverage': False,
|
|
'setMargin': False,
|
|
'setMarginMode': False,
|
|
'setPositionMode': False,
|
|
'signIn': True,
|
|
'transfer': False,
|
|
'withdraw': True,
|
|
'ws': False,
|
|
},
|
|
'timeframes': {
|
|
'1m': '1m',
|
|
'5m': '5m',
|
|
'15m': '15m',
|
|
'30m': '30m',
|
|
'1h': '1h',
|
|
'2h': '2h',
|
|
'3h': '3h',
|
|
'4h': '4h',
|
|
'6h': '6h',
|
|
'12h': '12h',
|
|
'1d': '1d',
|
|
'1w': '1w',
|
|
'1M': '1M',
|
|
},
|
|
'urls': {
|
|
'logo': 'https://user-images.githubusercontent.com/1294454/84547058-5fb27d80-ad0b-11ea-8711-78ac8b3c7f31.jpg',
|
|
'test': {
|
|
'matcher': 'https://matcher-testnet.wx.network',
|
|
'node': 'https://nodes-testnet.wavesnodes.com',
|
|
'public': 'https://api-testnet.wavesplatform.com/v0',
|
|
'private': 'https://api-testnet.wx.network/v1',
|
|
'forward': 'https://testnet.wx.network/api/v1/forward/matcher',
|
|
'market': 'https://testnet.wx.network/api/v1/forward/marketdata/api/v1',
|
|
},
|
|
'api': {
|
|
'matcher': 'https://matcher.wx.network',
|
|
'node': 'https://nodes.wx.network',
|
|
'public': 'https://api.wavesplatform.com/v0',
|
|
'private': 'https://api.wx.network/v1',
|
|
'forward': 'https://wx.network/api/v1/forward/matcher',
|
|
'market': 'https://wx.network/api/v1/forward/marketdata/api/v1',
|
|
},
|
|
'doc': [
|
|
'https://docs.wx.network',
|
|
'https://docs.waves.tech',
|
|
'https://api.wavesplatform.com/v0/docs/',
|
|
'https://nodes.wavesnodes.com/api-docs/index.html',
|
|
'https://matcher.waves.exchange/api-docs/index.html',
|
|
],
|
|
'www': 'https://wx.network',
|
|
},
|
|
'api': {
|
|
'matcher': {
|
|
'get': [
|
|
'matcher',
|
|
'matcher/settings',
|
|
'matcher/settings/rates',
|
|
'matcher/balance/reserved/{publicKey}',
|
|
'matcher/debug/allSnashotOffsets',
|
|
'matcher/debug/currentOffset',
|
|
'matcher/debug/lastOffset',
|
|
'matcher/debug/oldestSnapshotOffset',
|
|
'matcher/debug/config',
|
|
'matcher/debug/address/{address}',
|
|
'matcher/debug/status',
|
|
'matcher/debug/address/{address}/check',
|
|
'matcher/orderbook',
|
|
'matcher/orderbook/{baseId}/{quoteId}',
|
|
'matcher/orderbook/{baseId}/{quoteId}/publicKey/{publicKey}',
|
|
'matcher/orderbook/{baseId}/{quoteId}/{orderId}',
|
|
'matcher/orderbook/{baseId}/{quoteId}/info',
|
|
'matcher/orderbook/{baseId}/{quoteId}/status',
|
|
'matcher/orderbook/{baseId}/{quoteId}/tradableBalance/{address}',
|
|
'matcher/orderbook/{publicKey}',
|
|
'matcher/orderbook/{publicKey}/{orderId}',
|
|
'matcher/orders/{address}',
|
|
'matcher/orders/{address}/{orderId}',
|
|
'matcher/transactions/{orderId}',
|
|
'api/v1/orderbook/{baseId}/{quoteId}',
|
|
],
|
|
'post': [
|
|
'matcher/orderbook',
|
|
'matcher/orderbook/market',
|
|
'matcher/orderbook/cancel',
|
|
'matcher/orderbook/{baseId}/{quoteId}/cancel',
|
|
'matcher/orderbook/{baseId}/{quoteId}/calculateFee',
|
|
'matcher/orderbook/{baseId}/{quoteId}/delete',
|
|
'matcher/orderbook/{baseId}/{quoteId}/cancelAll',
|
|
'matcher/debug/saveSnapshots',
|
|
'matcher/orders/{address}/cancel',
|
|
'matcher/orders/cancel/{orderId}',
|
|
'matcher/orders/serialize',
|
|
],
|
|
'delete': [
|
|
'matcher/orderbook/{baseId}/{quoteId}',
|
|
'matcher/settings/rates/{assetId}',
|
|
],
|
|
'put': [
|
|
'matcher/settings/rates/{assetId}',
|
|
],
|
|
},
|
|
'node': {
|
|
'get': [
|
|
'addresses',
|
|
'addresses/balance/{address}',
|
|
'addresses/balance/{address}/{confirmations}',
|
|
'addresses/balance/details/{address}',
|
|
'addresses/data/{address}',
|
|
'addresses/data/{address}/{key}',
|
|
'addresses/effectiveBalance/{address}',
|
|
'addresses/effectiveBalance/{address}/{confirmations}',
|
|
'addresses/publicKey/{publicKey}',
|
|
'addresses/scriptInfo/{address}',
|
|
'addresses/scriptInfo/{address}/meta',
|
|
'addresses/seed/{address}',
|
|
'addresses/seq/{from}/{to}',
|
|
'addresses/validate/{address}',
|
|
'alias/by-address/{address}',
|
|
'alias/by-alias/{alias}',
|
|
'assets/{assetId}/distribution/{height}/{limit}',
|
|
'assets/balance/{address}',
|
|
'assets/balance/{address}/{assetId}',
|
|
'assets/details/{assetId}',
|
|
'assets/nft/{address}/limit/{limit}',
|
|
'blockchain/rewards',
|
|
'blockchain/rewards/height',
|
|
'blocks/address/{address}/{from}/{to}/',
|
|
'blocks/at/{height}',
|
|
'blocks/delay/{signature}/{blockNum}',
|
|
'blocks/first',
|
|
'blocks/headers/last',
|
|
'blocks/headers/seq/{from}/{to}',
|
|
'blocks/height',
|
|
'blocks/height/{signature}',
|
|
'blocks/last',
|
|
'blocks/seq/{from}/{to}',
|
|
'blocks/signature/{signature}',
|
|
'consensus/algo',
|
|
'consensus/basetarget',
|
|
'consensus/basetarget/{blockId}',
|
|
'consensus/{generatingbalance}/address',
|
|
'consensus/generationsignature',
|
|
'consensus/generationsignature/{blockId}',
|
|
'debug/balances/history/{address}',
|
|
'debug/blocks/{howMany}',
|
|
'debug/configInfo',
|
|
'debug/historyInfo',
|
|
'debug/info',
|
|
'debug/minerInfo',
|
|
'debug/portfolios/{address}',
|
|
'debug/state',
|
|
'debug/stateChanges/address/{address}',
|
|
'debug/stateChanges/info/{id}',
|
|
'debug/stateWaves/{height}',
|
|
'leasing/active/{address}',
|
|
'node/state',
|
|
'node/version',
|
|
'peers/all',
|
|
'peers/blacklisted',
|
|
'peers/connected',
|
|
'peers/suspended',
|
|
'transactions/address/{address}/limit/{limit}',
|
|
'transactions/info/{id}',
|
|
'transactions/status',
|
|
'transactions/unconfirmed',
|
|
'transactions/unconfirmed/info/{id}',
|
|
'transactions/unconfirmed/size',
|
|
'utils/seed',
|
|
'utils/seed/{length}',
|
|
'utils/time',
|
|
'wallet/seed',
|
|
],
|
|
'post': [
|
|
'addresses',
|
|
'addresses/data/{address}',
|
|
'addresses/sign/{address}',
|
|
'addresses/signText/{address}',
|
|
'addresses/verify/{address}',
|
|
'addresses/verifyText/{address}',
|
|
'debug/blacklist',
|
|
'debug/print',
|
|
'debug/rollback',
|
|
'debug/validate',
|
|
'node/stop',
|
|
'peers/clearblacklist',
|
|
'peers/connect',
|
|
'transactions/broadcast',
|
|
'transactions/calculateFee',
|
|
'tranasctions/sign',
|
|
'transactions/sign/{signerAddress}',
|
|
'tranasctions/status',
|
|
'utils/hash/fast',
|
|
'utils/hash/secure',
|
|
'utils/script/compileCode',
|
|
'utils/script/compileWithImports',
|
|
'utils/script/decompile',
|
|
'utils/script/estimate',
|
|
'utils/sign/{privateKey}',
|
|
'utils/transactionsSerialize',
|
|
],
|
|
'delete': [
|
|
'addresses/{address}',
|
|
'debug/rollback-to/{signature}',
|
|
],
|
|
},
|
|
'public': {
|
|
'get': [
|
|
'assets',
|
|
'pairs',
|
|
'candles/{baseId}/{quoteId}',
|
|
'transactions/exchange',
|
|
],
|
|
},
|
|
'private': {
|
|
'get': [
|
|
'deposit/addresses/{currency}',
|
|
'deposit/addresses/{currency}/{platform}',
|
|
'platforms',
|
|
'deposit/currencies',
|
|
'withdraw/currencies',
|
|
'withdraw/addresses/{currency}/{address}',
|
|
],
|
|
'post': [
|
|
'oauth2/token',
|
|
],
|
|
},
|
|
'forward': {
|
|
'get': [
|
|
'matcher/orders/{address}', # can't get the orders endpoint to work with the matcher api
|
|
'matcher/orders/{address}/{orderId}',
|
|
],
|
|
'post': [
|
|
'matcher/orders/{wavesAddress}/cancel',
|
|
],
|
|
},
|
|
'market': {
|
|
'get': [
|
|
'tickers',
|
|
],
|
|
},
|
|
},
|
|
'currencies': {
|
|
'WX': self.safe_currency_structure({'id': 'EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc', 'numericId': None, 'code': 'WX', 'precision': self.parse_number('1e-8')}),
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
'options': {
|
|
'allowedCandles': 1440,
|
|
'accessToken': None,
|
|
'createMarketBuyOrderRequiresPrice': True,
|
|
'matcherPublicKey': None,
|
|
'quotes': None,
|
|
'createOrderDefaultExpiry': 2419200000, # 60 * 60 * 24 * 28 * 1000
|
|
'wavesAddress': None,
|
|
'withdrawFeeUSDN': 7420,
|
|
'withdrawFeeWAVES': 100000,
|
|
'wavesPrecision': 1e-8,
|
|
'messagePrefix': 'W', # W for production, T for testnet
|
|
'networks': {
|
|
'ERC20': 'ETH',
|
|
'BEP20': 'BSC',
|
|
},
|
|
},
|
|
'features': {
|
|
'spot': {
|
|
'sandbox': True,
|
|
'createOrder': {
|
|
'marginMode': False,
|
|
'triggerPrice': True, # todo
|
|
'triggerDirection': False,
|
|
'triggerPriceType': None,
|
|
'stopLossPrice': False, # todo
|
|
'takeProfitPrice': False, # todo
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': False,
|
|
'FOK': False,
|
|
'PO': False,
|
|
'GTD': True, # todo
|
|
},
|
|
'hedged': False,
|
|
'trailing': False,
|
|
'leverage': False,
|
|
'marketBuyByCost': False, # todo
|
|
'marketBuyRequiresPrice': True,
|
|
'selfTradePrevention': False,
|
|
'iceberg': False,
|
|
},
|
|
'createOrders': None,
|
|
'fetchMyTrades': {
|
|
'marginMode': False,
|
|
'limit': 100, # todo
|
|
'daysBack': 100000, # todo
|
|
'untilDays': 100000, # todo
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': 100, # todo
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': {
|
|
'marginMode': False,
|
|
'limit': 100, # todo
|
|
'daysBack': None,
|
|
'untilDays': None,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': True,
|
|
}, # todo
|
|
'fetchClosedOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 100000, # todo
|
|
'daysBackCanceled': 1, # todo
|
|
'untilDays': 100000, # todo
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOHLCV': {
|
|
'limit': None, # todo
|
|
},
|
|
},
|
|
'swap': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
'future': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
},
|
|
'commonCurrencies': {
|
|
'EGG': 'Waves Ducks',
|
|
},
|
|
'requiresEddsa': True,
|
|
'exceptions': {
|
|
'3147270': InsufficientFunds, # https://github.com/wavesplatform/matcher/wiki/List-of-all-errors
|
|
'112': InsufficientFunds,
|
|
'4': ExchangeError,
|
|
'13': ExchangeNotAvailable,
|
|
'14': ExchangeNotAvailable,
|
|
'3145733': AccountSuspended,
|
|
'3148040': DuplicateOrderId,
|
|
'3148801': AuthenticationError,
|
|
'9440512': AuthenticationError,
|
|
'9440771': BadSymbol,
|
|
'9441026': InvalidOrder,
|
|
'9441282': InvalidOrder,
|
|
'9441286': InvalidOrder,
|
|
'9441295': InvalidOrder,
|
|
'9441540': InvalidOrder,
|
|
'9441542': InvalidOrder,
|
|
'106954752': AuthenticationError,
|
|
'106954769': AuthenticationError,
|
|
'106957828': AuthenticationError,
|
|
'106960131': AuthenticationError,
|
|
'106981137': AuthenticationError,
|
|
'9437184': BadRequest, # {"error":9437184,"message":"The order is invalid: SpendAmount should be > 0","template":"The order is invalid: {{details}}","params":{"details":"SpendAmount should be > 0"},"status":"OrderRejected","success":false}
|
|
'9437193': OrderNotFound,
|
|
'1048577': BadRequest,
|
|
'1051904': AuthenticationError,
|
|
},
|
|
})
|
|
|
|
def set_sandbox_mode(self, enabled):
|
|
self.options['messagePrefix'] = 'T' if enabled else 'W'
|
|
self.options['sandboxMode'] = enabled
|
|
super(wavesexchange, self).set_sandbox_mode(enabled)
|
|
|
|
async def get_fees_for_asset(self, symbol: str, side, amount, price, params={}):
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
amount = self.to_real_symbol_amount(symbol, amount)
|
|
price = self.to_real_symbol_price(symbol, price)
|
|
request = self.extend({
|
|
'baseId': market['baseId'],
|
|
'quoteId': market['quoteId'],
|
|
'orderType': side,
|
|
'amount': amount,
|
|
'price': price,
|
|
}, params)
|
|
return await self.matcherPostMatcherOrderbookBaseIdQuoteIdCalculateFee(request)
|
|
|
|
async def custom_calculate_fee(self, symbol: str, type, side, amount, price, takerOrMaker='taker', params={}):
|
|
response = await self.get_fees_for_asset(symbol, side, amount, price)
|
|
# {
|
|
# "base":{
|
|
# "feeAssetId":"WAVES",
|
|
# "matcherFee":"1000000"
|
|
# },
|
|
# "discount":{
|
|
# "feeAssetId":"EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc",
|
|
# "matcherFee":"4077612"
|
|
# }
|
|
# }
|
|
isDiscountFee = self.safe_bool(params, 'isDiscountFee', False)
|
|
mode = None
|
|
if isDiscountFee:
|
|
mode = self.safe_value(response, 'discount')
|
|
else:
|
|
mode = self.safe_value(response, 'base')
|
|
matcherFee = self.safe_string(mode, 'matcherFee')
|
|
feeAssetId = self.safe_string(mode, 'feeAssetId')
|
|
feeAsset = self.safe_currency_code(feeAssetId)
|
|
adjustedMatcherFee = self.from_real_currency_amount(feeAsset, matcherFee)
|
|
amountAsString = self.number_to_string(amount)
|
|
priceAsString = self.number_to_string(price)
|
|
feeCost = self.fee_to_precision(symbol, self.parse_number(adjustedMatcherFee))
|
|
feeRate = Precise.string_div(adjustedMatcherFee, Precise.string_mul(amountAsString, priceAsString))
|
|
return {
|
|
'type': takerOrMaker,
|
|
'currency': feeAsset,
|
|
'rate': self.parse_number(feeRate),
|
|
'cost': self.parse_number(feeCost),
|
|
}
|
|
|
|
async def get_quotes(self):
|
|
quotes = self.safe_value(self.options, 'quotes')
|
|
if quotes:
|
|
return quotes
|
|
else:
|
|
# currencies can have any name because you can create you own token
|
|
# result someone can create a fake token called BTC
|
|
# we use self mapping to determine the real tokens
|
|
# https://docs.wx.network/en/waves-matcher/matcher-api#asset-pair
|
|
response = await self.matcherGetMatcherSettings()
|
|
# {
|
|
# "orderVersions": [
|
|
# 1,
|
|
# 2,
|
|
# 3
|
|
# ],
|
|
# "success": True,
|
|
# "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "orderFee": {
|
|
# "dynamic": {
|
|
# "baseFee": 300000,
|
|
# "rates": {
|
|
# "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ": 1.22639597,
|
|
# "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH": 0.00989643,
|
|
# "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk": 0.0395674,
|
|
# "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS": 0.00018814,
|
|
# "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8": 26.19721262,
|
|
# "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu": 0.00752978,
|
|
# "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p": 1.84575,
|
|
# "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H": 0.02330273,
|
|
# "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy": 0.00721412,
|
|
# "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3": 0.02659103,
|
|
# "WAVES": 1,
|
|
# "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa": 0.03433583
|
|
# }
|
|
# }
|
|
# },
|
|
# "networkByte": 87,
|
|
# "matcherVersion": "2.1.3.5",
|
|
# "status": "SimpleResponse",
|
|
# "priceAssets": [
|
|
# "Ft8X1v1LTa1ABafufpaCWyVj8KkaxUWE6xBhW6sNFJck",
|
|
# "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
|
|
# "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ",
|
|
# "Gtb1WRznfchDnTh37ezoDTJ4wcoKaRsKqKjJjy7nm2zU",
|
|
# "2mX5DzVKWrAJw8iwdJnV2qtoeVG9h5nTDpTqC1wb1WEN",
|
|
# "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
|
|
# "WAVES",
|
|
# "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
|
|
# "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy",
|
|
# "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH",
|
|
# "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk",
|
|
# "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H",
|
|
# "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3",
|
|
# "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa",
|
|
# "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8"
|
|
# ]
|
|
# }
|
|
quotes = {}
|
|
priceAssets = self.safe_value(response, 'priceAssets')
|
|
for i in range(0, len(priceAssets)):
|
|
quotes[priceAssets[i]] = True
|
|
self.options['quotes'] = quotes
|
|
return quotes
|
|
|
|
async def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for wavesexchange
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
response = await self.marketGetTickers()
|
|
#
|
|
# [
|
|
# {
|
|
# "symbol": "WAVES/BTC",
|
|
# "amountAssetID": "WAVES",
|
|
# "amountAssetName": "Waves",
|
|
# "amountAssetDecimals": 8,
|
|
# "amountAssetTotalSupply": "106908766.00000000",
|
|
# "amountAssetMaxSupply": "106908766.00000000",
|
|
# "amountAssetCirculatingSupply": "106908766.00000000",
|
|
# "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
|
|
# "priceAssetName": "WBTC",
|
|
# "priceAssetDecimals": 8,
|
|
# "priceAssetTotalSupply": "20999999.96007507",
|
|
# "priceAssetMaxSupply": "20999999.96007507",
|
|
# "priceAssetCirculatingSupply": "20999999.66019601",
|
|
# "24h_open": "0.00032688",
|
|
# "24h_high": "0.00033508",
|
|
# "24h_low": "0.00032443",
|
|
# "24h_close": "0.00032806",
|
|
# "24h_vwap": "0.00032988",
|
|
# "24h_volume": "42349.69440104",
|
|
# "24h_priceVolume": "13.97037207",
|
|
# "timestamp":1640232379124
|
|
# }
|
|
# ...
|
|
# ]
|
|
#
|
|
result = []
|
|
for i in range(0, len(response)):
|
|
entry = response[i]
|
|
baseId = self.safe_string(entry, 'amountAssetID')
|
|
quoteId = self.safe_string(entry, 'priceAssetID')
|
|
id = baseId + '/' + quoteId
|
|
marketId = self.safe_string(entry, 'symbol')
|
|
base, quote = marketId.split('/')
|
|
base = self.safe_currency_code(base)
|
|
quote = self.safe_currency_code(quote)
|
|
symbol = base + '/' + quote
|
|
result.append({
|
|
'id': id,
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'quote': quote,
|
|
'settle': None,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
'settleId': None,
|
|
'type': 'spot',
|
|
'spot': True,
|
|
'margin': False,
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'active': None,
|
|
'contract': False,
|
|
'linear': None,
|
|
'inverse': None,
|
|
'contractSize': None,
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'precision': {
|
|
'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'amountAssetDecimals'))),
|
|
'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'priceAssetDecimals'))),
|
|
},
|
|
'limits': {
|
|
'leverage': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'price': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'cost': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'created': None,
|
|
'info': entry,
|
|
})
|
|
return result
|
|
|
|
async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://matcher.waves.exchange/api-docs/index.html#/markets/getOrderBook
|
|
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request = self.extend({
|
|
'baseId': market['baseId'],
|
|
'quoteId': market['quoteId'],
|
|
}, params)
|
|
response = await self.matcherGetMatcherOrderbookBaseIdQuoteId(request)
|
|
timestamp = self.safe_integer(response, 'timestamp')
|
|
bids = self.parse_order_book_side(self.safe_value(response, 'bids'), market, limit)
|
|
asks = self.parse_order_book_side(self.safe_value(response, 'asks'), market, limit)
|
|
return {
|
|
'symbol': symbol,
|
|
'bids': bids,
|
|
'asks': asks,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'nonce': None,
|
|
}
|
|
|
|
def parse_order_book_side(self, bookSide, market=None, limit: Int = None):
|
|
precision = market['precision']
|
|
wavesPrecision = self.safe_string(self.options, 'wavesPrecision', '1e-8')
|
|
amountPrecisionString = self.safe_string(precision, 'amount')
|
|
pricePrecisionString = self.safe_string(precision, 'price')
|
|
difference = Precise.string_div(amountPrecisionString, pricePrecisionString)
|
|
pricePrecision = Precise.string_div(wavesPrecision, difference)
|
|
result = []
|
|
for i in range(0, len(bookSide)):
|
|
entry = bookSide[i]
|
|
entryPrice = self.safe_string(entry, 'price', '0')
|
|
entryAmount = self.safe_string(entry, 'amount', '0')
|
|
price = None
|
|
amount = None
|
|
if (pricePrecision is not None) and (entryPrice is not None):
|
|
price = Precise.string_mul(entryPrice, pricePrecision)
|
|
if (amountPrecisionString is not None) and (entryAmount is not None):
|
|
amount = Precise.string_mul(entryAmount, amountPrecisionString)
|
|
if (limit is not None) and (i > limit):
|
|
break
|
|
result.append([
|
|
self.parse_number(price),
|
|
self.parse_number(amount),
|
|
])
|
|
return result
|
|
|
|
def check_required_keys(self):
|
|
if self.apiKey is None:
|
|
raise AuthenticationError(self.id + ' requires apiKey credential')
|
|
if self.secret is None:
|
|
raise AuthenticationError(self.id + ' requires secret credential')
|
|
apiKeyBytes = None
|
|
secretKeyBytes = None
|
|
try:
|
|
apiKeyBytes = self.base58_to_binary(self.apiKey)
|
|
except Exception as e:
|
|
raise AuthenticationError(self.id + ' apiKey must be a base58 encoded public key')
|
|
try:
|
|
secretKeyBytes = self.base58_to_binary(self.secret)
|
|
except Exception as e:
|
|
raise AuthenticationError(self.id + ' secret must be a base58 encoded private key')
|
|
hexApiKeyBytes = self.binary_to_base16(apiKeyBytes)
|
|
hexSecretKeyBytes = self.binary_to_base16(secretKeyBytes)
|
|
if len(hexApiKeyBytes) != 64:
|
|
raise AuthenticationError(self.id + ' apiKey must be a base58 encoded public key')
|
|
if len(hexSecretKeyBytes) != 64:
|
|
raise AuthenticationError(self.id + ' secret must be a base58 encoded private key')
|
|
return True
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
query = self.omit(params, self.extract_params(path))
|
|
isCancelOrder = path == 'matcher/orders/{wavesAddress}/cancel'
|
|
path = self.implode_params(path, params)
|
|
url = self.urls['api'][api] + '/' + path
|
|
queryString = self.urlencode_with_array_repeat(query)
|
|
if (api == 'private') or (api == 'forward'):
|
|
headers = {
|
|
'Accept': 'application/json',
|
|
}
|
|
accessToken = self.safe_string(self.options, 'accessToken')
|
|
if accessToken:
|
|
headers['Authorization'] = 'Bearer ' + accessToken
|
|
if method == 'POST':
|
|
headers['content-type'] = 'application/json'
|
|
else:
|
|
headers['content-type'] = 'application/x-www-form-urlencoded'
|
|
if isCancelOrder:
|
|
body = self.json([query['orderId']])
|
|
queryString = ''
|
|
if len(queryString) > 0:
|
|
url += '?' + queryString
|
|
elif api == 'matcher':
|
|
if method == 'POST':
|
|
headers = {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
}
|
|
body = self.json(query)
|
|
else:
|
|
headers = query
|
|
else:
|
|
if method == 'POST':
|
|
headers = {
|
|
'content-type': 'application/json',
|
|
}
|
|
body = self.json(query)
|
|
else:
|
|
headers = {
|
|
'content-type': 'application/x-www-form-urlencoded',
|
|
}
|
|
if len(queryString) > 0:
|
|
url += '?' + queryString
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
async def sign_in(self, params={}):
|
|
"""
|
|
sign in, must be called prior to using other authenticated methods
|
|
|
|
https://docs.wx.network/en/api/auth/oauth2-token
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns: response from exchange
|
|
"""
|
|
if not self.safe_string(self.options, 'accessToken'):
|
|
prefix = 'ffffff01'
|
|
expiresDelta = 60 * 60 * 24 * 7
|
|
seconds = self.sum(self.seconds(), expiresDelta)
|
|
seconds = str(seconds)
|
|
clientId = 'wx.network'
|
|
# W for production, T for testnet
|
|
defaultMessagePrefix = self.safe_string(self.options, 'messagePrefix', 'W')
|
|
message = defaultMessagePrefix + ':' + clientId + ':' + seconds
|
|
messageHex = self.binary_to_base16(self.encode(message))
|
|
payload = prefix + messageHex
|
|
hexKey = self.binary_to_base16(self.base58_to_binary(self.secret))
|
|
signature = self.axolotl(payload, hexKey, 'ed25519')
|
|
request: dict = {
|
|
'grant_type': 'password',
|
|
'scope': 'general',
|
|
'username': self.apiKey,
|
|
'password': seconds + ':' + signature,
|
|
'client_id': clientId,
|
|
}
|
|
response = await self.privatePostOauth2Token(request)
|
|
# {access_token: "eyJhbGciOXJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWciOiJiaTZiMVhMQlo0M1Q4QmRTSlVSejJBZGlQdVlpaFZQYVhhVjc4ZGVIOEpTM3M3NUdSeEU1VkZVOE5LRUI0UXViNkFHaUhpVFpuZ3pzcnhXdExUclRvZTgiLCJhIjoiM1A4VnpMU2EyM0VXNUNWY2tIYlY3ZDVCb043NWZGMWhoRkgiLCJuYiI6IlciLCJ1c2VyX25hbWUiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsInNjb3BlIjpbImdlbmVyYWwiXSwibHQiOjYwNDc5OSwicGsiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsImV4cCI6MTU5MTk3NTA1NywiZXhwMCI6MTU5MTk3NTA1NywianRpIjoiN2JhOTUxMTMtOGI2MS00NjEzLTlkZmYtNTEwYTc0NjlkOWI5IiwiY2lkIjoid2F2ZXMuZXhjaGFuZ2UifQ.B-XwexBnUAzbWknVN68RKT0ZP5w6Qk1SKJ8usL3OIwDEzCUUX9PjW-5TQHmiCRcA4oft8lqXEiCwEoNfsblCo_jTpRo518a1vZkIbHQk0-13Dm1K5ewGxfxAwBk0g49odcbKdjl64TN1yM_PO1VtLVuiTeZP-XF-S42Uj-7fcO-r7AulyQLuTE0uo-Qdep8HDCk47rduZwtJOmhFbCCnSgnLYvKWy3CVTeldsR77qxUY-vy8q9McqeP7Id-_MWnsob8vWXpkeJxaEsw1Fke1dxApJaJam09VU8EB3ZJWpkT7V8PdafIrQGeexx3jhKKxo7rRb4hDV8kfpVoCgkvFan",
|
|
# "token_type": "bearer",
|
|
# "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWciOiJiaTZiMVhMQlo0M1Q4QmRTSlVSejJBZGlQdVlpaFZQYVhhVjc4ZGVIOEpTM3M3NUdSeEU1VkZVOE5LRUI0UXViNkFHaUhpVFpuZ3pzcnhXdExUclRvZTgiLCJhIjoiM1A4VnpMU2EyM0VXNUNWY2tIYlY3ZDVCb043NWZGMWhoRkgiLCJuYiI6IlciLCJ1c2VyX25hbWUiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsInNjb3BlIjpbImdlbmVyYWwiXSwiYXRpIjoiN2JhOTUxMTMtOGI2MS00NjEzLTlkZmYtNTEwYTc0NjlkXWI5IiwibHQiOjYwNDc5OSwicGsiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsImV4cCI6MTU5Mzk2MjI1OCwiZXhwMCI6MTU5MTk3NTA1NywianRpIjoiM2MzZWRlMTktNjI5My00MTNlLWJmMWUtZTRlZDZlYzUzZTgzIiwiY2lkIjoid2F2ZXMuZXhjaGFuZ2UifQ.gD1Qj0jfqayfZpBvNY0t3ccMyK5hdbT7dY-_5L6LxwV0Knan4ndEtvygxlTOczmJUKtnA4T1r5GBFgNMZTvtViKZIbqZNysEg2OY8UxwDaF4VPeGJLg_QXEnn8wBeBQdyMafh9UQdwD2ci7x-saM4tOAGmncAygfTDxy80201gwDhfAkAGerb9kL00oWzSJScldxu--pNLDBUEHZt52MSEel10HGrzvZkkvvSh67vcQo5TOGb5KG6nh65UdJCwr41AVz4fbQPP-N2Nkxqy0TE_bqVzZxExXgvcS8TS0Z82T3ijJa_ct7B9wblpylBnvmyj3VycUzufD6uy8MUGq32D",
|
|
# "expires_in": 604798,
|
|
# "scope": "general"}
|
|
self.options['accessToken'] = self.safe_string(response, 'access_token')
|
|
return self.options['accessToken']
|
|
return None
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
#
|
|
# {
|
|
# "symbol": "WAVES/BTC",
|
|
# "amountAssetID": "WAVES",
|
|
# "amountAssetName": "Waves",
|
|
# "amountAssetDecimals": 8,
|
|
# "amountAssetTotalSupply": "106908766.00000000",
|
|
# "amountAssetMaxSupply": "106908766.00000000",
|
|
# "amountAssetCirculatingSupply": "106908766.00000000",
|
|
# "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
|
|
# "priceAssetName": "WBTC",
|
|
# "priceAssetDecimals": 8,
|
|
# "priceAssetTotalSupply": "20999999.96007507",
|
|
# "priceAssetMaxSupply": "20999999.96007507",
|
|
# "priceAssetCirculatingSupply": "20999999.66019601",
|
|
# "24h_open": "0.00032688",
|
|
# "24h_high": "0.00033508",
|
|
# "24h_low": "0.00032443",
|
|
# "24h_close": "0.00032806",
|
|
# "24h_vwap": "0.00032988",
|
|
# "24h_volume": "42349.69440104",
|
|
# "24h_priceVolume": "13.97037207",
|
|
# "timestamp":1640232379124
|
|
# }
|
|
#
|
|
# fetch ticker
|
|
#
|
|
# {
|
|
# "firstPrice": "21749",
|
|
# "lastPrice": "22000",
|
|
# "volume": "0.73747149",
|
|
# "quoteVolume": "16409.44564928645471",
|
|
# "high": "23589.999941",
|
|
# "low": "21010.000845",
|
|
# "weightedAveragePrice": "22250.955964",
|
|
# "txsCount": "148",
|
|
# "volumeWaves": "0.0000000000680511203072"
|
|
# }
|
|
#
|
|
timestamp = self.safe_integer(ticker, 'timestamp')
|
|
marketId = self.safe_string(ticker, 'symbol')
|
|
market = self.safe_market(marketId, market, '/')
|
|
symbol = market['symbol']
|
|
last = self.safe_string_2(ticker, '24h_close', 'lastPrice')
|
|
low = self.safe_string_2(ticker, '24h_low', 'low')
|
|
high = self.safe_string_2(ticker, '24h_high', 'high')
|
|
vwap = self.safe_string_2(ticker, '24h_vwap', 'weightedAveragePrice')
|
|
baseVolume = self.safe_string_2(ticker, '24h_volume', 'volume')
|
|
quoteVolume = self.safe_string_2(ticker, '24h_priceVolume', 'quoteVolume')
|
|
open = self.safe_string_2(ticker, '24h_open', 'firstPrice')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'high': high,
|
|
'low': low,
|
|
'bid': None,
|
|
'bidVolume': None,
|
|
'ask': None,
|
|
'askVolume': None,
|
|
'vwap': vwap,
|
|
'open': open,
|
|
'close': last,
|
|
'last': last,
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': None,
|
|
'average': None,
|
|
'baseVolume': baseVolume,
|
|
'quoteVolume': quoteVolume,
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
|
|
|
https://api.wavesplatform.com/v0/docs/#/pairs/getPairsListAll
|
|
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pairs': market['id'],
|
|
}
|
|
response = await self.publicGetPairs(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "__type":"list",
|
|
# "data":[
|
|
# {
|
|
# "__type":"pair",
|
|
# "data":{
|
|
# "firstPrice":0.00012512,
|
|
# "lastPrice":0.00012441,
|
|
# "low":0.00012167,
|
|
# "high":0.00012768,
|
|
# "weightedAveragePrice":0.000124710697407246,
|
|
# "volume":209554.26356614,
|
|
# "quoteVolume":26.1336583539951,
|
|
# "volumeWaves":209554.26356614,
|
|
# "txsCount":6655
|
|
# },
|
|
# "amountAsset":"WAVES",
|
|
# "priceAsset":"8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_value(response, 'data', [])
|
|
ticker = self.safe_value(data, 0, {})
|
|
dataTicker = self.safe_dict(ticker, 'data', {})
|
|
return self.parse_ticker(dataTicker, market)
|
|
|
|
async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
|
|
:param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.marketGetTickers(params)
|
|
#
|
|
# [
|
|
# {
|
|
# "symbol": "WAVES/BTC",
|
|
# "amountAssetID": "WAVES",
|
|
# "amountAssetName": "Waves",
|
|
# "amountAssetDecimals": 8,
|
|
# "amountAssetTotalSupply": "106908766.00000000",
|
|
# "amountAssetMaxSupply": "106908766.00000000",
|
|
# "amountAssetCirculatingSupply": "106908766.00000000",
|
|
# "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
|
|
# "priceAssetName": "WBTC",
|
|
# "priceAssetDecimals": 8,
|
|
# "priceAssetTotalSupply": "20999999.96007507",
|
|
# "priceAssetMaxSupply": "20999999.96007507",
|
|
# "priceAssetCirculatingSupply": "20999999.66019601",
|
|
# "24h_open": "0.00032688",
|
|
# "24h_high": "0.00033508",
|
|
# "24h_low": "0.00032443",
|
|
# "24h_close": "0.00032806",
|
|
# "24h_vwap": "0.00032988",
|
|
# "24h_volume": "42349.69440104",
|
|
# "24h_priceVolume": "13.97037207",
|
|
# "timestamp":1640232379124
|
|
# }
|
|
# ...
|
|
# ]
|
|
#
|
|
return self.parse_tickers(response, symbols)
|
|
|
|
async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://api.wavesplatform.com/v0/docs/#/candles/getCandles
|
|
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
:param str timeframe: the length of time each candle represents
|
|
:param int [since]: timestamp in ms of the earliest candle to fetch
|
|
:param int [limit]: the maximum amount of candles to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest candle to fetch
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'baseId': market['baseId'],
|
|
'quoteId': market['quoteId'],
|
|
'interval': self.safe_string(self.timeframes, timeframe, timeframe),
|
|
}
|
|
allowedCandles = self.safe_integer(self.options, 'allowedCandles', 1440)
|
|
until = self.safe_integer(params, 'until')
|
|
untilIsDefined = until is not None
|
|
if limit is None:
|
|
limit = allowedCandles
|
|
limit = min(allowedCandles, limit)
|
|
duration = self.parse_timeframe(timeframe) * 1000
|
|
if since is None:
|
|
now = self.milliseconds()
|
|
timeEnd = until if untilIsDefined else now
|
|
durationRoundedTimestamp = self.parse_to_int(timeEnd / duration) * duration
|
|
delta = (limit - 1) * duration
|
|
timeStart = durationRoundedTimestamp - delta
|
|
request['timeStart'] = str(timeStart)
|
|
if untilIsDefined:
|
|
request['timeEnd'] = str(until)
|
|
else:
|
|
request['timeStart'] = str(since)
|
|
if untilIsDefined:
|
|
request['timeEnd'] = str(until)
|
|
else:
|
|
timeEnd = self.sum(since, duration * limit)
|
|
request['timeEnd'] = str(timeEnd)
|
|
params = self.omit(params, 'until')
|
|
response = await self.publicGetCandlesBaseIdQuoteId(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "__type": "list",
|
|
# "data": [
|
|
# {
|
|
# "__type": "candle",
|
|
# "data": {
|
|
# "time": "2020-06-09T14:47:00.000Z",
|
|
# "open": 0.0250385,
|
|
# "close": 0.0250385,
|
|
# "high": 0.0250385,
|
|
# "low": 0.0250385,
|
|
# "volume": 0.01033012,
|
|
# "quoteVolume": 0.00025865,
|
|
# "weightedAveragePrice": 0.0250385,
|
|
# "maxHeight": 2099399,
|
|
# "txsCount": 5,
|
|
# "timeClose": "2020-06-09T14:47:59.999Z"
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_value(response, 'data', [])
|
|
result = self.parse_ohlcvs(data, market, timeframe, since, limit)
|
|
result = self.filter_future_candles(result)
|
|
lastClose = None
|
|
length = len(result)
|
|
for i in range(0, len(result)):
|
|
j = length - i - 1
|
|
entry = result[j]
|
|
open = entry[1]
|
|
if open is None:
|
|
entry[1] = lastClose
|
|
entry[2] = lastClose
|
|
entry[3] = lastClose
|
|
entry[4] = lastClose
|
|
result[j] = entry
|
|
lastClose = entry[4]
|
|
return result
|
|
|
|
def filter_future_candles(self, ohlcvs):
|
|
result = []
|
|
timestamp = self.milliseconds()
|
|
for i in range(0, len(ohlcvs)):
|
|
if ohlcvs[i][0] > timestamp:
|
|
# stop when getting data from the future
|
|
break
|
|
result.append(ohlcvs[i])
|
|
return result
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
#
|
|
# {
|
|
# "__type": "candle",
|
|
# "data": {
|
|
# "time": "2020-06-05T20:46:00.000Z",
|
|
# "open": 240.573975,
|
|
# "close": 240.573975,
|
|
# "high": 240.573975,
|
|
# "low": 240.573975,
|
|
# "volume": 0.01278413,
|
|
# "quoteVolume": 3.075528,
|
|
# "weightedAveragePrice": 240.573975,
|
|
# "maxHeight": 2093895,
|
|
# "txsCount": 5,
|
|
# "timeClose": "2020-06-05T20:46:59.999Z"
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_value(ohlcv, 'data', {})
|
|
return [
|
|
self.parse8601(self.safe_string(data, 'time')),
|
|
self.safe_number(data, 'open'),
|
|
self.safe_number(data, 'high'),
|
|
self.safe_number(data, 'low'),
|
|
self.safe_number(data, 'close'),
|
|
self.safe_number(data, 'volume', 0),
|
|
]
|
|
|
|
async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
fetch the deposit address for a currency associated with self account
|
|
:param str code: unified currency code
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`
|
|
"""
|
|
await self.sign_in()
|
|
networks = self.safe_value(self.options, 'networks', {})
|
|
rawNetwork = self.safe_string_upper(params, 'network')
|
|
network = self.safe_string(networks, rawNetwork, rawNetwork)
|
|
params = self.omit(params, ['network'])
|
|
supportedCurrencies = await self.privateGetPlatforms()
|
|
#
|
|
# {
|
|
# "type": "list",
|
|
# "page_info": {
|
|
# "has_next_page": False,
|
|
# "last_cursor": null
|
|
# },
|
|
# "items": [
|
|
# {
|
|
# "type": "platform",
|
|
# "id": "ETH",
|
|
# "name": "Ethereum",
|
|
# "currencies": [
|
|
# "BAG",
|
|
# "BNT",
|
|
# "CRV",
|
|
# "EGG",
|
|
# "ETH",
|
|
# "EURN",
|
|
# "FL",
|
|
# "NSBT",
|
|
# "USDAP",
|
|
# "USDC",
|
|
# "USDFL",
|
|
# "USDN",
|
|
# "USDT",
|
|
# "WAVES"
|
|
# ]
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
currencies: dict = {}
|
|
networksByCurrency: dict = {}
|
|
items = self.safe_value(supportedCurrencies, 'items', [])
|
|
for i in range(0, len(items)):
|
|
entry = items[i]
|
|
currencyId = self.safe_string(entry, 'id')
|
|
innerCurrencies = self.safe_value(entry, 'currencies', [])
|
|
for j in range(0, len(innerCurrencies)):
|
|
currencyCode = self.safe_string(innerCurrencies, j)
|
|
currencies[currencyCode] = True
|
|
if not (currencyCode in networksByCurrency):
|
|
networksByCurrency[currencyCode] = {}
|
|
networksByCurrency[currencyCode][currencyId] = True
|
|
if not (code in currencies):
|
|
codes = list(currencies.keys())
|
|
raise ExchangeError(self.id + ' fetchDepositAddress() ' + code + ' not supported. Currency code must be one of ' + ', '.join(codes))
|
|
response = None
|
|
if network is None:
|
|
request: dict = {
|
|
'currency': code,
|
|
}
|
|
response = await self.privateGetDepositAddressesCurrency(self.extend(request, params))
|
|
else:
|
|
supportedNetworks = networksByCurrency[code]
|
|
if not (network in supportedNetworks):
|
|
supportedNetworkKeys = list(supportedNetworks.keys())
|
|
raise ExchangeError(self.id + ' ' + network + ' network ' + code + ' deposit address not supported. Network must be one of ' + ', '.join(supportedNetworkKeys))
|
|
if network == 'WAVES':
|
|
request: dict = {
|
|
'publicKey': self.apiKey,
|
|
}
|
|
responseInner = await self.nodeGetAddressesPublicKeyPublicKey(self.extend(request, request))
|
|
addressInner = self.safe_string(response, 'address')
|
|
return {
|
|
'info': responseInner,
|
|
'currency': code,
|
|
'network': network,
|
|
'address': addressInner,
|
|
'tag': None,
|
|
}
|
|
else:
|
|
request: dict = {
|
|
'currency': code,
|
|
'platform': network,
|
|
}
|
|
response = await self.privateGetDepositAddressesCurrencyPlatform(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "type": "deposit_addresses",
|
|
# "currency": {
|
|
# "type": "deposit_currency",
|
|
# "id": "ERGO",
|
|
# "waves_asset_id": "5dJj4Hn9t2Ve3tRpNGirUHy4yBK6qdJRAJYV21yPPuGz",
|
|
# "platform_id": "BSC",
|
|
# "decimals": 9,
|
|
# "status": "active",
|
|
# "allowed_amount": {
|
|
# "min": 0.001,
|
|
# "max": 100000
|
|
# },
|
|
# "fees": {
|
|
# "flat": 0,
|
|
# "rate": 0
|
|
# }
|
|
# },
|
|
# "deposit_addresses": [
|
|
# "9fRAAQjF8Yqg7qicQCL884zjimsRnuwsSavsM1rUdDaoG8mThku"
|
|
# ]
|
|
# }
|
|
currency = self.safe_value(response, 'currency')
|
|
networkId = self.safe_string(currency, 'platform_id')
|
|
networkByIds = self.safe_value(self.options, 'networkByIds', {})
|
|
unifiedNetwork = self.safe_string(networkByIds, networkId, networkId)
|
|
addresses = self.safe_value(response, 'deposit_addresses')
|
|
address = self.safe_string(addresses, 0)
|
|
return {
|
|
'info': response,
|
|
'currency': code,
|
|
'network': unifiedNetwork,
|
|
'address': address,
|
|
'tag': None,
|
|
}
|
|
|
|
async def get_matcher_public_key(self):
|
|
# self method returns a single string
|
|
matcherPublicKey = self.safe_string(self.options, 'matcherPublicKey')
|
|
if matcherPublicKey:
|
|
return matcherPublicKey
|
|
else:
|
|
response = await self.matcherGetMatcher()
|
|
# remove trailing quotes from string response
|
|
self.options['matcherPublicKey'] = response[1:len(response) - 1]
|
|
return self.options['matcherPublicKey']
|
|
|
|
def get_asset_bytes(self, currencyId):
|
|
if currencyId == 'WAVES':
|
|
return self.number_to_be(0, 1)
|
|
else:
|
|
return self.binary_concat(self.number_to_be(1, 1), self.base58_to_binary(currencyId))
|
|
|
|
def get_asset_id(self, currencyId):
|
|
if currencyId == 'WAVES':
|
|
return ''
|
|
return currencyId
|
|
|
|
def to_real_currency_amount(self, code: str, amount: float, networkCode=None):
|
|
currency = self.currency(code)
|
|
stringValue = Precise.string_div(self.number_to_string(amount), self.safe_string(currency, 'precision'))
|
|
return int(stringValue)
|
|
|
|
def from_real_currency_amount(self, code: str, amountString: str):
|
|
if not (code in self.currencies):
|
|
return amountString
|
|
currency = self.currency(code)
|
|
precisionAmount = self.safe_string(currency, 'precision')
|
|
return Precise.string_mul(amountString, precisionAmount)
|
|
|
|
def to_real_symbol_price(self, symbol: str, price: float):
|
|
market = self.market(symbol)
|
|
stringValue = Precise.string_div(self.number_to_string(price), self.safe_string(market['precision'], 'price'))
|
|
return int(stringValue)
|
|
|
|
def from_real_symbol_price(self, symbol: str, priceString: str):
|
|
market = self.markets[symbol]
|
|
return Precise.string_mul(priceString, self.safe_string(market['precision'], 'price'))
|
|
|
|
def to_real_symbol_amount(self, symbol: str, amount: float):
|
|
market = self.market(symbol)
|
|
stringValue = Precise.string_div(self.number_to_string(amount), self.safe_string(market['precision'], 'amount'))
|
|
return int(stringValue)
|
|
|
|
def from_real_symbol_amount(self, symbol: str, amountString: str):
|
|
market = self.markets[symbol]
|
|
return Precise.string_mul(amountString, market['precision']['amount'])
|
|
|
|
def safe_get_dynamic(self, settings):
|
|
orderFee = self.safe_value(settings, 'orderFee')
|
|
if 'dynamic' in orderFee:
|
|
return self.safe_value(orderFee, 'dynamic')
|
|
else:
|
|
return self.safe_value(orderFee['composite']['default'], 'dynamic')
|
|
|
|
def safe_get_rates(self, dynamic):
|
|
rates = self.safe_value(dynamic, 'rates')
|
|
if rates is None:
|
|
return {'WAVES': 1}
|
|
return rates
|
|
|
|
async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
"""
|
|
create a trade order
|
|
|
|
https://matcher.waves.exchange/api-docs/index.html#/serialize/serializeOrder
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of currency you want to trade in units of base currency
|
|
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param float [params.triggerPrice]: The price at which a stop order is triggered at
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.check_required_dependencies()
|
|
self.check_required_keys()
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
matcherPublicKey = await self.get_matcher_public_key()
|
|
amountAsset = self.get_asset_id(market['baseId'])
|
|
priceAsset = self.get_asset_id(market['quoteId'])
|
|
isMarketOrder = (type == 'market')
|
|
triggerPrice = self.safe_float_2(params, 'triggerPrice', 'stopPrice')
|
|
isStopOrder = (triggerPrice is not None)
|
|
if (isMarketOrder) and (price is None):
|
|
raise InvalidOrder(self.id + ' createOrder() requires a price argument for ' + type + ' orders to determine the max price for buy and the min price for sell')
|
|
timestamp = self.milliseconds()
|
|
defaultExpiryDelta = None
|
|
defaultExpiryDelta, params = self.handle_option_and_params(params, 'createOrder', 'defaultExpiry', self.safe_integer(self.options, 'createOrderDefaultExpiry', 2419200000))
|
|
expiration = self.sum(timestamp, defaultExpiryDelta)
|
|
matcherFees = await self.get_fees_for_asset(symbol, side, amount, price)
|
|
# {
|
|
# "base":{
|
|
# "feeAssetId":"WAVES", # varies depending on the trading pair
|
|
# "matcherFee":"1000000"
|
|
# },
|
|
# "discount":{
|
|
# "feeAssetId":"EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc",
|
|
# "matcherFee":"4077612"
|
|
# }
|
|
# }
|
|
base = self.safe_value_2(matcherFees, 'base', 'discount')
|
|
baseFeeAssetId = self.safe_string(base, 'feeAssetId')
|
|
baseFeeAsset = self.safe_currency_code(baseFeeAssetId)
|
|
baseMatcherFee = self.safe_string(base, 'matcherFee')
|
|
discount = self.safe_value(matcherFees, 'discount')
|
|
discountFeeAssetId = self.safe_string(discount, 'feeAssetId')
|
|
discountFeeAsset = self.safe_currency_code(discountFeeAssetId)
|
|
discountMatcherFee = self.safe_string(discount, 'matcherFee')
|
|
matcherFeeAssetId = None
|
|
matcherFee = None
|
|
# check first if user supplied asset fee is valid
|
|
if ('feeAsset' in params) or ('feeAsset' in self.options):
|
|
feeAsset = self.safe_string(params, 'feeAsset', self.safe_string(self.options, 'feeAsset'))
|
|
feeCurrency = self.currency(feeAsset)
|
|
matcherFeeAssetId = self.safe_string(feeCurrency, 'id')
|
|
balances = await self.fetch_balance()
|
|
if matcherFeeAssetId is not None:
|
|
if baseFeeAssetId != matcherFeeAssetId and discountFeeAssetId != matcherFeeAssetId:
|
|
raise InvalidOrder(self.id + ' asset fee must be ' + baseFeeAsset + ' or ' + discountFeeAsset)
|
|
matcherFeeAsset = self.safe_currency_code(matcherFeeAssetId)
|
|
rawMatcherFee = baseMatcherFee if (matcherFeeAssetId == baseFeeAssetId) else discountMatcherFee
|
|
floatMatcherFee = float(self.from_real_currency_amount(matcherFeeAsset, rawMatcherFee))
|
|
if (matcherFeeAsset in balances) and (balances[matcherFeeAsset]['free'] >= floatMatcherFee):
|
|
matcherFee = int(rawMatcherFee)
|
|
else:
|
|
raise InsufficientFunds(self.id + ' not enough funds of the selected asset fee')
|
|
floatBaseMatcherFee = self.from_real_currency_amount(baseFeeAsset, baseMatcherFee)
|
|
floatDiscountMatcherFee = self.from_real_currency_amount(discountFeeAsset, discountMatcherFee)
|
|
if matcherFeeAssetId is None:
|
|
# try to the pay the fee using the base first then discount asset
|
|
if (baseFeeAsset in balances) and (balances[baseFeeAsset]['free'] >= float(floatBaseMatcherFee)):
|
|
matcherFeeAssetId = baseFeeAssetId
|
|
matcherFee = int(baseMatcherFee)
|
|
else:
|
|
if (discountFeeAsset in balances) and (balances[discountFeeAsset]['free'] >= float(floatDiscountMatcherFee)):
|
|
matcherFeeAssetId = discountFeeAssetId
|
|
matcherFee = int(discountMatcherFee)
|
|
if matcherFeeAssetId is None:
|
|
raise InsufficientFunds(self.id + ' not enough funds on none of the eligible asset fees: ' + baseFeeAsset + ' ' + floatBaseMatcherFee + ' or ' + discountFeeAsset + ' ' + floatDiscountMatcherFee)
|
|
amount = self.to_real_symbol_amount(symbol, amount)
|
|
price = self.to_real_symbol_price(symbol, price)
|
|
assetPair: dict = {
|
|
'amountAsset': amountAsset,
|
|
'priceAsset': priceAsset,
|
|
}
|
|
sandboxMode = self.safe_bool(self.options, 'sandboxMode', False)
|
|
chainId = 84 if (sandboxMode) else 87
|
|
body: dict = {
|
|
'senderPublicKey': self.apiKey,
|
|
'matcherPublicKey': matcherPublicKey,
|
|
'assetPair': assetPair,
|
|
'orderType': side,
|
|
'price': price,
|
|
'amount': amount,
|
|
'timestamp': timestamp,
|
|
'expiration': expiration,
|
|
'matcherFee': int(matcherFee),
|
|
'priceMode': 'assetDecimals',
|
|
'version': 4,
|
|
'chainId': chainId,
|
|
}
|
|
if isStopOrder:
|
|
#
|
|
# {
|
|
# "v": 1, # version(int)
|
|
# "c": { # condition(object)
|
|
# "t": "sp", # condition type. for now only "stop-price"(string)
|
|
# "v": { # value(object)
|
|
# "p": "123", # price(long)
|
|
# },
|
|
# },
|
|
# }
|
|
#
|
|
attachment: dict = {
|
|
'v': 1,
|
|
'c': {
|
|
't': 'sp',
|
|
'v': {
|
|
'p': self.to_real_symbol_price(symbol, triggerPrice),
|
|
},
|
|
},
|
|
}
|
|
body['attachment'] = self.binary_to_base58(self.encode(json.dumps(attachment)))
|
|
if matcherFeeAssetId != 'WAVES':
|
|
body['matcherFeeAssetId'] = matcherFeeAssetId
|
|
serializedOrder = await self.matcherPostMatcherOrdersSerialize(body)
|
|
if (serializedOrder[0] == '"') and (serializedOrder[(len(serializedOrder) - 1)] == '"'):
|
|
serializedOrder = serializedOrder[1:len(serializedOrder) - 1]
|
|
signature = self.axolotl(self.binary_to_base16(self.base58_to_binary(serializedOrder)), self.binary_to_base16(self.base58_to_binary(self.secret)), 'ed25519')
|
|
body['signature'] = signature
|
|
#
|
|
# {
|
|
# "success": True,
|
|
# "message": {
|
|
# "version": 4,
|
|
# "id": "8VR49dLZFaYcVwzx9TqVMTAZCSUoyB74kLUHrEPCSJgN",
|
|
# "sender": "3MpEdBXtsRHRj2TvZURSb8uLDxzneVbYczW",
|
|
# "senderPublicKey": "8aUTNqHGCBiubySBRhcS1N6NC5jLczhVcndRfMAuwtkY",
|
|
# "matcherPublicKey": "8QUAqtTckM5B8gvcuP7mMswat9SjKUuafJMusEoSn1Gy",
|
|
# "assetPair": {
|
|
# "amountAsset": "EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc",
|
|
# "priceAsset": "25FEqEjRkqK6yCkiT7Lz6SAYz7gUFCtxfCChnrVFD5AT"
|
|
# },
|
|
# "orderType": "sell",
|
|
# "amount": 100000,
|
|
# "price": 480000,
|
|
# "timestamp": 1690852043772,
|
|
# "expiration": 1693271243772,
|
|
# "matcherFee": 83327570,
|
|
# "signature": "3QYDWQVSP4kdqpTLodCuboh8bpWd6GW5s1pQyKdce1JBDwX6t4kH5Xtuq35pqo94gxjo3cfG6k6Xuic2JaYLubkK",
|
|
# "proofs": [
|
|
# "3QYDWQVSP4kdqpTLodCuboh8bpWd6GW5s1pQyKdce1JBDwX6t4kH5Xtuq35pqo94gxjo3cfG6k6Xuic2JaYLubkK"
|
|
# ],
|
|
# "matcherFeeAssetId": "EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc",
|
|
# "eip712Signature": null,
|
|
# "priceMode": "assetDecimals",
|
|
# "attachment": "2PQ4akZHnMSZrQissuu5uudoXbgsipeDnFcRtXtjVgkdm1gUWEgGzp"
|
|
# },
|
|
# "status": "OrderAccepted"
|
|
# }
|
|
#
|
|
if isMarketOrder:
|
|
response = await self.matcherPostMatcherOrderbookMarket(self.extend(body, params))
|
|
value = self.safe_dict(response, 'message')
|
|
return self.parse_order(value, market)
|
|
else:
|
|
response = await self.matcherPostMatcherOrderbook(self.extend(body, params))
|
|
value = self.safe_dict(response, 'message')
|
|
return self.parse_order(value, market)
|
|
|
|
async def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
cancels an open order
|
|
|
|
https://matcher.waves.exchange/api-docs/index.html#/cancel/cancelOrdersByIdsWithKeyOrSignature
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.check_required_dependencies()
|
|
self.check_required_keys()
|
|
await self.sign_in()
|
|
wavesAddress = await self.get_waves_address()
|
|
response = await self.forwardPostMatcherOrdersWavesAddressCancel({
|
|
'wavesAddress': wavesAddress,
|
|
'orderId': id,
|
|
})
|
|
# {
|
|
# "success":true,
|
|
# "message":[[{"orderId":"EBpJeGM36KKFz5gTJAUKDBm89V8wqxKipSFBdU35AN3c","success":true,"status":"OrderCanceled"}]],
|
|
# "status":"BatchCancelCompleted"
|
|
# }
|
|
message = self.safe_value(response, 'message')
|
|
firstMessage = self.safe_value(message, 0)
|
|
firstOrder = self.safe_value(firstMessage, 0)
|
|
returnedId = self.safe_string(firstOrder, 'orderId')
|
|
return self.safe_order({
|
|
'info': response,
|
|
'id': returnedId,
|
|
'clientOrderId': None,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'lastTradeTimestamp': None,
|
|
'symbol': symbol,
|
|
'type': None,
|
|
'side': None,
|
|
'price': None,
|
|
'amount': None,
|
|
'cost': None,
|
|
'average': None,
|
|
'filled': None,
|
|
'remaining': None,
|
|
'status': None,
|
|
'fee': None,
|
|
'trades': None,
|
|
})
|
|
|
|
async def fetch_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
fetches information on an order made by the user
|
|
|
|
https://matcher.waves.exchange/api-docs/index.html#/status/getOrderStatusByPKAndIdWithSig
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.check_required_dependencies()
|
|
self.check_required_keys()
|
|
await self.load_markets()
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
timestamp = self.milliseconds()
|
|
byteArray = [
|
|
self.base58_to_binary(self.apiKey),
|
|
self.number_to_be(timestamp, 8),
|
|
]
|
|
binary = self.binary_concat_array(byteArray)
|
|
hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret))
|
|
signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519')
|
|
request: dict = {
|
|
'Timestamp': str(timestamp),
|
|
'Signature': signature,
|
|
'publicKey': self.apiKey,
|
|
'orderId': id,
|
|
}
|
|
response = await self.matcherGetMatcherOrderbookPublicKeyOrderId(self.extend(request, params))
|
|
return self.parse_order(response, market)
|
|
|
|
async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple orders made by the user
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.check_required_dependencies()
|
|
self.check_required_keys()
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument')
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
timestamp = self.milliseconds()
|
|
byteArray = [
|
|
self.base58_to_binary(self.apiKey),
|
|
self.number_to_be(timestamp, 8),
|
|
]
|
|
binary = self.binary_concat_array(byteArray)
|
|
hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret))
|
|
signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519')
|
|
request: dict = {
|
|
'Accept': 'application/json',
|
|
'Timestamp': str(timestamp),
|
|
'Signature': signature,
|
|
'publicKey': self.apiKey,
|
|
'baseId': market['baseId'],
|
|
'quoteId': market['quoteId'],
|
|
}
|
|
response = await self.matcherGetMatcherOrderbookBaseIdQuoteIdPublicKeyPublicKey(self.extend(request, params))
|
|
# [{id: "3KicDeWayY2mdrRoYdCkP3gUAoUZUNT1AA6GAtWuPLfa",
|
|
# "type": "sell",
|
|
# "orderType": "limit",
|
|
# "amount": 1,
|
|
# "fee": 300000,
|
|
# "price": 100000000,
|
|
# "timestamp": 1591651254076,
|
|
# "filled": 0,
|
|
# "filledFee": 0,
|
|
# "feeAsset": "WAVES",
|
|
# "status": "Accepted",
|
|
# "assetPair":
|
|
# {amountAsset: null,
|
|
# "priceAsset": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS"},
|
|
# "avgWeighedPrice": 0}, ...]
|
|
return self.parse_orders(response, market, since, limit)
|
|
|
|
async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently open orders
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch open orders for
|
|
:param int [limit]: the maximum number of open orders structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.sign_in()
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
address = await self.get_waves_address()
|
|
request: dict = {
|
|
'address': address,
|
|
'activeOnly': True,
|
|
}
|
|
response = await self.forwardGetMatcherOrdersAddress(request)
|
|
return self.parse_orders(response, market, since, limit)
|
|
|
|
async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple closed orders made by the user
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.sign_in()
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
address = await self.get_waves_address()
|
|
request: dict = {
|
|
'address': address,
|
|
'closedOnly': True,
|
|
}
|
|
response = await self.forwardGetMatcherOrdersAddress(request)
|
|
# [
|
|
# {
|
|
# "id": "9aXcxvXai73jbAm7tQNnqaQ2PwUjdmWuyjvRTKAHsw4f",
|
|
# "type": "buy",
|
|
# "orderType": "limit",
|
|
# "amount": 23738330,
|
|
# "fee": 300000,
|
|
# "price": 3828348334,
|
|
# "timestamp": 1591926905636,
|
|
# "filled": 23738330,
|
|
# "filledFee": 300000,
|
|
# "feeAsset": "WAVES",
|
|
# "status": "Filled",
|
|
# "assetPair": {
|
|
# "amountAsset": "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk",
|
|
# "priceAsset": null
|
|
# },
|
|
# "avgWeighedPrice": 3828348334
|
|
# }, ...
|
|
# ]
|
|
return self.parse_orders(response, market, since, limit)
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'Cancelled': 'canceled',
|
|
'Accepted': 'open',
|
|
'Filled': 'closed',
|
|
'PartiallyFilled': 'open',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def get_symbol_from_asset_pair(self, assetPair):
|
|
# a blank string or null can indicate WAVES
|
|
baseId = self.safe_string(assetPair, 'amountAsset', 'WAVES')
|
|
quoteId = self.safe_string(assetPair, 'priceAsset', 'WAVES')
|
|
return self.safe_currency_code(baseId) + '/' + self.safe_currency_code(quoteId)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# createOrder
|
|
#
|
|
# {
|
|
# "version": 4,
|
|
# "id": "BshyeHXDfJmTnjTdBYt371jD4yWaT3JTP6KpjpsiZepS",
|
|
# "sender": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH",
|
|
# "senderPublicKey": "AHXn8nBA4SfLQF7hLQiSn16kxyehjizBGW1TdrmSZ1gF",
|
|
# "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "assetPair": {
|
|
# "amountAsset": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
|
|
# "priceAsset": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
|
|
# },
|
|
# "orderType": "buy",
|
|
# "amount": 10000,
|
|
# "price": 400000000,
|
|
# "timestamp": 1599848586891,
|
|
# "expiration": 1602267786891,
|
|
# "matcherFee": 3008,
|
|
# "matcherFeeAssetId": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
|
|
# "signature": "3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy",
|
|
# "proofs": [
|
|
# "3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy",
|
|
# ],
|
|
# "attachment":"77rnoyFX5BDr15hqZiUtgXKSN46zsbHHQjVNrTMLZcLz62mmFKr39FJ"
|
|
# }
|
|
#
|
|
#
|
|
# fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders
|
|
#
|
|
# {
|
|
# "id": "81D9uKk2NfmZzfG7uaJsDtxqWFbJXZmjYvrL88h15fk8",
|
|
# "type": "buy",
|
|
# "orderType": "limit",
|
|
# "amount": 30000000000,
|
|
# "filled": 0,
|
|
# "price": 1000000,
|
|
# "fee": 300000,
|
|
# "filledFee": 0,
|
|
# "feeAsset": "WAVES",
|
|
# "timestamp": 1594303779322,
|
|
# "status": "Cancelled",
|
|
# "assetPair": {
|
|
# "amountAsset": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
|
|
# "priceAsset": "WAVES"
|
|
# },
|
|
# "avgWeighedPrice": 0,
|
|
# "version": 4,
|
|
# "totalExecutedPriceAssets": 0, # in fetchOpenOrder/s
|
|
# "attachment":"77rnoyFX5BDr15hqZiUtgXKSN46zsbHHQjVNrTMLZcLz62mmFKr39FJ"
|
|
# }
|
|
#
|
|
timestamp = self.safe_integer(order, 'timestamp')
|
|
side = self.safe_string_2(order, 'type', 'orderType')
|
|
type = 'limit'
|
|
if 'type' in order:
|
|
# fetchOrders
|
|
type = self.safe_string(order, 'orderType', type)
|
|
id = self.safe_string(order, 'id')
|
|
filledString = self.safe_string(order, 'filled')
|
|
priceString = self.safe_string(order, 'price')
|
|
amountString = self.safe_string(order, 'amount')
|
|
assetPair = self.safe_value(order, 'assetPair')
|
|
symbol = None
|
|
if assetPair is not None:
|
|
symbol = self.get_symbol_from_asset_pair(assetPair)
|
|
elif market is not None:
|
|
symbol = market['symbol']
|
|
amountCurrency = self.safe_currency_code(self.safe_string(assetPair, 'amountAsset', 'WAVES'))
|
|
price = self.from_real_symbol_price(symbol, priceString)
|
|
amount = self.from_real_currency_amount(amountCurrency, amountString)
|
|
filled = self.from_real_currency_amount(amountCurrency, filledString)
|
|
average = self.from_real_symbol_price(symbol, self.safe_string(order, 'avgWeighedPrice'))
|
|
status = self.parse_order_status(self.safe_string(order, 'status'))
|
|
fee = None
|
|
if 'type' in order:
|
|
code = self.safe_currency_code(self.safe_string(order, 'feeAsset'))
|
|
fee = {
|
|
'currency': code,
|
|
'fee': self.parse_number(self.from_real_currency_amount(code, self.safe_string(order, 'filledFee'))),
|
|
}
|
|
else:
|
|
code = self.safe_currency_code(self.safe_string(order, 'matcherFeeAssetId', 'WAVES'))
|
|
fee = {
|
|
'currency': code,
|
|
'fee': self.parse_number(self.from_real_currency_amount(code, self.safe_string(order, 'matcherFee'))),
|
|
}
|
|
triggerPrice = None
|
|
attachment = self.safe_string(order, 'attachment')
|
|
if attachment is not None:
|
|
decodedAttachment = self.parse_json(self.decode(self.base58_to_binary(attachment)))
|
|
if decodedAttachment is not None:
|
|
c = self.safe_value(decodedAttachment, 'c')
|
|
if c is not None:
|
|
v = self.safe_value(c, 'v')
|
|
if v is not None:
|
|
triggerPrice = self.safe_string(v, 'p')
|
|
return self.safe_order({
|
|
'info': order,
|
|
'id': id,
|
|
'clientOrderId': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'symbol': symbol,
|
|
'type': type,
|
|
'timeInForce': None,
|
|
'postOnly': None,
|
|
'side': side,
|
|
'price': price,
|
|
'triggerPrice': triggerPrice,
|
|
'amount': amount,
|
|
'cost': None,
|
|
'average': average,
|
|
'filled': filled,
|
|
'remaining': None,
|
|
'status': status,
|
|
'fee': fee,
|
|
'trades': None,
|
|
}, market)
|
|
|
|
async def get_waves_address(self):
|
|
cachedAddreess = self.safe_string(self.options, 'wavesAddress')
|
|
if cachedAddreess is None:
|
|
request: dict = {
|
|
'publicKey': self.apiKey,
|
|
}
|
|
response = await self.nodeGetAddressesPublicKeyPublicKey(request)
|
|
self.options['wavesAddress'] = self.safe_string(response, 'address')
|
|
return self.options['wavesAddress']
|
|
else:
|
|
return cachedAddreess
|
|
|
|
async def fetch_balance(self, params={}) -> Balances:
|
|
"""
|
|
query for balance and get the amount of funds available for trading or funds locked in orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
# makes a lot of different requests to get all the data
|
|
# in particular:
|
|
# fetchMarkets, getWavesAddress,
|
|
# getTotalBalance(doesn't include waves), getReservedBalance(doesn't include waves)
|
|
# getReservedBalance(includes WAVES)
|
|
# I couldn't find another way to get all the data
|
|
self.check_required_dependencies()
|
|
self.check_required_keys()
|
|
await self.load_markets()
|
|
wavesAddress = await self.get_waves_address()
|
|
request: dict = {
|
|
'address': wavesAddress,
|
|
}
|
|
totalBalance = await self.nodeGetAssetsBalanceAddress(request)
|
|
# {
|
|
# "address": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH",
|
|
# "balances": [
|
|
# {
|
|
# "assetId": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
|
|
# "balance": 1177200,
|
|
# "reissuable": False,
|
|
# "minSponsoredAssetFee": 7420,
|
|
# "sponsorBalance": 47492147189709,
|
|
# "quantity": 999999999775381400,
|
|
# "issueTransaction": {
|
|
# "senderPublicKey": "BRnVwSVctnV8pge5vRpsJdWnkjWEJspFb6QvrmZvu3Ht",
|
|
# "quantity": 1000000000000000000,
|
|
# "fee": 100400000,
|
|
# "description": "Neutrino USD",
|
|
# "type": 3,
|
|
# "version": 2,
|
|
# "reissuable": False,
|
|
# "script": null,
|
|
# "sender": "3PC9BfRwJWWiw9AREE2B3eWzCks3CYtg4yo",
|
|
# "feeAssetId": null,
|
|
# "chainId": 87,
|
|
# "proofs": [
|
|
# "3HNpbVkgP69NWSeb9hGYauiQDaXrRXh3tXFzNsGwsAAXnFrA29SYGbLtziW9JLpXEq7qW1uytv5Fnm5XTUMB2BxU"
|
|
# ],
|
|
# "assetId": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
|
|
# "decimals": 6,
|
|
# "name": "USD-N",
|
|
# "id": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
|
|
# "timestamp": 1574429393962
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
balances = self.safe_value(totalBalance, 'balances', [])
|
|
result: dict = {}
|
|
timestamp = None
|
|
assetIds = []
|
|
nonStandardBalances = []
|
|
for i in range(0, len(balances)):
|
|
entry = balances[i]
|
|
entryTimestamp = self.safe_integer(entry, 'timestamp')
|
|
timestamp = entryTimestamp if (timestamp is None) else max(timestamp, entryTimestamp)
|
|
issueTransaction = self.safe_value(entry, 'issueTransaction')
|
|
currencyId = self.safe_string(entry, 'assetId')
|
|
balance = self.safe_string(entry, 'balance')
|
|
currencyExists = (currencyId in self.currencies_by_id)
|
|
if currencyExists:
|
|
code = self.safe_currency_code(currencyId)
|
|
result[code] = self.account()
|
|
result[code]['total'] = self.from_real_currency_amount(code, balance)
|
|
elif issueTransaction is None:
|
|
assetIds.append(currencyId)
|
|
nonStandardBalances.append(balance)
|
|
nonStandardAssets = len(assetIds)
|
|
if nonStandardAssets:
|
|
requestInner: dict = {
|
|
'ids': assetIds,
|
|
}
|
|
response = await self.publicGetAssets(requestInner)
|
|
data = self.safe_value(response, 'data', [])
|
|
for i in range(0, len(data)):
|
|
entry = data[i]
|
|
balance = nonStandardBalances[i]
|
|
inner = self.safe_value(entry, 'data')
|
|
precision = self.parse_precision(self.safe_string(inner, 'precision'))
|
|
ticker = self.safe_string(inner, 'ticker')
|
|
code = self.safe_currency_code(ticker)
|
|
result[code] = self.account()
|
|
result[code]['total'] = Precise.string_mul(balance, precision)
|
|
currentTimestamp = self.milliseconds()
|
|
byteArray = [
|
|
self.base58_to_binary(self.apiKey),
|
|
self.number_to_be(currentTimestamp, 8),
|
|
]
|
|
binary = self.binary_concat_array(byteArray)
|
|
hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret))
|
|
signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519')
|
|
matcherRequest: dict = {
|
|
'publicKey': self.apiKey,
|
|
'signature': signature,
|
|
'timestamp': str(currentTimestamp),
|
|
}
|
|
reservedBalance = await self.matcherGetMatcherBalanceReservedPublicKey(matcherRequest)
|
|
# {WAVES: 200300000}
|
|
reservedKeys = list(reservedBalance.keys())
|
|
for i in range(0, len(reservedKeys)):
|
|
currencyId = reservedKeys[i]
|
|
code = self.safe_currency_code(currencyId)
|
|
if not (code in result):
|
|
result[code] = self.account()
|
|
amount = self.safe_string(reservedBalance, currencyId)
|
|
result[code]['used'] = self.from_real_currency_amount(code, amount)
|
|
wavesRequest: dict = {
|
|
'address': wavesAddress,
|
|
}
|
|
wavesTotal = await self.nodeGetAddressesBalanceAddress(wavesRequest)
|
|
# {
|
|
# "address": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH",
|
|
# "confirmations": 0,
|
|
# "balance": 909085978
|
|
# }
|
|
result['WAVES'] = self.safe_value(result, 'WAVES', self.account())
|
|
result['WAVES']['total'] = self.from_real_currency_amount('WAVES', self.safe_string(wavesTotal, 'balance'))
|
|
result = self.set_undefined_balances_to_zero(result)
|
|
result['timestamp'] = timestamp
|
|
result['datetime'] = self.iso8601(timestamp)
|
|
return self.safe_balance(result)
|
|
|
|
def set_undefined_balances_to_zero(self, balances, key='used'):
|
|
codes = list(balances.keys())
|
|
for i in range(0, len(codes)):
|
|
code = codes[i]
|
|
if self.safe_value(balances[code], 'used') is None:
|
|
balances[code][key] = '0'
|
|
return balances
|
|
|
|
async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch all trades made by the user
|
|
|
|
https://api.wavesplatform.com/v0/docs/#/transactions/searchTxsExchange
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trades structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
address = await self.get_waves_address()
|
|
request: dict = {
|
|
'sender': address,
|
|
}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['amountAsset'] = market['baseId']
|
|
request['priceAsset'] = market['quoteId']
|
|
response = await self.publicGetTransactionsExchange(request)
|
|
data = self.safe_value(response, 'data')
|
|
#
|
|
# {
|
|
# "__type":"list",
|
|
# "isLastPage":true,
|
|
# "lastCursor":"MzA2MjQ0MzAwMDI5OjpkZXNj",
|
|
# "data": [
|
|
# {
|
|
# "__type":"transaction",
|
|
# "data": {
|
|
# "id":"GbjPqco2wRP5QSrY5LimFrUyJaM535K9nhK5zaQ7J7Tx",
|
|
# "timestamp":"2022-04-06T19:56:31.479Z",
|
|
# "height":3062443,
|
|
# "type":7,
|
|
# "version":2,
|
|
# "proofs":[
|
|
# "57mYrANw61eiArCTv2eYwzXm71jYC2KpZ5AeM9zHEstuRaYSAWSuSE7njAJYJu8zap6DMCm3nzqc6es3wQFDpRCN"
|
|
# ],
|
|
# "fee":0.003,
|
|
# "applicationStatus":"succeeded",
|
|
# "sender":"3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu",
|
|
# "senderPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "buyMatcherFee":0,
|
|
# "sellMatcherFee":0.00141728,
|
|
# "price":215.7431,
|
|
# "amount":0.09,
|
|
# "order1": {
|
|
# "id":"49qiuQj5frdZ6zpTCEpMuKPMAh1EimwXpXWB4BeCw33h",
|
|
# "senderPublicKey":"CjUfoH3dsDZsf5UuAjqqzpWHXgvKzBZpVG9YixF7L48K",
|
|
# "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "assetPair": {
|
|
# "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt",
|
|
# "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
|
|
# },
|
|
# "orderType":"buy",
|
|
# "price":215.7431,
|
|
# "sender":"3PR9WmaHV5ueVw2Wr9xsiCG3t4ySXzkkGLy",
|
|
# "amount":0.36265477,
|
|
# "timestamp":"2022-04-06T19:55:06.832Z",
|
|
# "expiration":"2022-05-05T19:55:06.832Z",
|
|
# "matcherFee":3.000334,
|
|
# "signature":"2rBWhdeuRJNpQfXfTFtcR8x8Lpic8FUHPdLML9uxABRUuxe48YRJcZxbncwWAh9LWFCEUZiztv7RZBZfGMWfFxTs",
|
|
# "matcherFeeAssetId":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
|
|
# },
|
|
# "order2": {
|
|
# "id":"AkxiJqCuv6wm8K41TUSgFNwShZMnCbMDT78MqrcWpQ53",
|
|
# "senderPublicKey":"72o7qNKyne5hthB1Ww6famE7uHrk5vTVB2ZfUMBEqL3Y",
|
|
# "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "assetPair": {
|
|
# "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt",
|
|
# "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
|
|
# },
|
|
# "orderType":"sell",
|
|
# "price":210,
|
|
# "sender":"3P3CzbjGgiqEyUBeKZYfgZtyaZfMG8fjoUD",
|
|
# "amount":0.09,
|
|
# "timestamp":"2022-04-06T19:56:18.535Z",
|
|
# "expiration":"2022-05-04T19:56:18.535Z",
|
|
# "matcherFee":0.00141728,
|
|
# "signature":"5BZCjYn6QzVkMXBFDBnzcAUBdCZqhq9hQfRXFHfLUQCsbis4zeriw4sUqLa1BZRT2isC6iY4Z4HtekikPqZ461PT",
|
|
# "matcherFeeAssetId":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt"
|
|
# }
|
|
# }
|
|
# },...
|
|
# ]
|
|
# }
|
|
#
|
|
return self.parse_trades(data, market, since, limit)
|
|
|
|
async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
|
|
https://api.wavesplatform.com/v0/docs/#/transactions/searchTxsExchange
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'amountAsset': market['baseId'],
|
|
'priceAsset': market['quoteId'],
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 100)
|
|
if since is not None:
|
|
request['timeStart'] = since
|
|
response = await self.publicGetTransactionsExchange(request)
|
|
data = self.safe_value(response, 'data')
|
|
#
|
|
# {
|
|
# "__type":"list",
|
|
# "isLastPage":false,
|
|
# "lastCursor":"MzA2MjM2MTAwMDU0OjpkZXNj",
|
|
# "data": [
|
|
# {
|
|
# "__type":"transaction",
|
|
# "data": {
|
|
# "id":"F42WsvSsyEzvpPLFjVhQKkSNuopooP4zMkjSUs47NeML",
|
|
# "timestamp":"2022-04-06T18:39:49.145Z",
|
|
# "height":3062361,
|
|
# "type":7,
|
|
# "version":2,
|
|
# "proofs": [
|
|
# "39iJv82kFi4pyuBxYeZpP45NXXjbrCXdVsHPAAvj32UMLmTXLjMTfV43PcmZDSAuS93HKSDo1aKJrin8UvkeE9Bs"
|
|
# ],
|
|
# "fee":0.003,
|
|
# "applicationStatus":"succeeded",
|
|
# "sender":"3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu",
|
|
# "senderPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "buyMatcherFee":0.02314421,
|
|
# "sellMatcherFee":0,
|
|
# "price":217.3893,
|
|
# "amount":0.34523025,
|
|
# "order1": {
|
|
# "id":"HkM36PHGaeeZdDKT1mYgZXhaU9PRZ54RZiJc2K4YMT3Q",
|
|
# "senderPublicKey":"7wYCaDcc6GX1Jx2uS7QgLHBypBKvrezTS1HfiW6Xe4Bk",
|
|
# "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "assetPair": {
|
|
# "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt",
|
|
# "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
|
|
# },
|
|
# "orderType":"buy",
|
|
# "price":225.2693,
|
|
# "sender":"3PLPc8f4DGYaF9C9bwJ2uVmHqRv3NCjg5VQ",
|
|
# "amount":2.529,
|
|
# "timestamp":"2022-04-06T18:39:48.796Z",
|
|
# "expiration":"2022-05-05T18:39:48.796Z",
|
|
# "matcherFee":0.17584444,
|
|
# "signature":"2yQfJoomv86evQDw36fg1uiRkHvPDZtRp3qvxqTBWPvz4JLTHGQtEHJF5NGTvym6U93CtgNprngzmD9ecHBjxf6U",
|
|
# "matcherFeeAssetId":"Atqv59EYzjFGuitKVnMRk6H8FukjoV3ktPorbEys25on"
|
|
# },
|
|
# "order2": {
|
|
# "id":"F7HKmeuzwWdk3wKitHLnVx5MuD4wBWPpphQ8kUGx4tT9",
|
|
# "senderPublicKey":"CjUfoH3dsDZsf5UuAjqqzpWHXgvKzBZpVG9YixF7L48K",
|
|
# "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "assetPair": {
|
|
# "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt",
|
|
# "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
|
|
# },
|
|
# "orderType":"sell",
|
|
# "price":217.3893,
|
|
# "sender":"3PR9WmaHV5ueVw2Wr9xsiCG3t4ySXzkkGLy",
|
|
# "amount":0.35767793,
|
|
# "timestamp":"2022-04-06T18:32:01.390Z",
|
|
# "expiration":"2022-05-05T18:32:01.390Z",
|
|
# "matcherFee":0.0139168,
|
|
# "signature":"34HgWVLPgeYWkiSvAc5ChVepGTYDQDug2dMTSincs6idEyoM7AtaZuH3mqQ5RJG2fcxxH2QSB723Qq3dgLQwQmKf",
|
|
# "matcherFeeAssetId":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt"
|
|
# }
|
|
# }
|
|
# }, ...
|
|
# ]
|
|
# }
|
|
#
|
|
return self.parse_trades(data, market, since, limit)
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# {__type: "transaction",
|
|
# "data":
|
|
# {id: "HSdruioHqvYHeyn9hhyoHdRWPB2bFA8ujeCPZMK6992c",
|
|
# "timestamp": "2020-06-09T19:34:51.897Z",
|
|
# "height": 2099684,
|
|
# "type": 7,
|
|
# "version": 2,
|
|
# "proofs":
|
|
# ["26teDHERQgwjjHqEn4REcDotNG8M21xjou3X42XuDuCvrRkQo6aPyrswByH3UrkWG8v27ZAaVNzoxDg4teNcLtde"],
|
|
# "fee": 0.003,
|
|
# "sender": "3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu",
|
|
# "senderPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "buyMatcherFee": 0.00299999,
|
|
# "sellMatcherFee": 0.00299999,
|
|
# "price": 0.00012003,
|
|
# "amount": 60.80421562,
|
|
# "order1":
|
|
# {id: "CBRwP3ar4oMvvpUiGyfxc1syh41488SDi2GkrjuBDegv",
|
|
# "senderPublicKey": "DBXSHBz96NFsMu7xh4fi2eT9ZnyxefAHXsMxUayzgC6a",
|
|
# "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "assetPair": [Object],
|
|
# "orderType": "buy",
|
|
# "price": 0.00012003,
|
|
# "sender": "3PJfFRgVuJ47UY4ckb74EGzEBzkHXtmG1LA",
|
|
# "amount": 60.80424773,
|
|
# "timestamp": "2020-06-09T19:34:51.885Z",
|
|
# "expiration": "2020-06-10T12:31:31.885Z",
|
|
# "matcherFee": 0.003,
|
|
# "signature": "4cA3ZAb3XAEEXaFG7caqpto5TRbpR5PkhZpxoNQZ9ZReNvjuJQs5a3THnumv7rcqmVUiVtuHAgk2f67ANcqtKyJ8",
|
|
# "matcherFeeAssetId": null},
|
|
# "order2":
|
|
# {id: "CHJSLQ6dfSPs6gu2mAegrMUcRiDEDqaj2GKfvptMjS3M",
|
|
# "senderPublicKey": "3RUC4NGFZm9H8VJhSSjJyFLdiE42qNiUagDcZPwjgDf8",
|
|
# "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
|
|
# "assetPair": [Object],
|
|
# "orderType": "sell",
|
|
# "price": 0.00012003,
|
|
# "sender": "3P9vKoQpMZtaSkHKpNh977YY9ZPzTuntLAq",
|
|
# "amount": 60.80424773,
|
|
# "timestamp": "2020-06-09T19:34:51.887Z",
|
|
# "expiration": "2020-06-10T12:31:31.887Z",
|
|
# "matcherFee": 0.003,
|
|
# "signature": "3SFyrcqzou2ddZyNisnLYaGhLt5qRjKxH8Nw3s4T5U7CEKGX9DDo8dS27RgThPVGbYF1rYET1FwrWoQ2UFZ6SMTR",
|
|
# "matcherFeeAssetId": null}}}
|
|
#
|
|
data = self.safe_value(trade, 'data')
|
|
datetime = self.safe_string(data, 'timestamp')
|
|
timestamp = self.parse8601(datetime)
|
|
id = self.safe_string(data, 'id')
|
|
priceString = self.safe_string(data, 'price')
|
|
amountString = self.safe_string(data, 'amount')
|
|
order1 = self.safe_value(data, 'order1')
|
|
order2 = self.safe_value(data, 'order2')
|
|
order = None
|
|
# at first, detect if response is from `fetch_my_trades`
|
|
if self.safe_string(order1, 'senderPublicKey') == self.apiKey:
|
|
order = order1
|
|
elif self.safe_string(order2, 'senderPublicKey') == self.apiKey:
|
|
order = order2
|
|
else:
|
|
# response is from `fetch_trades`, so find only taker order
|
|
date1 = self.safe_string(order1, 'timestamp')
|
|
date2 = self.safe_string(order2, 'timestamp')
|
|
ts1 = self.parse8601(date1)
|
|
ts2 = self.parse8601(date2)
|
|
if ts1 > ts2:
|
|
order = order1
|
|
else:
|
|
order = order2
|
|
symbol = None
|
|
assetPair = self.safe_value(order, 'assetPair')
|
|
if assetPair is not None:
|
|
symbol = self.get_symbol_from_asset_pair(assetPair)
|
|
elif market is not None:
|
|
symbol = market['symbol']
|
|
side = self.safe_string(order, 'orderType')
|
|
orderId = self.safe_string(order, 'id')
|
|
fee = {
|
|
'cost': self.safe_string(order, 'matcherFee'),
|
|
'currency': self.safe_currency_code(self.safe_string(order, 'matcherFeeAssetId', 'WAVES')),
|
|
}
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': datetime,
|
|
'symbol': symbol,
|
|
'id': id,
|
|
'order': orderId,
|
|
'type': None,
|
|
'side': side,
|
|
'takerOrMaker': None,
|
|
'price': priceString,
|
|
'amount': amountString,
|
|
'cost': None,
|
|
'fee': fee,
|
|
}, market)
|
|
|
|
def parse_deposit_withdraw_fees(self, response, codes: Strings = None, currencyIdKey=None) -> Any:
|
|
depositWithdrawFees: dict = {}
|
|
codes = self.market_codes(codes)
|
|
for i in range(0, len(response)):
|
|
entry = response[i]
|
|
dictionary = entry
|
|
currencyId = self.safe_string(dictionary, currencyIdKey)
|
|
currency = self.safe_value(self.currencies_by_id, currencyId)
|
|
code = self.safe_string(currency, 'code', currencyId)
|
|
if (codes is None) or (self.in_array(code, codes)):
|
|
depositWithdrawFee = self.safe_value(depositWithdrawFees, code)
|
|
if depositWithdrawFee is None:
|
|
depositWithdrawFee = {
|
|
'info': [dictionary],
|
|
'withdraw': {
|
|
'fee': None,
|
|
'percentage': None,
|
|
},
|
|
'deposit': {
|
|
'fee': None,
|
|
'percentage': None,
|
|
},
|
|
'networks': {},
|
|
}
|
|
else:
|
|
depositWithdrawFee = depositWithdrawFees[code]
|
|
depositWithdrawFee['info'] = self.array_concat(depositWithdrawFee['info'], [dictionary])
|
|
networkId = self.safe_string(dictionary, 'platform_id')
|
|
currencyCode = self.safe_string(currency, 'code')
|
|
networkCode = self.network_id_to_code(networkId, currencyCode)
|
|
network = self.safe_value(depositWithdrawFee['networks'], networkCode)
|
|
if network is None:
|
|
network = {
|
|
'withdraw': {
|
|
'fee': None,
|
|
'percentage': None,
|
|
},
|
|
'deposit': {
|
|
'fee': None,
|
|
'percentage': None,
|
|
},
|
|
}
|
|
feeType = self.safe_string(dictionary, 'type')
|
|
fees = self.safe_value(dictionary, 'fees')
|
|
networkKey = 'deposit'
|
|
if feeType == 'withdrawal_currency':
|
|
networkKey = 'withdraw'
|
|
network[networkKey] = {'fee': self.safe_number(fees, 'flat'), 'percentage': False}
|
|
depositWithdrawFee['networks'][networkCode] = network
|
|
depositWithdrawFees[code] = depositWithdrawFee
|
|
depositWithdrawFeesKeys = list(depositWithdrawFees.keys())
|
|
for i in range(0, len(depositWithdrawFeesKeys)):
|
|
code = depositWithdrawFeesKeys[i]
|
|
entry = depositWithdrawFees[code]
|
|
networks = self.safe_value(entry, 'networks')
|
|
networkKeys = list(networks.keys())
|
|
networkKeysLength = len(networkKeys)
|
|
if networkKeysLength == 1:
|
|
network = self.safe_value(networks, networkKeys[0])
|
|
depositWithdrawFees[code]['withdraw'] = self.safe_value(network, 'withdraw')
|
|
depositWithdrawFees[code]['deposit'] = self.safe_value(network, 'deposit')
|
|
return depositWithdrawFees
|
|
|
|
async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}):
|
|
"""
|
|
fetch deposit and withdraw fees
|
|
|
|
https://docs.wx.network/en/api/gateways/deposit/currencies
|
|
https://docs.wx.network/en/api/gateways/withdraw/currencies
|
|
|
|
:param str[]|None codes: list of unified currency codes
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `fee structures <https://docs.ccxt.com/#/?id=fee-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
data = []
|
|
promises = []
|
|
promises.append(self.privateGetDepositCurrencies(params))
|
|
promises.append(self.privateGetWithdrawCurrencies(params))
|
|
promises = await asyncio.gather(*promises)
|
|
#
|
|
# {
|
|
# "type": "list",
|
|
# "page_info": {
|
|
# "has_next_page": False,
|
|
# "last_cursor": null
|
|
# },
|
|
# "items": [
|
|
# {
|
|
# "type": "deposit_currency",
|
|
# "id": "WEST",
|
|
# "platform_id": "WEST",
|
|
# "waves_asset_id": "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8",
|
|
# "platform_asset_id": "WEST",
|
|
# "decimals": 8,
|
|
# "status": "active",
|
|
# "allowed_amount": {
|
|
# "min": 0.1,
|
|
# "max": 2000000
|
|
# },
|
|
# "fees": {
|
|
# "flat": 0,
|
|
# "rate": 0
|
|
# }
|
|
# },
|
|
# ]
|
|
# }
|
|
#
|
|
#
|
|
# {
|
|
# "type": "list",
|
|
# "page_info": {
|
|
# "has_next_page": False,
|
|
# "last_cursor": null
|
|
# },
|
|
# "items": [
|
|
# {
|
|
# "type": "withdrawal_currency",
|
|
# "id": "BTC",
|
|
# "platform_id": "BTC",
|
|
# "waves_asset_id": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
|
|
# "platform_asset_id": "BTC",
|
|
# "decimals": 8,
|
|
# "status": "inactive",
|
|
# "allowed_amount": {
|
|
# "min": 0.001,
|
|
# "max": 10
|
|
# },
|
|
# "fees": {
|
|
# "flat": 0.001,
|
|
# "rate": 0
|
|
# }
|
|
# },
|
|
# ]
|
|
# }
|
|
#
|
|
for i in range(0, len(promises)):
|
|
items = self.safe_value(promises[i], 'items')
|
|
data = self.array_concat(data, items)
|
|
return self.parse_deposit_withdraw_fees(data, codes, 'id')
|
|
|
|
def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
errorCode = self.safe_string(response, 'error')
|
|
success = self.safe_bool(response, 'success', True)
|
|
Exception = self.safe_value(self.exceptions, errorCode)
|
|
if Exception is not None:
|
|
messageInner = self.safe_string(response, 'message')
|
|
raise Exception(self.id + ' ' + messageInner)
|
|
message = self.safe_string(response, 'message')
|
|
if message == 'Validation Error':
|
|
raise BadRequest(self.id + ' ' + body)
|
|
if not success:
|
|
raise ExchangeError(self.id + ' ' + body)
|
|
return None
|
|
|
|
async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a withdrawal
|
|
:param str code: unified currency code
|
|
:param float amount: the amount to withdraw
|
|
:param str address: the address to withdraw to
|
|
:param str tag:
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
tag, params = self.handle_withdraw_tag_and_params(tag, params)
|
|
# currently only works for BTC and WAVES
|
|
if code != 'WAVES':
|
|
supportedCurrencies = await self.privateGetWithdrawCurrencies()
|
|
currencies: dict = {}
|
|
items = self.safe_value(supportedCurrencies, 'items', [])
|
|
for i in range(0, len(items)):
|
|
entry = items[i]
|
|
currencyCode = self.safe_string(entry, 'id')
|
|
currencies[currencyCode] = True
|
|
if not (code in currencies):
|
|
codes = list(currencies.keys())
|
|
raise ExchangeError(self.id + ' withdraw() ' + code + ' not supported. Currency code must be one of ' + str(codes))
|
|
await self.load_markets()
|
|
hexChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
|
|
set: dict = {}
|
|
for i in range(0, len(hexChars)):
|
|
key = hexChars[i]
|
|
set[key] = True
|
|
isErc20 = True
|
|
noPrefix = self.remove0x_prefix(address)
|
|
lower = noPrefix.lower()
|
|
stringLength = len(lower) * 1
|
|
for i in range(0, stringLength):
|
|
character = lower[i]
|
|
if not (character in set):
|
|
isErc20 = False
|
|
break
|
|
await self.sign_in()
|
|
proxyAddress = None
|
|
if code == 'WAVES' and not isErc20:
|
|
proxyAddress = address
|
|
else:
|
|
withdrawAddressRequest: dict = {
|
|
'address': address,
|
|
'currency': code,
|
|
}
|
|
withdrawAddress = await self.privateGetWithdrawAddressesCurrencyAddress(withdrawAddressRequest)
|
|
currencyInner = self.safe_value(withdrawAddress, 'currency')
|
|
allowedAmount = self.safe_value(currencyInner, 'allowed_amount')
|
|
minimum = self.safe_number(allowedAmount, 'min')
|
|
if amount <= minimum:
|
|
raise BadRequest(self.id + ' ' + code + ' withdraw failed, amount ' + str(amount) + ' must be greater than the minimum allowed amount of ' + str(minimum))
|
|
# {
|
|
# "type": "withdrawal_addresses",
|
|
# "currency": {
|
|
# "type": "withdrawal_currency",
|
|
# "id": "BTC",
|
|
# "waves_asset_id": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
|
|
# "decimals": 8,
|
|
# "status": "active",
|
|
# "allowed_amount": {
|
|
# "min": 0.001,
|
|
# "max": 20
|
|
# },
|
|
# "fees": {
|
|
# "flat": 0.001,
|
|
# "rate": 0
|
|
# }
|
|
# },
|
|
# "proxy_addresses": [
|
|
# "3P3qqmkiLwNHB7x1FeoE8bvkRtULwGpo9ga"
|
|
# ]
|
|
# }
|
|
proxyAddresses = self.safe_value(withdrawAddress, 'proxy_addresses', [])
|
|
proxyAddress = self.safe_string(proxyAddresses, 0)
|
|
fee = self.safe_integer(self.options, 'withdrawFeeWAVES', 100000) # 0.001 WAVES
|
|
feeAssetId = 'WAVES'
|
|
type = 4 # transfer
|
|
version = 2
|
|
amountInteger = self.to_real_currency_amount(code, amount)
|
|
currency = self.currency(code)
|
|
timestamp = self.milliseconds()
|
|
byteArray = [
|
|
self.number_to_be(4, 1),
|
|
self.number_to_be(2, 1),
|
|
self.base58_to_binary(self.apiKey),
|
|
self.get_asset_bytes(currency['id']),
|
|
self.get_asset_bytes(feeAssetId),
|
|
self.number_to_be(timestamp, 8),
|
|
self.number_to_be(amountInteger, 8),
|
|
self.number_to_be(fee, 8),
|
|
self.base58_to_binary(proxyAddress),
|
|
self.number_to_be(0, 2),
|
|
]
|
|
binary = self.binary_concat_array(byteArray)
|
|
hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret))
|
|
signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519')
|
|
request: dict = {
|
|
'senderPublicKey': self.apiKey,
|
|
'amount': amountInteger,
|
|
'fee': fee,
|
|
'type': type,
|
|
'version': version,
|
|
'attachment': '',
|
|
'feeAssetId': self.get_asset_id(feeAssetId),
|
|
'proofs': [
|
|
signature,
|
|
],
|
|
'assetId': self.get_asset_id(currency['id']),
|
|
'recipient': proxyAddress,
|
|
'timestamp': timestamp,
|
|
'signature': signature,
|
|
}
|
|
result = await self.nodePostTransactionsBroadcast(request)
|
|
#
|
|
# {
|
|
# "id": "string",
|
|
# "signature": "string",
|
|
# "fee": 0,
|
|
# "timestamp": 1460678400000,
|
|
# "recipient": "3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k",
|
|
# "amount": 0
|
|
# }
|
|
#
|
|
return self.parse_transaction(result, currency)
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# withdraw
|
|
#
|
|
# {
|
|
# "id": "string",
|
|
# "signature": "string",
|
|
# "fee": 0,
|
|
# "timestamp": 1460678400000,
|
|
# "recipient": "3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k",
|
|
# "amount": 0
|
|
# }
|
|
#
|
|
# withdraw new:
|
|
# {
|
|
# type: "4",
|
|
# id: "2xnWTqG9ar7jEDrLxfbVyyspPZ6XZNrrw9ai9sQ81Eya",
|
|
# fee: "100000",
|
|
# feeAssetId: null,
|
|
# timestamp: "1715786263807",
|
|
# version: "2",
|
|
# sender: "3P81LLX1kk2CSJC9L8C2enxdHB7XvnSGAEE",
|
|
# senderPublicKey: "DdmzmXf9mty1FBE8AdVGnrncVLEAzP4gR4nWoTFAJoXz",
|
|
# proofs: ["RyoKwdSYv3EqotJCYftfFM9JE2j1ZpDRxKwYfiRhLAFeyNp6VfJUXNDS884XfeCeHeNypNmTCZt5NYR1ekyjCX3",],
|
|
# recipient: "3P9tXxu38a8tgewNEKFzourVxeqHd11ppOc",
|
|
# assetId: null,
|
|
# feeAsset: null,
|
|
# amount: "2000000",
|
|
# attachment: "",
|
|
# }
|
|
#
|
|
currency = self.safe_currency(None, currency)
|
|
code = currency['code']
|
|
typeRaw = self.safe_string(transaction, 'type')
|
|
type = 'withdraw' if (typeRaw == '4') else 'deposit'
|
|
amount = self.parse_number(self.from_real_currency_amount(code, self.safe_string(transaction, 'amount')))
|
|
feeString = self.safe_string(transaction, 'fee')
|
|
feeAssetId = self.safe_string(transaction, 'feeAssetId', 'WAVES')
|
|
feeCode = self.safe_currency_code(feeAssetId)
|
|
feeAmount = self.parse_number(self.from_real_currency_amount(feeCode, feeString))
|
|
timestamp = self.safe_integer(transaction, 'timestamp')
|
|
return {
|
|
'id': self.safe_string(transaction, 'id'),
|
|
'txid': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'network': None,
|
|
'addressFrom': self.safe_string(transaction, 'sender'),
|
|
'address': None,
|
|
'addressTo': self.safe_string(transaction, 'recipient'),
|
|
'amount': amount,
|
|
'type': type,
|
|
'currency': currency['code'],
|
|
'status': None,
|
|
'updated': None,
|
|
'tagFrom': None,
|
|
'tag': None,
|
|
'tagTo': None,
|
|
'comment': None,
|
|
'internal': None,
|
|
'fee': {
|
|
'currency': feeCode,
|
|
'cost': feeAmount,
|
|
},
|
|
'info': transaction,
|
|
}
|