Skip to content

Commit 3c66773

Browse files
committed
WIP
Signed-off-by: Herklos <herklos@drakkar.software>
1 parent 6a4f500 commit 3c66773

1 file changed

Lines changed: 280 additions & 18 deletions

File tree

ts/src/bisq.ts

Lines changed: 280 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default class bisq extends Exchange {
4545
'fetchOrderBook': true,
4646
'fetchOrders': false,
4747
'fetchTicker': true,
48-
'fetchTickers': false,
48+
'fetchTickers': true,
4949
'fetchTrades': false,
5050
},
5151
'urls': {
@@ -120,6 +120,7 @@ export default class bisq extends Exchange {
120120
'wallets/getfundingaddresses': 1,
121121
'wallets/getnetwork': 1,
122122
'wallets/gettransaction': 1,
123+
'wallets/gettransactions': 1,
123124
'wallets/gettxfeerate': 1,
124125
'wallets/getunusedbsqaddress': 1,
125126
'wallets/lockwallet': 1,
@@ -192,6 +193,7 @@ export default class bisq extends Exchange {
192193
'wallets/getfundingaddresses': 'api/v1/wallet/addresses',
193194
'wallets/getnetwork': 'api/v1/wallet/network',
194195
'wallets/gettransaction': 'api/v1/wallet/transactions/{txId}',
196+
'wallets/gettransactions': 'api/v1/wallet/transactions',
195197
'wallets/gettxfeerate': 'api/v1/wallet/tx-fee-rate',
196198
'wallets/getunusedbsqaddress': 'api/v1/wallet/bsq/unused-address',
197199
'wallets/lockwallet': 'api/v1/wallet/lock',
@@ -231,6 +233,7 @@ export default class bisq extends Exchange {
231233
'wallets/getfundingaddresses': 'GET',
232234
'wallets/getnetwork': 'GET',
233235
'wallets/gettransaction': 'GET',
236+
'wallets/gettransactions': 'GET',
234237
'wallets/gettxfeerate': 'GET',
235238
'wallets/getunusedbsqaddress': 'GET',
236239
'wallets/removewalletpassword': 'DELETE',
@@ -320,6 +323,19 @@ export default class bisq extends Exchange {
320323
return Precise.stringDiv (satoshis, '100000000');
321324
}
322325

326+
parseCurrencyBalance (currency: string, rawAmount: string): string {
327+
// Convert a raw integer balance amount from the Bisq API to its decimal representation.
328+
// BTC amounts are in satoshis (1 BTC = 1e8 satoshis).
329+
// All other Bisq-native coins (e.g. BSQ) are in their smallest sub-unit (1 unit = 100 sub-units).
330+
if (rawAmount === undefined) {
331+
return undefined;
332+
}
333+
if (currency === 'BTC') {
334+
return Precise.stringDiv (rawAmount, '100000000');
335+
}
336+
return Precise.stringDiv (rawAmount, '100');
337+
}
338+
323339
safeList2 (object: Dict, key1: string, key2: string, defaultValue = []) {
324340
const value = this.safeList (object, key1);
325341
if (value !== undefined) {
@@ -568,14 +584,35 @@ export default class bisq extends Exchange {
568584
/**
569585
* @method
570586
* @name bisq#fetchTickers
571-
* @description fetches price tickers for multiple markets, not supported by Bisq
587+
* @description fetches price tickers for all markets, or for the markets specified by `symbols`
572588
* @see https://bisq-network.github.io/slate/#rpc-method-getmarketprice
573-
* @param {string[]} [symbols] unified market symbols
589+
* @note specific behavior: calls `Price.GetMarketPrice` once per market; markets whose price feed is temporarily
590+
* unavailable (e.g. BSQ on a fresh daemon) are silently skipped rather than failing the whole call
591+
* @param {string[]} [symbols] unified market symbols to fetch tickers for; defaults to all loaded markets
574592
* @param {object} [params] extra parameters specific to the exchange API endpoint
575-
* @returns {object} a dictionary of ticker structures
593+
* @returns {object} a dictionary of [ticker structures]{@link https://docs.ccxt.com/?id=ticker-structure}
576594
*/
577595
async fetchTickers (symbols: string[] = undefined, params = {}): Promise<Tickers> {
578-
throw new NotSupported (this.id + ' fetchTickers() is not supported, use fetchTicker() instead');
596+
await this.loadMarkets ();
597+
let targetSymbols = this.symbols;
598+
if (symbols !== undefined) {
599+
targetSymbols = symbols;
600+
}
601+
const result: Dict = {};
602+
for (let i = 0; i < targetSymbols.length; i++) {
603+
const symbol = this.safeString (targetSymbols, i);
604+
try {
605+
result[symbol] = await this.fetchTicker (symbol, params);
606+
} catch (e) {
607+
// Skip markets whose price feed is unavailable (e.g. BSQ on a fresh daemon
608+
// returns "price feed service has no prices"). Other errors are re-thrown.
609+
if (e instanceof ExchangeNotAvailable) {
610+
continue;
611+
}
612+
throw e;
613+
}
614+
}
615+
return result as Tickers;
579616
}
580617

581618
/**
@@ -695,7 +732,7 @@ export default class bisq extends Exchange {
695732
* @name bisq#fetchBalance
696733
* @description query account balances
697734
* @see https://bisq-network.github.io/slate/#service-wallets
698-
* @note specific behavior: maps `Wallets.GetBalances` into CCXT balances for BTC and BSQ
735+
* @note specific behavior: maps `Wallets.GetBalances` into CCXT balances; iterates over all currency keys returned by the API
699736
* @param {object} [params] extra parameters specific to the exchange API endpoint
700737
* @returns {object} a [balance structure]{@link https://docs.ccxt.com/?id=balance-structure}
701738
*/
@@ -706,21 +743,246 @@ export default class bisq extends Exchange {
706743
const result: Dict = {
707744
'info': response,
708745
};
709-
const btc = this.safeDict (balances, 'btc', {});
710-
const bsq = this.safeDict (balances, 'bsq', {});
711-
const accountBtc = this.account ();
712-
accountBtc['free'] = this.safeString2 (btc, 'available_balance', 'availableBalance');
713-
accountBtc['used'] = this.safeString2 (btc, 'reserved_balance', 'reservedBalance');
714-
accountBtc['total'] = this.safeString2 (btc, 'total_available_balance', 'totalAvailableBalance');
715-
result['BTC'] = accountBtc;
716-
const accountBsq = this.account ();
717-
accountBsq['free'] = this.safeString2 (bsq, 'available_confirmed_balance', 'availableConfirmedBalance');
718-
accountBsq['used'] = this.safeString2 (bsq, 'unverified_balance', 'unverifiedBalance');
719-
accountBsq['total'] = this.safeString2 (bsq, 'total_available_balance', 'totalAvailableBalance');
720-
result['BSQ'] = accountBsq;
746+
const currencyKeys = Object.keys (balances);
747+
for (let i = 0; i < currencyKeys.length; i++) {
748+
const key = currencyKeys[i];
749+
const data = this.safeDict (balances, key, {});
750+
const code = key.toUpperCase ();
751+
const account = this.account ();
752+
const freeRaw = this.safeString2 (data, 'available_balance', 'availableBalance') || this.safeString2 (data, 'available_confirmed_balance', 'availableConfirmedBalance');
753+
const usedRaw = this.safeString2 (data, 'reserved_balance', 'reservedBalance') || this.safeString2 (data, 'unverified_balance', 'unverifiedBalance');
754+
const totalRaw = this.safeString2 (data, 'total_available_balance', 'totalAvailableBalance');
755+
account['free'] = this.parseCurrencyBalance (code, freeRaw);
756+
account['used'] = this.parseCurrencyBalance (code, usedRaw);
757+
account['total'] = this.parseCurrencyBalance (code, totalRaw);
758+
result[code] = account;
759+
}
721760
return this.safeBalance (result);
722761
}
723762

763+
parseTransaction (tx: Dict): Transaction {
764+
const txId = this.safeString2 (tx, 'tx_id', 'txId');
765+
const isPending = this.safeBool2 (tx, 'is_pending', 'isPending', false);
766+
const inputSum = this.satoshisToAmount (this.safeString2 (tx, 'input_sum', 'inputSum'));
767+
const outputSum = this.satoshisToAmount (this.safeString2 (tx, 'output_sum', 'outputSum'));
768+
const fee = this.satoshisToAmount (this.safeString2 (tx, 'fee', 'fee'));
769+
let status = 'ok';
770+
if (isPending) {
771+
status = 'pending';
772+
}
773+
return ({
774+
'id': txId,
775+
'txid': txId,
776+
'info': tx,
777+
'type': undefined,
778+
'currency': 'BTC',
779+
'amount': outputSum,
780+
'fee': { 'currency': 'BTC', 'cost': fee },
781+
'status': status,
782+
'timestamp': undefined,
783+
'datetime': undefined,
784+
'address': undefined,
785+
'addressFrom': undefined,
786+
'addressTo': undefined,
787+
'tag': undefined,
788+
'tagFrom': undefined,
789+
'tagTo': undefined,
790+
'updated': undefined,
791+
'comment': this.safeString (tx, 'memo'),
792+
});
793+
}
794+
795+
/**
796+
* @method
797+
* @name bisq#fetchTransactions
798+
* @description fetch all BTC transactions in the Bisq wallet
799+
* @see https://bisq-network.github.io/slate/#service-wallets
800+
* @param {string} [code] currency code — only 'BTC' is supported
801+
* @param {int} [since] not used by the Bisq API (client-side filter applied when provided)
802+
* @param {int} [limit] not used by the Bisq API (client-side filter applied when provided)
803+
* @param {object} [params] extra parameters specific to the exchange API endpoint
804+
* @returns {object[]} a list of [transaction structures]{@link https://docs.ccxt.com/?id=transaction-structure}
805+
*/
806+
async fetchTransactions (code: Str = undefined, since: Int = undefined, limit: Int = undefined, params = {}): Promise<Transaction[]> {
807+
await this.loadMarkets ();
808+
const response = await this.privatePostWalletsGettransactions (params);
809+
const txInfoList = this.safeList2 (response, 'tx_info', 'txInfo', []);
810+
const transactions = [];
811+
for (let i = 0; i < txInfoList.length; i++) {
812+
transactions.push (this.parseTransaction (txInfoList[i]));
813+
}
814+
return this.filterBySinceLimit (transactions, since, limit);
815+
}
816+
817+
/**
818+
* @method
819+
* @name bisq#fetchDepositAddress
820+
* @description get the first unused BTC funding address in the Bisq wallet
821+
* @see https://bisq-network.github.io/slate/#service-wallets
822+
* @note specific behavior: Bisq has no "create address" RPC; this returns the first unused address from GetFundingAddresses
823+
* @param {string} code currency code — only 'BTC' is supported
824+
* @param {object} [params] extra parameters specific to the exchange API endpoint
825+
* @returns {object} an [address structure]{@link https://docs.ccxt.com/?id=address-structure}
826+
*/
827+
async fetchDepositAddress (code: string, params = {}): Promise<DepositAddress> {
828+
await this.loadMarkets ();
829+
if (code !== 'BTC') {
830+
throw new ExchangeError (this.id + ' fetchDepositAddress() only supports BTC');
831+
}
832+
const response = await this.privatePostWalletsGetfundingaddresses (params);
833+
const addressList = this.safeList2 (response, 'address_balance_info', 'addressBalanceInfo', []);
834+
let unusedAddress = undefined;
835+
for (let i = 0; i < addressList.length; i++) {
836+
const entry = addressList[i];
837+
const isUnused = this.safeBool2 (entry, 'is_address_unused', 'isAddressUnused', false);
838+
if (isUnused) {
839+
unusedAddress = entry;
840+
break;
841+
}
842+
}
843+
if (unusedAddress === undefined) {
844+
throw new ExchangeError (this.id + ' fetchDepositAddress() no unused BTC address available');
845+
}
846+
const address = this.safeString (unusedAddress, 'address');
847+
this.checkAddress (address);
848+
return {
849+
'currency': code,
850+
'address': address,
851+
'tag': undefined,
852+
'network': undefined,
853+
'info': unusedAddress,
854+
};
855+
}
856+
857+
/**
858+
* @method
859+
* @name bisq#fetchPaymentAccounts
860+
* @description fetch all saved fiat payment accounts from the Bisq daemon
861+
* @see https://bisq-network.github.io/slate/#service-paymentaccounts
862+
* @param {object} [params] extra parameters specific to the exchange API endpoint
863+
* @returns {object[]} array of normalised payment account structures
864+
*/
865+
async fetchPaymentAccounts (params = {}): Promise<Dict[]> {
866+
await this.loadMarkets ();
867+
const response = await this.privatePostPaymentaccountsGetpaymentaccounts (params);
868+
const accounts = this.safeList2 (response, 'payment_accounts', 'paymentAccounts', []);
869+
const result = [];
870+
for (let i = 0; i < accounts.length; i++) {
871+
result.push (this.parsePaymentAccount (this.safeDict (accounts, i, {})));
872+
}
873+
return result;
874+
}
875+
876+
/**
877+
* Parse a raw Bisq payment account dict into a normalised structure.
878+
*/
879+
parsePaymentAccount (account: Dict): Dict {
880+
const id = this.safeString2 (account, 'id', 'account_id');
881+
const name = this.safeString (account, 'accountName') || this.safeString (account, 'account_name');
882+
const paymentMethod = this.safeDict2 (account, 'paymentMethod', 'payment_method', {});
883+
const methodId = this.safeString (paymentMethod, 'id');
884+
const tradeCurrencies = this.safeList2 (account, 'tradeCurrencies', 'trade_currencies', []);
885+
const currencyCodes = [];
886+
for (let i = 0; i < tradeCurrencies.length; i++) {
887+
const tc = this.safeDict (tradeCurrencies, i, {});
888+
const code = this.safeString (tc, 'code');
889+
if (code !== undefined) {
890+
currencyCodes.push (code);
891+
}
892+
}
893+
return {
894+
'id': id,
895+
'name': name,
896+
'paymentMethodId': methodId,
897+
'currencies': currencyCodes,
898+
'info': account,
899+
};
900+
}
901+
902+
/**
903+
* @method
904+
* @name bisq#createCryptoCurrencyPaymentAccount
905+
* @description create an altcoin payment account in the Bisq daemon
906+
* @see https://bisq-network.github.io/slate/#service-paymentaccounts
907+
* @param {string} currency uppercase currency code, e.g. 'XMR'
908+
* @param {string} address the altcoin wallet address
909+
* @param {object} [params] extra parameters
910+
* @param {string} [params.accountName] human-readable name (defaults to '<CURRENCY> account')
911+
* @param {boolean} [params.tradeInstant] whether to enable instant trade (default false)
912+
* @returns {object} normalised payment account structure
913+
*/
914+
async createCryptoCurrencyPaymentAccount (currency: string, address: string, params = {}): Promise<Dict> {
915+
await this.loadMarkets ();
916+
const code = currency.toUpperCase ();
917+
const accountName = this.safeString (params, 'accountName') || this.safeString (params, 'account_name') || (code + ' account');
918+
const tradeInstant = this.safeBool2 (params, 'tradeInstant', 'trade_instant', false);
919+
const request = {
920+
'account_name': accountName,
921+
'currency_code': code,
922+
'address': address,
923+
'trade_instant': tradeInstant,
924+
};
925+
const omitKeys = [ 'accountName', 'account_name', 'tradeInstant', 'trade_instant' ];
926+
const response = await this.privatePostPaymentaccountsCreatecryptocurrencypaymentaccount (this.extend (request, this.omit (params, omitKeys)));
927+
const paymentAccount = this.safeDict2 (response, 'payment_account', 'paymentAccount', {});
928+
return this.parsePaymentAccount (paymentAccount);
929+
}
930+
931+
/**
932+
* @method
933+
* @name bisq#createFiatPaymentAccount
934+
* @description create a fiat payment account using a pre-filled Bisq account form JSON
935+
* @see https://bisq-network.github.io/slate/#service-paymentaccounts
936+
* @note specific behavior: obtain the blank form via GetPaymentAccountForm(paymentMethodId), fill the required fields, then pass the JSON string here
937+
* @param {string} paymentMethodId Bisq payment method identifier, e.g. 'SEPA', 'ZELLE'
938+
* @param {string} formJson filled payment account form as a JSON string
939+
* @param {object} [params] extra parameters specific to the exchange API endpoint
940+
* @returns {object} normalised payment account structure
941+
*/
942+
async createFiatPaymentAccount (paymentMethodId: string, formJson: string, params = {}): Promise<Dict> {
943+
await this.loadMarkets ();
944+
const request = {
945+
'payment_account_form': formJson,
946+
};
947+
const response = await this.privatePostPaymentaccountsCreatepaymentaccount (this.extend (request, params));
948+
const paymentAccount = this.safeDict2 (response, 'payment_account', 'paymentAccount', {});
949+
return this.parsePaymentAccount (paymentAccount);
950+
}
951+
952+
/**
953+
* @method
954+
* @name bisq#createPaymentAccount
955+
* @description create a new payment account (altcoin or fiat) in the Bisq daemon
956+
* @see https://bisq-network.github.io/slate/#service-paymentaccounts
957+
* @note specific behavior: pass params.address for altcoin/crypto accounts; pass params.paymentMethodId + params.formJson for fiat accounts
958+
* @param {string} currency currency code, e.g. 'XMR' for altcoin or 'EUR' for fiat
959+
* @param {object} [params] extra parameters
960+
* @param {string} [params.address] altcoin wallet address (required for crypto accounts)
961+
* @param {string} [params.accountName] human-readable name (crypto accounts only, defaults to '<CURRENCY> account')
962+
* @param {boolean} [params.tradeInstant] enable instant trade (crypto accounts only, default false)
963+
* @param {string} [params.paymentMethodId] Bisq payment method id (required for fiat accounts, e.g. 'SEPA')
964+
* @param {string} [params.formJson] filled payment account form JSON string (required for fiat accounts)
965+
* @returns {object} normalised payment account structure
966+
*/
967+
async createPaymentAccount (currency: string, params = {}): Promise<Dict> {
968+
await this.loadMarkets ();
969+
const address = this.safeString (params, 'address') || this.safeString (params, 'walletAddress');
970+
const paymentMethodId = this.safeString (params, 'paymentMethodId') || this.safeString (params, 'payment_method_id');
971+
if (address !== undefined) {
972+
const omitKeys = [ 'address', 'walletAddress' ];
973+
return await this.createCryptoCurrencyPaymentAccount (currency, address, this.omit (params, omitKeys));
974+
}
975+
if (paymentMethodId !== undefined) {
976+
const formJson = this.safeString (params, 'formJson') || this.safeString (params, 'form_json');
977+
if (formJson === undefined) {
978+
throw new ExchangeError (this.id + ' createPaymentAccount() requires params.formJson for fiat accounts');
979+
}
980+
const omitKeys = [ 'paymentMethodId', 'payment_method_id', 'formJson', 'form_json' ];
981+
return await this.createFiatPaymentAccount (paymentMethodId, formJson, this.omit (params, omitKeys));
982+
}
983+
throw new ExchangeError (this.id + ' createPaymentAccount() requires params.address (crypto) or params.paymentMethodId + params.formJson (fiat)');
984+
}
985+
724986
/**
725987
* @method
726988
* @name bisq#createOrder

0 commit comments

Comments
 (0)