ccxt 4.5.82 → 4.5.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/ccxt.browser.min.js +2 -2
- package/dist/cjs/ccxt.js +3 -1
- package/dist/cjs/src/abstract/prediction/sxbet.js +11 -0
- package/dist/cjs/src/apex.js +1 -0
- package/dist/cjs/src/aster.js +2 -2
- package/dist/cjs/src/base/Exchange.js +1 -1
- package/dist/cjs/src/bigone.js +2 -2
- package/dist/cjs/src/binance.js +10 -10
- package/dist/cjs/src/bingx.js +3 -3
- package/dist/cjs/src/bit2c.js +4 -4
- package/dist/cjs/src/bitbank.js +15 -15
- package/dist/cjs/src/bitfinex.js +15 -15
- package/dist/cjs/src/bitget.js +109 -73
- package/dist/cjs/src/bitmex.js +7 -7
- package/dist/cjs/src/bitopro.js +2 -2
- package/dist/cjs/src/bitrue.js +6 -6
- package/dist/cjs/src/bitso.js +18 -18
- package/dist/cjs/src/bitstamp.js +5 -5
- package/dist/cjs/src/bitteam.js +32 -32
- package/dist/cjs/src/bittrade.js +10 -10
- package/dist/cjs/src/bitvavo.js +9 -9
- package/dist/cjs/src/blockchaincom.js +2 -2
- package/dist/cjs/src/btcbox.js +2 -2
- package/dist/cjs/src/btcmarkets.js +2 -2
- package/dist/cjs/src/btcturk.js +2 -2
- package/dist/cjs/src/btse.js +2 -2
- package/dist/cjs/src/bybitid.js +4 -0
- package/dist/cjs/src/coinbase.js +1 -1
- package/dist/cjs/src/coinbaseexchange.js +13 -13
- package/dist/cjs/src/coincheck.js +4 -4
- package/dist/cjs/src/coinex.js +10 -10
- package/dist/cjs/src/coinmate.js +4 -4
- package/dist/cjs/src/coinone.js +4 -4
- package/dist/cjs/src/coinsph.js +5 -5
- package/dist/cjs/src/cryptocom.js +5 -5
- package/dist/cjs/src/deribit.js +31 -31
- package/dist/cjs/src/derive.js +1 -1
- package/dist/cjs/src/digifinex.js +31 -31
- package/dist/cjs/src/gate.js +25 -25
- package/dist/cjs/src/gemini.js +5 -5
- package/dist/cjs/src/grvt.js +1 -1
- package/dist/cjs/src/hibachi.js +5 -5
- package/dist/cjs/src/hitbtc.js +15 -15
- package/dist/cjs/src/hollaex.js +15 -15
- package/dist/cjs/src/htx.js +52 -52
- package/dist/cjs/src/hyperliquid.js +2 -2
- package/dist/cjs/src/independentreserve.js +2 -2
- package/dist/cjs/src/indodax.js +8 -8
- package/dist/cjs/src/kraken.js +29 -29
- package/dist/cjs/src/krakenfutures.js +16 -16
- package/dist/cjs/src/kucoin.js +15 -15
- package/dist/cjs/src/latoken.js +8 -8
- package/dist/cjs/src/lbank.js +31 -31
- package/dist/cjs/src/lighter.js +2 -2
- package/dist/cjs/src/luno.js +2 -2
- package/dist/cjs/src/mercado.js +12 -12
- package/dist/cjs/src/mexc.js +27 -27
- package/dist/cjs/src/modetrade.js +10 -10
- package/dist/cjs/src/ndax.js +4 -4
- package/dist/cjs/src/okx.js +19 -20
- package/dist/cjs/src/onetrading.js +7 -7
- package/dist/cjs/src/p2b.js +8 -8
- package/dist/cjs/src/pacifica.js +9 -3
- package/dist/cjs/src/paymium.js +2 -2
- package/dist/cjs/src/phemex.js +37 -37
- package/dist/cjs/src/poloniex.js +14 -14
- package/dist/cjs/src/prediction/sxbet.js +2710 -0
- package/dist/cjs/src/pro/alpaca.js +5 -5
- package/dist/cjs/src/pro/apex.js +6 -6
- package/dist/cjs/src/pro/aster.js +8 -8
- package/dist/cjs/src/pro/backpack.js +1 -1
- package/dist/cjs/src/pro/binance.js +26 -26
- package/dist/cjs/src/pro/bingx.js +11 -11
- package/dist/cjs/src/pro/bitfinex.js +11 -11
- package/dist/cjs/src/pro/bitget.js +20 -20
- package/dist/cjs/src/pro/bithumb.js +1 -1
- package/dist/cjs/src/pro/bitmex.js +12 -12
- package/dist/cjs/src/pro/bitopro.js +5 -5
- package/dist/cjs/src/pro/bitrue.js +7 -7
- package/dist/cjs/src/pro/bitstamp.js +5 -5
- package/dist/cjs/src/pro/bittrade.js +11 -11
- package/dist/cjs/src/pro/bitvavo.js +16 -16
- package/dist/cjs/src/pro/blockchaincom.js +5 -5
- package/dist/cjs/src/pro/blofin.js +2 -2
- package/dist/cjs/src/pro/bybit.js +21 -21
- package/dist/cjs/src/pro/bydfi.js +2 -2
- package/dist/cjs/src/pro/cex.js +19 -19
- package/dist/cjs/src/pro/coinbase.js +2 -2
- package/dist/cjs/src/pro/coinbaseexchange.js +9 -9
- package/dist/cjs/src/pro/coinbaseinternational.js +2 -2
- package/dist/cjs/src/pro/coincheck.js +2 -2
- package/dist/cjs/src/pro/coinex.js +3 -3
- package/dist/cjs/src/pro/coinone.js +6 -6
- package/dist/cjs/src/pro/cryptocom.js +10 -10
- package/dist/cjs/src/pro/deribit.js +13 -13
- package/dist/cjs/src/pro/derive.js +4 -4
- package/dist/cjs/src/pro/dydx.js +1 -1
- package/dist/cjs/src/pro/extended.js +16 -10
- package/dist/cjs/src/pro/gate.js +10 -10
- package/dist/cjs/src/pro/gemini.js +3 -3
- package/dist/cjs/src/pro/grvt.js +2 -2
- package/dist/cjs/src/pro/hashkey.js +2 -2
- package/dist/cjs/src/pro/hitbtc.js +6 -6
- package/dist/cjs/src/pro/hollaex.js +3 -3
- package/dist/cjs/src/pro/htx.js +28 -28
- package/dist/cjs/src/pro/hyperliquid.js +1 -1
- package/dist/cjs/src/pro/independentreserve.js +2 -2
- package/dist/cjs/src/pro/kraken.js +7 -7
- package/dist/cjs/src/pro/krakenfutures.js +12 -12
- package/dist/cjs/src/pro/kucoin.js +12 -12
- package/dist/cjs/src/pro/lbank.js +16 -16
- package/dist/cjs/src/pro/luno.js +2 -2
- package/dist/cjs/src/pro/mexc.js +8 -8
- package/dist/cjs/src/pro/modetrade.js +3 -3
- package/dist/cjs/src/pro/mudrex.js +2 -2
- package/dist/cjs/src/pro/nado.js +3 -3
- package/dist/cjs/src/pro/ndax.js +6 -6
- package/dist/cjs/src/pro/okx.js +20 -20
- package/dist/cjs/src/pro/onetrading.js +14 -14
- package/dist/cjs/src/pro/p2b.js +5 -5
- package/dist/cjs/src/pro/pacifica.js +1 -1
- package/dist/cjs/src/pro/paradex.js +1 -1
- package/dist/cjs/src/pro/phemex.js +13 -13
- package/dist/cjs/src/pro/poloniex.js +17 -17
- package/dist/cjs/src/pro/toobit.js +2 -2
- package/dist/cjs/src/pro/upbit.js +4 -4
- package/dist/cjs/src/pro/weex.js +2 -2
- package/dist/cjs/src/pro/whitebit.js +12 -12
- package/dist/cjs/src/pro/woo.js +13 -13
- package/dist/cjs/src/pro/woofipro.js +3 -3
- package/dist/cjs/src/pro/xt.js +3 -3
- package/dist/cjs/src/tokocrypto.js +20 -20
- package/dist/cjs/src/upbit.js +12 -12
- package/dist/cjs/src/whitebit.js +15 -15
- package/dist/cjs/src/woo.js +3 -3
- package/dist/cjs/src/woofipro.js +14 -14
- package/dist/cjs/src/zaif.js +9 -3
- package/dist/cjs/src/zebpay.js +1 -1
- package/js/ccxt.d.ts +4 -1
- package/js/ccxt.js +3 -1
- package/js/src/abstract/prediction/sxbet.d.ts +41 -0
- package/js/src/abstract/prediction/sxbet.js +11 -0
- package/js/src/alpaca.d.ts +28 -33
- package/js/src/apex.d.ts +28 -42
- package/js/src/apex.js +1 -0
- package/js/src/aster.d.ts +58 -108
- package/js/src/aster.js +2 -2
- package/js/src/backpack.d.ts +34 -48
- package/js/src/base/Exchange.d.ts +304 -320
- package/js/src/base/Exchange.js +1 -1
- package/js/src/bigone.d.ts +24 -29
- package/js/src/bigone.js +2 -2
- package/js/src/binance.d.ts +112 -138
- package/js/src/binance.js +10 -10
- package/js/src/binancecoinm.d.ts +3 -2
- package/js/src/binanceusdm.d.ts +3 -2
- package/js/src/bingx.d.ts +65 -84
- package/js/src/bingx.js +3 -3
- package/js/src/bit2c.d.ts +13 -18
- package/js/src/bit2c.js +4 -4
- package/js/src/bitbank.d.ts +16 -21
- package/js/src/bitbank.js +15 -15
- package/js/src/bitbns.d.ts +15 -20
- package/js/src/bitfinex.d.ts +46 -51
- package/js/src/bitfinex.js +15 -15
- package/js/src/bitflyer.d.ts +20 -25
- package/js/src/bitget.d.ts +105 -93
- package/js/src/bitget.js +109 -73
- package/js/src/bithumb.d.ts +26 -31
- package/js/src/bitmex.d.ts +48 -65
- package/js/src/bitmex.js +7 -7
- package/js/src/bitopro.d.ts +25 -41
- package/js/src/bitopro.js +2 -2
- package/js/src/bitrue.d.ts +30 -45
- package/js/src/bitrue.js +6 -6
- package/js/src/bitso.d.ts +24 -29
- package/js/src/bitso.js +18 -18
- package/js/src/bitstamp.d.ts +36 -47
- package/js/src/bitstamp.js +5 -5
- package/js/src/bitteam.d.ts +22 -27
- package/js/src/bitteam.js +32 -32
- package/js/src/bittrade.d.ts +35 -46
- package/js/src/bittrade.js +10 -10
- package/js/src/bitvavo.d.ts +43 -48
- package/js/src/bitvavo.js +9 -9
- package/js/src/blockchaincom.d.ts +26 -31
- package/js/src/blockchaincom.js +2 -2
- package/js/src/blofin.d.ts +38 -43
- package/js/src/btcbox.d.ts +14 -19
- package/js/src/btcbox.js +2 -2
- package/js/src/btcmarkets.d.ts +25 -30
- package/js/src/btcmarkets.js +2 -2
- package/js/src/btcturk.d.ts +13 -18
- package/js/src/btcturk.js +2 -2
- package/js/src/btse.d.ts +44 -50
- package/js/src/btse.js +2 -2
- package/js/src/bullish.d.ts +35 -57
- package/js/src/bybit.d.ts +95 -106
- package/js/src/bybitid.js +4 -0
- package/js/src/bydfi.d.ts +37 -48
- package/js/src/cex.d.ts +29 -34
- package/js/src/coinbase.d.ts +66 -82
- package/js/src/coinbase.js +1 -1
- package/js/src/coinbaseexchange.d.ts +33 -43
- package/js/src/coinbaseexchange.js +13 -13
- package/js/src/coinbaseinternational.d.ts +34 -73
- package/js/src/coincheck.d.ts +13 -18
- package/js/src/coincheck.js +4 -4
- package/js/src/coinex.d.ts +63 -68
- package/js/src/coinex.js +10 -10
- package/js/src/coinmate.d.ts +15 -20
- package/js/src/coinmate.js +4 -4
- package/js/src/coinone.d.ts +14 -19
- package/js/src/coinone.js +4 -4
- package/js/src/coinsph.d.ts +32 -37
- package/js/src/coinsph.js +5 -5
- package/js/src/coinspot.d.ts +10 -15
- package/js/src/cryptocom.d.ts +48 -64
- package/js/src/cryptocom.js +5 -5
- package/js/src/cryptomus.d.ts +15 -23
- package/js/src/deepcoin.d.ts +43 -48
- package/js/src/delta.d.ts +43 -54
- package/js/src/deribit.d.ts +48 -69
- package/js/src/deribit.js +31 -31
- package/js/src/derive.d.ts +28 -42
- package/js/src/derive.js +1 -1
- package/js/src/digifinex.d.ts +54 -67
- package/js/src/digifinex.js +31 -31
- package/js/src/dydx.d.ts +26 -31
- package/js/src/extended.d.ts +51 -56
- package/js/src/foxbit.d.ts +34 -64
- package/js/src/gate.d.ts +94 -162
- package/js/src/gate.js +25 -25
- package/js/src/gemini.d.ts +33 -43
- package/js/src/gemini.js +5 -5
- package/js/src/grvt.d.ts +36 -47
- package/js/src/grvt.js +1 -1
- package/js/src/hashkey.d.ts +58 -78
- package/js/src/hibachi.d.ts +42 -52
- package/js/src/hibachi.js +5 -5
- package/js/src/hitbtc.d.ts +49 -56
- package/js/src/hitbtc.js +15 -15
- package/js/src/hollaex.d.ts +26 -31
- package/js/src/hollaex.js +15 -15
- package/js/src/htx.d.ts +80 -121
- package/js/src/htx.js +52 -52
- package/js/src/hyperliquid.d.ts +77 -127
- package/js/src/hyperliquid.js +2 -2
- package/js/src/independentreserve.d.ts +15 -20
- package/js/src/independentreserve.js +2 -2
- package/js/src/indodax.d.ts +19 -24
- package/js/src/indodax.js +8 -8
- package/js/src/kraken.d.ts +43 -55
- package/js/src/kraken.js +29 -29
- package/js/src/krakenfutures.d.ts +34 -39
- package/js/src/krakenfutures.js +16 -16
- package/js/src/kucoin.d.ts +121 -132
- package/js/src/kucoin.js +15 -15
- package/js/src/kucoinfutures.d.ts +4 -4
- package/js/src/latoken.d.ts +24 -29
- package/js/src/latoken.js +8 -8
- package/js/src/lbank.d.ts +40 -53
- package/js/src/lbank.js +31 -31
- package/js/src/lighter.d.ts +40 -45
- package/js/src/lighter.js +2 -2
- package/js/src/luno.d.ts +25 -30
- package/js/src/luno.js +2 -2
- package/js/src/mercado.d.ts +15 -20
- package/js/src/mercado.js +12 -12
- package/js/src/mexc.d.ts +70 -83
- package/js/src/mexc.js +27 -27
- package/js/src/modetrade.d.ts +43 -57
- package/js/src/modetrade.js +10 -10
- package/js/src/mudrex.d.ts +25 -35
- package/js/src/nado.d.ts +45 -50
- package/js/src/ndax.d.ts +27 -32
- package/js/src/ndax.js +4 -4
- package/js/src/okx.d.ts +97 -108
- package/js/src/okx.js +19 -20
- package/js/src/onetrading.d.ts +23 -31
- package/js/src/onetrading.js +7 -7
- package/js/src/p2b.d.ts +17 -22
- package/js/src/p2b.js +8 -8
- package/js/src/pacifica.d.ts +55 -73
- package/js/src/pacifica.js +9 -3
- package/js/src/paradex.d.ts +47 -68
- package/js/src/paymium.d.ts +12 -17
- package/js/src/paymium.js +2 -2
- package/js/src/phemex.d.ts +47 -52
- package/js/src/phemex.js +37 -37
- package/js/src/poloniex.d.ts +45 -50
- package/js/src/poloniex.js +14 -14
- package/js/src/prediction/sxbet.d.ts +595 -0
- package/js/src/prediction/sxbet.js +2708 -0
- package/js/src/pro/alpaca.d.ts +23 -23
- package/js/src/pro/alpaca.js +5 -5
- package/js/src/pro/apex.d.ts +28 -28
- package/js/src/pro/apex.js +6 -6
- package/js/src/pro/aster.d.ts +33 -33
- package/js/src/pro/aster.js +8 -8
- package/js/src/pro/backpack.d.ts +28 -28
- package/js/src/pro/backpack.js +1 -1
- package/js/src/pro/binance.d.ts +57 -57
- package/js/src/pro/binance.js +26 -26
- package/js/src/pro/bingx.d.ts +26 -26
- package/js/src/pro/bingx.js +11 -11
- package/js/src/pro/bitfinex.d.ts +25 -25
- package/js/src/pro/bitfinex.js +11 -11
- package/js/src/pro/bitget.d.ts +41 -41
- package/js/src/pro/bitget.js +20 -20
- package/js/src/pro/bithumb.d.ts +15 -15
- package/js/src/pro/bithumb.js +1 -1
- package/js/src/pro/bitmex.d.ts +29 -29
- package/js/src/pro/bitmex.js +12 -12
- package/js/src/pro/bitopro.d.ts +13 -13
- package/js/src/pro/bitopro.js +5 -5
- package/js/src/pro/bitrue.d.ts +25 -25
- package/js/src/pro/bitrue.js +7 -7
- package/js/src/pro/bitstamp.d.ts +21 -21
- package/js/src/pro/bitstamp.js +5 -5
- package/js/src/pro/bittrade.d.ts +19 -19
- package/js/src/pro/bittrade.js +11 -11
- package/js/src/pro/bitvavo.d.ts +62 -62
- package/js/src/pro/bitvavo.js +16 -16
- package/js/src/pro/blockchaincom.d.ts +19 -19
- package/js/src/pro/blockchaincom.js +5 -5
- package/js/src/pro/blofin.d.ts +23 -23
- package/js/src/pro/blofin.js +2 -2
- package/js/src/pro/bullish.d.ts +19 -25
- package/js/src/pro/bybit.d.ts +42 -45
- package/js/src/pro/bybit.js +21 -21
- package/js/src/pro/bydfi.d.ts +24 -24
- package/js/src/pro/bydfi.js +2 -2
- package/js/src/pro/cex.d.ts +37 -37
- package/js/src/pro/cex.js +19 -19
- package/js/src/pro/coinbase.d.ts +19 -19
- package/js/src/pro/coinbase.js +2 -2
- package/js/src/pro/coinbaseexchange.d.ts +25 -25
- package/js/src/pro/coinbaseexchange.js +9 -9
- package/js/src/pro/coinbaseinternational.d.ts +20 -20
- package/js/src/pro/coinbaseinternational.js +2 -2
- package/js/src/pro/coincheck.d.ts +5 -5
- package/js/src/pro/coincheck.js +2 -2
- package/js/src/pro/coinex.d.ts +24 -24
- package/js/src/pro/coinex.js +3 -3
- package/js/src/pro/coinone.d.ts +9 -11
- package/js/src/pro/coinone.js +6 -6
- package/js/src/pro/cryptocom.d.ts +40 -40
- package/js/src/pro/cryptocom.js +10 -10
- package/js/src/pro/deepcoin.d.ts +24 -32
- package/js/src/pro/deribit.d.ts +24 -24
- package/js/src/pro/deribit.js +13 -13
- package/js/src/pro/derive.d.ts +22 -22
- package/js/src/pro/derive.js +4 -4
- package/js/src/pro/dydx.d.ts +9 -9
- package/js/src/pro/dydx.js +1 -1
- package/js/src/pro/extended.d.ts +22 -22
- package/js/src/pro/extended.js +16 -10
- package/js/src/pro/gate.d.ts +50 -50
- package/js/src/pro/gate.js +10 -10
- package/js/src/pro/gemini.d.ts +26 -26
- package/js/src/pro/gemini.js +3 -3
- package/js/src/pro/grvt.d.ts +22 -22
- package/js/src/pro/grvt.js +2 -2
- package/js/src/pro/hashkey.d.ts +19 -19
- package/js/src/pro/hashkey.js +2 -2
- package/js/src/pro/hitbtc.d.ts +32 -32
- package/js/src/pro/hitbtc.js +6 -6
- package/js/src/pro/hollaex.d.ts +15 -17
- package/js/src/pro/hollaex.js +3 -3
- package/js/src/pro/htx.d.ts +36 -36
- package/js/src/pro/htx.js +28 -28
- package/js/src/pro/hyperliquid.d.ts +29 -31
- package/js/src/pro/hyperliquid.js +1 -1
- package/js/src/pro/independentreserve.d.ts +10 -10
- package/js/src/pro/independentreserve.js +2 -2
- package/js/src/pro/kraken.d.ts +41 -55
- package/js/src/pro/kraken.js +7 -7
- package/js/src/pro/krakenfutures.d.ts +32 -32
- package/js/src/pro/krakenfutures.js +12 -12
- package/js/src/pro/kucoin.d.ts +69 -72
- package/js/src/pro/kucoin.js +12 -12
- package/js/src/pro/kucoinfutures.d.ts +4 -4
- package/js/src/pro/lbank.d.ts +22 -22
- package/js/src/pro/lbank.js +16 -16
- package/js/src/pro/lighter.d.ts +27 -27
- package/js/src/pro/luno.d.ts +8 -8
- package/js/src/pro/luno.js +2 -2
- package/js/src/pro/mexc.d.ts +33 -33
- package/js/src/pro/mexc.js +8 -8
- package/js/src/pro/modetrade.d.ts +37 -39
- package/js/src/pro/modetrade.js +3 -3
- package/js/src/pro/mudrex.d.ts +8 -8
- package/js/src/pro/mudrex.js +2 -2
- package/js/src/pro/nado.d.ts +42 -46
- package/js/src/pro/nado.js +3 -3
- package/js/src/pro/ndax.d.ts +12 -12
- package/js/src/pro/ndax.js +6 -6
- package/js/src/pro/okx.d.ts +49 -49
- package/js/src/pro/okx.js +20 -20
- package/js/src/pro/onetrading.d.ts +28 -28
- package/js/src/pro/onetrading.js +14 -14
- package/js/src/pro/p2b.d.ts +15 -19
- package/js/src/pro/p2b.js +5 -5
- package/js/src/pro/pacifica.d.ts +22 -22
- package/js/src/pro/pacifica.js +1 -1
- package/js/src/pro/paradex.d.ts +18 -18
- package/js/src/pro/paradex.js +1 -1
- package/js/src/pro/phemex.d.ts +25 -25
- package/js/src/pro/phemex.js +13 -13
- package/js/src/pro/poloniex.d.ts +30 -32
- package/js/src/pro/poloniex.js +17 -17
- package/js/src/pro/toobit.d.ts +31 -31
- package/js/src/pro/toobit.js +2 -2
- package/js/src/pro/upbit.d.ts +24 -24
- package/js/src/pro/upbit.js +4 -4
- package/js/src/pro/weex.d.ts +39 -39
- package/js/src/pro/weex.js +2 -2
- package/js/src/pro/whitebit.d.ts +26 -30
- package/js/src/pro/whitebit.js +12 -12
- package/js/src/pro/woo.d.ts +44 -46
- package/js/src/pro/woo.js +13 -13
- package/js/src/pro/woofipro.d.ts +39 -41
- package/js/src/pro/woofipro.js +3 -3
- package/js/src/pro/xt.d.ts +20 -20
- package/js/src/pro/xt.js +3 -3
- package/js/src/revolutx.d.ts +17 -17
- package/js/src/tokocrypto.d.ts +25 -30
- package/js/src/tokocrypto.js +20 -20
- package/js/src/toobit.d.ts +45 -79
- package/js/src/upbit.d.ts +28 -61
- package/js/src/upbit.js +12 -12
- package/js/src/weex.d.ts +57 -68
- package/js/src/whitebit.d.ts +53 -64
- package/js/src/whitebit.js +15 -15
- package/js/src/woo.d.ts +61 -75
- package/js/src/woo.js +3 -3
- package/js/src/woofipro.d.ts +53 -67
- package/js/src/woofipro.js +14 -14
- package/js/src/xt.d.ts +57 -67
- package/js/src/zaif.d.ts +11 -16
- package/js/src/zaif.js +9 -3
- package/js/src/zebpay.d.ts +32 -37
- package/js/src/zebpay.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,2708 @@
|
|
|
1
|
+
// ----------------------------------------------------------------------------
|
|
2
|
+
|
|
3
|
+
// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
|
4
|
+
// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
|
5
|
+
// EDIT THE CORRESPONDENT .ts FILE INSTEAD
|
|
6
|
+
|
|
7
|
+
import { keccak_256 as keccak } from '@noble/hashes/sha3.js';
|
|
8
|
+
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
9
|
+
import Exchange from '../abstract/prediction/sxbet.js';
|
|
10
|
+
import { ecdsa } from '../base/functions/crypto.js';
|
|
11
|
+
import { ROUND, DECIMAL_PLACES, TICK_SIZE } from '../base/functions/number.js';
|
|
12
|
+
import { Precise } from '../base/Precise.js';
|
|
13
|
+
import { ArrayCache, ArrayCacheByOutcomeById } from '../base/ws/Cache.js';
|
|
14
|
+
import { AccountNotEnabled, ArgumentsRequired, AuthenticationError, BadRequest, BadSymbol, DuplicateOrderId, ExchangeError, InsufficientFunds, InvalidOrder, MarketClosed, NotSupported, OrderNotFillable, OrderNotFound, PermissionDenied } from '../base/errors.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* @class sxbet
|
|
18
|
+
* @augments Exchange
|
|
19
|
+
*/
|
|
20
|
+
export default class sxbet extends Exchange {
|
|
21
|
+
describe() {
|
|
22
|
+
return this.deepExtend(super.describe(), {
|
|
23
|
+
'id': 'sxbet',
|
|
24
|
+
'name': 'SX Bet',
|
|
25
|
+
'countries': [],
|
|
26
|
+
'rateLimit': 120, // 500 req/min baseline; /orders is 5500/min, /trades is 200/min
|
|
27
|
+
'certified': false,
|
|
28
|
+
'pro': true,
|
|
29
|
+
'has': {
|
|
30
|
+
'CORS': undefined,
|
|
31
|
+
'spot': false,
|
|
32
|
+
'margin': false,
|
|
33
|
+
'swap': false,
|
|
34
|
+
'future': false,
|
|
35
|
+
'option': false,
|
|
36
|
+
'approve': true,
|
|
37
|
+
'cancelAllOrders': true,
|
|
38
|
+
'cancelOrder': true,
|
|
39
|
+
'cancelOrders': true,
|
|
40
|
+
'createOrder': true,
|
|
41
|
+
'fetchBalance': true,
|
|
42
|
+
'fetchClosedOrders': false,
|
|
43
|
+
'fetchEvent': true,
|
|
44
|
+
'fetchEvents': true,
|
|
45
|
+
'fetchMarkets': true,
|
|
46
|
+
'fetchMyTrades': true,
|
|
47
|
+
'fetchOHLCV': false,
|
|
48
|
+
'fetchOpenOrders': true,
|
|
49
|
+
'fetchOrder': true,
|
|
50
|
+
'fetchOrderBook': true,
|
|
51
|
+
'fetchOrders': true,
|
|
52
|
+
'fetchPositions': true,
|
|
53
|
+
'fetchSettlements': true,
|
|
54
|
+
'fetchTicker': true,
|
|
55
|
+
'fetchTickers': true,
|
|
56
|
+
'fetchTrades': true,
|
|
57
|
+
'prediction': true,
|
|
58
|
+
'watchMyTrades': true,
|
|
59
|
+
'watchOrderBook': true,
|
|
60
|
+
'watchOrders': true,
|
|
61
|
+
'watchTicker': true,
|
|
62
|
+
'watchTrades': true,
|
|
63
|
+
},
|
|
64
|
+
'urls': {
|
|
65
|
+
'logo': 'https://github.com/user-attachments/assets/6d1d1e37-2b9c-4011-93c6-c9c25cd2c2f7',
|
|
66
|
+
'api': {
|
|
67
|
+
'sxbet': 'https://api.sx.bet',
|
|
68
|
+
'ws': 'wss://realtime.sx.bet/connection/websocket',
|
|
69
|
+
},
|
|
70
|
+
'test': {
|
|
71
|
+
'sxbet': 'https://api.toronto.sx.bet',
|
|
72
|
+
'ws': 'wss://realtime.toronto.sx.bet/connection/websocket',
|
|
73
|
+
},
|
|
74
|
+
'www': 'https://sx.bet',
|
|
75
|
+
'doc': ['https://docs.sx.bet'],
|
|
76
|
+
},
|
|
77
|
+
'api': {
|
|
78
|
+
'sxbet': {
|
|
79
|
+
'public': {
|
|
80
|
+
'get': {
|
|
81
|
+
'metadata/obv3': 1,
|
|
82
|
+
'orderbook-v3/snapshot': 1,
|
|
83
|
+
'trades-v3/public': 1,
|
|
84
|
+
'markets/active': 1,
|
|
85
|
+
'markets/find': 1,
|
|
86
|
+
'markets/popular': 1,
|
|
87
|
+
'trades/consolidated': 1,
|
|
88
|
+
'trades/orders': 1,
|
|
89
|
+
'trades/portfolio/refunds': 1,
|
|
90
|
+
'fixture/active': 1,
|
|
91
|
+
'fixture/status': 1,
|
|
92
|
+
'sports': 1,
|
|
93
|
+
'leagues': 1,
|
|
94
|
+
'leagues/active': 1,
|
|
95
|
+
'teams': 1,
|
|
96
|
+
'live-scores': 1,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
'private': {
|
|
100
|
+
'get': {
|
|
101
|
+
'user/realtime-token-v3/api-key': 1,
|
|
102
|
+
'user/proxy': 1,
|
|
103
|
+
'user/balance-v3': 1,
|
|
104
|
+
'user/transfer-to-proxy/pending': 1,
|
|
105
|
+
'user/transfer-to-proxy/status': 1,
|
|
106
|
+
'orders-v3': 1,
|
|
107
|
+
'orders-v3/{orderId}': 1,
|
|
108
|
+
'orders-v3/odds/best': 1,
|
|
109
|
+
'trades-v3': 1,
|
|
110
|
+
'fills-v3': 1,
|
|
111
|
+
'positions-v3': 1,
|
|
112
|
+
},
|
|
113
|
+
'delete': {
|
|
114
|
+
'orders-v3': 1,
|
|
115
|
+
'orders-v3/event': 1,
|
|
116
|
+
'orders-v3/all': 1,
|
|
117
|
+
},
|
|
118
|
+
'post': {
|
|
119
|
+
'orders-v3': 1,
|
|
120
|
+
'user/deploy-proxy': 1,
|
|
121
|
+
'user/transfer-to-proxy': 1,
|
|
122
|
+
'heartbeat/v3': 1,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
'requiredCredentials': {
|
|
128
|
+
// apiKey is the x-sx-api-key header - every private v3 route authenticates with it;
|
|
129
|
+
// trading additionally needs walletAddress (order 'maker' field) and privateKey
|
|
130
|
+
'apiKey': true,
|
|
131
|
+
'secret': false,
|
|
132
|
+
'walletAddress': true,
|
|
133
|
+
'privateKey': true,
|
|
134
|
+
},
|
|
135
|
+
'fees': {
|
|
136
|
+
'trading': {
|
|
137
|
+
'tierBased': false,
|
|
138
|
+
'percentage': true,
|
|
139
|
+
// per-account fee rates live behind GET /user/fees-v3 and default to zero;
|
|
140
|
+
// there is no public venue-wide maker/taker schedule to mirror here
|
|
141
|
+
'maker': 0.0,
|
|
142
|
+
'taker': 0.0,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
'exceptions': {
|
|
146
|
+
// sx.bet errors come as { "error": "Bad Request", "message": <CODE or [strings]>, "statusCode": 4xx };
|
|
147
|
+
// per-order statuses inside a 200 createOrder response reuse the same codes
|
|
148
|
+
'exact': {
|
|
149
|
+
'INSUFFICIENT_BALANCE': InsufficientFunds, // maker token balance too low
|
|
150
|
+
'INVALID_MARKET': BadRequest, // non-unique marketHashes in one request
|
|
151
|
+
'ORDERS_ALREADY_EXIST': DuplicateOrderId,
|
|
152
|
+
'TOO_MANY_DIFFERENT_MARKETS': BadRequest, // more than 3 markets in a single request
|
|
153
|
+
'ORDERS_MUST_HAVE_IDENTICAL_MARKET': BadRequest,
|
|
154
|
+
'BAD_BASE_TOKEN': BadRequest, // all orders must share one base token
|
|
155
|
+
'INSUFFICIENT_KYC': PermissionDenied,
|
|
156
|
+
'PROXY_NOT_DEPLOYED': AccountNotEnabled, // live-verified v3: the account's obv3 proxy wallet is not deployed yet - call approve() (deploys and funds it)
|
|
157
|
+
'INVALID_USER': AuthenticationError, // live-verified v3: the x-sx-api-key does not belong to a registered user on the target network
|
|
158
|
+
'BAD_AUTH': AuthenticationError, // live-verified v3: missing or malformed x-sx-api-key
|
|
159
|
+
'ERC20_PERMIT_BAD_SPENDER': BadRequest, // live-verified v3: transfer-to-proxy permit signed for the wrong executor
|
|
160
|
+
'BASE_TOKENS_NOT_SAME': BadRequest,
|
|
161
|
+
'MARKETS_NOT_SAME': BadRequest,
|
|
162
|
+
'DIRECTIONS_NOT_SAME': BadRequest,
|
|
163
|
+
'INVALID_ORDERS': OrderNotFillable, // a targeted order is no longer active
|
|
164
|
+
'INVALID_ODDS': InvalidOrder, // desiredOdds must be below 10^20
|
|
165
|
+
'PERCENTAGEODDS_UNDEFINED_OR_MALFORMED': InvalidOrder, // live-verified: maker percentageOdds above the 10^20 cap (price > 1) or malformed
|
|
166
|
+
'TOTAL_BET_SIZE_TOO_LOW': InvalidOrder, // live-verified: stake below the venue's minimum bet size
|
|
167
|
+
'MATCH_STATE_INVALID': MarketClosed, // fixture not available for betting
|
|
168
|
+
'PROXY_ACCOUNT_INVALID': BadRequest,
|
|
169
|
+
'RATE_LIMIT_ORDER_REQUEST_MARKET_COUNT': BadRequest, // more than 1000 marketHashes queried
|
|
170
|
+
'BOTH_SPORTXEVENTID_MARKETHASHES_PRESENT': BadRequest,
|
|
171
|
+
'BAD_MARKET_HASHES': BadRequest, // invalid or more than 30 hashes on /markets/find
|
|
172
|
+
'Order not found': OrderNotFound, // live-verified v3: GET /orders-v3/{orderId} 404s for a missing or foreign id
|
|
173
|
+
},
|
|
174
|
+
'broad': {
|
|
175
|
+
'no longer supported': NotSupported, // e.g. "OrderBook V2 is no longer supported" (testnet V3 canary gate)
|
|
176
|
+
'must be': BadRequest, // request validation messages like "orders must be an array"
|
|
177
|
+
'Insufficient available balance': InsufficientFunds, // live-verified v3: the proxy wallet holds less than the order size
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
'options': {
|
|
181
|
+
'marketsPageSize': 100, // markets per GET /markets/active page (cursor pagination)
|
|
182
|
+
'maxMarketsPages': 50, // safety cap on pages fetched per markets scan (100 * 50 = 5000 markets)
|
|
183
|
+
// venue-specific fetchEvents scope params accepted by requireEventQuery
|
|
184
|
+
'eventScopeParams': ['leagueId', 'sportId'],
|
|
185
|
+
// per-chain RPC endpoint for the on-chain reads approve() needs (ERC20 name()/nonces());
|
|
186
|
+
// sx.bet's own REST API has no RPC proxy, and /metadata carries no rpcUrl field
|
|
187
|
+
'chains': {
|
|
188
|
+
'4162': { 'rpcUrl': 'https://rpc-rollup.sx.technology' },
|
|
189
|
+
'79479957': { 'rpcUrl': 'https://rpc-rollup.toronto.sx.technology' },
|
|
190
|
+
},
|
|
191
|
+
'approveDeadlineSeconds': 7200,
|
|
192
|
+
// the venue rejects expiry 0 and past values - default resting orders to one day
|
|
193
|
+
'defaultOrderExpirySeconds': 86400,
|
|
194
|
+
// the venue caps GET /orders-v3/odds/best at 100 market hashes per request
|
|
195
|
+
'bestOddsBatchSize': 100,
|
|
196
|
+
// the venue caps DELETE /orders-v3 at maxCancelOrders (100, see /metadata/obv3) ids per request
|
|
197
|
+
'cancelOrdersBatchSize': 100,
|
|
198
|
+
'tradesLimit': 1000,
|
|
199
|
+
'ordersLimit': 1000,
|
|
200
|
+
'myTradesLimit': 1000,
|
|
201
|
+
},
|
|
202
|
+
'streaming': {
|
|
203
|
+
// Centrifugo server pings arrive shaped like empty frames and are answered by pong();
|
|
204
|
+
// no client-initiated keepAlive is needed
|
|
205
|
+
'keepAlive': 30000,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* @method
|
|
211
|
+
* @name sxbet#fetchMarkets
|
|
212
|
+
* @description retrieves data on all active markets, each becomes one market with its two sides listed under the outcomes key
|
|
213
|
+
* @see https://docs.sx.bet/api-reference/get-markets-active
|
|
214
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
215
|
+
* @param {int} [params.limit] max number of markets to collect (defaults to options.marketsPageSize * options.maxMarketsPages, 5000)
|
|
216
|
+
* @returns {object[]} an array of objects representing market data
|
|
217
|
+
*/
|
|
218
|
+
async fetchMarkets(params = {}) {
|
|
219
|
+
const rest = this.omit(params, ['limit']);
|
|
220
|
+
const userLimit = this.safeInteger(params, 'limit');
|
|
221
|
+
const rawMarkets = await this.fetchRawMarketsPaged(rest, userLimit);
|
|
222
|
+
const markets = [];
|
|
223
|
+
const rawMarketsLength = rawMarkets.length;
|
|
224
|
+
for (let i = 0; i < rawMarketsLength; i++) {
|
|
225
|
+
markets.push(this.parseSxbetMarket(rawMarkets[i]));
|
|
226
|
+
}
|
|
227
|
+
const marketsLength = markets.length;
|
|
228
|
+
if ((userLimit !== undefined) && (marketsLength > userLimit)) {
|
|
229
|
+
return this.arraySlice(markets, 0, userLimit);
|
|
230
|
+
}
|
|
231
|
+
return markets;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* @ignore
|
|
235
|
+
* @method
|
|
236
|
+
* @name sxbet#fetchRawMarketsPaged
|
|
237
|
+
* @description pages through GET /markets/active (cursor-based via paginationKey/nextKey), stopping once options.maxMarketsPages pages or userLimit raw markets have been collected
|
|
238
|
+
* @param {object} [extra] extra request params merged into every page (e.g. leagueId, sportId, sportXeventId)
|
|
239
|
+
* @param {int} [userLimit] stop collecting once this many raw markets have been gathered
|
|
240
|
+
* @returns {object[]} the raw (unparsed) sx.bet market objects
|
|
241
|
+
*/
|
|
242
|
+
async fetchRawMarketsPaged(extra = {}, userLimit = undefined) {
|
|
243
|
+
const pageSize = this.safeInteger(this.options, 'marketsPageSize', 100);
|
|
244
|
+
const maxPages = this.safeInteger(this.options, 'maxMarketsPages', 50);
|
|
245
|
+
const rawMarkets = [];
|
|
246
|
+
let paginationKey = undefined;
|
|
247
|
+
let page = 0;
|
|
248
|
+
while (true) {
|
|
249
|
+
const request = { 'pageSize': pageSize };
|
|
250
|
+
if (paginationKey !== undefined) {
|
|
251
|
+
request['paginationKey'] = paginationKey;
|
|
252
|
+
}
|
|
253
|
+
const response = await this.sxbetPublicGetMarketsActive(this.extend(request, extra));
|
|
254
|
+
const result = this.safeDict(response, 'data', {});
|
|
255
|
+
const pageMarkets = this.safeList(result, 'markets', []);
|
|
256
|
+
const pageMarketsLength = pageMarkets.length;
|
|
257
|
+
for (let i = 0; i < pageMarketsLength; i++) {
|
|
258
|
+
rawMarkets.push(pageMarkets[i]);
|
|
259
|
+
}
|
|
260
|
+
paginationKey = this.safeString(result, 'nextKey');
|
|
261
|
+
page = this.sum(page, 1);
|
|
262
|
+
const collectedLength = rawMarkets.length;
|
|
263
|
+
if ((pageMarketsLength < pageSize) || (page >= maxPages) || (paginationKey === undefined) || ((userLimit !== undefined) && (collectedLength >= userLimit))) {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return rawMarkets;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* @ignore
|
|
271
|
+
* @method
|
|
272
|
+
* @name sxbet#filterRawMarketsByFixture
|
|
273
|
+
* @description keeps only the raw markets belonging to one fixture — the venue's own sportXeventId filter on /markets/active is unreliable (observed live returning every fixture), so the scope is enforced client-side
|
|
274
|
+
* @param {object[]} rawMarkets the raw sx.bet market objects
|
|
275
|
+
* @param {string} sportXeventId the fixture id to keep
|
|
276
|
+
* @returns {object[]} the raw markets of that fixture only
|
|
277
|
+
*/
|
|
278
|
+
filterRawMarketsByFixture(rawMarkets, sportXeventId) {
|
|
279
|
+
const result = [];
|
|
280
|
+
const rawMarketsLength = rawMarkets.length;
|
|
281
|
+
for (let i = 0; i < rawMarketsLength; i++) {
|
|
282
|
+
const raw = rawMarkets[i];
|
|
283
|
+
if (this.safeString(raw, 'sportXeventId') === sportXeventId) {
|
|
284
|
+
result.push(raw);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* @ignore
|
|
291
|
+
* @method
|
|
292
|
+
* @name sxbet#parseSxbetMarket
|
|
293
|
+
* @description converts a single raw sx.bet market into one ccxt market whose two sides become the outcomes
|
|
294
|
+
* @param {object} raw the raw sx.bet market object
|
|
295
|
+
* @returns {object} a [market structure](https://docs.ccxt.com/#/?id=market-structure)
|
|
296
|
+
*/
|
|
297
|
+
parseSxbetMarket(raw) {
|
|
298
|
+
//
|
|
299
|
+
// {
|
|
300
|
+
// "status": "ACTIVE",
|
|
301
|
+
// "marketHash": "0x7154aa3580267e276cccd0f4f826464a05e3efd8e1c81d7717bef6b5ae88b07d",
|
|
302
|
+
// "outcomeOneName": "Los Angeles Rams",
|
|
303
|
+
// "outcomeTwoName": "San Francisco 49ers",
|
|
304
|
+
// "outcomeVoidName": "NO_CONTEST",
|
|
305
|
+
// "teamOneName": "Los Angeles Rams",
|
|
306
|
+
// "teamTwoName": "San Francisco 49ers",
|
|
307
|
+
// "type": 226,
|
|
308
|
+
// "gameTime": 1789086900,
|
|
309
|
+
// "line": -2.5, // present on spread/total markets only
|
|
310
|
+
// "sportXeventId": "L18870109",
|
|
311
|
+
// "liveEnabled": true,
|
|
312
|
+
// "sportLabel": "Football",
|
|
313
|
+
// "sportId": 8,
|
|
314
|
+
// "leagueId": 243,
|
|
315
|
+
// "leagueLabel": "NFL",
|
|
316
|
+
// "mainLine": true,
|
|
317
|
+
// "isQuarterLineMarket": false
|
|
318
|
+
// }
|
|
319
|
+
//
|
|
320
|
+
const marketHash = this.safeString(raw, 'marketHash', '');
|
|
321
|
+
const teamOneName = this.safeString(raw, 'teamOneName');
|
|
322
|
+
const teamTwoName = this.safeString(raw, 'teamTwoName');
|
|
323
|
+
const outcomeOneName = this.safeString(raw, 'outcomeOneName');
|
|
324
|
+
const outcomeTwoName = this.safeString(raw, 'outcomeTwoName');
|
|
325
|
+
const eventSlug = this.shortenSlug(teamOneName + ' ' + teamTwoName);
|
|
326
|
+
// one fixture carries many markets (moneyline, several spread/total lines, quarter/half
|
|
327
|
+
// variants) whose outcomeOneName text can coincide or nearly coincide, so a text-only
|
|
328
|
+
// slug isn't guaranteed unique. suffix with the market hash instead (always unique,
|
|
329
|
+
// letters+digits only so it survives shortenSlug in one atomic word)
|
|
330
|
+
// parseToInt-wrapped .length: the bare `const n = str.length;` statement is the php
|
|
331
|
+
// transpiler's ARRAY hint (count()), which breaks on a string — this form emits
|
|
332
|
+
// strlen()/len() correctly in both python and php
|
|
333
|
+
const hashLength = this.parseToInt(marketHash.length);
|
|
334
|
+
const hashSuffix = marketHash.slice(hashLength - 6);
|
|
335
|
+
const marketSlug = this.shortenSlug(outcomeOneName) + '_' + hashSuffix;
|
|
336
|
+
const marketSymbol = this.slugToMarketSymbol(eventSlug, marketSlug);
|
|
337
|
+
const status = this.safeStringUpper(raw, 'status');
|
|
338
|
+
const active = (status === 'ACTIVE');
|
|
339
|
+
// guard against a zero sentinel for "no scheduled game time" - safeTimestamp would
|
|
340
|
+
// turn it into the 1970 epoch
|
|
341
|
+
let gameTime = undefined;
|
|
342
|
+
if (this.safeInteger(raw, 'gameTime', 0) !== 0) {
|
|
343
|
+
gameTime = this.safeTimestamp(raw, 'gameTime');
|
|
344
|
+
}
|
|
345
|
+
const outcomeLabels = [outcomeOneName, outcomeTwoName];
|
|
346
|
+
const outcomeIds = [marketHash, marketHash + '-2'];
|
|
347
|
+
const outcomes = [];
|
|
348
|
+
for (let oi = 0; oi < outcomeLabels.length; oi++) {
|
|
349
|
+
const label = outcomeLabels[oi];
|
|
350
|
+
const outcomeHandle = this.slugToOutcomeSymbol(eventSlug, marketSlug, label);
|
|
351
|
+
outcomes.push({
|
|
352
|
+
'id': outcomeIds[oi],
|
|
353
|
+
'outcomeId': outcomeIds[oi],
|
|
354
|
+
'outcome': outcomeHandle,
|
|
355
|
+
'market': marketSymbol,
|
|
356
|
+
'label': label,
|
|
357
|
+
'active': active,
|
|
358
|
+
'winner': undefined,
|
|
359
|
+
'settleFraction': undefined,
|
|
360
|
+
'info': raw,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
'id': marketHash,
|
|
365
|
+
'market': marketSymbol,
|
|
366
|
+
'base': 'USDC',
|
|
367
|
+
'quote': 'USDC',
|
|
368
|
+
'settle': undefined,
|
|
369
|
+
'baseId': marketHash,
|
|
370
|
+
'quoteId': 'USDC',
|
|
371
|
+
'settleId': undefined,
|
|
372
|
+
'type': 'prediction',
|
|
373
|
+
'marketType': 'binary',
|
|
374
|
+
'executionModel': 'clob',
|
|
375
|
+
'spot': false,
|
|
376
|
+
'margin': false,
|
|
377
|
+
'swap': false,
|
|
378
|
+
'future': false,
|
|
379
|
+
'option': false,
|
|
380
|
+
'prediction': true,
|
|
381
|
+
'active': active,
|
|
382
|
+
'resolved': false,
|
|
383
|
+
'resolvedOutcome': undefined,
|
|
384
|
+
'contract': false,
|
|
385
|
+
'linear': undefined,
|
|
386
|
+
'inverse': undefined,
|
|
387
|
+
'contractSize': undefined,
|
|
388
|
+
'expiry': gameTime,
|
|
389
|
+
'expiryDatetime': this.iso8601(gameTime),
|
|
390
|
+
'strike': undefined,
|
|
391
|
+
'optionType': undefined,
|
|
392
|
+
// no trading-fee percentage found anywhere in the docs/metadata (oracleFees is a
|
|
393
|
+
// resolution/oracle fee, not a per-trade maker/taker cut) - 0 until live data says otherwise
|
|
394
|
+
'taker': 0,
|
|
395
|
+
'maker': 0,
|
|
396
|
+
'percentage': true,
|
|
397
|
+
'tierBased': false,
|
|
398
|
+
'feeSide': 'get',
|
|
399
|
+
'precision': {
|
|
400
|
+
// USDC stakes have 6 decimals; prices sit on the odds ladder (oddsLadderStepSize
|
|
401
|
+
// 125 in 1e-5 units on both networks — see /metadata), i.e. a 0.00125 step
|
|
402
|
+
'amount': 0.000001,
|
|
403
|
+
'price': 0.00125,
|
|
404
|
+
},
|
|
405
|
+
'limits': {
|
|
406
|
+
'leverage': { 'min': 1, 'max': 1 },
|
|
407
|
+
// /metadata/obv3 publishes limits.orderSizeMinimum '1' (1 USDC, 1000000 base units)
|
|
408
|
+
// on both networks - the stake and its cost share the same floor
|
|
409
|
+
'amount': { 'min': 1, 'max': undefined },
|
|
410
|
+
'price': { 'min': 0, 'max': 1 },
|
|
411
|
+
'cost': { 'min': 1, 'max': undefined },
|
|
412
|
+
},
|
|
413
|
+
'outcomes': outcomes,
|
|
414
|
+
'info': raw,
|
|
415
|
+
'created': undefined,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* @method
|
|
420
|
+
* @name sxbet#fetchEvents
|
|
421
|
+
* @description fetches sx.bet fixtures (one fixture = one event, its markets are every moneyline/spread/total line on that fixture) scoped by eventId, leagueId, sportId or a free-text query/tags match against team and league names — always live from the API, never the local cache (it POPULATES the cache for later event()/outcome lookups). query/queries/tags are matched client-side over a bounded scan of /markets/active — the venue's GET /search covers team names only (not league or sport labels) and is not wired here yet
|
|
422
|
+
* @see https://docs.sx.bet/api-reference/get-markets-active
|
|
423
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
424
|
+
* @param {string} [params.eventId] direct lookup by unified event id (the sx.bet sportXeventId, e.g. 'L18870109')
|
|
425
|
+
* @param {string} [params.query] free-text search matched against team and league names
|
|
426
|
+
* @param {string[]} [params.queries] multiple free-text searches (alternative to query, unioned)
|
|
427
|
+
* @param {string[]} [params.tags] matched identically to query/queries (sx.bet has no tag taxonomy)
|
|
428
|
+
* @param {int} [params.leagueId] sx.bet league id (e.g. 243 for NFL) — fetched server-side
|
|
429
|
+
* @param {int} [params.sportId] sx.bet sport id (e.g. 8 for Football) — fetched server-side
|
|
430
|
+
* @param {string} [params.status] 'active' | 'inactive' | 'closed' | 'all'
|
|
431
|
+
* @param {int} [params.limit] max number of events to return
|
|
432
|
+
* @returns {object[]} an array of event structures
|
|
433
|
+
*/
|
|
434
|
+
async fetchEvents(params = {}) {
|
|
435
|
+
this.requireEventQuery(params);
|
|
436
|
+
const eventId = this.safeString2(params, 'eventId', 'slug');
|
|
437
|
+
const leagueId = this.safeString(params, 'leagueId');
|
|
438
|
+
const sportId = this.safeString(params, 'sportId');
|
|
439
|
+
const queries = this.parseSearchQueries(params);
|
|
440
|
+
const tags = this.safeList(params, 'tags', []);
|
|
441
|
+
const tagsLength = tags.length;
|
|
442
|
+
for (let i = 0; i < tagsLength; i++) {
|
|
443
|
+
queries.push(tags[i]);
|
|
444
|
+
}
|
|
445
|
+
const rest = this.omit(params, ['eventId', 'slug', 'leagueId', 'sportId', 'query', 'queries', 'tags', 'status', 'sort', 'searchIn', 'limit']);
|
|
446
|
+
let rawMarkets;
|
|
447
|
+
if (eventId !== undefined) {
|
|
448
|
+
rawMarkets = await this.fetchRawMarketsPaged(this.extend({ 'sportXeventId': eventId }, rest), undefined);
|
|
449
|
+
// the venue's sportXeventId filter on /markets/active is unreliable (observed live
|
|
450
|
+
// returning every fixture) — enforce the scope client-side
|
|
451
|
+
rawMarkets = this.filterRawMarketsByFixture(rawMarkets, eventId);
|
|
452
|
+
}
|
|
453
|
+
else if (leagueId !== undefined) {
|
|
454
|
+
rawMarkets = await this.fetchRawMarketsPaged(this.extend({ 'leagueId': leagueId }, rest), undefined);
|
|
455
|
+
}
|
|
456
|
+
else if (sportId !== undefined) {
|
|
457
|
+
rawMarkets = await this.fetchRawMarketsPaged(this.extend({ 'sportId': sportId }, rest), undefined);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// no server-side scope left, only query/tags — full scan honoring the fetchMarkets
|
|
461
|
+
// bound, then filter client-side (the venue's /search covers team names only)
|
|
462
|
+
rawMarkets = await this.fetchRawMarketsPaged(rest, undefined);
|
|
463
|
+
}
|
|
464
|
+
const queriesLength = queries.length;
|
|
465
|
+
if (queriesLength > 0) {
|
|
466
|
+
const filtered = [];
|
|
467
|
+
const preFilterLength = rawMarkets.length;
|
|
468
|
+
for (let i = 0; i < preFilterLength; i++) {
|
|
469
|
+
if (this.matchesEventQuery(rawMarkets[i], queries)) {
|
|
470
|
+
filtered.push(rawMarkets[i]);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
rawMarkets = filtered;
|
|
474
|
+
}
|
|
475
|
+
const grouped = {};
|
|
476
|
+
const order = [];
|
|
477
|
+
const rawMarketsLength = rawMarkets.length;
|
|
478
|
+
for (let i = 0; i < rawMarketsLength; i++) {
|
|
479
|
+
const raw = rawMarkets[i];
|
|
480
|
+
const sportXeventId = this.safeString(raw, 'sportXeventId');
|
|
481
|
+
if (sportXeventId === undefined) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (!(sportXeventId in grouped)) {
|
|
485
|
+
grouped[sportXeventId] = [];
|
|
486
|
+
order.push(sportXeventId);
|
|
487
|
+
}
|
|
488
|
+
grouped[sportXeventId].push(raw);
|
|
489
|
+
}
|
|
490
|
+
if (this.markets === undefined) {
|
|
491
|
+
this.markets = this.createSafeDictionary();
|
|
492
|
+
}
|
|
493
|
+
const result = [];
|
|
494
|
+
const orderLength = order.length;
|
|
495
|
+
for (let i = 0; i < orderLength; i++) {
|
|
496
|
+
const fixtureId = order[i];
|
|
497
|
+
const event = this.parseEvent(fixtureId, grouped[fixtureId]);
|
|
498
|
+
const evMarkets = this.safeList(event, 'markets', []);
|
|
499
|
+
const evMarketsLength = evMarkets.length;
|
|
500
|
+
for (let j = 0; j < evMarketsLength; j++) {
|
|
501
|
+
const m = evMarkets[j];
|
|
502
|
+
this.markets[m['market']] = m;
|
|
503
|
+
}
|
|
504
|
+
result.push(event);
|
|
505
|
+
}
|
|
506
|
+
this.populateOutcomes();
|
|
507
|
+
this.setEvents(result);
|
|
508
|
+
const postParams = this.omit(params, ['leagueId', 'sportId', 'query', 'queries', 'tags']);
|
|
509
|
+
return this.applyEventFetchParams(result, postParams, []);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* @method
|
|
513
|
+
* @name sxbet#fetchEvent
|
|
514
|
+
* @description fetches a single sx.bet fixture (event) by its sportXeventId
|
|
515
|
+
* @see https://docs.sx.bet/api-reference/get-markets-active
|
|
516
|
+
* @param {string} id the sx.bet sportXeventId, e.g. 'L18870109'
|
|
517
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
518
|
+
* @returns {object} a [prediction event structure](https://docs.ccxt.com/#/?id=prediction-event-structure)
|
|
519
|
+
*/
|
|
520
|
+
async fetchEvent(id, params = {}) {
|
|
521
|
+
let rawMarkets = await this.fetchRawMarketsPaged(this.extend({ 'sportXeventId': id }, params), undefined);
|
|
522
|
+
// enforce the fixture scope client-side — see filterRawMarketsByFixture
|
|
523
|
+
rawMarkets = this.filterRawMarketsByFixture(rawMarkets, id);
|
|
524
|
+
const rawMarketsLength = rawMarkets.length;
|
|
525
|
+
if (rawMarketsLength === 0) {
|
|
526
|
+
throw new BadSymbol(this.id + ' fetchEvent() could not find an active fixture ' + id);
|
|
527
|
+
}
|
|
528
|
+
const event = this.parseEvent(id, rawMarkets);
|
|
529
|
+
this.indexEventOutcomes(event);
|
|
530
|
+
this.setEvents([event]);
|
|
531
|
+
return event;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* @ignore
|
|
535
|
+
* @method
|
|
536
|
+
* @name sxbet#matchesEventQuery
|
|
537
|
+
* @description checks a raw market's team/league names against a list of free-text queries, matching in either direction (a short user query inside a long name, or — since fetchEvents derives its fallback test query from an already-slugified handle — a long joined query containing a short name)
|
|
538
|
+
* @param {object} raw the raw sx.bet market object
|
|
539
|
+
* @param {string[]} queries lowercase-insensitive free-text queries
|
|
540
|
+
* @returns {boolean} whether any query matches any of the market's team/league/outcome names
|
|
541
|
+
*/
|
|
542
|
+
matchesEventQuery(raw, queries) {
|
|
543
|
+
const fields = [
|
|
544
|
+
this.safeString(raw, 'teamOneName'),
|
|
545
|
+
this.safeString(raw, 'teamTwoName'),
|
|
546
|
+
this.safeString(raw, 'leagueLabel'),
|
|
547
|
+
this.safeString(raw, 'sportLabel'),
|
|
548
|
+
this.safeString(raw, 'outcomeOneName'),
|
|
549
|
+
this.safeString(raw, 'outcomeTwoName'),
|
|
550
|
+
];
|
|
551
|
+
const queriesLength = queries.length;
|
|
552
|
+
for (let qi = 0; qi < queriesLength; qi++) {
|
|
553
|
+
const query = queries[qi].toLowerCase();
|
|
554
|
+
if (query === '') {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
for (let fi = 0; fi < fields.length; fi++) {
|
|
558
|
+
const field = fields[fi];
|
|
559
|
+
if (field === undefined) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const fieldLower = field.toLowerCase();
|
|
563
|
+
if ((query.indexOf(fieldLower) >= 0) || (fieldLower.indexOf(query) >= 0)) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* @ignore
|
|
572
|
+
* @method
|
|
573
|
+
* @name sxbet#parseEvent
|
|
574
|
+
* @description groups a fixture's raw markets (all sharing one sportXeventId) into one unified event structure
|
|
575
|
+
* @param {string} fixtureId the sx.bet sportXeventId
|
|
576
|
+
* @param {object[]} rawMarkets the raw sx.bet market objects belonging to this fixture
|
|
577
|
+
* @returns {object} an event structure
|
|
578
|
+
*/
|
|
579
|
+
parseEvent(fixtureId, rawMarkets) {
|
|
580
|
+
const marketsList = [];
|
|
581
|
+
let anyActive = false;
|
|
582
|
+
let earliestGameTime = undefined;
|
|
583
|
+
let teamOneName = undefined;
|
|
584
|
+
let teamTwoName = undefined;
|
|
585
|
+
let leagueLabel = undefined;
|
|
586
|
+
const rawMarketsLength = rawMarkets.length;
|
|
587
|
+
for (let i = 0; i < rawMarketsLength; i++) {
|
|
588
|
+
const raw = rawMarkets[i];
|
|
589
|
+
const parsed = this.parseSxbetMarket(raw);
|
|
590
|
+
if (parsed === undefined) {
|
|
591
|
+
throw new ExchangeError(this.id + ' parseEvent() could not resolve parsed market');
|
|
592
|
+
}
|
|
593
|
+
marketsList.push(parsed);
|
|
594
|
+
if (parsed['active'] === true) {
|
|
595
|
+
anyActive = true;
|
|
596
|
+
}
|
|
597
|
+
if (teamOneName === undefined) {
|
|
598
|
+
teamOneName = this.safeString(raw, 'teamOneName');
|
|
599
|
+
teamTwoName = this.safeString(raw, 'teamTwoName');
|
|
600
|
+
leagueLabel = this.safeString(raw, 'leagueLabel');
|
|
601
|
+
}
|
|
602
|
+
const gameTime = this.safeString(raw, 'gameTime');
|
|
603
|
+
// skip the zero sentinel for "no scheduled game time" so it can't become the epoch
|
|
604
|
+
if ((gameTime !== undefined) && (gameTime !== '0') && ((earliestGameTime === undefined) || (gameTime < earliestGameTime))) {
|
|
605
|
+
earliestGameTime = gameTime;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const end = this.safeTimestamp({ 'gameTime': earliestGameTime }, 'gameTime');
|
|
609
|
+
// some fixtures carry no team names on their market rows - fall back to the fixture id
|
|
610
|
+
// so the unified event handle is never empty or built from stringified nulls
|
|
611
|
+
let title = undefined;
|
|
612
|
+
let eventSlug = undefined;
|
|
613
|
+
if ((teamOneName !== undefined) && (teamTwoName !== undefined)) {
|
|
614
|
+
title = teamOneName + ' vs ' + teamTwoName;
|
|
615
|
+
eventSlug = this.shortenSlug(teamOneName + ' ' + teamTwoName);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
eventSlug = this.shortenSlug(fixtureId);
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
'id': fixtureId,
|
|
622
|
+
'slug': fixtureId,
|
|
623
|
+
'event': eventSlug,
|
|
624
|
+
'title': title,
|
|
625
|
+
'description': undefined,
|
|
626
|
+
'category': leagueLabel,
|
|
627
|
+
'tags': undefined,
|
|
628
|
+
'markets': marketsList,
|
|
629
|
+
'mutuallyExclusive': false,
|
|
630
|
+
'active': anyActive,
|
|
631
|
+
'resolved': undefined,
|
|
632
|
+
'volume': undefined,
|
|
633
|
+
'liquidity': undefined,
|
|
634
|
+
'created': undefined,
|
|
635
|
+
'createdDatetime': undefined,
|
|
636
|
+
'end': end,
|
|
637
|
+
'endDatetime': this.iso8601(end),
|
|
638
|
+
'image': undefined,
|
|
639
|
+
'url': undefined,
|
|
640
|
+
'info': rawMarkets,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* @ignore
|
|
645
|
+
* @method
|
|
646
|
+
* @name sxbet#loadSxObv3Metadata
|
|
647
|
+
* @description fetches and caches GET /metadata/obv3 - the v3 orderbook metadata carrying the ready-made EIP-712 domain, the active base asset and the order size limits
|
|
648
|
+
* @see https://docs.sx.bet/api-reference/get-metadata-obv3
|
|
649
|
+
* @returns {object} the cached obv3 metadata data object
|
|
650
|
+
*/
|
|
651
|
+
async loadSxObv3Metadata() {
|
|
652
|
+
const cached = this.safeDict(this.options, 'sxObv3Metadata');
|
|
653
|
+
if (cached !== undefined) {
|
|
654
|
+
return cached;
|
|
655
|
+
}
|
|
656
|
+
const response = await this.sxbetPublicGetMetadataObv3();
|
|
657
|
+
const data = this.safeDict(response, 'data', {});
|
|
658
|
+
this.options['sxObv3Metadata'] = data;
|
|
659
|
+
return data;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* @ignore
|
|
663
|
+
* @method
|
|
664
|
+
* @name sxbet#roundOddsToLadder
|
|
665
|
+
* @description rounds an implied probability (0-1) to sx.bet's odds ladder (oddsLadderStepSize is in units of 1e-5, e.g. 125 -> a 0.125% step) — a maker's percentageOdds is rejected unless it lands exactly on the ladder
|
|
666
|
+
* @param {string} probability the implied probability in decimal-string form (0-1)
|
|
667
|
+
* @param {string} oddsLadderStepSize the raw oddsLadderStepSize from /metadata/obv3 (e.g. '125')
|
|
668
|
+
* @returns {string} the probability rounded to the nearest ladder step, in decimal-string form
|
|
669
|
+
*/
|
|
670
|
+
roundOddsToLadder(probability, oddsLadderStepSize) {
|
|
671
|
+
const tickSize = Precise.stringDiv(oddsLadderStepSize, '100000', 10);
|
|
672
|
+
return this.decimalToPrecision(probability, ROUND, tickSize, TICK_SIZE);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* @ignore
|
|
676
|
+
* @method
|
|
677
|
+
* @name sxbet#hashEip712Digest
|
|
678
|
+
* @description hashes an EIP-712 encoded payload (domainSeparator‖structHash, prefixed with 0x1901 by ethEncodeStructuredData) down to the final 32-byte digest to sign, used by sx.bet's taker-fill and cancel signatures
|
|
679
|
+
* @param {Uint8Array} encoded the output of this.ethEncodeStructuredData (domain, types, message)
|
|
680
|
+
* @returns {string} the 32-byte digest to ecdsa-sign, in '0x'-prefixed hex form
|
|
681
|
+
*/
|
|
682
|
+
hashEip712Digest(encoded) {
|
|
683
|
+
return '0x' + this.hash(encoded, keccak, 'hex');
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* @ignore
|
|
687
|
+
* @method
|
|
688
|
+
* @name sxbet#signDigest
|
|
689
|
+
* @description ecdsa-signs a 32-byte digest and assembles the r‖s‖v hex signature sx.bet expects
|
|
690
|
+
* @param {string} digest the '0x'-prefixed 32-byte digest to sign
|
|
691
|
+
* @param {string} privateKey the signer's private key
|
|
692
|
+
* @returns {string} a '0x'-prefixed 65-byte hex signature (r‖s‖v)
|
|
693
|
+
*/
|
|
694
|
+
signDigest(digest, privateKey) {
|
|
695
|
+
const signature = ecdsa(digest.slice(-64), privateKey.slice(-64), secp256k1, undefined);
|
|
696
|
+
// assign to bare locals before padStart — the php transpiler's str_pad regex only
|
|
697
|
+
// matches a simple identifier, an expression form leaks a raw padStart() call
|
|
698
|
+
const rRaw = signature['r'];
|
|
699
|
+
const sRaw = signature['s'];
|
|
700
|
+
const r = rRaw.padStart(64, '0');
|
|
701
|
+
const s = sRaw.padStart(64, '0');
|
|
702
|
+
const v = this.intToBase16(this.sum(27, signature['v']));
|
|
703
|
+
return '0x' + r + s + v;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* @ignore
|
|
707
|
+
* @method
|
|
708
|
+
* @name sxbet#fetchErc20Name
|
|
709
|
+
* @description reads an ERC20 token's name() via eth_call (needed for the Permit EIP-712 domain — must match the token's real on-chain name or the signature fails verification)
|
|
710
|
+
* @param {string} rpcUrl the RPC endpoint to call
|
|
711
|
+
* @param {string} tokenAddress the token contract address
|
|
712
|
+
* @returns {string} the token's on-chain name
|
|
713
|
+
*/
|
|
714
|
+
async fetchErc20Name(rpcUrl, tokenAddress) {
|
|
715
|
+
const nameCallData = '0x06fdde03'; // name()
|
|
716
|
+
const result = await this.ethRpc(rpcUrl, 'eth_call', [{ 'to': tokenAddress, 'data': nameCallData }, 'latest']);
|
|
717
|
+
const hex = this.remove0xPrefix(result);
|
|
718
|
+
// dynamic ABI string return: [32-byte offset][32-byte length][utf8 bytes, right-padded]
|
|
719
|
+
const lengthHex = hex.slice(64, 128);
|
|
720
|
+
const length = this.hexToInt(lengthHex);
|
|
721
|
+
const dataEnd = this.sum(128, length * 2);
|
|
722
|
+
const dataHex = hex.slice(128, dataEnd);
|
|
723
|
+
return this.decode(this.base16ToBinary(dataHex));
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* @ignore
|
|
727
|
+
* @method
|
|
728
|
+
* @name sxbet#hexToInt
|
|
729
|
+
* @description parses a hex string (no '0x' prefix) into an integer — plain digit-by-digit, since JS's parseInt (hex, 16) two-argument form has no portable equivalent across languages/transpilers. Only used for small values (byte lengths, permit nonces), safe well within the float-precision range
|
|
730
|
+
* @param {string} hex the hex string, no '0x' prefix
|
|
731
|
+
* @returns {int} the parsed integer
|
|
732
|
+
*/
|
|
733
|
+
hexToInt(hex) {
|
|
734
|
+
const digits = '0123456789abcdef';
|
|
735
|
+
const lowerHex = hex.toLowerCase();
|
|
736
|
+
const hexLength = lowerHex.length;
|
|
737
|
+
let result = 0;
|
|
738
|
+
for (let i = 0; i < hexLength; i++) {
|
|
739
|
+
const ch = lowerHex[i];
|
|
740
|
+
const digitValue = digits.indexOf(ch);
|
|
741
|
+
result = (result * 16) + digitValue;
|
|
742
|
+
}
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* @ignore
|
|
747
|
+
* @method
|
|
748
|
+
* @name sxbet#fetchSxbetProxy
|
|
749
|
+
* @description fetches the account's obv3 proxy wallet state
|
|
750
|
+
* @see https://docs.sx.bet/api-reference/get-user-proxy
|
|
751
|
+
* @returns {object} the raw proxy data ({obv3ProxyWalletAddress, deployed, multisigSafeAddress})
|
|
752
|
+
*/
|
|
753
|
+
async fetchSxbetProxy() {
|
|
754
|
+
const response = await this.sxbetPrivateGetUserProxy();
|
|
755
|
+
return this.safeDict(response, 'data', {});
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* @method
|
|
759
|
+
* @name sxbet#approve
|
|
760
|
+
* @description funds the account's obv3 proxy wallet - v3 trading capital must sit inside the proxy. Deploys the proxy first when absent, then moves USDC from the wallet into it via a gasless EIP-2612 Permit signature (POST /user/transfer-to-proxy)
|
|
761
|
+
* @see https://docs.sx.bet/api-reference/post-user-transfer-to-proxy
|
|
762
|
+
* @see https://docs.sx.bet/api-reference/post-user-deploy-proxy
|
|
763
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
764
|
+
* @param {float} [params.amount] the USDC amount to move into the proxy (required)
|
|
765
|
+
* @param {string} [params.tokenAddress] the token to transfer, defaults to the active base token
|
|
766
|
+
* @param {string} [params.spender] the transfer executor granted the permit, defaults to options.transferToProxySpender or the obv3 transferToProxyExecutorAddress
|
|
767
|
+
* @param {int} [params.deadline] unix seconds the permit signature expires at, defaults to options.approveDeadlineSeconds from now
|
|
768
|
+
* @param {string} [params.rpcUrl] overrides the chain's default RPC endpoint (see options.chains)
|
|
769
|
+
* @returns {object} a dict with the raw response and the transfer sessionId
|
|
770
|
+
*/
|
|
771
|
+
async approve(params = {}) {
|
|
772
|
+
this.checkRequiredCredentials();
|
|
773
|
+
const amount = this.safeNumber(params, 'amount');
|
|
774
|
+
if (amount === undefined) {
|
|
775
|
+
throw new ArgumentsRequired(this.id + ' approve() requires params.amount - the USDC amount to move into the proxy wallet');
|
|
776
|
+
}
|
|
777
|
+
const proxy = await this.fetchSxbetProxy();
|
|
778
|
+
const deployed = this.safeBool(proxy, 'deployed', false);
|
|
779
|
+
if (deployed !== true) {
|
|
780
|
+
await this.sxbetPrivatePostUserDeployProxy();
|
|
781
|
+
// deployment is asynchronous - poll until the proxy exists
|
|
782
|
+
for (let i = 0; i < 30; i++) {
|
|
783
|
+
await this.sleep(2000);
|
|
784
|
+
const state = await this.fetchSxbetProxy();
|
|
785
|
+
if (this.safeBool(state, 'deployed', false)) {
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const obv3 = await this.loadSxObv3Metadata();
|
|
791
|
+
const activeAsset = this.safeDict(obv3, 'activeAsset', {});
|
|
792
|
+
const chainId = this.safeInteger(obv3, 'chainId');
|
|
793
|
+
const usdcAddress = this.safeString(activeAsset, 'baseToken');
|
|
794
|
+
// the permit spender is the venue's transfer-to-proxy executor, published in /metadata/obv3
|
|
795
|
+
const executorAddress = this.safeString(obv3, 'transferToProxyExecutorAddress');
|
|
796
|
+
const tokenAddress = this.safeString(params, 'tokenAddress', usdcAddress);
|
|
797
|
+
if (tokenAddress === undefined) {
|
|
798
|
+
throw new BadRequest(this.id + ' approve() could not resolve the base token address from /metadata/obv3');
|
|
799
|
+
}
|
|
800
|
+
let spender = undefined;
|
|
801
|
+
[spender, params] = this.handleOptionAndParams2(params, 'approve', 'spender', 'transferToProxySpender', executorAddress);
|
|
802
|
+
if (spender === undefined) {
|
|
803
|
+
throw new BadRequest(this.id + ' approve() could not resolve the transfer-to-proxy executor from /metadata/obv3 - pass params.spender');
|
|
804
|
+
}
|
|
805
|
+
const chains = this.safeDict(this.options, 'chains', {});
|
|
806
|
+
const chainConfig = this.safeDict(chains, this.numberToString(chainId), {});
|
|
807
|
+
const rpcUrl = this.safeString(params, 'rpcUrl', this.safeString(chainConfig, 'rpcUrl'));
|
|
808
|
+
if (rpcUrl === undefined) {
|
|
809
|
+
throw new ArgumentsRequired(this.id + ' approve() has no RPC endpoint configured for chainId ' + this.numberToString(chainId) + ' - pass params.rpcUrl');
|
|
810
|
+
}
|
|
811
|
+
const owner = this.walletAddress;
|
|
812
|
+
const nonceCallData = '0x7ecebe00' + this.padHexAddress(owner); // nonces(address)
|
|
813
|
+
const nonceResult = await this.ethRpc(rpcUrl, 'eth_call', [{ 'to': tokenAddress, 'data': nonceCallData }, 'latest']);
|
|
814
|
+
const nonceHex = this.hexToRlpBytes(nonceResult);
|
|
815
|
+
const nonce = (nonceHex === '') ? '0' : this.numberToString(this.hexToInt(nonceHex));
|
|
816
|
+
const tokenName = await this.fetchErc20Name(rpcUrl, tokenAddress);
|
|
817
|
+
const defaultDeadlineSeconds = this.safeInteger(this.options, 'approveDeadlineSeconds', 7200);
|
|
818
|
+
const deadline = this.safeInteger(params, 'deadline', this.sum(this.seconds(), defaultDeadlineSeconds));
|
|
819
|
+
const value = this.decimalToPrecision(Precise.stringMul(this.numberToString(amount), '1000000'), ROUND, 0, DECIMAL_PLACES);
|
|
820
|
+
const domain = { 'name': tokenName, 'version': '1', 'chainId': chainId, 'verifyingContract': tokenAddress };
|
|
821
|
+
const messageTypes = {
|
|
822
|
+
'Permit': [
|
|
823
|
+
{ 'name': 'owner', 'type': 'address' },
|
|
824
|
+
{ 'name': 'spender', 'type': 'address' },
|
|
825
|
+
{ 'name': 'value', 'type': 'uint256' },
|
|
826
|
+
{ 'name': 'nonce', 'type': 'uint256' },
|
|
827
|
+
{ 'name': 'deadline', 'type': 'uint256' },
|
|
828
|
+
],
|
|
829
|
+
};
|
|
830
|
+
const messageData = { 'owner': owner, 'spender': spender, 'value': value, 'nonce': nonce, 'deadline': deadline };
|
|
831
|
+
const encoded = this.ethEncodeStructuredData(domain, messageTypes, messageData);
|
|
832
|
+
const digest = this.hashEip712Digest(encoded);
|
|
833
|
+
const signature = this.signDigest(digest, this.privateKey);
|
|
834
|
+
const request = {
|
|
835
|
+
'owner': owner,
|
|
836
|
+
'spender': spender,
|
|
837
|
+
'tokenAddress': tokenAddress,
|
|
838
|
+
'value': value,
|
|
839
|
+
'deadline': this.numberToString(deadline),
|
|
840
|
+
'signature': signature,
|
|
841
|
+
};
|
|
842
|
+
const rest = this.omit(params, ['amount', 'tokenAddress', 'deadline', 'rpcUrl']);
|
|
843
|
+
const response = await this.sxbetPrivatePostUserTransferToProxy(this.extend(request, rest));
|
|
844
|
+
const data = this.safeDict(response, 'data', {});
|
|
845
|
+
return {
|
|
846
|
+
'info': response,
|
|
847
|
+
'id': this.safeString(data, 'sessionId'),
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* @method
|
|
852
|
+
* @name sxbet#createOrder
|
|
853
|
+
* @description places an order on sx.bet's v3 unified orderbook - a 'limit' order rests with GTC time-in-force, a 'market' order fills immediately with IOC (or FOK via params.timeInForce). sx.bet has no shares - 'amount' is the USDC stake to risk, and 'price' is the implied probability (0-1) of the requested outcome. 'sell' bets the OPPOSITE outcome of the one requested (sx.bet is bilateral: there is no owned position to sell, only the complementary side of the same market)
|
|
854
|
+
* @see https://docs.sx.bet/api-reference/post-orders-v3
|
|
855
|
+
* @param {string} outcome unified outcome or outcome token id
|
|
856
|
+
* @param {string} type 'limit' (GTC resting order) or 'market' (IOC immediate fill)
|
|
857
|
+
* @param {string} side 'buy' backs the requested outcome, 'sell' backs the complementary one
|
|
858
|
+
* @param {float} amount the USDC amount to stake/risk
|
|
859
|
+
* @param {float} [price] implied probability (0-1) of the requested outcome; required for both order types
|
|
860
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
861
|
+
* @param {string} [params.timeInForce] overrides the derived value - 'GTC', 'IOC' or 'FOK'
|
|
862
|
+
* @param {int} [params.expiry] unix seconds the order expires at; must be in the future (zero and past values are rejected, so is anything inside the fixture's betting-delay window - /metadata/obv3 resolves the delay per sport/league, live vs pregame), defaults to options.defaultOrderExpirySeconds from now
|
|
863
|
+
* @param {string} [params.salt] overrides the random salt differentiating this order
|
|
864
|
+
* @param {string} [params.clientOrderId] caller-chosen id echoed back on reads (max 64 chars)
|
|
865
|
+
* @param {boolean} [params.waitForOutcome] wait for the matching outcome inline (default true)
|
|
866
|
+
* @param {boolean} [params.useBetCredits] fund the stake from bet credits instead of the proxy balance (IOC/FOK only)
|
|
867
|
+
* @param {string} [params.externalUserId] partner attribution id echoed back on order, fill and trade reads
|
|
868
|
+
* @returns {object} a [prediction order structure](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
869
|
+
*/
|
|
870
|
+
async createOrder(outcome, type, side, amount, price = undefined, params = {}) {
|
|
871
|
+
this.checkRequiredCredentials();
|
|
872
|
+
await this.loadOutcome(outcome);
|
|
873
|
+
const outcomeObj = this.outcome(outcome);
|
|
874
|
+
if ((type !== 'limit') && (type !== 'market')) {
|
|
875
|
+
throw new InvalidOrder(this.id + " createOrder() type must be 'limit' or 'market', got " + type);
|
|
876
|
+
}
|
|
877
|
+
if (price === undefined) {
|
|
878
|
+
throw new ArgumentsRequired(this.id + ' createOrder() requires a price - the implied probability of the requested outcome');
|
|
879
|
+
}
|
|
880
|
+
// the venue has no post-only or trigger mechanics - reject the unified params instead
|
|
881
|
+
// of forwarding fields the exchange would silently ignore
|
|
882
|
+
const postOnly = this.safeBool(params, 'postOnly', false);
|
|
883
|
+
if (postOnly === true) {
|
|
884
|
+
throw new NotSupported(this.id + ' createOrder() does not support postOnly - GTC orders may cross on entry');
|
|
885
|
+
}
|
|
886
|
+
const triggerPrice = this.safeStringN(params, ['triggerPrice', 'stopLossPrice', 'takeProfitPrice']);
|
|
887
|
+
if (triggerPrice !== undefined) {
|
|
888
|
+
throw new NotSupported(this.id + ' createOrder() does not support trigger, stop-loss or take-profit orders');
|
|
889
|
+
}
|
|
890
|
+
const marketHash = this.safeString(outcomeObj['info'], 'marketHash', '');
|
|
891
|
+
const outcomeId = this.safeString(outcomeObj, 'outcomeId');
|
|
892
|
+
const isOutcomeOne = (outcomeId === marketHash);
|
|
893
|
+
const isBuy = (side === 'buy');
|
|
894
|
+
// 'sell' bets the complementary outcome, mirroring the requested outcome's own probability -
|
|
895
|
+
// matches the normalize-to-one-book convention used by other prediction venues
|
|
896
|
+
const isMakerBettingOutcomeOne = (isBuy) ? isOutcomeOne : !isOutcomeOne;
|
|
897
|
+
const priceStr = this.numberToString(price);
|
|
898
|
+
const probability = (isBuy) ? priceStr : Precise.stringSub('1', priceStr);
|
|
899
|
+
const obv3 = await this.loadSxObv3Metadata();
|
|
900
|
+
const domain = this.safeDict(obv3, 'domain', {});
|
|
901
|
+
const activeAsset = this.safeDict(obv3, 'activeAsset', {});
|
|
902
|
+
const baseToken = this.safeString(activeAsset, 'baseToken');
|
|
903
|
+
const oddsLadderStepSize = this.numberToString(this.safeInteger(obv3, 'oddsLadderStepSize', 125));
|
|
904
|
+
const roundedProbability = this.roundOddsToLadder(probability, oddsLadderStepSize);
|
|
905
|
+
const percentageOdds = this.decimalToPrecision(Precise.stringMul(roundedProbability, '100000000000000000000'), ROUND, 0, DECIMAL_PLACES);
|
|
906
|
+
const amountStr = this.numberToString(amount);
|
|
907
|
+
const totalBetSize = this.decimalToPrecision(Precise.stringMul(amountStr, '1000000'), ROUND, 0, DECIMAL_PLACES);
|
|
908
|
+
const saltNumber = this.safeString(params, 'salt', this.numberToString(this.milliseconds()));
|
|
909
|
+
// the request body carries the salt in 32-byte-hex form; the signed struct carries the
|
|
910
|
+
// same value in uint256 form - both parse to one number server-side. assign to bare locals
|
|
911
|
+
// before padStart - see the transpiler notes
|
|
912
|
+
const saltHexRaw = this.intToBase16(this.parseToInt(saltNumber));
|
|
913
|
+
const saltHexPadded = saltHexRaw.padStart(64, '0');
|
|
914
|
+
const saltHex = '0x' + saltHexPadded;
|
|
915
|
+
const defaultExpirySeconds = this.safeInteger(this.options, 'defaultOrderExpirySeconds', 86400);
|
|
916
|
+
const expiry = this.safeInteger(params, 'expiry', this.sum(this.seconds(), defaultExpirySeconds));
|
|
917
|
+
const defaultTif = (type === 'limit') ? 'GTC' : 'IOC';
|
|
918
|
+
let timeInForce = undefined;
|
|
919
|
+
[timeInForce, params] = this.handleOptionAndParams(params, 'createOrder', 'timeInForce', defaultTif);
|
|
920
|
+
// an explicit IOC/FOK on a 'limit' order is honored verbatim - the venue executes exactly
|
|
921
|
+
// that time-in-force. only GTC on a 'market' order is refused: it would silently rest,
|
|
922
|
+
// contradicting the immediate-fill semantics the type promises
|
|
923
|
+
if ((type === 'market') && (timeInForce === 'GTC')) {
|
|
924
|
+
throw new InvalidOrder(this.id + " createOrder() market orders cannot be GTC - use type 'limit' for a resting order");
|
|
925
|
+
}
|
|
926
|
+
const maker = this.walletAddress;
|
|
927
|
+
const messageTypes = {
|
|
928
|
+
'Order': [
|
|
929
|
+
{ 'name': 'marketHash', 'type': 'bytes32' },
|
|
930
|
+
{ 'name': 'baseToken', 'type': 'address' },
|
|
931
|
+
{ 'name': 'totalBetSize', 'type': 'uint256' },
|
|
932
|
+
{ 'name': 'percentageOdds', 'type': 'uint256' },
|
|
933
|
+
{ 'name': 'salt', 'type': 'uint256' },
|
|
934
|
+
{ 'name': 'expiry', 'type': 'uint256' },
|
|
935
|
+
{ 'name': 'maker', 'type': 'address' },
|
|
936
|
+
{ 'name': 'isMakerBettingOutcomeOne', 'type': 'bool' },
|
|
937
|
+
],
|
|
938
|
+
};
|
|
939
|
+
const messageData = {
|
|
940
|
+
'marketHash': marketHash,
|
|
941
|
+
'baseToken': baseToken,
|
|
942
|
+
'totalBetSize': totalBetSize,
|
|
943
|
+
'percentageOdds': percentageOdds,
|
|
944
|
+
'salt': saltNumber,
|
|
945
|
+
'expiry': expiry,
|
|
946
|
+
'maker': maker,
|
|
947
|
+
'isMakerBettingOutcomeOne': isMakerBettingOutcomeOne,
|
|
948
|
+
};
|
|
949
|
+
const encoded = this.ethEncodeStructuredData(domain, messageTypes, messageData);
|
|
950
|
+
const digest = this.hashEip712Digest(encoded);
|
|
951
|
+
const orderSignature = this.signDigest(digest, this.privateKey);
|
|
952
|
+
const orderItem = {
|
|
953
|
+
'marketHash': marketHash,
|
|
954
|
+
'maker': maker,
|
|
955
|
+
'totalBetSize': totalBetSize,
|
|
956
|
+
'percentageOdds': percentageOdds,
|
|
957
|
+
'salt': saltHex,
|
|
958
|
+
'expiry': expiry,
|
|
959
|
+
'baseToken': baseToken,
|
|
960
|
+
'isMakerBettingOutcomeOne': isMakerBettingOutcomeOne,
|
|
961
|
+
'timeInForce': timeInForce,
|
|
962
|
+
'orderSignature': orderSignature,
|
|
963
|
+
};
|
|
964
|
+
const clientOrderId = this.safeString(params, 'clientOrderId');
|
|
965
|
+
if (clientOrderId !== undefined) {
|
|
966
|
+
orderItem['clientOrderId'] = clientOrderId;
|
|
967
|
+
}
|
|
968
|
+
// useBetCredits and externalUserId are per-order fields - route them into the order item,
|
|
969
|
+
// not the top-level body, where the venue would silently ignore them
|
|
970
|
+
const useBetCredits = this.safeBool(params, 'useBetCredits');
|
|
971
|
+
if (useBetCredits !== undefined) {
|
|
972
|
+
orderItem['useBetCredits'] = useBetCredits;
|
|
973
|
+
}
|
|
974
|
+
const externalUserId = this.safeString(params, 'externalUserId');
|
|
975
|
+
if (externalUserId !== undefined) {
|
|
976
|
+
orderItem['externalUserId'] = externalUserId;
|
|
977
|
+
}
|
|
978
|
+
const waitForOutcome = this.safeBool(params, 'waitForOutcome', true);
|
|
979
|
+
const rest = this.omit(params, ['salt', 'expiry', 'clientOrderId', 'waitForOutcome', 'useBetCredits', 'externalUserId']);
|
|
980
|
+
const request = { 'orders': [orderItem], 'waitForOutcome': waitForOutcome };
|
|
981
|
+
const response = await this.sxbetPrivatePostOrdersV3(this.extend(request, rest));
|
|
982
|
+
const data = this.safeDict(response, 'data', {});
|
|
983
|
+
const results = this.safeList(data, 'orders', []);
|
|
984
|
+
const first = this.safeDict(results, 0, {});
|
|
985
|
+
const status = this.safeStringUpper(first, 'status');
|
|
986
|
+
if (status === 'FAILED') {
|
|
987
|
+
const reason = this.safeString(first, 'reason', 'FAILED');
|
|
988
|
+
const feedback = this.id + ' createOrder() rejected: ' + reason;
|
|
989
|
+
this.throwExactlyMatchedException(this.exceptions['exact'], reason, feedback);
|
|
990
|
+
throw new InvalidOrder(feedback);
|
|
991
|
+
}
|
|
992
|
+
// with waitForOutcome the venue reports the matching result inline:
|
|
993
|
+
// "outcome": { "state": "FULLY_FILLED", "fillAmount": "2000000", "remainingAmount": "0", "matchIds": [...] }
|
|
994
|
+
// documented states: RESTED, FULLY_FILLED, PARTIAL_FILL_DONE, PARTIAL_FILL_RESTED, TIMEOUT and
|
|
995
|
+
// CANCELLED with cancelReason NO_LIQUIDITY, INTERNAL_ERROR, ENGINE_SHUTDOWN, EXPIRED or INSUFFICIENT_BALANCE
|
|
996
|
+
const matchOutcome = this.safeDict(first, 'outcome');
|
|
997
|
+
const usdcDecimals = '1000000';
|
|
998
|
+
let filled = undefined;
|
|
999
|
+
let remaining = undefined;
|
|
1000
|
+
let orderStatus = undefined;
|
|
1001
|
+
if (matchOutcome !== undefined) {
|
|
1002
|
+
const fillAmountRaw = this.safeString(matchOutcome, 'fillAmount');
|
|
1003
|
+
const remainingRaw = this.safeString(matchOutcome, 'remainingAmount');
|
|
1004
|
+
if (fillAmountRaw !== undefined) {
|
|
1005
|
+
filled = this.parseNumber(Precise.stringDiv(fillAmountRaw, usdcDecimals, 6));
|
|
1006
|
+
}
|
|
1007
|
+
if (remainingRaw !== undefined) {
|
|
1008
|
+
remaining = this.parseNumber(Precise.stringDiv(remainingRaw, usdcDecimals, 6));
|
|
1009
|
+
}
|
|
1010
|
+
const state = this.safeStringUpper(matchOutcome, 'state');
|
|
1011
|
+
if ((state === 'RESTED') || (state === 'PARTIAL_FILL_RESTED')) {
|
|
1012
|
+
orderStatus = 'open';
|
|
1013
|
+
}
|
|
1014
|
+
else if (state === 'FULLY_FILLED') {
|
|
1015
|
+
orderStatus = 'closed';
|
|
1016
|
+
}
|
|
1017
|
+
else if (state === 'PARTIAL_FILL_DONE') {
|
|
1018
|
+
// the unmatched remainder of an IOC was cancelled by the venue
|
|
1019
|
+
orderStatus = 'canceled';
|
|
1020
|
+
}
|
|
1021
|
+
else if (state === 'CANCELLED') {
|
|
1022
|
+
// the documented field is cancelReason; 'reason' is kept for older responses
|
|
1023
|
+
const reason = this.safeStringUpper2(matchOutcome, 'cancelReason', 'reason');
|
|
1024
|
+
orderStatus = (reason === 'EXPIRED') ? 'expired' : 'canceled';
|
|
1025
|
+
}
|
|
1026
|
+
// TIMEOUT: the wait elapsed before matching resolved - the order state is unknown here
|
|
1027
|
+
}
|
|
1028
|
+
const now = this.milliseconds();
|
|
1029
|
+
return this.safePredictionOrder({
|
|
1030
|
+
'id': this.safeString2(first, 'orderId', 'orderHash'),
|
|
1031
|
+
'clientOrderId': this.safeString(first, 'clientOrderId', clientOrderId),
|
|
1032
|
+
'info': response,
|
|
1033
|
+
'timestamp': now,
|
|
1034
|
+
'datetime': this.iso8601(now),
|
|
1035
|
+
'status': orderStatus,
|
|
1036
|
+
'filled': filled,
|
|
1037
|
+
'remaining': remaining,
|
|
1038
|
+
'cost': filled,
|
|
1039
|
+
'outcome': this.safeString(outcomeObj, 'outcome'),
|
|
1040
|
+
'outcomeId': outcomeId,
|
|
1041
|
+
'label': this.safeString(outcomeObj, 'label'),
|
|
1042
|
+
'market': this.safeString(outcomeObj, 'market'),
|
|
1043
|
+
'type': type,
|
|
1044
|
+
'timeInForce': timeInForce,
|
|
1045
|
+
'side': side,
|
|
1046
|
+
'price': this.parseNumber(roundedProbability),
|
|
1047
|
+
'amount': amount,
|
|
1048
|
+
'fee': undefined,
|
|
1049
|
+
'trades': [],
|
|
1050
|
+
}, outcomeObj);
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* @ignore
|
|
1054
|
+
* @method
|
|
1055
|
+
* @name sxbet#parseSxbetCancelResponse
|
|
1056
|
+
* @description parses the shared v3 cancel response ({cancelled, notCancelled, unconfirmed}) into order structures
|
|
1057
|
+
* @param {object} response the raw DELETE /orders-v3 response
|
|
1058
|
+
* @returns {object[]} a list of [prediction order structures](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
1059
|
+
*/
|
|
1060
|
+
parseSxbetCancelResponse(response) {
|
|
1061
|
+
const data = this.safeDict(response, 'data', {});
|
|
1062
|
+
const result = [];
|
|
1063
|
+
// by-id cancels report 'cancelled'; the /all and /event paths submit asynchronously and
|
|
1064
|
+
// report 'cancelledSubmitted' instead - treat both like cancelled
|
|
1065
|
+
const cancelled = this.safeList2(data, 'cancelled', 'cancelledSubmitted', []);
|
|
1066
|
+
const cancelledLength = cancelled.length;
|
|
1067
|
+
for (let i = 0; i < cancelledLength; i++) {
|
|
1068
|
+
result.push(this.safePredictionOrder({
|
|
1069
|
+
'id': this.safeString(cancelled[i], 'orderId'),
|
|
1070
|
+
'status': 'canceled',
|
|
1071
|
+
'info': response,
|
|
1072
|
+
}));
|
|
1073
|
+
}
|
|
1074
|
+
const notCancelled = this.safeList(data, 'notCancelled', []);
|
|
1075
|
+
const notCancelledLength = notCancelled.length;
|
|
1076
|
+
for (let i = 0; i < notCancelledLength; i++) {
|
|
1077
|
+
// the venue reports why (e.g. NOT_FOUND) - the order was not cancelled, report it honestly
|
|
1078
|
+
result.push(this.safePredictionOrder({
|
|
1079
|
+
'id': this.safeString(notCancelled[i], 'orderId'),
|
|
1080
|
+
'status': undefined,
|
|
1081
|
+
'info': response,
|
|
1082
|
+
}));
|
|
1083
|
+
}
|
|
1084
|
+
const unconfirmed = this.safeList(data, 'unconfirmed', []);
|
|
1085
|
+
const unconfirmedLength = unconfirmed.length;
|
|
1086
|
+
for (let i = 0; i < unconfirmedLength; i++) {
|
|
1087
|
+
result.push(this.safePredictionOrder({
|
|
1088
|
+
'id': this.safeString(unconfirmed[i], 'orderId'),
|
|
1089
|
+
'status': undefined,
|
|
1090
|
+
'info': response,
|
|
1091
|
+
}));
|
|
1092
|
+
}
|
|
1093
|
+
return result;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* @method
|
|
1097
|
+
* @name sxbet#cancelOrder
|
|
1098
|
+
* @description cancels one resting maker order - v3 cancels are plain api-key-authenticated DELETE requests, no signature involved
|
|
1099
|
+
* @see https://docs.sx.bet/api-reference/delete-orders-v3
|
|
1100
|
+
* @param {string} id the order id
|
|
1101
|
+
* @param {string} [outcome] not used by sxbet.cancelOrder
|
|
1102
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1103
|
+
* @returns {object} a [prediction order structure](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
1104
|
+
*/
|
|
1105
|
+
async cancelOrder(id, outcome = undefined, params = {}) {
|
|
1106
|
+
this.checkRequiredCredentials();
|
|
1107
|
+
const request = { 'orders': [{ 'orderId': id }] };
|
|
1108
|
+
const response = await this.sxbetPrivateDeleteOrdersV3(this.extend(request, params));
|
|
1109
|
+
const orders = this.parseSxbetCancelResponse(response);
|
|
1110
|
+
return this.safeDict(orders, 0);
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* @method
|
|
1114
|
+
* @name sxbet#cancelOrders
|
|
1115
|
+
* @description cancels multiple resting maker orders in one request - v3 cancels are plain api-key-authenticated DELETE requests, no signature involved
|
|
1116
|
+
* @see https://docs.sx.bet/api-reference/delete-orders-v3
|
|
1117
|
+
* @param {string[]} ids the order ids to cancel
|
|
1118
|
+
* @param {string} [outcome] not used by sxbet.cancelOrders
|
|
1119
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1120
|
+
* @returns {object[]} a list of [prediction order structures](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
1121
|
+
*/
|
|
1122
|
+
async cancelOrders(ids, outcome = undefined, params = {}) {
|
|
1123
|
+
this.checkRequiredCredentials();
|
|
1124
|
+
const idsLength = ids.length;
|
|
1125
|
+
if (idsLength === 0) {
|
|
1126
|
+
throw new ArgumentsRequired(this.id + ' cancelOrders() requires a non-empty ids argument');
|
|
1127
|
+
}
|
|
1128
|
+
// the venue caps one cancel request at maxCancelOrders ids (100, see /metadata/obv3) -
|
|
1129
|
+
// chunk larger batches instead of letting the whole request 400
|
|
1130
|
+
const chunkSize = this.safeInteger(this.options, 'cancelOrdersBatchSize', 100);
|
|
1131
|
+
const chunkCount = this.parseToInt(this.sum(idsLength, chunkSize - 1) / chunkSize);
|
|
1132
|
+
let result = [];
|
|
1133
|
+
for (let c = 0; c < chunkCount; c++) {
|
|
1134
|
+
const start = c * chunkSize;
|
|
1135
|
+
let end = start + chunkSize;
|
|
1136
|
+
if (end > idsLength) {
|
|
1137
|
+
end = idsLength;
|
|
1138
|
+
}
|
|
1139
|
+
const orderItems = [];
|
|
1140
|
+
for (let i = start; i < end; i++) {
|
|
1141
|
+
orderItems.push({ 'orderId': ids[i] });
|
|
1142
|
+
}
|
|
1143
|
+
const request = { 'orders': orderItems };
|
|
1144
|
+
const response = await this.sxbetPrivateDeleteOrdersV3(this.extend(request, params));
|
|
1145
|
+
result = this.arrayConcat(result, this.parseSxbetCancelResponse(response));
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* @method
|
|
1151
|
+
* @name sxbet#cancelAllOrders
|
|
1152
|
+
* @description cancels every resting maker order of the account, or every order of one fixture via params.eventId - v3 cancels are plain api-key-authenticated DELETE requests, no signature involved
|
|
1153
|
+
* @see https://docs.sx.bet/api-reference/delete-orders-v3-all
|
|
1154
|
+
* @see https://docs.sx.bet/api-reference/delete-orders-v3-event
|
|
1155
|
+
* @param {string} [outcome] not used by sxbet.cancelAllOrders
|
|
1156
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1157
|
+
* @param {string} [params.eventId] cancels every order across every market of this fixture instead of the account-wide path (params.sportXeventId is accepted too)
|
|
1158
|
+
* @returns {object[]} a list of [prediction order structures](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
1159
|
+
*/
|
|
1160
|
+
async cancelAllOrders(outcome = undefined, params = {}) {
|
|
1161
|
+
this.checkRequiredCredentials();
|
|
1162
|
+
const eventId = this.safeString2(params, 'eventId', 'sportXeventId');
|
|
1163
|
+
const rest = this.omit(params, ['eventId', 'sportXeventId']);
|
|
1164
|
+
const isEventScoped = (eventId !== undefined);
|
|
1165
|
+
let response = undefined;
|
|
1166
|
+
if (isEventScoped) {
|
|
1167
|
+
// the event route takes eventId in the QUERY string, not the body - sign() urlencodes
|
|
1168
|
+
// body-less DELETE params
|
|
1169
|
+
const request = { 'eventId': eventId };
|
|
1170
|
+
response = await this.sxbetPrivateDeleteOrdersV3Event(this.extend(request, rest));
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
response = await this.sxbetPrivateDeleteOrdersV3All(rest);
|
|
1174
|
+
}
|
|
1175
|
+
let result = this.parseSxbetCancelResponse(response);
|
|
1176
|
+
// both paths paginate their async submission - keep going on the SAME route while more remain
|
|
1177
|
+
let hasMore = this.safeBool(this.safeDict(response, 'data', {}), 'hasMore', false);
|
|
1178
|
+
let guard = 0;
|
|
1179
|
+
while ((hasMore === true) && (guard < 50)) {
|
|
1180
|
+
let nextResponse = undefined;
|
|
1181
|
+
if (isEventScoped) {
|
|
1182
|
+
const nextRequest = { 'eventId': eventId };
|
|
1183
|
+
nextResponse = await this.sxbetPrivateDeleteOrdersV3Event(this.extend(nextRequest, rest));
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
nextResponse = await this.sxbetPrivateDeleteOrdersV3All(rest);
|
|
1187
|
+
}
|
|
1188
|
+
result = this.arrayConcat(result, this.parseSxbetCancelResponse(nextResponse));
|
|
1189
|
+
hasMore = this.safeBool(this.safeDict(nextResponse, 'data', {}), 'hasMore', false);
|
|
1190
|
+
guard = this.sum(guard, 1);
|
|
1191
|
+
}
|
|
1192
|
+
return result;
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* @ignore
|
|
1196
|
+
* @method
|
|
1197
|
+
* @name sxbet#parsePredictionOrder
|
|
1198
|
+
* @description parses one raw GET /orders row into a unified prediction order. every sx.bet maker order is a 'buy' of the outcome it bets on (isMakerBettingOutcomeOne selects the side), priced at the maker's own implied probability
|
|
1199
|
+
* @param {object} order the raw sx.bet order object
|
|
1200
|
+
* @param {object} [market] the outcome object the order belongs to (resolved from the cache by outcomeId when omitted)
|
|
1201
|
+
* @returns {object} a [prediction order structure](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
1202
|
+
*/
|
|
1203
|
+
parsePredictionOrder(order, market = undefined) {
|
|
1204
|
+
//
|
|
1205
|
+
// {
|
|
1206
|
+
// "id": "0x112a4f19f20395aefcdb5d26f3f8aab0d96b1aff7ce346c0bfb13d9490340cdf",
|
|
1207
|
+
// "marketHash": "0xbf06c6379c922d8118612d1d7493b20f6df6929437fbf6022904327f9516a2eb",
|
|
1208
|
+
// "userAddress": "0xbcc6D643e4159A75ED1dB4e13330230B82F2AEe5",
|
|
1209
|
+
// "wallet": "0x4361123dbdc1D812fdf7D27045aF358C9C8AA70A",
|
|
1210
|
+
// "isBettingOutcomeOne": true,
|
|
1211
|
+
// "percentageOdds": "40000000000000000000",
|
|
1212
|
+
// "totalBetSize": "3000000",
|
|
1213
|
+
// "remainingSize": "3000000",
|
|
1214
|
+
// "expiry": null,
|
|
1215
|
+
// "status": "ACTIVE",
|
|
1216
|
+
// "inactiveReason": null,
|
|
1217
|
+
// "eventId": "L12003787",
|
|
1218
|
+
// "createdAt": "2026-07-31T18:45:42.636Z",
|
|
1219
|
+
// "updatedAt": "2026-07-31T18:45:42.838Z"
|
|
1220
|
+
// }
|
|
1221
|
+
//
|
|
1222
|
+
const orderId = this.safeString2(order, 'id', 'orderId');
|
|
1223
|
+
const marketHash = this.safeString(order, 'marketHash', '');
|
|
1224
|
+
const isBettingOutcomeOne = this.safeBool(order, 'isBettingOutcomeOne', true);
|
|
1225
|
+
const outcomeId = (isBettingOutcomeOne) ? marketHash : (marketHash + '-2');
|
|
1226
|
+
const outcomeObj = this.safeOutcome(outcomeId, market);
|
|
1227
|
+
const oneDenom = '100000000000000000000';
|
|
1228
|
+
const usdcDecimals = '1000000';
|
|
1229
|
+
const percentageOdds = this.safeString(order, 'percentageOdds');
|
|
1230
|
+
const price = (percentageOdds !== undefined) ? this.parseNumber(Precise.stringDiv(percentageOdds, oneDenom)) : undefined;
|
|
1231
|
+
const totalBetSize = this.safeString(order, 'totalBetSize', '0');
|
|
1232
|
+
const remainingSize = this.safeString(order, 'remainingSize', totalBetSize);
|
|
1233
|
+
const filledRaw = Precise.stringSub(totalBetSize, remainingSize);
|
|
1234
|
+
const amount = this.parseNumber(Precise.stringDiv(totalBetSize, usdcDecimals, 6));
|
|
1235
|
+
const filled = this.parseNumber(Precise.stringDiv(filledRaw, usdcDecimals, 6));
|
|
1236
|
+
const remaining = this.parseNumber(Precise.stringDiv(remainingSize, usdcDecimals, 6));
|
|
1237
|
+
const orderStatus = this.safeStringUpper(order, 'status');
|
|
1238
|
+
const inactiveReason = this.safeStringUpper(order, 'inactiveReason');
|
|
1239
|
+
let status = 'open';
|
|
1240
|
+
if (orderStatus === 'INACTIVE') {
|
|
1241
|
+
// documented inactiveReason enum: FILLED, USER_REQUESTED, EXPIRED, NO_LIQUIDITY,
|
|
1242
|
+
// INSUFFICIENT_BALANCE, EVENT_LIFECYCLE, MARKET_HALTED, HEARTBEAT_TIMEOUT, SYSTEM
|
|
1243
|
+
if (inactiveReason === 'FILLED') {
|
|
1244
|
+
status = 'closed';
|
|
1245
|
+
}
|
|
1246
|
+
else if (inactiveReason === 'EXPIRED') {
|
|
1247
|
+
status = 'expired';
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
status = 'canceled';
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const timestamp = this.parse8601(this.safeString(order, 'createdAt'));
|
|
1254
|
+
return this.safePredictionOrder({
|
|
1255
|
+
'id': orderId,
|
|
1256
|
+
'clientOrderId': this.safeString(order, 'clientOrderId'),
|
|
1257
|
+
'timestamp': timestamp,
|
|
1258
|
+
'datetime': this.iso8601(timestamp),
|
|
1259
|
+
'lastTradeTimestamp': this.parse8601(this.safeString(order, 'updatedAt')),
|
|
1260
|
+
'status': status,
|
|
1261
|
+
'type': 'limit',
|
|
1262
|
+
'timeInForce': undefined,
|
|
1263
|
+
'side': 'buy',
|
|
1264
|
+
'price': price,
|
|
1265
|
+
'average': undefined,
|
|
1266
|
+
'amount': amount,
|
|
1267
|
+
'filled': filled,
|
|
1268
|
+
'remaining': remaining,
|
|
1269
|
+
'cost': undefined,
|
|
1270
|
+
'fee': undefined,
|
|
1271
|
+
'reduceOnly': undefined,
|
|
1272
|
+
'postOnly': undefined,
|
|
1273
|
+
'trades': [],
|
|
1274
|
+
'outcome': this.safeString(outcomeObj, 'outcome'),
|
|
1275
|
+
'outcomeId': this.safeString(outcomeObj, 'outcomeId', outcomeId),
|
|
1276
|
+
'label': this.safeString(outcomeObj, 'label'),
|
|
1277
|
+
'market': this.safeString(outcomeObj, 'market'),
|
|
1278
|
+
'info': order,
|
|
1279
|
+
}, outcomeObj);
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* @ignore
|
|
1283
|
+
* @method
|
|
1284
|
+
* @name sxbet#clampSxbetPerPage
|
|
1285
|
+
* @description clamps a user limit to the venue's hard perPage ceiling - every v3 listing route rejects perPage above 100, and deeper history is reachable through the nextKey cursor instead
|
|
1286
|
+
* @param {int} limit the user-requested limit
|
|
1287
|
+
* @returns {int} the limit clamped to at most 100
|
|
1288
|
+
*/
|
|
1289
|
+
clampSxbetPerPage(limit) {
|
|
1290
|
+
const maxPerPage = 100;
|
|
1291
|
+
if (limit === undefined) {
|
|
1292
|
+
return undefined;
|
|
1293
|
+
}
|
|
1294
|
+
return (limit > maxPerPage) ? maxPerPage : limit;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* @method
|
|
1298
|
+
* @name sxbet#fetchOpenOrders
|
|
1299
|
+
* @description fetches the account's resting maker orders via the api-key-authenticated GET /orders-v3 (the route is hardcoded to ACTIVE orders and scoped to the key's account)
|
|
1300
|
+
* @see https://docs.sx.bet/api-reference/get-orders-v3
|
|
1301
|
+
* @param {string} [outcome] unified outcome or outcomeId — narrows to that outcome's market
|
|
1302
|
+
* @param {int} [since] applied client-side (the route has no date filter; rows carry createdAt)
|
|
1303
|
+
* @param {int} [limit] the maximum number of orders to return (server-side perPage, max 100, default 50)
|
|
1304
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint (e.g. eventId, sortBy, sortAsc, nextKey)
|
|
1305
|
+
* @returns {object[]} a list of [prediction order structures](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
1306
|
+
*/
|
|
1307
|
+
async fetchOpenOrders(outcome = undefined, since = undefined, limit = undefined, params = {}) {
|
|
1308
|
+
const request = {};
|
|
1309
|
+
let outcomeObj = undefined;
|
|
1310
|
+
if (outcome !== undefined) {
|
|
1311
|
+
await this.loadOutcome(outcome);
|
|
1312
|
+
outcomeObj = this.outcome(outcome);
|
|
1313
|
+
request['marketHash'] = this.safeString(outcomeObj['info'], 'marketHash');
|
|
1314
|
+
}
|
|
1315
|
+
if (limit !== undefined) {
|
|
1316
|
+
request['perPage'] = this.clampSxbetPerPage(limit);
|
|
1317
|
+
}
|
|
1318
|
+
const response = await this.sxbetPrivateGetOrdersV3(this.extend(request, params));
|
|
1319
|
+
const data = this.safeDict(response, 'data', {});
|
|
1320
|
+
const rawOrders = this.safeList(data, 'orders', []);
|
|
1321
|
+
return this.parsePredictionOrders(rawOrders, outcomeObj, since, limit);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* @method
|
|
1325
|
+
* @name sxbet#fetchOrders
|
|
1326
|
+
* @description fetches the account's maker orders. sx.bet's GET /orders-v3 listing is hardcoded to ACTIVE orders — filled/cancelled/expired orders leave the listing permanently (their history is only reconstructable from fills), so this returns the same set that fetchOpenOrders returns
|
|
1327
|
+
* @see https://docs.sx.bet/api-reference/get-orders-v3
|
|
1328
|
+
* @param {string} [outcome] unified outcome or outcomeId — narrows to that outcome's market
|
|
1329
|
+
* @param {int} [since] applied client-side (the route has no date filter; rows carry createdAt)
|
|
1330
|
+
* @param {int} [limit] the maximum number of orders to return (server-side perPage, max 100, default 50)
|
|
1331
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint (e.g. eventId, sortBy, sortAsc, nextKey)
|
|
1332
|
+
* @returns {object[]} a list of [prediction order structures](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
1333
|
+
*/
|
|
1334
|
+
async fetchOrders(outcome = undefined, since = undefined, limit = undefined, params = {}) {
|
|
1335
|
+
return await this.fetchOpenOrders(outcome, since, limit, params);
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* @method
|
|
1339
|
+
* @name sxbet#fetchOrder
|
|
1340
|
+
* @description fetches a single maker order by its order hash - unlike the listing, GET /orders-v3/{orderId} also serves filled, cancelled and expired orders while they still exist. a missing or foreign id 404s with 'Order not found', surfaced through handleErrors's OrderNotFound mapping
|
|
1341
|
+
* @see https://docs.sx.bet/api-reference/get-order-v3
|
|
1342
|
+
* @param {string} id the order hash
|
|
1343
|
+
* @param {string} [outcome] unified outcome or outcomeId (labelling hint only, the request needs just the id)
|
|
1344
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1345
|
+
* @returns {object} a [prediction order structure](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
1346
|
+
*/
|
|
1347
|
+
async fetchOrder(id, outcome = undefined, params = {}) {
|
|
1348
|
+
this.checkRequiredCredentials();
|
|
1349
|
+
if (id === undefined) {
|
|
1350
|
+
throw new ArgumentsRequired(this.id + ' fetchOrder() requires an id argument');
|
|
1351
|
+
}
|
|
1352
|
+
let outcomeObj = undefined;
|
|
1353
|
+
if (outcome !== undefined) {
|
|
1354
|
+
await this.loadOutcome(outcome);
|
|
1355
|
+
outcomeObj = this.outcome(outcome);
|
|
1356
|
+
}
|
|
1357
|
+
const request = { 'orderId': id };
|
|
1358
|
+
const response = await this.sxbetPrivateGetOrdersV3OrderId(this.extend(request, params));
|
|
1359
|
+
const data = this.safeDict(response, 'data', {});
|
|
1360
|
+
const row = this.safeDict(data, 'order', data);
|
|
1361
|
+
return this.parsePredictionOrder(row, outcomeObj);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* @method
|
|
1365
|
+
* @name sxbet#fetchTrades
|
|
1366
|
+
* @description fetches the public trade tape of one outcome's market — every bettor's settled and in-flight bets on that market. the venue requires the trades listing to be scoped, so the outcome argument is mandatory
|
|
1367
|
+
* @see https://docs.sx.bet/api-reference/get-trades-v3-public
|
|
1368
|
+
* @param {string} outcome unified outcome or outcomeId
|
|
1369
|
+
* @param {int} [since] timestamp in ms of the earliest trade to return — applied client-side over the newest page (the public tape serves newest-first and has no date filter; older pages are reachable through params.nextKey)
|
|
1370
|
+
* @param {int} [limit] the maximum number of trades to return (server-side perPage, max 100, default 50)
|
|
1371
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint (e.g. eventId, nextKey)
|
|
1372
|
+
* @returns {object[]} a list of [prediction trade structures](https://docs.ccxt.com/#/?id=prediction-trade-structure)
|
|
1373
|
+
*/
|
|
1374
|
+
async fetchTrades(outcome, since = undefined, limit = undefined, params = {}) {
|
|
1375
|
+
if (outcome === undefined) {
|
|
1376
|
+
throw new ArgumentsRequired(this.id + ' fetchTrades() requires an outcome argument - the venue requires the trades listing to be scoped');
|
|
1377
|
+
}
|
|
1378
|
+
await this.loadOutcome(outcome);
|
|
1379
|
+
const outcomeObj = this.outcome(outcome);
|
|
1380
|
+
const request = { 'marketHash': this.safeString(outcomeObj['info'], 'marketHash') };
|
|
1381
|
+
if (limit !== undefined) {
|
|
1382
|
+
request['perPage'] = this.clampSxbetPerPage(limit);
|
|
1383
|
+
}
|
|
1384
|
+
const response = await this.sxbetPublicGetTradesV3Public(this.extend(request, params));
|
|
1385
|
+
const data = this.safeDict(response, 'data', {});
|
|
1386
|
+
const rawTrades = this.safeList(data, 'trades', []);
|
|
1387
|
+
let trades = [];
|
|
1388
|
+
const rawTradesLength = rawTrades.length;
|
|
1389
|
+
for (let i = 0; i < rawTradesLength; i++) {
|
|
1390
|
+
trades.push(this.parseSxbetV3PublicTrade(rawTrades[i]));
|
|
1391
|
+
}
|
|
1392
|
+
// the venue serves the tape newest-first - the unified contract is ascending by timestamp
|
|
1393
|
+
trades = this.sortBy(trades, 'timestamp');
|
|
1394
|
+
const sym = this.safeString(outcomeObj, 'outcome');
|
|
1395
|
+
return this.filterByValueSinceLimit(trades, 'outcome', sym, since, limit, 'timestamp', true);
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* @method
|
|
1399
|
+
* @name sxbet#fetchMyTrades
|
|
1400
|
+
* @description fetches the account's fills (matched legs of its own orders, both taker and maker side) via the api-key-authenticated GET /fills-v3
|
|
1401
|
+
* @see https://docs.sx.bet/api-reference/get-fills-v3
|
|
1402
|
+
* @param {string} [outcome] unified outcome or outcomeId — narrows to that outcome's market and drops the opposite side's legs
|
|
1403
|
+
* @param {int} [since] timestamp in ms of the earliest fill to fetch (server-side startDate)
|
|
1404
|
+
* @param {int} [limit] the maximum number of fills to return (server-side perPage, max 100, default 50)
|
|
1405
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint (e.g. tradeId, orderId, endDate, sortAsc, nextKey)
|
|
1406
|
+
* @returns {object[]} a list of [prediction trade structures](https://docs.ccxt.com/#/?id=prediction-trade-structure)
|
|
1407
|
+
*/
|
|
1408
|
+
async fetchMyTrades(outcome = undefined, since = undefined, limit = undefined, params = {}) {
|
|
1409
|
+
this.checkRequiredCredentials();
|
|
1410
|
+
const request = {};
|
|
1411
|
+
let outcomeObj = undefined;
|
|
1412
|
+
if (outcome !== undefined) {
|
|
1413
|
+
await this.loadOutcome(outcome);
|
|
1414
|
+
outcomeObj = this.outcome(outcome);
|
|
1415
|
+
}
|
|
1416
|
+
if (since !== undefined) {
|
|
1417
|
+
request['startDate'] = this.iso8601(since);
|
|
1418
|
+
}
|
|
1419
|
+
if ((limit !== undefined) && (outcome === undefined)) {
|
|
1420
|
+
// with an outcome filter the rows are narrowed client-side - a server-side page
|
|
1421
|
+
// size would truncate the page before the filter and under-fill the result
|
|
1422
|
+
request['perPage'] = this.clampSxbetPerPage(limit);
|
|
1423
|
+
}
|
|
1424
|
+
const response = await this.sxbetPrivateGetFillsV3(this.extend(request, params));
|
|
1425
|
+
const data = this.safeDict(response, 'data', {});
|
|
1426
|
+
const rawFills = this.safeList(data, 'fills', []);
|
|
1427
|
+
let trades = [];
|
|
1428
|
+
const rawFillsLength = rawFills.length;
|
|
1429
|
+
for (let i = 0; i < rawFillsLength; i++) {
|
|
1430
|
+
trades.push(this.parseSxbetV3Fill(rawFills[i]));
|
|
1431
|
+
}
|
|
1432
|
+
trades = this.sortBy(trades, 'timestamp');
|
|
1433
|
+
const sym = (outcomeObj !== undefined) ? this.safeString(outcomeObj, 'outcome') : undefined;
|
|
1434
|
+
return this.filterByValueSinceLimit(trades, 'outcome', sym, since, limit, 'timestamp', true);
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* @ignore
|
|
1438
|
+
* @method
|
|
1439
|
+
* @name sxbet#parseSxbetV3Fill
|
|
1440
|
+
* @description parses one GET /fills-v3 row into a unified trade - each fill is one maker/taker match of the wallet's own order
|
|
1441
|
+
* @param {object} fill the raw fill row
|
|
1442
|
+
* @param {object} [market] the outcome object labelling hint
|
|
1443
|
+
* @returns {object} a [prediction trade structure](https://docs.ccxt.com/#/?id=prediction-trade-structure)
|
|
1444
|
+
*/
|
|
1445
|
+
parseSxbetV3Fill(fill, market = undefined) {
|
|
1446
|
+
//
|
|
1447
|
+
// {
|
|
1448
|
+
// "id": "0xec8e...:0x02d2...:0x1fec...",
|
|
1449
|
+
// "matchId": "0xec8e...", "orderId": "0x02d2...", "tradeId": "0xac6b...",
|
|
1450
|
+
// "marketHash": "0x1fec...", "isParlay": false,
|
|
1451
|
+
// "userAddress": "0xbcc6...", "wallet": "0x4361...",
|
|
1452
|
+
// "fillAmount": "333334", "fillOdds": "40000000000000000000",
|
|
1453
|
+
// "returnAmount": "833335", "isBettingOutcomeOne": false,
|
|
1454
|
+
// "isMaker": false, "status": "LOCKED", "txHash": "0x6430...",
|
|
1455
|
+
// "ceRefundAmount": "0", "ceRefundFeeAmount": "0",
|
|
1456
|
+
// "usedBetCredits": false, "createdAt": "2026-07-31T18:47:09.586Z",
|
|
1457
|
+
// "updatedAt": "2026-07-31T18:47:12.837Z", "settlement": null
|
|
1458
|
+
// }
|
|
1459
|
+
//
|
|
1460
|
+
const marketHash = this.safeString(fill, 'marketHash', '');
|
|
1461
|
+
const isBettingOutcomeOne = this.safeBool(fill, 'isBettingOutcomeOne', true);
|
|
1462
|
+
const outcomeId = (isBettingOutcomeOne) ? marketHash : (marketHash + '-2');
|
|
1463
|
+
const outcomeObj = this.safeOutcome(outcomeId, market);
|
|
1464
|
+
const oneDenom = '100000000000000000000';
|
|
1465
|
+
const usdcDecimals = '1000000';
|
|
1466
|
+
const fillOdds = this.safeString(fill, 'fillOdds');
|
|
1467
|
+
const price = (fillOdds !== undefined) ? this.parseNumber(Precise.stringDiv(fillOdds, oneDenom)) : undefined;
|
|
1468
|
+
const fillAmount = this.safeString(fill, 'fillAmount', '0');
|
|
1469
|
+
const amount = this.parseNumber(Precise.stringDiv(fillAmount, usdcDecimals, 6));
|
|
1470
|
+
const timestamp = this.parse8601(this.safeString(fill, 'createdAt'));
|
|
1471
|
+
// isMaker: true when the account's order was the resting one - absent on older rows
|
|
1472
|
+
const isMaker = this.safeBool(fill, 'isMaker');
|
|
1473
|
+
let takerOrMaker = undefined;
|
|
1474
|
+
if (isMaker !== undefined) {
|
|
1475
|
+
takerOrMaker = (isMaker === true) ? 'maker' : 'taker';
|
|
1476
|
+
}
|
|
1477
|
+
return this.safePredictionTrade({
|
|
1478
|
+
'id': this.safeString(fill, 'id'),
|
|
1479
|
+
'info': fill,
|
|
1480
|
+
'timestamp': timestamp,
|
|
1481
|
+
'datetime': this.iso8601(timestamp),
|
|
1482
|
+
'outcome': this.safeString(outcomeObj, 'outcome'),
|
|
1483
|
+
'outcomeId': this.safeString(outcomeObj, 'outcomeId', outcomeId),
|
|
1484
|
+
'label': this.safeString(outcomeObj, 'label'),
|
|
1485
|
+
'market': this.safeString(outcomeObj, 'market'),
|
|
1486
|
+
'order': this.safeString(fill, 'orderId'),
|
|
1487
|
+
'type': undefined,
|
|
1488
|
+
'side': 'buy',
|
|
1489
|
+
'takerOrMaker': takerOrMaker,
|
|
1490
|
+
'price': price,
|
|
1491
|
+
'amount': amount,
|
|
1492
|
+
'cost': amount,
|
|
1493
|
+
'fee': undefined,
|
|
1494
|
+
}, market);
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* @method
|
|
1498
|
+
* @name sxbet#fetchBalance
|
|
1499
|
+
* @description fetches the account's order-spendable proxy balance from GET /user/balance-v3 - v3 trading capital sits inside the obv3 proxy wallet (funded via approve()), and GTC posting is checked against availableAmount. free is the spendable availableAmount, used the escrowedAmount locked behind open bets
|
|
1500
|
+
* @see https://docs.sx.bet/api-reference/get-user-balance-v3
|
|
1501
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1502
|
+
* @returns {object} a [balance structure](https://docs.ccxt.com/#/?id=balance-structure)
|
|
1503
|
+
*/
|
|
1504
|
+
async fetchBalance(params = {}) {
|
|
1505
|
+
const obv3 = await this.loadSxObv3Metadata();
|
|
1506
|
+
const activeAsset = this.safeDict(obv3, 'activeAsset', {});
|
|
1507
|
+
const usdcAddress = this.safeStringLower(activeAsset, 'baseToken', '');
|
|
1508
|
+
const response = await this.sxbetPrivateGetUserBalanceV3(params);
|
|
1509
|
+
//
|
|
1510
|
+
// { "status": "success", "data": { "balances": [ {
|
|
1511
|
+
// "userAddress": "0xC3f4...", "wallet": "0x19D1...",
|
|
1512
|
+
// "tokenAddress": "0x1BC6...", "escrowAddress": "0x0Db3...",
|
|
1513
|
+
// "availableAmount": "0", "pendingAvailableAmount": "0",
|
|
1514
|
+
// "escrowedAmount": "0", "pendingEscrowAmount": "0" } ] } }
|
|
1515
|
+
//
|
|
1516
|
+
const data = this.safeDict(response, 'data', {});
|
|
1517
|
+
const balances = this.safeList(data, 'balances', []);
|
|
1518
|
+
const result = { 'info': response };
|
|
1519
|
+
const usdcDecimals = '1000000';
|
|
1520
|
+
const balancesLength = balances.length;
|
|
1521
|
+
for (let i = 0; i < balancesLength; i++) {
|
|
1522
|
+
const row = balances[i];
|
|
1523
|
+
const tokenAddress = this.safeStringLower(row, 'tokenAddress', '');
|
|
1524
|
+
// every sxbet market is denominated in the active base token, surfaced under 'USDC';
|
|
1525
|
+
// rows of any other token keep their contract address for the code
|
|
1526
|
+
let code = this.safeString(row, 'tokenAddress', '');
|
|
1527
|
+
if (tokenAddress === usdcAddress) {
|
|
1528
|
+
code = 'USDC';
|
|
1529
|
+
}
|
|
1530
|
+
const free = Precise.stringDiv(this.safeString(row, 'availableAmount', '0'), usdcDecimals, 6);
|
|
1531
|
+
const used = Precise.stringDiv(this.safeString(row, 'escrowedAmount', '0'), usdcDecimals, 6);
|
|
1532
|
+
result[code] = {
|
|
1533
|
+
'free': this.parseNumber(free),
|
|
1534
|
+
'used': this.parseNumber(used),
|
|
1535
|
+
'total': this.parseNumber(Precise.stringAdd(free, used)),
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
return this.safeBalance(result);
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* @method
|
|
1542
|
+
* @name sxbet#fetchPositions
|
|
1543
|
+
* @description fetches the account's open positions from the venue's per-market aggregates (GET /positions-v3, MATCHED and LOCKED bets by default; override with params.status - the enum is MATCHED, LOCKED, SETTLED, FAILED, and pnl is populated only when the filter is exclusively SETTLED). contracts is the total stake at risk, entryPrice the blended implied probability of the market's best-case outcome
|
|
1544
|
+
* @see https://docs.sx.bet/api-reference/get-positions-v3
|
|
1545
|
+
* @param {string[]} [outcomes] filter by unified outcomes or outcomeIds
|
|
1546
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1547
|
+
* @returns {object[]} a list of [prediction position structures](https://docs.ccxt.com/#/?id=prediction-position-structure)
|
|
1548
|
+
*/
|
|
1549
|
+
async fetchPositions(outcomes = undefined, params = {}) {
|
|
1550
|
+
this.checkRequiredCredentials();
|
|
1551
|
+
// copy to a plain list so the transpilers and the strict null checks see one shape
|
|
1552
|
+
const outcomesList = (outcomes === undefined) ? [] : outcomes;
|
|
1553
|
+
const outcomesLength = outcomesList.length;
|
|
1554
|
+
const wantedMarkets = {};
|
|
1555
|
+
if (outcomesLength > 0) {
|
|
1556
|
+
await this.loadOutcomes(outcomesList);
|
|
1557
|
+
for (let i = 0; i < outcomesLength; i++) {
|
|
1558
|
+
const outcomeObj = this.outcome(outcomesList[i]);
|
|
1559
|
+
const hash = this.safeString(outcomeObj['info'], 'marketHash', '');
|
|
1560
|
+
wantedMarkets[hash] = true;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
// open exposure lives in MATCHED and LOCKED bets - the venue requires an explicit status
|
|
1564
|
+
// list drawn from MATCHED, LOCKED, SETTLED, FAILED
|
|
1565
|
+
const request = { 'status': this.safeString(params, 'status', 'MATCHED,LOCKED') };
|
|
1566
|
+
const rest = this.omit(params, ['status']);
|
|
1567
|
+
const response = await this.sxbetPrivateGetPositionsV3(this.extend(request, rest));
|
|
1568
|
+
const data = this.safeDict(response, 'data', {});
|
|
1569
|
+
const rawPositions = this.safeList(data, 'positions', []);
|
|
1570
|
+
const result = [];
|
|
1571
|
+
const rawPositionsLength = rawPositions.length;
|
|
1572
|
+
for (let i = 0; i < rawPositionsLength; i++) {
|
|
1573
|
+
const raw = rawPositions[i];
|
|
1574
|
+
const marketHash = this.safeString(raw, 'marketHash', '');
|
|
1575
|
+
if (outcomesLength > 0) {
|
|
1576
|
+
if (this.safeBool(wantedMarkets, marketHash) === undefined) {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
result.push(this.parseSxbetV3Position(raw));
|
|
1581
|
+
}
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* @ignore
|
|
1586
|
+
* @method
|
|
1587
|
+
* @name sxbet#parseSxbetV3Position
|
|
1588
|
+
* @description parses one GET /positions-v3 row (per-market aggregate) into a unified position on the market's best-case outcome
|
|
1589
|
+
* @param {object} raw the raw position row
|
|
1590
|
+
* @returns {object} a [prediction position structure](https://docs.ccxt.com/#/?id=prediction-position-structure)
|
|
1591
|
+
*/
|
|
1592
|
+
parseSxbetV3Position(raw) {
|
|
1593
|
+
//
|
|
1594
|
+
// {
|
|
1595
|
+
// "marketHash": "0x8120...", "market": {...},
|
|
1596
|
+
// "totalStake": "300000000", "maxWin": "200000000", "maxLoss": "-100000000",
|
|
1597
|
+
// "isOutcomeOneMaxWin": true,
|
|
1598
|
+
// "odds": {"outcomeOne": "40000000000000000000", "outcomeTwo": "50000000000000000000"},
|
|
1599
|
+
// "pnl": null, "tradeCount": 2,
|
|
1600
|
+
// "betTime": "2026-03-13T21:14:02.118Z", "updatedAt": "2026-03-13T22:03:41.502Z"
|
|
1601
|
+
// }
|
|
1602
|
+
//
|
|
1603
|
+
const marketHash = this.safeString(raw, 'marketHash', '');
|
|
1604
|
+
const isOutcomeOneMaxWin = this.safeBool(raw, 'isOutcomeOneMaxWin', true);
|
|
1605
|
+
const outcomeId = (isOutcomeOneMaxWin) ? marketHash : (marketHash + '-2');
|
|
1606
|
+
const outcomeObj = this.safeOutcome(outcomeId);
|
|
1607
|
+
const oneDenom = '100000000000000000000';
|
|
1608
|
+
const usdcDecimals = '1000000';
|
|
1609
|
+
const odds = this.safeDict(raw, 'odds', {});
|
|
1610
|
+
const ownOdds = (isOutcomeOneMaxWin) ? this.safeString(odds, 'outcomeOne') : this.safeString(odds, 'outcomeTwo');
|
|
1611
|
+
const entryPrice = (ownOdds !== undefined) ? this.parseNumber(Precise.stringDiv(ownOdds, oneDenom)) : undefined;
|
|
1612
|
+
const totalStake = this.safeString(raw, 'totalStake', '0');
|
|
1613
|
+
const pnl = this.safeString(raw, 'pnl');
|
|
1614
|
+
const timestamp = this.parse8601(this.safeString(raw, 'betTime'));
|
|
1615
|
+
return this.safePredictionPosition({
|
|
1616
|
+
'outcome': this.safeString(outcomeObj, 'outcome'),
|
|
1617
|
+
'outcomeId': this.safeString(outcomeObj, 'outcomeId', outcomeId),
|
|
1618
|
+
'label': this.safeString(outcomeObj, 'label'),
|
|
1619
|
+
'market': this.safeString(outcomeObj, 'market'),
|
|
1620
|
+
'timestamp': timestamp,
|
|
1621
|
+
'datetime': this.iso8601(timestamp),
|
|
1622
|
+
'lastUpdateTimestamp': this.parse8601(this.safeString(raw, 'updatedAt')),
|
|
1623
|
+
'contracts': this.parseNumber(Precise.stringDiv(totalStake, usdcDecimals, 6)),
|
|
1624
|
+
'entryPrice': entryPrice,
|
|
1625
|
+
'markPrice': undefined,
|
|
1626
|
+
'unrealizedPnl': undefined,
|
|
1627
|
+
'realizedPnl': (pnl !== undefined) ? this.parseNumber(Precise.stringDiv(pnl, usdcDecimals, 6)) : undefined,
|
|
1628
|
+
'side': 'long',
|
|
1629
|
+
'info': raw,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* @method
|
|
1634
|
+
* @name sxbet#fetchSettlements
|
|
1635
|
+
* @description fetches the account's settled bets — each settled GET /trades-v3 row becomes one settlement with the resolved winner, the payout (stake / odds when won, the stake back when the market voided, zero when lost) and the realized pnl
|
|
1636
|
+
* @see https://docs.sx.bet/api-reference/get-trades-v3
|
|
1637
|
+
* @param {string} [outcome] filter to a single unified outcome or outcomeId
|
|
1638
|
+
* @param {int} [since] timestamp in ms of the earliest settlement to fetch (server-side startDate on the bet time)
|
|
1639
|
+
* @param {int} [limit] the maximum number of settlements to fetch (server-side perPage, max 100, default 50)
|
|
1640
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint (e.g. eventId, endDate, sortAsc, nextKey)
|
|
1641
|
+
* @returns {object[]} a list of prediction settlement structures
|
|
1642
|
+
*/
|
|
1643
|
+
async fetchSettlements(outcome = undefined, since = undefined, limit = undefined, params = {}) {
|
|
1644
|
+
this.checkRequiredCredentials();
|
|
1645
|
+
const request = { 'status': 'SETTLED' };
|
|
1646
|
+
let wantedOutcomeId = undefined;
|
|
1647
|
+
if (outcome !== undefined) {
|
|
1648
|
+
await this.loadOutcome(outcome);
|
|
1649
|
+
const outcomeObj = this.outcome(outcome);
|
|
1650
|
+
request['marketHash'] = this.safeString(outcomeObj['info'], 'marketHash', '');
|
|
1651
|
+
wantedOutcomeId = this.safeString(outcomeObj, 'outcomeId');
|
|
1652
|
+
}
|
|
1653
|
+
if (since !== undefined) {
|
|
1654
|
+
request['startDate'] = this.iso8601(since);
|
|
1655
|
+
}
|
|
1656
|
+
if ((limit !== undefined) && (outcome === undefined)) {
|
|
1657
|
+
// the outcome scope narrows rows client-side (the venue filter is per market, not
|
|
1658
|
+
// per side) - a server-side page size would under-fill the filtered result
|
|
1659
|
+
request['perPage'] = this.clampSxbetPerPage(limit);
|
|
1660
|
+
}
|
|
1661
|
+
const response = await this.sxbetPrivateGetTradesV3(this.extend(request, params));
|
|
1662
|
+
const data = this.safeDict(response, 'data', {});
|
|
1663
|
+
const rawTrades = this.safeList(data, 'trades', []);
|
|
1664
|
+
const result = [];
|
|
1665
|
+
const rawTradesLength = rawTrades.length;
|
|
1666
|
+
for (let i = 0; i < rawTradesLength; i++) {
|
|
1667
|
+
const settlement = this.parseSettlement(rawTrades[i]);
|
|
1668
|
+
if ((wantedOutcomeId === undefined) || (this.safeString(settlement, 'outcomeId') === wantedOutcomeId)) {
|
|
1669
|
+
result.push(settlement);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return result;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* @ignore
|
|
1676
|
+
* @method
|
|
1677
|
+
* @name sxbet#parseSettlement
|
|
1678
|
+
* @description parses one settled GET /trades-v3 row into the unified prediction settlement shape. the raw `outcome` field carries the winner, 1 or 2, with 0 for a voided market, and the full payout of a winning bet is stake / odds (the implied probability the bettor received)
|
|
1679
|
+
* @param {object} trade the raw settled sx.bet trade
|
|
1680
|
+
* @param {object} [market] a resolved outcome/market hint
|
|
1681
|
+
* @returns {object} a prediction settlement structure
|
|
1682
|
+
*/
|
|
1683
|
+
parseSettlement(trade, market = undefined) {
|
|
1684
|
+
//
|
|
1685
|
+
// TradeV3 row with a settlement sub-object:
|
|
1686
|
+
// {
|
|
1687
|
+
// "tradeId": "0xac6b...", "marketHash": "0x1fec...",
|
|
1688
|
+
// "isBettingOutcomeOne": false, "totalStake": "1000000", "totalReturn": "2500000",
|
|
1689
|
+
// "weightedAverageOdds": "40000000000000000000", "status": "SETTLED",
|
|
1690
|
+
// "betTime": "2026-07-31T18:47:09.577Z",
|
|
1691
|
+
// "settlement": { "outcome": 2, "settleReturnAmount": "2500000",
|
|
1692
|
+
// "settleFeeAmount": "0", "settleCeRefundAmount": "0",
|
|
1693
|
+
// "settleDate": "2026-08-01T00:10:00.000Z" }
|
|
1694
|
+
// }
|
|
1695
|
+
//
|
|
1696
|
+
const marketHash = this.safeString(trade, 'marketHash', '');
|
|
1697
|
+
const isBettingOutcomeOne = this.safeBool(trade, 'isBettingOutcomeOne', true);
|
|
1698
|
+
const outcomeId = (isBettingOutcomeOne) ? marketHash : (marketHash + '-2');
|
|
1699
|
+
const outcomeObj = this.safeOutcome(outcomeId, market);
|
|
1700
|
+
const settlement = this.safeDict(trade, 'settlement', {});
|
|
1701
|
+
const winner = this.safeInteger(settlement, 'outcome');
|
|
1702
|
+
const isVoid = (winner === 0);
|
|
1703
|
+
const heldNumber = (isBettingOutcomeOne) ? 1 : 2;
|
|
1704
|
+
let won = undefined;
|
|
1705
|
+
if ((winner !== undefined) && !isVoid) {
|
|
1706
|
+
won = (winner === heldNumber);
|
|
1707
|
+
}
|
|
1708
|
+
const usdcDecimals = '1000000';
|
|
1709
|
+
const stake = Precise.stringDiv(this.safeString(trade, 'totalStake', '0'), usdcDecimals, 6);
|
|
1710
|
+
const payout = Precise.stringDiv(this.safeString(settlement, 'settleReturnAmount', '0'), usdcDecimals, 6);
|
|
1711
|
+
const pnl = Precise.stringSub(payout, stake);
|
|
1712
|
+
// resolve the winning side's human label through the cached market when available
|
|
1713
|
+
let resultLabel = undefined;
|
|
1714
|
+
if (isVoid) {
|
|
1715
|
+
resultLabel = 'VOID';
|
|
1716
|
+
}
|
|
1717
|
+
else if (winner !== undefined) {
|
|
1718
|
+
const info = this.safeDict(outcomeObj, 'info', {});
|
|
1719
|
+
const labelKey = (winner === 1) ? 'outcomeOneName' : 'outcomeTwoName';
|
|
1720
|
+
resultLabel = this.safeString(info, labelKey, this.numberToString(winner));
|
|
1721
|
+
}
|
|
1722
|
+
const timestamp = this.parse8601(this.safeString(settlement, 'settleDate'));
|
|
1723
|
+
let settlePrice = undefined;
|
|
1724
|
+
if (won !== undefined) {
|
|
1725
|
+
settlePrice = (won === true) ? 1 : 0;
|
|
1726
|
+
}
|
|
1727
|
+
return {
|
|
1728
|
+
'info': trade,
|
|
1729
|
+
'id': this.safeString(trade, 'tradeId'),
|
|
1730
|
+
'timestamp': timestamp,
|
|
1731
|
+
'datetime': this.iso8601(timestamp),
|
|
1732
|
+
'outcome': this.safeString(outcomeObj, 'outcome', outcomeId),
|
|
1733
|
+
'outcomeId': this.safeString(outcomeObj, 'outcomeId', outcomeId),
|
|
1734
|
+
'market': this.safeString(outcomeObj, 'market'),
|
|
1735
|
+
'event': undefined,
|
|
1736
|
+
'result': resultLabel,
|
|
1737
|
+
'won': won,
|
|
1738
|
+
'amount': this.parseNumber(stake),
|
|
1739
|
+
'price': settlePrice,
|
|
1740
|
+
'cost': this.parseNumber(stake),
|
|
1741
|
+
'payout': this.parseNumber(payout),
|
|
1742
|
+
'pnl': this.parseNumber(pnl),
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* @ignore
|
|
1747
|
+
* @method
|
|
1748
|
+
* @name sxbet#fetchSxbetBookSnapshot
|
|
1749
|
+
* @description fetches the v3 aggregated order book snapshot of one market ({outcomeOne, outcomeTwo, version} price levels)
|
|
1750
|
+
* @see https://docs.sx.bet/api-reference/get-orderbook-snapshot
|
|
1751
|
+
* @param {string} marketHash the market hash
|
|
1752
|
+
* @returns {object} the raw snapshot data
|
|
1753
|
+
*/
|
|
1754
|
+
async fetchSxbetBookSnapshot(marketHash) {
|
|
1755
|
+
const request = { 'marketHash': marketHash };
|
|
1756
|
+
const response = await this.sxbetPublicGetOrderbookV3Snapshot(request);
|
|
1757
|
+
return this.safeDict(response, 'data', {});
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* @method
|
|
1761
|
+
* @name sxbet#fetchTicker
|
|
1762
|
+
* @description fetches the current best resting odds for a single sx.bet outcome. sx.bet is a peer-to-peer odds book (no matched-trade tape or candles), so bid/ask are the best (highest) percentageOdds resting on this outcome's own side and its mirror (1 - best percentageOdds resting on the opposite outcome)
|
|
1763
|
+
* @see https://docs.sx.bet/api-reference/get-orderbook-snapshot
|
|
1764
|
+
* @param {string} outcome unified outcome handle or outcomeId (marketHash or marketHash + '-2')
|
|
1765
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1766
|
+
* @returns {object} a [prediction ticker structure](https://docs.ccxt.com/#/?id=prediction-ticker-structure)
|
|
1767
|
+
*/
|
|
1768
|
+
async fetchTicker(outcome, params = {}) {
|
|
1769
|
+
await this.loadOutcome(outcome);
|
|
1770
|
+
const outcomeObj = this.outcome(outcome);
|
|
1771
|
+
const marketHash = this.safeString(outcomeObj['info'], 'marketHash');
|
|
1772
|
+
// the book snapshot is public and carries the same top of book - the batched best-odds
|
|
1773
|
+
// route needs an apiKey, so it only pays off for the multi-market path
|
|
1774
|
+
const snapshot = await this.fetchSxbetBookSnapshot(marketHash);
|
|
1775
|
+
const raw = this.parseSxbetSnapshotBestOdds(snapshot);
|
|
1776
|
+
return this.parsePredictionTicker(raw, outcomeObj);
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* @ignore
|
|
1780
|
+
* @method
|
|
1781
|
+
* @name sxbet#fetchSxbetBestOdds
|
|
1782
|
+
* @description fetches the best resting level of both sides for a set of markets in one request
|
|
1783
|
+
* @see https://docs.sx.bet/api-reference/get-best-odds-v3
|
|
1784
|
+
* @param {string[]} marketHashes the market hashes to query, at most 100 per call
|
|
1785
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1786
|
+
* @returns {object[]} the raw bestOdds rows ({marketHash, outcomeOne, outcomeTwo})
|
|
1787
|
+
*/
|
|
1788
|
+
async fetchSxbetBestOdds(marketHashes, params = {}) {
|
|
1789
|
+
const request = { 'marketHashes': marketHashes.join(',') };
|
|
1790
|
+
const response = await this.sxbetPrivateGetOrdersV3OddsBest(this.extend(request, params));
|
|
1791
|
+
const data = this.safeDict(response, 'data', {});
|
|
1792
|
+
return this.safeList(data, 'bestOdds', []);
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* @ignore
|
|
1796
|
+
* @method
|
|
1797
|
+
* @name sxbet#parseSxbetSnapshotBestOdds
|
|
1798
|
+
* @description reduces a v3 book snapshot to the legacy best-odds shape the ticker parser consumes ({marketHash, outcomeOne: {percentageOdds}, outcomeTwo: {percentageOdds}})
|
|
1799
|
+
* @param {object} snapshot the raw snapshot data
|
|
1800
|
+
* @returns {object} the best-odds shaped dict
|
|
1801
|
+
*/
|
|
1802
|
+
parseSxbetSnapshotBestOdds(snapshot) {
|
|
1803
|
+
// levels arrive sorted best-first; the best level's odds mirror the v2 best-odds fields
|
|
1804
|
+
const oneLevels = this.safeList(snapshot, 'outcomeOne', []);
|
|
1805
|
+
const twoLevels = this.safeList(snapshot, 'outcomeTwo', []);
|
|
1806
|
+
const bestOne = this.safeDict(oneLevels, 0, {});
|
|
1807
|
+
const bestTwo = this.safeDict(twoLevels, 0, {});
|
|
1808
|
+
return {
|
|
1809
|
+
'marketHash': this.safeString(snapshot, 'marketHash'),
|
|
1810
|
+
'outcomeOne': { 'percentageOdds': this.safeString(bestOne, 'percentageOdds') },
|
|
1811
|
+
'outcomeTwo': { 'percentageOdds': this.safeString(bestTwo, 'percentageOdds') },
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* @method
|
|
1816
|
+
* @name sxbet#fetchTickers
|
|
1817
|
+
* @description fetches the current best resting odds for multiple sx.bet outcomes, one book snapshot per market
|
|
1818
|
+
* @see https://docs.sx.bet/api-reference/get-orderbook-snapshot
|
|
1819
|
+
* @param {string[]} outcomes unified outcomes - required: sx.bet has thousands of markets and no endpoint returning all of them at once
|
|
1820
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1821
|
+
* @returns {object} a dictionary of [prediction ticker structures](https://docs.ccxt.com/#/?id=prediction-ticker-structure) indexed by outcome
|
|
1822
|
+
*/
|
|
1823
|
+
async fetchTickers(outcomes = undefined, params = {}) {
|
|
1824
|
+
if (outcomes === undefined) {
|
|
1825
|
+
throw new ArgumentsRequired(this.id + ' fetchTickers() requires an outcomes argument - sx.bet has thousands of markets and serves best odds per market list');
|
|
1826
|
+
}
|
|
1827
|
+
const outcomesList = outcomes;
|
|
1828
|
+
const outcomesLength = outcomesList.length;
|
|
1829
|
+
const hashesOrder = [];
|
|
1830
|
+
const seenHashes = {};
|
|
1831
|
+
for (let i = 0; i < outcomesLength; i++) {
|
|
1832
|
+
await this.loadOutcome(outcomesList[i]);
|
|
1833
|
+
const outcomeObj = this.outcome(outcomesList[i]);
|
|
1834
|
+
const marketHash = this.safeString(outcomeObj['info'], 'marketHash', '');
|
|
1835
|
+
if (this.safeBool(seenHashes, marketHash) === undefined) {
|
|
1836
|
+
seenHashes[marketHash] = true;
|
|
1837
|
+
hashesOrder.push(marketHash);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
// both outcomes of a market share one row, and the venue takes at most 100 hashes per call.
|
|
1841
|
+
// the batched route is apiKey-authenticated, so fall back to the public per-market
|
|
1842
|
+
// snapshots when the instance carries no credentials
|
|
1843
|
+
const rowsByHash = {};
|
|
1844
|
+
const hashesLength = hashesOrder.length;
|
|
1845
|
+
const hasApiKey = !this.isEmptyString(this.apiKey);
|
|
1846
|
+
if (!hasApiKey) {
|
|
1847
|
+
for (let i = 0; i < hashesLength; i++) {
|
|
1848
|
+
const marketHash = hashesOrder[i];
|
|
1849
|
+
const snapshot = await this.fetchSxbetBookSnapshot(marketHash);
|
|
1850
|
+
rowsByHash[marketHash] = this.parseSxbetSnapshotBestOdds(snapshot);
|
|
1851
|
+
}
|
|
1852
|
+
return this.parseSxbetTickersByHash(outcomesList, rowsByHash);
|
|
1853
|
+
}
|
|
1854
|
+
const chunkSize = this.safeInteger(this.options, 'bestOddsBatchSize', 100);
|
|
1855
|
+
const chunkCount = this.parseToInt(this.sum(hashesLength, chunkSize - 1) / chunkSize);
|
|
1856
|
+
for (let c = 0; c < chunkCount; c++) {
|
|
1857
|
+
const start = c * chunkSize;
|
|
1858
|
+
let end = start + chunkSize;
|
|
1859
|
+
if (end > hashesLength) {
|
|
1860
|
+
end = hashesLength;
|
|
1861
|
+
}
|
|
1862
|
+
const chunk = this.arraySlice(hashesOrder, start, end);
|
|
1863
|
+
const rows = await this.fetchSxbetBestOdds(chunk, params);
|
|
1864
|
+
const rowsLength = rows.length;
|
|
1865
|
+
for (let j = 0; j < rowsLength; j++) {
|
|
1866
|
+
const row = rows[j];
|
|
1867
|
+
const rowHash = this.safeString(row, 'marketHash');
|
|
1868
|
+
if (rowHash !== undefined) {
|
|
1869
|
+
rowsByHash[rowHash] = row;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return this.parseSxbetTickersByHash(outcomesList, rowsByHash);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* @ignore
|
|
1877
|
+
* @method
|
|
1878
|
+
* @name sxbet#parseSxbetTickersByHash
|
|
1879
|
+
* @description assembles the unified tickers dict from best-odds shaped rows keyed by market hash
|
|
1880
|
+
* @param {string[]} outcomesList the requested unified outcomes
|
|
1881
|
+
* @param {object} rowsByHash best-odds rows indexed by market hash
|
|
1882
|
+
* @returns {object} a dictionary of [prediction ticker structures](https://docs.ccxt.com/#/?id=prediction-ticker-structure) indexed by outcome
|
|
1883
|
+
*/
|
|
1884
|
+
parseSxbetTickersByHash(outcomesList, rowsByHash) {
|
|
1885
|
+
const result = {};
|
|
1886
|
+
const outcomesLength = outcomesList.length;
|
|
1887
|
+
for (let i = 0; i < outcomesLength; i++) {
|
|
1888
|
+
const outcomeObj = this.outcome(outcomesList[i]);
|
|
1889
|
+
const marketHash = this.safeString(outcomeObj['info'], 'marketHash', '');
|
|
1890
|
+
const raw = this.safeDict(rowsByHash, marketHash);
|
|
1891
|
+
if (raw === undefined) {
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
const ticker = this.parsePredictionTicker(raw, outcomeObj);
|
|
1895
|
+
const sym = this.safeString(ticker, 'outcome');
|
|
1896
|
+
if (sym !== undefined) {
|
|
1897
|
+
result[sym] = ticker;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return result;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* @ignore
|
|
1904
|
+
* @method
|
|
1905
|
+
* @name sxbet#parsePredictionTicker
|
|
1906
|
+
* @description parses one /orders/odds/best entry into a unified ticker for one side of the market
|
|
1907
|
+
* @param {object} raw one bestOdds entry ({ marketHash, baseToken, outcomeOne: { percentageOdds, updatedAt }, outcomeTwo: {...} })
|
|
1908
|
+
* @param {object} [market] the outcome object the ticker belongs to
|
|
1909
|
+
* @returns {object} a [prediction ticker structure](https://docs.ccxt.com/#/?id=prediction-ticker-structure)
|
|
1910
|
+
*/
|
|
1911
|
+
parsePredictionTicker(raw, market = undefined) {
|
|
1912
|
+
//
|
|
1913
|
+
// {
|
|
1914
|
+
// "marketHash": "0x48ce0ef287c59fb3bb3ca6deb80de0d5791eb761ea8e16d6f03c8e93ef28d1de",
|
|
1915
|
+
// "baseToken": "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B",
|
|
1916
|
+
// "outcomeOne": { "percentageOdds": "50250000000000000000", "updatedAt": 1785697831141 },
|
|
1917
|
+
// "outcomeTwo": { "percentageOdds": "48000000000000000000", "updatedAt": 1785699649869 }
|
|
1918
|
+
// }
|
|
1919
|
+
//
|
|
1920
|
+
const marketAny = market;
|
|
1921
|
+
const outcomeObj = this.safeOutcome(this.safeString(marketAny, 'outcome'), marketAny);
|
|
1922
|
+
const outcomeId = this.safeString(outcomeObj, 'outcomeId');
|
|
1923
|
+
const marketHash = this.safeString(raw, 'marketHash');
|
|
1924
|
+
const isOutcomeOne = (outcomeId === marketHash);
|
|
1925
|
+
const outcomeOneOdds = this.safeDict(raw, 'outcomeOne', {});
|
|
1926
|
+
const outcomeTwoOdds = this.safeDict(raw, 'outcomeTwo', {});
|
|
1927
|
+
const ownOdds = (isOutcomeOne) ? outcomeOneOdds : outcomeTwoOdds;
|
|
1928
|
+
const oppositeOdds = (isOutcomeOne) ? outcomeTwoOdds : outcomeOneOdds;
|
|
1929
|
+
// percentageOdds is the maker's own implied probability * 1e20 (sx.bet protocol format);
|
|
1930
|
+
// the opposite side's best resting maker mirrors into this outcome's ask via 1 - p
|
|
1931
|
+
const oneDenom = '100000000000000000000';
|
|
1932
|
+
const ownPercentage = this.safeString(ownOdds, 'percentageOdds');
|
|
1933
|
+
const oppositePercentage = this.safeString(oppositeOdds, 'percentageOdds');
|
|
1934
|
+
const bid = (ownPercentage !== undefined) ? this.parseNumber(Precise.stringDiv(ownPercentage, oneDenom)) : undefined;
|
|
1935
|
+
const ask = (oppositePercentage !== undefined) ? this.parseNumber(Precise.stringSub('1', Precise.stringDiv(oppositePercentage, oneDenom))) : undefined;
|
|
1936
|
+
const updatedAt = this.safeInteger(ownOdds, 'updatedAt');
|
|
1937
|
+
const now = this.milliseconds();
|
|
1938
|
+
const timestamp = (updatedAt !== undefined) ? updatedAt : now;
|
|
1939
|
+
let average = undefined;
|
|
1940
|
+
if ((bid !== undefined) && (ask !== undefined)) {
|
|
1941
|
+
average = this.parseNumber(Precise.stringDiv(Precise.stringAdd(this.numberToString(bid), this.numberToString(ask)), '2'));
|
|
1942
|
+
}
|
|
1943
|
+
return this.safePredictionTicker({
|
|
1944
|
+
'outcome': this.safeString(outcomeObj, 'outcome'),
|
|
1945
|
+
'outcomeId': outcomeId,
|
|
1946
|
+
'label': this.safeString(outcomeObj, 'label'),
|
|
1947
|
+
'market': this.safeString(outcomeObj, 'market'),
|
|
1948
|
+
'timestamp': timestamp,
|
|
1949
|
+
'datetime': this.iso8601(timestamp),
|
|
1950
|
+
'high': undefined,
|
|
1951
|
+
'low': undefined,
|
|
1952
|
+
'bid': bid,
|
|
1953
|
+
'bidVolume': undefined,
|
|
1954
|
+
'ask': ask,
|
|
1955
|
+
'askVolume': undefined,
|
|
1956
|
+
'open': undefined,
|
|
1957
|
+
'close': bid,
|
|
1958
|
+
'last': bid,
|
|
1959
|
+
'change': undefined,
|
|
1960
|
+
'percentage': undefined,
|
|
1961
|
+
'average': average,
|
|
1962
|
+
'baseVolume': undefined,
|
|
1963
|
+
'quoteVolume': undefined,
|
|
1964
|
+
'info': raw,
|
|
1965
|
+
}, market);
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* @method
|
|
1969
|
+
* @name sxbet#fetchOrderBook
|
|
1970
|
+
* @description fetches the resting maker order book for a single sx.bet outcome. bids are maker orders already betting on this outcome (priced at each maker's own implied probability, sized by their remaining stake); asks mirror the opposite outcome's maker orders (price = 1 - their implied probability, sized by how much a taker could bet against them, per sx.bet's remaining-taker-space formula) — the same YES/NO-style mirrored construction used across this codebase's other binary prediction venues
|
|
1971
|
+
* @see https://docs.sx.bet/api-reference/get-orderbook-snapshot
|
|
1972
|
+
* @param {string} outcome unified outcome handle or outcomeId
|
|
1973
|
+
* @param {int} [limit] the maximum number of bids/asks to return
|
|
1974
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
1975
|
+
* @returns {object} a [prediction order book structure](https://docs.ccxt.com/#/?id=prediction-order-book-structure)
|
|
1976
|
+
*/
|
|
1977
|
+
async fetchOrderBook(outcome, limit = undefined, params = {}) {
|
|
1978
|
+
await this.loadOutcome(outcome);
|
|
1979
|
+
const outcomeObj = this.outcome(outcome);
|
|
1980
|
+
const marketHash = this.safeString(outcomeObj['info'], 'marketHash');
|
|
1981
|
+
const outcomeId = this.safeString(outcomeObj, 'outcomeId');
|
|
1982
|
+
const isOutcomeOne = (outcomeId === marketHash);
|
|
1983
|
+
const request = { 'marketHash': marketHash };
|
|
1984
|
+
const response = await this.sxbetPublicGetOrderbookV3Snapshot(this.extend(request, params));
|
|
1985
|
+
const snapshot = this.safeDict(response, 'data', {});
|
|
1986
|
+
const sides = this.parseSxbetV3BookSides(snapshot, isOutcomeOne);
|
|
1987
|
+
let sortedBids = this.safeList(sides, 'bids', []);
|
|
1988
|
+
let sortedAsks = this.safeList(sides, 'asks', []);
|
|
1989
|
+
if (limit !== undefined) {
|
|
1990
|
+
const bidsLength = sortedBids.length;
|
|
1991
|
+
let bidsEnd = limit;
|
|
1992
|
+
if (bidsEnd > bidsLength) {
|
|
1993
|
+
bidsEnd = bidsLength;
|
|
1994
|
+
}
|
|
1995
|
+
sortedBids = this.arraySlice(sortedBids, 0, bidsEnd);
|
|
1996
|
+
const asksLength = sortedAsks.length;
|
|
1997
|
+
let asksEnd = limit;
|
|
1998
|
+
if (asksEnd > asksLength) {
|
|
1999
|
+
asksEnd = asksLength;
|
|
2000
|
+
}
|
|
2001
|
+
sortedAsks = this.arraySlice(sortedAsks, 0, asksEnd);
|
|
2002
|
+
}
|
|
2003
|
+
const timestamp = this.milliseconds();
|
|
2004
|
+
return this.safePredictionOrderBook({
|
|
2005
|
+
'outcome': this.safeString(outcomeObj, 'outcome', outcome),
|
|
2006
|
+
'bids': sortedBids,
|
|
2007
|
+
'asks': sortedAsks,
|
|
2008
|
+
'timestamp': timestamp,
|
|
2009
|
+
'datetime': this.iso8601(timestamp),
|
|
2010
|
+
'nonce': undefined,
|
|
2011
|
+
}, outcomeObj);
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* @ignore
|
|
2015
|
+
* @method
|
|
2016
|
+
* @name sxbet#parseSxbetV3BookSides
|
|
2017
|
+
* @description converts a v3 aggregated book snapshot into sorted [price, amount] bid/ask levels for one outcome; shared by fetchOrderBook and the websocket book handler
|
|
2018
|
+
* @param {object} snapshot the raw snapshot ({outcomeOne, outcomeTwo} level lists of {percentageOdds, size})
|
|
2019
|
+
* @param {boolean} isOutcomeOne whether the book is built for the market's outcome one
|
|
2020
|
+
* @returns {object} a dict with sorted 'bids' and 'asks' lists
|
|
2021
|
+
*/
|
|
2022
|
+
parseSxbetV3BookSides(snapshot, isOutcomeOne) {
|
|
2023
|
+
const oneDenom = '100000000000000000000';
|
|
2024
|
+
const usdcDecimals = '1000000';
|
|
2025
|
+
const ownLevels = (isOutcomeOne) ? this.safeList(snapshot, 'outcomeOne', []) : this.safeList(snapshot, 'outcomeTwo', []);
|
|
2026
|
+
const oppositeLevels = (isOutcomeOne) ? this.safeList(snapshot, 'outcomeTwo', []) : this.safeList(snapshot, 'outcomeOne', []);
|
|
2027
|
+
const bids = [];
|
|
2028
|
+
const ownLevelsLength = ownLevels.length;
|
|
2029
|
+
for (let i = 0; i < ownLevelsLength; i++) {
|
|
2030
|
+
const level = ownLevels[i];
|
|
2031
|
+
const percentageOdds = this.safeString(level, 'percentageOdds');
|
|
2032
|
+
const size = this.safeString(level, 'size', '0');
|
|
2033
|
+
const price = this.parseNumber(Precise.stringDiv(percentageOdds, oneDenom));
|
|
2034
|
+
const amount = this.parseNumber(Precise.stringDiv(size, usdcDecimals, 6));
|
|
2035
|
+
bids.push([price, amount]);
|
|
2036
|
+
}
|
|
2037
|
+
const asks = [];
|
|
2038
|
+
const oppositeLevelsLength = oppositeLevels.length;
|
|
2039
|
+
for (let i = 0; i < oppositeLevelsLength; i++) {
|
|
2040
|
+
const level = oppositeLevels[i];
|
|
2041
|
+
const percentageOdds = this.safeString(level, 'percentageOdds');
|
|
2042
|
+
const size = this.safeString(level, 'size', '0');
|
|
2043
|
+
// the opposite side's resting stake mirrors into this outcome's ask - the price is the
|
|
2044
|
+
// complement, and the takeable amount follows the remaining-taker-space formula
|
|
2045
|
+
const price = this.parseNumber(Precise.stringSub('1', Precise.stringDiv(percentageOdds, oneDenom)));
|
|
2046
|
+
const ratio = Precise.stringDiv(oneDenom, percentageOdds, 12);
|
|
2047
|
+
const remainingTaker = Precise.stringSub(Precise.stringMul(size, ratio), size);
|
|
2048
|
+
const amount = this.parseNumber(Precise.stringDiv(remainingTaker, usdcDecimals, 6));
|
|
2049
|
+
asks.push([price, amount]);
|
|
2050
|
+
}
|
|
2051
|
+
return {
|
|
2052
|
+
'bids': this.sortBy(bids, 0, true),
|
|
2053
|
+
'asks': this.sortBy(asks, 0),
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
requestId(url) {
|
|
2057
|
+
const existing = this.safeValue(this.options, 'requestId');
|
|
2058
|
+
if (existing === undefined) {
|
|
2059
|
+
this.options['requestId'] = this.createSafeDictionary();
|
|
2060
|
+
}
|
|
2061
|
+
const options = this.options['requestId'];
|
|
2062
|
+
const previousValue = this.safeInteger(options, url, 0);
|
|
2063
|
+
const newValue = this.sum(previousValue, 1);
|
|
2064
|
+
if (url !== undefined) {
|
|
2065
|
+
this.options['requestId'][url] = newValue;
|
|
2066
|
+
}
|
|
2067
|
+
return newValue;
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* @ignore
|
|
2071
|
+
* @method
|
|
2072
|
+
* @name sxbet#registerSxbetWsRequest
|
|
2073
|
+
* @description tracks an outgoing Centrifugo connect/subscribe command by its request id, so an error reply can be correlated back to the awaiting future and its registered subscription hash (see handleCentrifugoFrame)
|
|
2074
|
+
* @param {int} requestId the id sent with the command
|
|
2075
|
+
* @param {string} messageHash the future hash the caller awaits
|
|
2076
|
+
* @param {string} subscription the subscription hash registered by watch() ('connect' or the channel name)
|
|
2077
|
+
*/
|
|
2078
|
+
registerSxbetWsRequest(requestId, messageHash, subscription) {
|
|
2079
|
+
const existing = this.safeDict(this.options, 'wsPendingRequests');
|
|
2080
|
+
if (existing === undefined) {
|
|
2081
|
+
this.options['wsPendingRequests'] = this.createSafeDictionary();
|
|
2082
|
+
}
|
|
2083
|
+
const requestIdString = this.numberToString(requestId);
|
|
2084
|
+
this.options['wsPendingRequests'][requestIdString] = { 'messageHash': messageHash, 'subscription': subscription };
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* @ignore
|
|
2088
|
+
* @method
|
|
2089
|
+
* @name sxbet#fetchSxbetRealtimeToken
|
|
2090
|
+
* @description fetches the short-lived Centrifugo connection token from the relayer; requires the apiKey credential (X-Api-Key header)
|
|
2091
|
+
* @see https://docs.sx.bet/developers/realtime-initialization
|
|
2092
|
+
* @returns {string} the JWT connection token
|
|
2093
|
+
*/
|
|
2094
|
+
async fetchSxbetRealtimeToken() {
|
|
2095
|
+
if (this.apiKey === undefined) {
|
|
2096
|
+
throw new ArgumentsRequired(this.id + ' websocket streaming requires the apiKey credential - the realtime token endpoint authenticates with the X-Api-Key header');
|
|
2097
|
+
}
|
|
2098
|
+
const response = await this.sxbetPrivateGetUserRealtimeTokenV3ApiKey();
|
|
2099
|
+
const data = this.safeDict(response, 'data', {});
|
|
2100
|
+
return this.safeString2(data, 'token', 'realtimeToken', this.safeString(response, 'token'));
|
|
2101
|
+
}
|
|
2102
|
+
async connectSxbetCentrifugo(url) {
|
|
2103
|
+
// Centrifugo requires a token-carrying connect command before any subscribe. This sends it once
|
|
2104
|
+
// per connection and resolves when the connect reply arrives (see handleCentrifugoFrame). The base
|
|
2105
|
+
// clears client.subscriptions on reconnect, so an absent 'connect' marker means a fresh handshake.
|
|
2106
|
+
const client = this.client(url);
|
|
2107
|
+
const connectSent = this.safeValue(client.subscriptions, 'connect');
|
|
2108
|
+
if (connectSent === undefined) {
|
|
2109
|
+
this.options['wsConnected'] = false;
|
|
2110
|
+
const token = await this.fetchSxbetRealtimeToken();
|
|
2111
|
+
const requestId = this.requestId(url);
|
|
2112
|
+
this.registerSxbetWsRequest(requestId, 'centrifugoConnected', 'connect');
|
|
2113
|
+
const connectMsg = { 'connect': { 'token': token, 'name': 'ccxt' }, 'id': requestId };
|
|
2114
|
+
return await this.watch(url, 'centrifugoConnected', connectMsg, 'connect');
|
|
2115
|
+
}
|
|
2116
|
+
if (this.safeBool(this.options, 'wsConnected', false)) {
|
|
2117
|
+
// the connect reply already arrived on the current connection - safe to subscribe immediately
|
|
2118
|
+
return undefined;
|
|
2119
|
+
}
|
|
2120
|
+
// connect is in flight, sent by a concurrent subscribe - wait on the shared reply future
|
|
2121
|
+
return await client.future('centrifugoConnected');
|
|
2122
|
+
}
|
|
2123
|
+
async pong(client, message = undefined) {
|
|
2124
|
+
// Centrifugo server pings are empty frames; reply with the same empty frame to keep the link alive
|
|
2125
|
+
await client.send('{}');
|
|
2126
|
+
}
|
|
2127
|
+
async subscribeSxbetChannel(messageHash, channel) {
|
|
2128
|
+
const url = this.safeString(this.urls['api'], 'ws');
|
|
2129
|
+
// finish the connect handshake first so the subscribe frame follows the connect reply
|
|
2130
|
+
await this.connectSxbetCentrifugo(url);
|
|
2131
|
+
const requestId = this.requestId(url);
|
|
2132
|
+
this.registerSxbetWsRequest(requestId, messageHash, channel);
|
|
2133
|
+
const subscribeMsg = { 'subscribe': { 'channel': channel }, 'id': requestId };
|
|
2134
|
+
return await this.watch(url, messageHash, subscribeMsg, channel);
|
|
2135
|
+
}
|
|
2136
|
+
handleMessage(client, message) {
|
|
2137
|
+
// Centrifugo packs several commands per frame joined by newlines; a multi-command frame fails the
|
|
2138
|
+
// base JSON.parse and arrives here in raw-string form, a single command arrives already parsed
|
|
2139
|
+
if (typeof message === 'string') {
|
|
2140
|
+
const lines = message.split('\n');
|
|
2141
|
+
const linesLength = lines.length;
|
|
2142
|
+
for (let i = 0; i < linesLength; i++) {
|
|
2143
|
+
const line = lines[i];
|
|
2144
|
+
if (line.length > 0) {
|
|
2145
|
+
const parsed = JSON.parse(line);
|
|
2146
|
+
this.handleCentrifugoFrame(client, parsed);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
this.handleCentrifugoFrame(client, message);
|
|
2152
|
+
}
|
|
2153
|
+
handleCentrifugoFrame(client, msg) {
|
|
2154
|
+
const keys = Object.keys(msg);
|
|
2155
|
+
const keysLength = keys.length;
|
|
2156
|
+
if (keysLength === 0) {
|
|
2157
|
+
this.spawn(this.pong, client, msg);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
const requestIdString = this.safeString(msg, 'id');
|
|
2161
|
+
const pendingRequests = this.safeDict(this.options, 'wsPendingRequests', {});
|
|
2162
|
+
const pendingEntry = this.safeDict(pendingRequests, requestIdString);
|
|
2163
|
+
const errorReply = this.safeDict(msg, 'error');
|
|
2164
|
+
if (errorReply !== undefined) {
|
|
2165
|
+
// a rejected connect (bad or expired realtime token) or subscribe (unauthorized or
|
|
2166
|
+
// unknown channel) - fail the awaiting future and clear the subscription hash that
|
|
2167
|
+
// watch() registered at send time, so the next call re-sends the command instead
|
|
2168
|
+
// of waiting forever behind a marker that will never be served
|
|
2169
|
+
const error = new ExchangeError(this.id + ' ' + this.json(msg));
|
|
2170
|
+
if (pendingEntry !== undefined) {
|
|
2171
|
+
const failedHash = this.safeString(pendingEntry, 'messageHash');
|
|
2172
|
+
const subscription = this.safeString(pendingEntry, 'subscription');
|
|
2173
|
+
if (subscription === 'connect') {
|
|
2174
|
+
this.options['wsConnected'] = false;
|
|
2175
|
+
}
|
|
2176
|
+
if ((subscription !== undefined) && (subscription in client.subscriptions)) {
|
|
2177
|
+
delete client.subscriptions[subscription];
|
|
2178
|
+
}
|
|
2179
|
+
this.options['wsPendingRequests'] = this.omit(pendingRequests, requestIdString);
|
|
2180
|
+
client.reject(error, failedHash);
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
// an error that cannot be correlated to one command fails the whole connection
|
|
2184
|
+
client.reject(error);
|
|
2185
|
+
}
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
if (pendingEntry !== undefined) {
|
|
2189
|
+
// a successful command ack - the tracked request has served its purpose
|
|
2190
|
+
this.options['wsPendingRequests'] = this.omit(pendingRequests, requestIdString);
|
|
2191
|
+
}
|
|
2192
|
+
const connectReply = this.safeDict(msg, 'connect');
|
|
2193
|
+
if (connectReply !== undefined) {
|
|
2194
|
+
// connect acknowledged - unblock connectSxbetCentrifugo so channel subscribes can be sent
|
|
2195
|
+
this.options['wsConnected'] = true;
|
|
2196
|
+
client.resolve(true, 'centrifugoConnected');
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
const push = this.safeDict(msg, 'push');
|
|
2200
|
+
if (push === undefined) {
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const channel = this.safeString(push, 'channel');
|
|
2204
|
+
if (channel === undefined) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const pub = this.safeDict(push, 'pub', {});
|
|
2208
|
+
const data = this.safeValue(pub, 'data');
|
|
2209
|
+
if (data === undefined) {
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
// rows arrive either shaped like one object or like an array of objects - normalize to a list
|
|
2213
|
+
let rows = [];
|
|
2214
|
+
if (Array.isArray(data)) {
|
|
2215
|
+
rows = data;
|
|
2216
|
+
}
|
|
2217
|
+
else {
|
|
2218
|
+
rows = [data];
|
|
2219
|
+
}
|
|
2220
|
+
const parts = channel.split(':');
|
|
2221
|
+
const channelType = this.safeString(parts, 0);
|
|
2222
|
+
if (channelType === 'orderbook_v3') {
|
|
2223
|
+
this.handleOrderBook(client, rows);
|
|
2224
|
+
}
|
|
2225
|
+
else if (channelType === 'best_odds_v3') {
|
|
2226
|
+
this.handleTicker(client, rows);
|
|
2227
|
+
}
|
|
2228
|
+
else if (channelType === 'recent_trades_v3') {
|
|
2229
|
+
this.handleTrades(client, rows);
|
|
2230
|
+
}
|
|
2231
|
+
else if (channelType === 'account') {
|
|
2232
|
+
// per-account channels: account:orders_v3_#addr / account:fills_v3_#addr
|
|
2233
|
+
if (channel.indexOf('orders_v3') >= 0) {
|
|
2234
|
+
this.handleOrder(client, rows);
|
|
2235
|
+
}
|
|
2236
|
+
else if (channel.indexOf('fills_v3') >= 0) {
|
|
2237
|
+
this.handleMyTrade(client, rows);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* @method
|
|
2243
|
+
* @name sxbet#watchOrderBook
|
|
2244
|
+
* @description streams the order book of an outcome - the v3 channel publishes the entire aggregated book on every update with a monotonic version, so each message replaces the held book
|
|
2245
|
+
* @see https://docs.sx.bet/api-reference/channel-orderbook-v3
|
|
2246
|
+
* @param {string} outcome unified outcome or outcome token id
|
|
2247
|
+
* @param {int} [limit] the maximum number of order book entries to return
|
|
2248
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
2249
|
+
* @returns {object} a [prediction order book structure](https://docs.ccxt.com/#/?id=prediction-order-book-structure)
|
|
2250
|
+
*/
|
|
2251
|
+
async watchOrderBook(outcome, limit = undefined, params = {}) {
|
|
2252
|
+
await this.loadOutcome(outcome);
|
|
2253
|
+
const outcomeObj = this.outcome(outcome);
|
|
2254
|
+
const sym = this.safeString(outcomeObj, 'outcome');
|
|
2255
|
+
const marketHash = this.safeString(outcomeObj['info'], 'marketHash');
|
|
2256
|
+
const channel = 'orderbook_v3:' + marketHash;
|
|
2257
|
+
const messageHash = 'orderbook::' + sym;
|
|
2258
|
+
const url = this.safeString(this.urls['api'], 'ws');
|
|
2259
|
+
await this.connectSxbetCentrifugo(url);
|
|
2260
|
+
const client = this.client(url);
|
|
2261
|
+
const isNewSubscription = this.safeValue(client.subscriptions, channel) === undefined;
|
|
2262
|
+
if (this.safeValue(this.orderbooks, sym) === undefined) {
|
|
2263
|
+
// seed from the REST snapshot so the book is served before the first publication
|
|
2264
|
+
const watchedBooks = this.safeDict(this.options, 'wsWatchedBooks');
|
|
2265
|
+
if (watchedBooks === undefined) {
|
|
2266
|
+
this.options['wsWatchedBooks'] = this.createSafeDictionary();
|
|
2267
|
+
}
|
|
2268
|
+
this.options['wsWatchedBooks'][sym] = marketHash;
|
|
2269
|
+
const snapshot = await this.fetchSxbetBookSnapshot(marketHash);
|
|
2270
|
+
this.applySxbetWsSnapshot(snapshot);
|
|
2271
|
+
if (this.safeValue(this.orderbooks, sym) === undefined) {
|
|
2272
|
+
const emptyBook = this.orderBook({});
|
|
2273
|
+
this.orderbooks[sym] = emptyBook;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
const requestId = this.requestId(url);
|
|
2277
|
+
this.registerSxbetWsRequest(requestId, messageHash, channel);
|
|
2278
|
+
const subscribeMsg = { 'subscribe': { 'channel': channel }, 'id': requestId };
|
|
2279
|
+
const future = this.watch(url, messageHash, subscribeMsg, channel);
|
|
2280
|
+
if (isNewSubscription) {
|
|
2281
|
+
// return the freshly-seeded book immediately instead of blocking until the next publication
|
|
2282
|
+
client.resolve(this.safeValue(this.orderbooks, sym), messageHash);
|
|
2283
|
+
}
|
|
2284
|
+
const orderbook = await future;
|
|
2285
|
+
return orderbook.limit();
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* @ignore
|
|
2289
|
+
* @method
|
|
2290
|
+
* @name sxbet#applySxbetWsSnapshot
|
|
2291
|
+
* @description applies one full v3 book publication ({marketHash, version, outcomeOne, outcomeTwo}) to every watched outcome of that market, honoring the monotonic version
|
|
2292
|
+
* @param {object} snapshot the raw book publication
|
|
2293
|
+
* @returns {string[]} the outcome handles whose books were refreshed
|
|
2294
|
+
*/
|
|
2295
|
+
applySxbetWsSnapshot(snapshot) {
|
|
2296
|
+
const marketHash = this.safeString(snapshot, 'marketHash');
|
|
2297
|
+
const version = this.safeString(snapshot, 'version', '');
|
|
2298
|
+
const refreshed = [];
|
|
2299
|
+
if (marketHash === undefined) {
|
|
2300
|
+
return refreshed;
|
|
2301
|
+
}
|
|
2302
|
+
const versionsAll = this.safeDict(this.options, 'wsBookVersions');
|
|
2303
|
+
if (versionsAll === undefined) {
|
|
2304
|
+
this.options['wsBookVersions'] = this.createSafeDictionary();
|
|
2305
|
+
}
|
|
2306
|
+
const held = this.safeString(this.options['wsBookVersions'], marketHash, '');
|
|
2307
|
+
// versions are fixed-width numeric strings - replace only on a strictly newer publication
|
|
2308
|
+
if ((held !== '') && (version <= held)) {
|
|
2309
|
+
return refreshed;
|
|
2310
|
+
}
|
|
2311
|
+
this.options['wsBookVersions'][marketHash] = version;
|
|
2312
|
+
const watchedBooks = this.safeDict(this.options, 'wsWatchedBooks', {});
|
|
2313
|
+
const watchedSyms = Object.keys(watchedBooks);
|
|
2314
|
+
const timestamp = this.milliseconds();
|
|
2315
|
+
const watchedSymsLength = watchedSyms.length;
|
|
2316
|
+
for (let i = 0; i < watchedSymsLength; i++) {
|
|
2317
|
+
const sym = watchedSyms[i];
|
|
2318
|
+
if (this.safeString(watchedBooks, sym) !== marketHash) {
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
const outcomeObj = this.outcome(sym);
|
|
2322
|
+
const outcomeId = this.safeString(outcomeObj, 'outcomeId');
|
|
2323
|
+
const isOutcomeOne = (outcomeId === marketHash);
|
|
2324
|
+
const sides = this.parseSxbetV3BookSides(snapshot, isOutcomeOne);
|
|
2325
|
+
let orderbook = this.safeValue(this.orderbooks, sym);
|
|
2326
|
+
if (orderbook === undefined) {
|
|
2327
|
+
orderbook = this.orderBook({});
|
|
2328
|
+
this.orderbooks[sym] = orderbook;
|
|
2329
|
+
}
|
|
2330
|
+
const bookSnapshot = {
|
|
2331
|
+
'bids': this.safeList(sides, 'bids', []),
|
|
2332
|
+
'asks': this.safeList(sides, 'asks', []),
|
|
2333
|
+
'timestamp': timestamp,
|
|
2334
|
+
'datetime': this.iso8601(timestamp),
|
|
2335
|
+
'nonce': undefined,
|
|
2336
|
+
};
|
|
2337
|
+
orderbook.reset(bookSnapshot);
|
|
2338
|
+
refreshed.push(sym);
|
|
2339
|
+
}
|
|
2340
|
+
return refreshed;
|
|
2341
|
+
}
|
|
2342
|
+
handleOrderBook(client, rows) {
|
|
2343
|
+
//
|
|
2344
|
+
// {
|
|
2345
|
+
// "marketHash": "0x...",
|
|
2346
|
+
// "version": "00100000000000010015000",
|
|
2347
|
+
// "outcomeOne": [ { "percentageOdds": "40000000000000000000", "size": "1000000" } ],
|
|
2348
|
+
// "outcomeTwo": []
|
|
2349
|
+
// }
|
|
2350
|
+
//
|
|
2351
|
+
const rowsLength = rows.length;
|
|
2352
|
+
for (let i = 0; i < rowsLength; i++) {
|
|
2353
|
+
const refreshed = this.applySxbetWsSnapshot(rows[i]);
|
|
2354
|
+
const refreshedLength = refreshed.length;
|
|
2355
|
+
for (let j = 0; j < refreshedLength; j++) {
|
|
2356
|
+
const sym = refreshed[j];
|
|
2357
|
+
client.resolve(this.safeValue(this.orderbooks, sym), 'orderbook::' + sym);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* @method
|
|
2363
|
+
* @name sxbet#watchTicker
|
|
2364
|
+
* @description streams best-odds updates of an outcome; the venue channel is global, entries are filtered down to the requested outcome's market
|
|
2365
|
+
* @see https://docs.sx.bet/api-reference/channel-best-odds-v3
|
|
2366
|
+
* @param {string} outcome unified outcome or outcome token id
|
|
2367
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
2368
|
+
* @returns {object} a [prediction ticker structure](https://docs.ccxt.com/#/?id=prediction-ticker-structure)
|
|
2369
|
+
*/
|
|
2370
|
+
async watchTicker(outcome, params = {}) {
|
|
2371
|
+
await this.loadOutcome(outcome);
|
|
2372
|
+
const outcomeObj = this.outcome(outcome);
|
|
2373
|
+
const sym = this.safeString(outcomeObj, 'outcome');
|
|
2374
|
+
const marketHash = this.safeString(outcomeObj['info'], 'marketHash');
|
|
2375
|
+
const messageHash = 'ticker::' + sym;
|
|
2376
|
+
const watchedTickers = this.safeDict(this.options, 'wsWatchedTickers');
|
|
2377
|
+
if (watchedTickers === undefined) {
|
|
2378
|
+
this.options['wsWatchedTickers'] = this.createSafeDictionary();
|
|
2379
|
+
}
|
|
2380
|
+
this.options['wsWatchedTickers'][sym] = marketHash;
|
|
2381
|
+
const url = this.safeString(this.urls['api'], 'ws');
|
|
2382
|
+
await this.connectSxbetCentrifugo(url);
|
|
2383
|
+
const client = this.client(url);
|
|
2384
|
+
const channel = 'best_odds_v3:global';
|
|
2385
|
+
let hydrated = false;
|
|
2386
|
+
if (this.safeValue(this.tickers, sym) === undefined) {
|
|
2387
|
+
// hydrate from the REST snapshot so the first call does not hang until the
|
|
2388
|
+
// market's next top-of-book change - the channel is global, so the seed must fire
|
|
2389
|
+
// for every newly watched market, not only on a fresh subscription
|
|
2390
|
+
const snapshot = await this.fetchSxbetBookSnapshot(marketHash);
|
|
2391
|
+
const raw = this.parseSxbetSnapshotBestOdds(snapshot);
|
|
2392
|
+
const ticker = this.parsePredictionTicker(raw, outcomeObj);
|
|
2393
|
+
this.tickers[sym] = ticker;
|
|
2394
|
+
hydrated = true;
|
|
2395
|
+
}
|
|
2396
|
+
const requestId = this.requestId(url);
|
|
2397
|
+
this.registerSxbetWsRequest(requestId, messageHash, channel);
|
|
2398
|
+
const subscribeMsg = { 'subscribe': { 'channel': channel }, 'id': requestId };
|
|
2399
|
+
const future = this.watch(url, messageHash, subscribeMsg, channel);
|
|
2400
|
+
if (hydrated) {
|
|
2401
|
+
client.resolve(this.safeValue(this.tickers, sym), messageHash);
|
|
2402
|
+
}
|
|
2403
|
+
const tickerResult = await future;
|
|
2404
|
+
return tickerResult;
|
|
2405
|
+
}
|
|
2406
|
+
handleTicker(client, rows) {
|
|
2407
|
+
//
|
|
2408
|
+
// {
|
|
2409
|
+
// "marketHash": "0xbf06...16a2eb",
|
|
2410
|
+
// "outcomeOne": { "percentageOdds": "40000000000000000000", "size": "1000000" },
|
|
2411
|
+
// "outcomeTwo": null
|
|
2412
|
+
// }
|
|
2413
|
+
//
|
|
2414
|
+
const watchedTickers = this.safeDict(this.options, 'wsWatchedTickers', {});
|
|
2415
|
+
const watchedSyms = Object.keys(watchedTickers);
|
|
2416
|
+
const rowsLength = rows.length;
|
|
2417
|
+
for (let i = 0; i < rowsLength; i++) {
|
|
2418
|
+
const entry = rows[i];
|
|
2419
|
+
const marketHash = this.safeString(entry, 'marketHash');
|
|
2420
|
+
if (marketHash === undefined) {
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
// every publication carries BOTH sides (null = no resting liquidity), so the legacy
|
|
2424
|
+
// best-odds shape rebuilds directly without any merge cache
|
|
2425
|
+
const bestOne = this.safeDict(entry, 'outcomeOne', {});
|
|
2426
|
+
const bestTwo = this.safeDict(entry, 'outcomeTwo', {});
|
|
2427
|
+
const raw = {
|
|
2428
|
+
'marketHash': marketHash,
|
|
2429
|
+
'outcomeOne': { 'percentageOdds': this.safeString(bestOne, 'percentageOdds') },
|
|
2430
|
+
'outcomeTwo': { 'percentageOdds': this.safeString(bestTwo, 'percentageOdds') },
|
|
2431
|
+
};
|
|
2432
|
+
const watchedSymsLength = watchedSyms.length;
|
|
2433
|
+
for (let j = 0; j < watchedSymsLength; j++) {
|
|
2434
|
+
const sym = watchedSyms[j];
|
|
2435
|
+
if (this.safeString(watchedTickers, sym) !== marketHash) {
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
const outcomeObj = this.outcome(sym);
|
|
2439
|
+
const ticker = this.parsePredictionTicker(raw, outcomeObj);
|
|
2440
|
+
this.tickers[sym] = ticker;
|
|
2441
|
+
client.resolve(ticker, 'ticker::' + sym);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* @method
|
|
2447
|
+
* @name sxbet#watchTrades
|
|
2448
|
+
* @description streams public bets of an outcome; the venue channel is global, entries are filtered down to the requested outcome
|
|
2449
|
+
* @see https://docs.sx.bet/api-reference/channel-recent-trades-v3
|
|
2450
|
+
* @param {string} outcome unified outcome or outcome token id
|
|
2451
|
+
* @param {int} [since] timestamp in ms of the earliest trade to return
|
|
2452
|
+
* @param {int} [limit] the maximum number of trades to return
|
|
2453
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
2454
|
+
* @returns {object[]} a list of [prediction trade structures](https://docs.ccxt.com/#/?id=prediction-trade-structure)
|
|
2455
|
+
*/
|
|
2456
|
+
async watchTrades(outcome, since = undefined, limit = undefined, params = {}) {
|
|
2457
|
+
await this.loadOutcome(outcome);
|
|
2458
|
+
const outcomeObj = this.outcome(outcome);
|
|
2459
|
+
const sym = this.safeString(outcomeObj, 'outcome');
|
|
2460
|
+
const messageHash = 'trades::' + sym;
|
|
2461
|
+
const trades = await this.subscribeSxbetChannel(messageHash, 'recent_trades_v3:global');
|
|
2462
|
+
return this.filterBySinceLimit(trades, since, limit, 'timestamp', true);
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* @ignore
|
|
2466
|
+
* @method
|
|
2467
|
+
* @name sxbet#parseSxbetV3PublicTrade
|
|
2468
|
+
* @description parses one recent_trades_v3 publication (public TradeV3 shape) into a unified trade
|
|
2469
|
+
* @param {object} trade the raw public trade row
|
|
2470
|
+
* @returns {object} a [prediction trade structure](https://docs.ccxt.com/#/?id=prediction-trade-structure)
|
|
2471
|
+
*/
|
|
2472
|
+
parseSxbetV3PublicTrade(trade) {
|
|
2473
|
+
const marketHash = this.safeString(trade, 'marketHash', '');
|
|
2474
|
+
const isBettingOutcomeOne = this.safeBool(trade, 'isBettingOutcomeOne', true);
|
|
2475
|
+
const outcomeId = (isBettingOutcomeOne) ? marketHash : (marketHash + '-2');
|
|
2476
|
+
const outcomeObj = this.safeOutcome(outcomeId);
|
|
2477
|
+
const oneDenom = '100000000000000000000';
|
|
2478
|
+
const usdcDecimals = '1000000';
|
|
2479
|
+
const odds = this.safeString(trade, 'weightedAverageOdds');
|
|
2480
|
+
const price = (odds !== undefined) ? this.parseNumber(Precise.stringDiv(odds, oneDenom)) : undefined;
|
|
2481
|
+
const stake = this.safeString(trade, 'totalStake', '0');
|
|
2482
|
+
const amount = this.parseNumber(Precise.stringDiv(stake, usdcDecimals, 6));
|
|
2483
|
+
const timestamp = this.parse8601(this.safeString(trade, 'betTime'));
|
|
2484
|
+
return this.safePredictionTrade({
|
|
2485
|
+
'id': this.safeString(trade, 'tradeId'),
|
|
2486
|
+
'info': trade,
|
|
2487
|
+
'timestamp': timestamp,
|
|
2488
|
+
'datetime': this.iso8601(timestamp),
|
|
2489
|
+
'outcome': this.safeString(outcomeObj, 'outcome'),
|
|
2490
|
+
'outcomeId': this.safeString(outcomeObj, 'outcomeId', outcomeId),
|
|
2491
|
+
'label': this.safeString(outcomeObj, 'label'),
|
|
2492
|
+
'market': this.safeString(outcomeObj, 'market'),
|
|
2493
|
+
'order': undefined,
|
|
2494
|
+
'type': undefined,
|
|
2495
|
+
'side': 'buy',
|
|
2496
|
+
'takerOrMaker': undefined,
|
|
2497
|
+
'price': price,
|
|
2498
|
+
'amount': amount,
|
|
2499
|
+
'cost': amount,
|
|
2500
|
+
'fee': undefined,
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
handleTrades(client, rows) {
|
|
2504
|
+
//
|
|
2505
|
+
// { "trade": { "tradeId": "0xac6b...", "marketHash": "0x1fec...", "isBettingOutcomeOne": true,
|
|
2506
|
+
// "totalStake": "1000000", "weightedAverageOdds": "40000000000000000000",
|
|
2507
|
+
// "betTime": "2026-07-31T18:22:41.000Z", ... } }
|
|
2508
|
+
//
|
|
2509
|
+
const rowsLength = rows.length;
|
|
2510
|
+
for (let i = 0; i < rowsLength; i++) {
|
|
2511
|
+
const row = this.safeDict(rows[i], 'trade', rows[i]);
|
|
2512
|
+
const trade = this.parseSxbetV3PublicTrade(row);
|
|
2513
|
+
const sym = this.safeString(trade, 'outcome');
|
|
2514
|
+
if (sym === undefined) {
|
|
2515
|
+
// bet in a market absent from the loaded outcome cache
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
if (this.trades === undefined) {
|
|
2519
|
+
this.trades = this.createSafeDictionary();
|
|
2520
|
+
}
|
|
2521
|
+
if (this.safeValue(this.trades, sym) === undefined) {
|
|
2522
|
+
const tradesLimit = this.safeInteger(this.options, 'tradesLimit', 1000);
|
|
2523
|
+
this.trades[sym] = new ArrayCache(tradesLimit);
|
|
2524
|
+
}
|
|
2525
|
+
const stored = this.trades[sym];
|
|
2526
|
+
stored.append(trade);
|
|
2527
|
+
client.resolve(stored, 'trades::' + sym);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* @method
|
|
2532
|
+
* @name sxbet#watchMyTrades
|
|
2533
|
+
* @description streams the authenticated wallet's fills over its per-account v3 channel
|
|
2534
|
+
* @see https://docs.sx.bet/api-reference/channel-fills-v3
|
|
2535
|
+
* @param {string} [outcome] unified outcome or outcome token id to narrow the stream down to
|
|
2536
|
+
* @param {int} [since] timestamp in ms of the earliest trade to return
|
|
2537
|
+
* @param {int} [limit] the maximum number of trades to return
|
|
2538
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
2539
|
+
* @returns {object[]} a list of [prediction trade structures](https://docs.ccxt.com/#/?id=prediction-trade-structure)
|
|
2540
|
+
*/
|
|
2541
|
+
async watchMyTrades(outcome = undefined, since = undefined, limit = undefined, params = {}) {
|
|
2542
|
+
if (this.walletAddress === undefined) {
|
|
2543
|
+
throw new ArgumentsRequired(this.id + ' watchMyTrades() requires a walletAddress');
|
|
2544
|
+
}
|
|
2545
|
+
let messageHash = 'myTrades';
|
|
2546
|
+
if (outcome !== undefined) {
|
|
2547
|
+
await this.loadOutcome(outcome);
|
|
2548
|
+
const outcomeObj = this.outcome(outcome);
|
|
2549
|
+
const sym = this.safeString(outcomeObj, 'outcome');
|
|
2550
|
+
messageHash = 'myTrades::' + sym;
|
|
2551
|
+
}
|
|
2552
|
+
const channel = 'account:fills_v3_#' + this.walletAddress;
|
|
2553
|
+
const trades = await this.subscribeSxbetChannel(messageHash, channel);
|
|
2554
|
+
return this.filterBySinceLimit(trades, since, limit, 'timestamp', true);
|
|
2555
|
+
}
|
|
2556
|
+
handleMyTrade(client, rows) {
|
|
2557
|
+
//
|
|
2558
|
+
// FillV3 rows - identical to GET /fills-v3
|
|
2559
|
+
//
|
|
2560
|
+
const rowsLength = rows.length;
|
|
2561
|
+
for (let i = 0; i < rowsLength; i++) {
|
|
2562
|
+
const row = this.safeDict(rows[i], 'fill', rows[i]);
|
|
2563
|
+
const trade = this.parseSxbetV3Fill(row);
|
|
2564
|
+
const sym = this.safeString(trade, 'outcome');
|
|
2565
|
+
if (sym === undefined) {
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
if (this.myTrades === undefined) {
|
|
2569
|
+
const myTradesLimit = this.safeInteger(this.options, 'myTradesLimit', 1000);
|
|
2570
|
+
this.myTrades = new ArrayCacheByOutcomeById(myTradesLimit);
|
|
2571
|
+
}
|
|
2572
|
+
const stored = this.myTrades;
|
|
2573
|
+
stored.append(trade);
|
|
2574
|
+
client.resolve(stored, 'myTrades');
|
|
2575
|
+
client.resolve(stored, 'myTrades::' + sym);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
/**
|
|
2579
|
+
* @method
|
|
2580
|
+
* @name sxbet#watchOrders
|
|
2581
|
+
* @description streams updates of the authenticated wallet's orders over its per-account v3 channel
|
|
2582
|
+
* @see https://docs.sx.bet/api-reference/channel-orders-v3
|
|
2583
|
+
* @param {string} [outcome] unified outcome or outcome token id to narrow the stream down to
|
|
2584
|
+
* @param {int} [since] timestamp in ms of the earliest order to return
|
|
2585
|
+
* @param {int} [limit] the maximum number of orders to return
|
|
2586
|
+
* @param {object} [params] extra parameters specific to the exchange API endpoint
|
|
2587
|
+
* @returns {object[]} a list of [prediction order structures](https://docs.ccxt.com/#/?id=prediction-order-structure)
|
|
2588
|
+
*/
|
|
2589
|
+
async watchOrders(outcome = undefined, since = undefined, limit = undefined, params = {}) {
|
|
2590
|
+
if (this.walletAddress === undefined) {
|
|
2591
|
+
throw new ArgumentsRequired(this.id + ' watchOrders() requires a walletAddress');
|
|
2592
|
+
}
|
|
2593
|
+
let messageHash = 'orders';
|
|
2594
|
+
if (outcome !== undefined) {
|
|
2595
|
+
await this.loadOutcome(outcome);
|
|
2596
|
+
const outcomeObj = this.outcome(outcome);
|
|
2597
|
+
const sym = this.safeString(outcomeObj, 'outcome');
|
|
2598
|
+
messageHash = 'orders::' + sym;
|
|
2599
|
+
}
|
|
2600
|
+
const channel = 'account:orders_v3_#' + this.walletAddress;
|
|
2601
|
+
const orders = await this.subscribeSxbetChannel(messageHash, channel);
|
|
2602
|
+
return this.filterBySinceLimit(orders, since, limit, 'timestamp', true);
|
|
2603
|
+
}
|
|
2604
|
+
handleOrder(client, rows) {
|
|
2605
|
+
//
|
|
2606
|
+
// OrderV3 rows - identical to GET /orders-v3 (plus INACTIVE states with inactiveReason)
|
|
2607
|
+
//
|
|
2608
|
+
const rowsLength = rows.length;
|
|
2609
|
+
for (let i = 0; i < rowsLength; i++) {
|
|
2610
|
+
const row = this.safeDict(rows[i], 'order', rows[i]);
|
|
2611
|
+
const order = this.parsePredictionOrder(row);
|
|
2612
|
+
if (this.orders === undefined) {
|
|
2613
|
+
const cacheLimit = this.safeInteger(this.options, 'ordersLimit', 1000);
|
|
2614
|
+
this.orders = new ArrayCacheByOutcomeById(cacheLimit);
|
|
2615
|
+
}
|
|
2616
|
+
const stored = this.orders;
|
|
2617
|
+
stored.append(order);
|
|
2618
|
+
client.resolve(stored, 'orders');
|
|
2619
|
+
const sym = this.safeString(order, 'outcome');
|
|
2620
|
+
if (sym !== undefined) {
|
|
2621
|
+
client.resolve(stored, 'orders::' + sym);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
handleErrors(code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
|
2626
|
+
// sx.bet returns { "error": "Bad Request", "message": <code or [strings]>, "statusCode": 4xx };
|
|
2627
|
+
// map the known codes to ccxt errors so callers can distinguish a rejected order or a
|
|
2628
|
+
// validation problem from a transport outage (the base otherwise maps a bare 4xx to the
|
|
2629
|
+
// exchange-not-available error). unmapped codes fall through to the base http-status handling
|
|
2630
|
+
if (response === undefined) {
|
|
2631
|
+
return undefined;
|
|
2632
|
+
}
|
|
2633
|
+
const message = this.safeValue(response, 'message');
|
|
2634
|
+
if (message === undefined) {
|
|
2635
|
+
return undefined;
|
|
2636
|
+
}
|
|
2637
|
+
// validation failures arrive in string-array form, business errors in single-code-string form
|
|
2638
|
+
let messageString = undefined;
|
|
2639
|
+
if (Array.isArray(message)) {
|
|
2640
|
+
messageString = message.join(', ');
|
|
2641
|
+
}
|
|
2642
|
+
else {
|
|
2643
|
+
messageString = message;
|
|
2644
|
+
}
|
|
2645
|
+
const feedback = this.id + ' ' + body;
|
|
2646
|
+
this.throwExactlyMatchedException(this.exceptions['exact'], messageString, feedback);
|
|
2647
|
+
this.throwBroadlyMatchedException(this.exceptions['broad'], messageString, feedback);
|
|
2648
|
+
// a 400 is a client-side bad request (bad params, invalid order), not a transport outage —
|
|
2649
|
+
// throw BadRequest instead of letting the base map the bare 400 to a retryable network-unavailable error
|
|
2650
|
+
if (code === 400) {
|
|
2651
|
+
throw new BadRequest(feedback);
|
|
2652
|
+
}
|
|
2653
|
+
return undefined;
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* @ignore
|
|
2657
|
+
* @method
|
|
2658
|
+
* @name sxbet#sign
|
|
2659
|
+
* @description builds the request url and attaches the x-sx-api-key header; every private v3 route authenticates with the apiKey credential, so its absence fails fast instead of surfacing a raw 401
|
|
2660
|
+
* @param {string} path the endpoint path
|
|
2661
|
+
* @param {string|string[]} api the api group and access level
|
|
2662
|
+
* @param {string} method the http method
|
|
2663
|
+
* @param {object} params the request parameters
|
|
2664
|
+
* @param {object} [headers] request headers
|
|
2665
|
+
* @param {string} [body] the request body
|
|
2666
|
+
* @returns {object} a dict with url, method, body and headers
|
|
2667
|
+
*/
|
|
2668
|
+
sign(path, api = 'sxbet', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
|
2669
|
+
const apiGroup = typeof api === 'string' ? api : api[0];
|
|
2670
|
+
const accessLevel = typeof api === 'string' ? 'public' : api[1];
|
|
2671
|
+
if ((accessLevel === 'private') && (this.apiKey === undefined)) {
|
|
2672
|
+
throw new AuthenticationError(this.id + ' ' + path + ' is a private endpoint and requires the apiKey credential (the x-sx-api-key header)');
|
|
2673
|
+
}
|
|
2674
|
+
const baseUrls = this.urls['api'];
|
|
2675
|
+
const baseUrl = this.safeString(baseUrls, apiGroup, baseUrls['sxbet']);
|
|
2676
|
+
let url = baseUrl + '/' + this.implodeParams(path, params);
|
|
2677
|
+
const query = this.omit(params, this.extractParams(path));
|
|
2678
|
+
const existingHeaders = (headers !== undefined) ? headers : {};
|
|
2679
|
+
headers = this.extend({
|
|
2680
|
+
'Accept': 'application/json',
|
|
2681
|
+
'Content-Type': 'application/json',
|
|
2682
|
+
}, existingHeaders);
|
|
2683
|
+
if (this.apiKey !== undefined) {
|
|
2684
|
+
headers['x-sx-api-key'] = this.apiKey;
|
|
2685
|
+
}
|
|
2686
|
+
// DELETE /orders-v3 carries its order ids in a JSON body; the other DELETE routes -
|
|
2687
|
+
// /orders-v3/all and /orders-v3/event - take query parameters, like every GET
|
|
2688
|
+
let sendAsQuery = (method === 'GET');
|
|
2689
|
+
if (method === 'DELETE') {
|
|
2690
|
+
const hasOrdersList = ('orders' in query);
|
|
2691
|
+
sendAsQuery = !hasOrdersList;
|
|
2692
|
+
}
|
|
2693
|
+
if (sendAsQuery) {
|
|
2694
|
+
const querystring = this.urlencode(query);
|
|
2695
|
+
if (querystring !== '') {
|
|
2696
|
+
url += '?' + querystring;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
else {
|
|
2700
|
+
const queryKeys = Object.keys(query);
|
|
2701
|
+
const queryKeysLength = queryKeys.length;
|
|
2702
|
+
if (queryKeysLength > 0) {
|
|
2703
|
+
body = this.json(query);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
|
2707
|
+
}
|
|
2708
|
+
}
|