diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index c0c9fc9043f..1208383decc 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -87,18 +87,15 @@ func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.C exch.SetDefaults() exchCfg.API.AuthenticatedSupport = true exchCfg.API.Credentials = getExchangeCredentials(name) - err = exch.Setup(exchCfg) if err != nil { t.Fatalf("Cannot setup %v exchange Setup %v", name, err) } - err = exch.UpdateTradablePairs(ctx, true) if err != nil && !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("Cannot setup %v UpdateTradablePairs %v", name, err) } b := exch.GetBase() - assets := b.CurrencyPairs.GetAssetTypes(false) if len(assets) == 0 { t.Fatalf("Cannot setup %v, exchange has no assets", name) @@ -109,7 +106,6 @@ func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.C t.Fatalf("Cannot setup %v SetAssetEnabled %v", name, err) } } - // Add +1 to len to verify that exchanges can handle requests with unset pairs and assets assetPairs := make([]assetPair, 0, len(assets)+1) assets: @@ -151,7 +147,6 @@ assets: }) } assetPairs = append(assetPairs, assetPair{}) - return exch, assetPairs } @@ -182,7 +177,6 @@ func executeExchangeWrapperTests(ctx context.Context, t *testing.T, exch exchang continue } method := actualExchange.MethodByName(methodName) - var assetLen int for y := range method.Type().NumIn() { input := method.Type().In(y) @@ -382,6 +376,7 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr Description: "1337", Amount: 1, ClientOrderID: "1337", + WalletID: "7331", } if argGenerator.MethodName == "WithdrawCryptocurrencyFunds" { req.Type = withdraw.Crypto @@ -606,10 +601,9 @@ var unsupportedAssets = []asset.Item{ var unsupportedExchangeNames = []string{ "testexch", "alphapoint", - "bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here - "btse", // TODO rm once timeout issues resolved - "poloniex", // outdated API // TODO rm once updated - "coinbasepro", // outdated API // TODO rm once updated + "bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here + "btse", // TODO rm once timeout issues resolved + "poloniex", // outdated API // TODO rm once updated } // cryptoChainPerExchange holds the deposit address chain per exchange diff --git a/common/common.go b/common/common.go index 382d914090a..b1df157f0ae 100644 --- a/common/common.go +++ b/common/common.go @@ -57,6 +57,7 @@ var ( // Public common Errors var ( ErrNotYetImplemented = errors.New("not yet implemented") + ErrExchangeNameUnset = errors.New("exchange name unset") ErrFunctionNotSupported = errors.New("unsupported wrapper function") errInvalidCryptoCurrency = errors.New("invalid crypto currency") ErrDateUnset = errors.New("date unset") diff --git a/config_example.json b/config_example.json index 97053af4445..919310a02ad 100644 --- a/config_example.json +++ b/config_example.json @@ -1210,15 +1210,16 @@ }, { "name": "CoinbasePro", - "enabled": true, + "enabled": false, "verbose": false, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, + "connectionMonitorDelay": 2000000000, "baseCurrencies": "USD,GBP,EUR", "currencyPairs": { + "bypassConfigFormatUpgrades": false, "requestFormat": { "uppercase": true, "delimiter": "-" @@ -1228,49 +1229,99 @@ "delimiter": "-" }, "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], "pairs": { + "futures": { + "assetEnabled": true, + "enabled": "GOL-28JAN25-CDE", + "available": "BIT-27SEP24-CDE,ET-27SEP24-CDE,GOL-27NOV24-CDE,BIT-25OCT24-CDE,NOL-19SEP24-CDE,BCH-27SEP24-CDE,LC-27SEP24-CDE,AVA-27SEP24-CDE,LNK-27SEP24-CDE,LC-25OCT24-CDE,DOT-27SEP24-CDE,DOG-27SEP24-CDE,SHB-27SEP24-CDE,BIT-29NOV24-CDE,AVA-25OCT24-CDE,ET-25OCT24-CDE,DOT-25OCT24-CDE,ET-29NOV24-CDE,DOG-29NOV24-CDE,GOL-28JAN25-CDE,GOL-31MAR25-CDE,DOG-25OCT24-CDE,DOT-29NOV24-CDE,LC-29NOV24-CDE,LNK-25OCT24-CDE,BCH-29NOV24-CDE,LNK-29NOV24-CDE,NOL-19NOV24-CDE,BCH-25OCT24-CDE,NOL-21OCT24-CDE,SHB-25OCT24-CDE,AVA-29NOV24-CDE,SHB-29NOV24-CDE" + }, "spot": { - "enabled": "BTC-USD", - "available": "ETC-GBP,CVC-USDC,LINK-ETH,KNC-BTC,GNT-USDC,EOS-BTC,ETC-BTC,LTC-BTC,ZRX-USD,XRP-EUR,ZRX-EUR,ATOM-USD,BTC-USD,LTC-EUR,XRP-USD,MANA-USDC,XRP-BTC,LTC-GBP,DAI-USD,COMP-BTC,ETH-DAI,XTZ-USD,DASH-BTC,OMG-BTC,BTC-USDC,BCH-USD,DNT-USDC,COMP-USD,LOOM-USDC,OMG-GBP,BCH-GBP,ZRX-BTC,ATOM-BTC,EOS-EUR,ETH-USD,XLM-EUR,KNC-USD,OXT-USD,ETC-EUR,OMG-USD,BTC-GBP,OMG-EUR,DASH-USD,MKR-BTC,XTZ-BTC,BAT-ETH,REP-USD,XLM-BTC,ETH-USDC,REP-BTC,LTC-USD,ZEC-BTC,ZEC-USDC,EOS-USD,MKR-USD,ALGO-USD,LINK-USD,BCH-EUR,XLM-USD,ETH-GBP,ETC-USD,ETH-EUR,BCH-BTC,BTC-EUR,ETH-BTC,DAI-USDC,BAT-USDC" + "assetEnabled": true, + "enabled": "BTC-USD,BTC-USDC,USDT-USD,ETH-USD,ETH-USDC,SOL-USD", + "available": "BTC-USD,BTC-USDC,ETH-USDC,ETH-USD,USDT-USD,SOL-USDC,SOL-USD,USDT-USDC,FET-USDC,FET-USD,BTC-USDT,XRP-USD,XRP-USDC,DOGE-USDC,DOGE-USD,BONK-USDC,BONK-USD,USDT-EUR,ETH-USDT,BTC-EUR,USDC-EUR,AAVE-USDC,AAVE-USD,SUI-USDC,SUI-USD,LINK-USD,LINK-USDC,LTC-USD,LTC-USDC,SHIB-USDC,SHIB-USD,BTC-GBP,JASMY-USD,JASMY-USDC,ETH-EUR,AVAX-USDC,AVAX-USD,NEAR-USD,NEAR-USDC,ONDO-USD,ONDO-USDC,ADA-USD,ADA-USDC,SUPER-USDC,SUPER-USD,UNI-USD,UNI-USDC,RARE-USD,RARE-USDC,RNDR-USD,RNDR-USDC,INJ-USDC,INJ-USD,SEI-USDC,SEI-USD,HBAR-USDC,HBAR-USD,XLM-USDC,XLM-USD,SOL-EUR,HNT-USD,HNT-USDC,BCH-USD,BCH-USDC,STX-USDC,STX-USD,TRB-USDC,TRB-USD,PNG-USD,PNG-USDC,MATIC-USDC,MATIC-USD,MKR-USD,MKR-USDC,SOL-USDT,APT-USDC,APT-USD,EURC-USDC,ICP-USD,ICP-USDC,IDEX-USDC,IDEX-USD,AERO-USDC,AERO-USD,ETH-GBP,DAI-USD,DAI-USDC,00-USD,00-USDC,USDC-GBP,MASK-USD,MASK-USDC,USDT-GBP,RENDER-USD,RENDER-USDC,ZRO-USDC,ZRO-USD,CRV-USDC,CRV-USD,DOT-USDC,DOT-USD,TIA-USDC,TIA-USD,GRT-USD,GRT-USDC,FIL-USDC,FIL-USD,IMX-USDC,IMX-USD,OP-USD,OP-USDC,JTO-USD,JTO-USDC,FET-USDT,LDO-USD,LDO-USDC,SOL-GBP,ARB-USD,ARB-USDC,MSOL-USDC,MSOL-USD,GFI-USD,GFI-USDC,MINA-USD,MINA-USDC,ATOM-USD,ATOM-USDC,QNT-USDC,QNT-USD,NEAR-USDT,PRO-USD,PRO-USDC,FORT-USDC,FORT-USD,PRIME-USD,PRIME-USDC,ZEC-USDC,ZEC-USD,TVK-USD,TVK-USDC,XRP-USDT,DOGE-USDT,1INCH-USDC,1INCH-USD,AIOZ-USD,AIOZ-USDC,ABT-USD,ABT-USDC,ROSE-USDC,ROSE-USD,SKL-USDC,SKL-USD,COMP-USDC,COMP-USD,ZETA-USD,ZETA-USDC,TRU-USD,TRU-USDC,AXL-USDC,AXL-USD,AKT-USDC,AKT-USD,XRP-EUR,KARRAT-USD,KARRAT-USDC,CBETH-USD,CBETH-USDC,BICO-USDT,VARA-USD,VARA-USDC,AAVE-EUR,SUSHI-USD,SUSHI-USDC,ALGO-USDC,ALGO-USD,BICO-USDC,BICO-USD,VELO-USDC,VELO-USD,UMA-USD,UMA-USDC,ETC-USDC,ETC-USD,LRDS-USDC,LRDS-USD,ENS-USD,ENS-USDC,LTC-EUR,VET-USDC,VET-USD,SYN-USD,SYN-USDC,LPT-USDC,LPT-USD,OCEAN-USDC,OCEAN-USD,LQTY-USDC,LQTY-USD,AMP-USDC,AMP-USD,BIGTIME-USD,BIGTIME-USDC,LRC-USD,LRC-USDC,ACH-USD,ACH-USDC,VOXEL-USDC,VOXEL-USD,SAND-USDC,SAND-USD,MPL-USD,MPL-USDC,SHIB-EUR,GTC-USDC,GTC-USD,ARKM-USD,ARKM-USDC,COTI-USD,COTI-USDC,ILV-USD,ILV-USDC,FLR-USD,FLR-USDC,ICP-USDT,LCX-USD,LCX-USDC,MOBILE-USD,MOBILE-USDC,DRIFT-USDC,DRIFT-USD,DOGE-EUR,SHIB-USDT,APE-USD,APE-USDC,GODS-USDC,GODS-USD,EOS-USD,EOS-USDC,XTZ-USDC,XTZ-USD,BLUR-USD,BLUR-USDC,ZRX-USDC,ZRX-USD,AUDIO-USD,AUDIO-USDC,ANKR-USD,ANKR-USDC,UNI-EUR,GST-USD,GST-USDC,ORCA-USD,ORCA-USDC,PRQ-USDC,PRQ-USD,AVAX-EUR,SHPING-USDC,SHPING-USD,LOKA-USDC,LOKA-USD,NEON-USD,NEON-USDC,API3-USDC,API3-USD,STRK-USD,STRK-USDC,XCN-USD,XCN-USDC,AVAX-USDT,ADA-EUR,LTC-GBP,WBTC-USD,WBTC-USDC,METIS-USDC,METIS-USD,HIGH-USDC,HIGH-USD,EGLD-USD,EGLD-USDC,ETH-DAI,DESO-USDC,DESO-USD,BIT-USD,BIT-USDC,YFI-USDC,YFI-USD,MASK-EUR,CRO-USDC,CRO-USD,HOPR-USD,HOPR-USDC,CHZ-USD,CHZ-USDC,MATIC-EUR,JASMY-USDT,RBN-USDC,RBN-USD,ADA-USDT,RARI-USDC,RARI-USD,OP-USDT,POLS-USD,POLS-USDC,DIMO-USD,DIMO-USDC,OGN-USD,OGN-USDC,BAL-USD,BAL-USDC,SNX-USD,SNX-USDC,TRAC-USD,TRAC-USDC,QI-USD,QI-USDC,IOTX-USDC,IOTX-USD,MNDE-USD,MNDE-USDC,ACX-USDC,ACX-USD,LINK-EUR,MAGIC-USDC,MAGIC-USD,ALEPH-USDC,ALEPH-USD,T-USDC,T-USD,RONIN-USD,RONIN-USDC,RPL-USD,RPL-USDC,AAVE-GBP,CLV-USDC,CLV-USD,MANA-USDC,MANA-USD,BLZ-USDC,BLZ-USD,AVT-USD,AVT-USDC,PYR-USDC,PYR-USD,DYP-USDC,DYP-USD,OXT-USDC,OXT-USD,A8-USD,A8-USDC,SWFTC-USDC,SWFTC-USD,MATIC-USDT,KAVA-USD,KAVA-USDC,HONEY-USD,HONEY-USDC,ICP-EUR,NCT-USD,NCT-USDC,BLAST-USDC,BLAST-USD,NMR-USD,NMR-USDC,DASH-USDC,DASH-USD,DAR-USD,DAR-USDC,BTRST-USD,BTRST-USDC,CGLD-USDC,CGLD-USD,SPA-USDC,SPA-USD,RAD-USDC,RAD-USD,DNT-USD,DNT-USDC,PIRATE-USDC,PIRATE-USD,MDT-USD,MDT-USDC,CVX-USD,CVX-USDC,APE-EUR,BCH-EUR,AUCTION-USD,AUCTION-USDC,RNDR-USDT,WCFG-USDC,WCFG-USD,APT-USDT,MASK-USDT,CTSI-USD,CTSI-USDC,POND-USDC,POND-USD,DOT-EUR,ADA-GBP,INDEX-USD,INDEX-USDC,SPELL-USDC,SPELL-USD,BAT-USD,BAT-USDC,SUKU-USDC,SUKU-USD,ALGO-EUR,SEAM-USD,SEAM-USDC,BOBA-USDC,BOBA-USD,ALGO-GBP,ACS-USDC,ACS-USD,MANA-EUR,MLN-USD,MLN-USDC,TNSR-USDC,TNSR-USD,FIL-EUR,FLOW-USDC,FLOW-USD,XYO-USDC,XYO-USD,GUSD-USD,GUSD-USDC,VTHO-USD,VTHO-USDC,RLC-USDC,RLC-USD,1INCH-EUR,ALCX-USDC,ALCX-USD,CORECHAIN-USD,CORECHAIN-USDC,SD-USD,SD-USDC,STX-USDT,FX-USDC,FX-USD,XLM-EUR,SHDW-USDC,SHDW-USD,HFT-USD,HFT-USDC,STORJ-USDC,STORJ-USD,SAFE-USDC,SAFE-USD,FIDA-USD,FIDA-USDC,MATH-USD,MATH-USDC,NKN-USDC,NKN-USD,PYUSD-USDC,PYUSD-USD,CELR-USDC,CELR-USD,AXS-USD,AXS-USDC,ALICE-USDC,ALICE-USD,ICP-GBP,GLM-USDC,GLM-USD,FOX-USDC,FOX-USD,AURORA-USDC,AURORA-USD,FARM-USD,FARM-USDC,EOS-EUR,DOT-USDT,HBAR-USDT,GMT-USDC,GMT-USD,GRT-EUR,CTX-USDC,CTX-USD,CHZ-EUR,ETC-EUR,ORN-USDC,ORN-USD,MEDIA-USD,MEDIA-USDC,ATOM-EUR,ASM-USD,ASM-USDC,AGLD-USDC,AGLD-USD,LINK-USDT,KNC-USD,KNC-USDC,DOGE-GBP,SAND-USDT,INV-USDC,INV-USD,GAL-USD,GAL-USDC,PERP-USD,PERP-USDC,POWR-USDC,POWR-USD,KSM-USD,KSM-USDC,BAND-USD,BAND-USDC,ELA-USDC,ELA-USD,G-USD,G-USDC,PLU-USDC,PLU-USD,ROSE-USDT,QNT-USDT,APE-USDT,1INCH-GBP,REQ-USD,REQ-USDC,AXS-USDT,WAXL-USD,WAXL-USDC,ZEN-USD,ZEN-USDC,BCH-GBP,BADGER-USDC,BADGER-USD,FORTH-USD,FORTH-USDC,FIL-GBP,CVC-USDC,CVC-USD,DOT-GBP,LIT-USDC,LIT-USD,CRV-EUR,LINK-GBP,AST-USDC,AST-USD,FIS-USDC,FIS-USD,ENS-EUR,ATOM-USDT,IMX-USDT,SHIB-GBP,MATIC-GBP,DIA-USD,DIA-USDC,ERN-USDC,ERN-USD,OSMO-USD,OSMO-USDC,CHZ-USDT,ARPA-USD,ARPA-USDC,RNDR-EUR,TIME-USD,TIME-USDC,ENS-USDT,OMNI-USD,OMNI-USDC,ATOM-GBP,C98-USD,C98-USDC,PUNDIX-USD,PUNDIX-USDC,MUSE-USD,MUSE-USDC,LRC-USDT,MINA-EUR,GHST-USD,GHST-USDC,ETC-GBP,AXS-EUR,LSETH-USDC,LSETH-USD,GNO-USD,GNO-USDC,BNT-USDC,BNT-USD,AERGO-USD,AERGO-USDC,SNX-EUR,CHZ-GBP,UNI-GBP,FLOW-USDT,BAT-EUR,MINA-USDT,DEXT-USDC,DEXT-USD,XLM-USDT,GYEN-USDC,GYEN-USD,BICO-EUR,CRV-GBP,ANKR-EUR,CGLD-EUR,XTZ-GBP,MASK-GBP,KRL-USDC,KRL-USD,CRO-EUR,ANKR-GBP,XTZ-EUR,GRT-GBP,GMT-USDT,SNX-GBP,STG-USD,STG-USDC,CRO-USDT,PAX-USD,PAX-USDC,CGLD-GBP,SOL-ETH,ETH-BTC,ADA-ETH,SOL-BTC,AAVE-BTC,WBTC-BTC,ZEC-BTC,CBETH-ETH,LINK-ETH,LTC-BTC,UNI-BTC,BCH-BTC,DOGE-BTC,AVAX-BTC,ADA-BTC,LSETH-ETH,MKR-BTC,GRT-BTC,MATIC-BTC,COMP-BTC,LRC-BTC,DOT-BTC,XLM-BTC,FIL-BTC,ICP-BTC,EOS-BTC,BAT-ETH,LINK-BTC,CRV-BTC,ATOM-BTC,BAL-BTC,ALGO-BTC,1INCH-BTC,BAT-BTC,MANA-ETH,ETC-BTC,DASH-BTC,SNX-BTC,YFI-BTC,XTZ-BTC,AXS-BTC,ANKR-BTC,MANA-BTC,CGLD-BTC" } } }, "api": { "authenticatedSupport": false, "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, "credentials": { - "key": "Key", - "secret": "Secret", - "clientID": "ClientID" + "key": "", + "secret": "", + "clientID": "" }, "credentialsValidator": { "requiresKey": true, "requiresSecret": true, "requiresClientID": true, "requiresBase64DecodeSecret": true + }, + "urlEndpoints": { + "RestSandboxURL": "https://api-public.sandbox.exchange.coinbase.com/", + "RestSpotURL": "https://api.coinbase.com", + "WebsocketSpotURL": "wss://advanced-trade-ws.coinbase.com" } }, "features": { "supports": { "restAPI": true, "restCapabilities": { - "autoPairUpdates": true + "autoPairUpdates": true, + "fundingRateFetching": false }, "websocketAPI": true, - "websocketCapabilities": {} + "websocketCapabilities": { + "fundingRateFetching": false + } }, "enabled": { "autoPairUpdates": true, - "websocketAPI": false - } + "websocketAPI": true, + "saveTradeData": false, + "tradeFeed": false, + "fillsFeed": false + }, + "subscriptions": [ + { + "enabled": true, + "channel": "heartbeat" + }, + { + "enabled": false, + "channel": "status", + "authenticated": true + }, + { + "enabled": true, + "channel": "ticker", + "asset": "spot" + }, + { + "enabled": true, + "channel": "candles", + "asset": "spot" + }, + { + "enabled": true, + "channel": "allTrades", + "asset": "spot" + }, + { + "enabled": true, + "channel": "orderbook", + "asset": "spot" + }, + { + "enabled": true, + "channel": "account", + "authenticated": true + }, + { + "enabled": false, + "channel": "ticker_batch", + "asset": "spot" + } + ] }, "bankAccounts": [ { @@ -1286,7 +1337,13 @@ "iban": "", "supportedCurrencies": "" } - ] + ], + "orderbook": { + "verificationBypass": false, + "websocketBufferLimit": 5, + "websocketBufferEnabled": false, + "publishPeriod": 10000000000 + } }, { "name": "Deribit", diff --git a/currency/code_types.go b/currency/code_types.go index b77ae41a962..11476df773b 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -3021,12 +3021,15 @@ var ( FI = NewCode("FI") USDM = NewCode("USDM") USDTM = NewCode("USDTM") + CBETH = NewCode("CBETH") + PYUSD = NewCode("PYUSD") + EUROC = NewCode("EUROC") + LSETH = NewCode("LSETH") LEVER = NewCode("LEVER") NESS = NewCode("NESS") KAS = NewCode("KAS") NEXT = NewCode("NEXT") VEXT = NewCode("VEXT") - PYUSD = NewCode("PYUSD") SAIL = NewCode("SAIL") VV = NewCode("VV") ORDI = NewCode("ORDI") diff --git a/currency/pairs.go b/currency/pairs.go index cc5e2f6db52..4c1ae267262 100644 --- a/currency/pairs.go +++ b/currency/pairs.go @@ -143,7 +143,6 @@ list: if p.Contains(check[x], exact) { return fmt.Errorf("%s %w", check[x], ErrPairDuplication) } - return fmt.Errorf("%s %w", check[x], ErrPairNotContainedInAvailablePairs) } return nil diff --git a/engine/datahistory_manager.go b/engine/datahistory_manager.go index 61a37b2a00f..5e818683e36 100644 --- a/engine/datahistory_manager.go +++ b/engine/datahistory_manager.go @@ -1195,11 +1195,11 @@ func (m *DataHistoryManager) validateJob(job *DataHistoryJob) error { exchangeName := job.Exchange if job.DataType == dataHistoryCandleValidationSecondarySourceType { if job.SecondaryExchangeSource == "" { - return fmt.Errorf("job %s %w, secondary exchange name required to lookup existing results", job.Nickname, errExchangeNameUnset) + return fmt.Errorf("job %s %w, secondary exchange name required to lookup existing results", job.Nickname, common.ErrExchangeNameUnset) } exchangeName = job.SecondaryExchangeSource if job.Exchange == "" { - return fmt.Errorf("job %s %w, exchange name required to lookup existing results", job.Nickname, errExchangeNameUnset) + return fmt.Errorf("job %s %w, exchange name required to lookup existing results", job.Nickname, common.ErrExchangeNameUnset) } } exch, err := m.exchangeManager.GetExchangeByName(exchangeName) diff --git a/engine/datahistory_manager_test.go b/engine/datahistory_manager_test.go index 7a955033b79..fd4f26ea6c8 100644 --- a/engine/datahistory_manager_test.go +++ b/engine/datahistory_manager_test.go @@ -529,14 +529,14 @@ func TestValidateJob(t *testing.T) { dhj.DataType = dataHistoryCandleValidationSecondarySourceType err = m.validateJob(dhj) - if !errors.Is(err, errExchangeNameUnset) { - t.Errorf("error '%v', expected '%v'", err, errExchangeNameUnset) + if !errors.Is(err, common.ErrExchangeNameUnset) { + t.Errorf("error '%v', expected '%v'", err, common.ErrExchangeNameUnset) } dhj.SecondaryExchangeSource = "lol" dhj.Exchange = "" err = m.validateJob(dhj) - if !errors.Is(err, errExchangeNameUnset) { - t.Errorf("error '%v', expected '%v'", err, errExchangeNameUnset) + if !errors.Is(err, common.ErrExchangeNameUnset) { + t.Errorf("error '%v', expected '%v'", err, common.ErrExchangeNameUnset) } } diff --git a/engine/rpcserver.go b/engine/rpcserver.go index 72e7e76e8b6..cd6cc1b1bb4 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -64,7 +64,6 @@ var ( errExchangeNotEnabled = errors.New("exchange is not enabled") errExchangeBaseNotFound = errors.New("cannot get exchange base") errInvalidArguments = errors.New("invalid arguments received") - errExchangeNameUnset = errors.New("exchange name unset") errCurrencyPairUnset = errors.New("currency pair unset") errInvalidTimes = errors.New("invalid start and end times") errAssetTypeUnset = errors.New("asset type unset") @@ -2199,7 +2198,7 @@ func (s *RPCServer) GetOrderbookStream(r *gctrpc.GetOrderbookStreamRequest, stre // GetExchangeOrderbookStream streams all orderbooks associated with an exchange func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStreamRequest, stream gctrpc.GoCryptoTraderService_GetExchangeOrderbookStreamServer) error { if r.Exchange == "" { - return errExchangeNameUnset + return common.ErrExchangeNameUnset } if _, err := s.GetExchangeByName(r.Exchange); err != nil { @@ -2267,7 +2266,7 @@ func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStr // GetTickerStream streams the requested updated ticker func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gctrpc.GoCryptoTraderService_GetTickerStreamServer) error { if r.Exchange == "" { - return errExchangeNameUnset + return common.ErrExchangeNameUnset } if _, err := s.GetExchangeByName(r.Exchange); err != nil { @@ -2338,7 +2337,7 @@ func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gct // GetExchangeTickerStream streams all tickers associated with an exchange func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamRequest, stream gctrpc.GoCryptoTraderService_GetExchangeTickerStreamServer) error { if r.Exchange == "" { - return errExchangeNameUnset + return common.ErrExchangeNameUnset } if _, err := s.GetExchangeByName(r.Exchange); err != nil { diff --git a/exchanges/account/account.go b/exchanges/account/account.go index d389ddde210..8aa975e37cc 100644 --- a/exchanges/account/account.go +++ b/exchanges/account/account.go @@ -20,7 +20,6 @@ func init() { var ( errHoldingsIsNil = errors.New("holdings cannot be nil") - errExchangeNameUnset = errors.New("exchange name unset") errExchangeHoldingsNotFound = errors.New("exchange holdings not found") errAssetHoldingsNotFound = errors.New("asset holdings not found") errExchangeAccountsNotFound = errors.New("exchange accounts not found") @@ -75,7 +74,7 @@ func Process(h *Holdings, c *Credentials) error { // TODO: Add jurisdiction and differentiation between APIKEY holdings. func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holdings, error) { if exch == "" { - return Holdings{}, errExchangeNameUnset + return Holdings{}, common.ErrExchangeNameUnset } if creds.IsEmpty() { @@ -143,7 +142,7 @@ func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holding // GetBalance returns the internal balance for that asset item. func GetBalance(exch, subAccount string, creds *Credentials, ai asset.Item, c currency.Code) (*ProtectedBalance, error) { if exch == "" { - return nil, fmt.Errorf("cannot get balance: %w", errExchangeNameUnset) + return nil, fmt.Errorf("cannot get balance: %w", common.ErrExchangeNameUnset) } if !ai.IsValid() { @@ -192,7 +191,7 @@ func (s *Service) Update(incoming *Holdings, creds *Credentials) error { } if incoming.Exchange == "" { - return fmt.Errorf("cannot update holdings: %w", errExchangeNameUnset) + return fmt.Errorf("cannot update holdings: %w", common.ErrExchangeNameUnset) } if creds.IsEmpty() { diff --git a/exchanges/account/account_test.go b/exchanges/account/account_test.go index a22fe0352ee..04bb44fada5 100644 --- a/exchanges/account/account_test.go +++ b/exchanges/account/account_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" @@ -72,8 +73,8 @@ func TestGetHoldings(t *testing.T) { } err = Process(&Holdings{}, nil) - if !errors.Is(err, errExchangeNameUnset) { - t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeNameUnset) + if !errors.Is(err, common.ErrExchangeNameUnset) { + t.Fatalf("received: '%v' but expected: '%v'", err, common.ErrExchangeNameUnset) } holdings := Holdings{ @@ -145,8 +146,8 @@ func TestGetHoldings(t *testing.T) { } _, err = GetHoldings("", nil, asset.Spot) - if !errors.Is(err, errExchangeNameUnset) { - t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeNameUnset) + if !errors.Is(err, common.ErrExchangeNameUnset) { + t.Fatalf("received: '%v' but expected: '%v'", err, common.ErrExchangeNameUnset) } _, err = GetHoldings("bla", nil, asset.Spot) @@ -245,8 +246,8 @@ func TestGetHoldings(t *testing.T) { func TestGetBalance(t *testing.T) { _, err := GetBalance("", "", nil, asset.Empty, currency.Code{}) - if !errors.Is(err, errExchangeNameUnset) { - t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeNameUnset) + if !errors.Is(err, common.ErrExchangeNameUnset) { + t.Fatalf("received: '%v' but expected: '%v'", err, common.ErrExchangeNameUnset) } _, err = GetBalance("bruh", "", nil, asset.Empty, currency.Code{}) @@ -408,8 +409,8 @@ func TestUpdate(t *testing.T) { } err = s.Update(&Holdings{}, nil) - if !errors.Is(err, errExchangeNameUnset) { - t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeNameUnset) + if !errors.Is(err, common.ErrExchangeNameUnset) { + t.Fatalf("received: '%v' but expected: '%v'", err, common.ErrExchangeNameUnset) } err = s.Update(&Holdings{ diff --git a/exchanges/asset/asset.go b/exchanges/asset/asset.go index 8e20e59e6dd..b1e32557935 100644 --- a/exchanges/asset/asset.go +++ b/exchanges/asset/asset.go @@ -129,6 +129,11 @@ func (a Item) String() string { } } +// Upper returns the item's upper case string +func (a Item) Upper() string { + return strings.ToUpper(a.String()) +} + // Strings converts an asset type array to a string array func (a Items) Strings() []string { assets := make([]string, len(a)) diff --git a/exchanges/asset/asset_test.go b/exchanges/asset/asset_test.go index cef070415d3..98a0d1e15d4 100644 --- a/exchanges/asset/asset_test.go +++ b/exchanges/asset/asset_test.go @@ -21,6 +21,14 @@ func TestString(t *testing.T) { } } +func TestUpper(t *testing.T) { + t.Parallel() + a := Spot + require.Equal(t, "SPOT", a.Upper()) + a = 0 + require.Empty(t, a.Upper()) +} + func TestStrings(t *testing.T) { t.Parallel() assert.ElementsMatch(t, Items{Spot, Futures}.Strings(), []string{"spot", "futures"}) diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index d62f24ef344..fead064361b 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -3,761 +3,1387 @@ package coinbasepro import ( "bytes" "context" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "net/http" "net/url" - "slices" "strconv" "strings" "time" + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/types" ) const ( - coinbaseproAPIURL = "https://api.pro.coinbase.com/" - coinbaseproSandboxAPIURL = "https://api-public.sandbox.pro.coinbase.com/" - tradeBaseURL = "https://www.coinbase.com/advanced-trade/spot/" - coinbaseproAPIVersion = "0" - coinbaseproProducts = "products" - coinbaseproOrderbook = "book" - coinbaseproTicker = "ticker" - coinbaseproTrades = "trades" - coinbaseproHistory = "candles" - coinbaseproStats = "stats" - coinbaseproCurrencies = "currencies" - coinbaseproAccounts = "accounts" - coinbaseproLedger = "ledger" - coinbaseproHolds = "holds" - coinbaseproOrders = "orders" - coinbaseproFills = "fills" - coinbaseproTransfers = "transfers" - coinbaseproReports = "reports" - coinbaseproTime = "time" - coinbaseproMarginTransfer = "profiles/margin-transfer" - coinbaseproPosition = "position" - coinbaseproPositionClose = "position/close" - coinbaseproPaymentMethod = "payment-methods" - coinbaseproPaymentMethodDeposit = "deposits/payment-method" - coinbaseproDepositCoinbase = "deposits/coinbase-account" - coinbaseproWithdrawalPaymentMethod = "withdrawals/payment-method" - coinbaseproWithdrawalCoinbase = "withdrawals/coinbase" - coinbaseproWithdrawalCrypto = "withdrawals/crypto" - coinbaseproCoinbaseAccounts = "coinbase-accounts" - coinbaseproTrailingVolume = "users/self/trailing-volume" + coinbaseAPIURL = "https://api.coinbase.com" + coinbaseV1APIURL = "https://api.exchange.coinbase.com/" + coinbaseproSandboxAPIURL = "https://api-public.sandbox.exchange.coinbase.com/" + tradeBaseURL = "https://www.coinbase.com/advanced-trade/spot/" + coinbaseV3 = "/api/v3/brokerage/" + coinbaseAccounts = "accounts" + coinbaseBestBidAsk = "best_bid_ask" + coinbaseProductBook = "product_book" + coinbaseProducts = "products" + coinbaseOrders = "orders" + coinbaseBatchCancel = "batch_cancel" + coinbaseHistorical = "historical" + coinbaseBatch = "batch" + coinbaseEdit = "edit" + coinbaseEditPreview = "edit_preview" + coinbaseFills = "fills" + coinbaseCandles = "candles" + coinbaseTicker = "ticker" + coinbasePreview = "preview" + coinbasePortfolios = "portfolios" + coinbaseMoveFunds = "move_funds" + coinbaseCFM = "cfm" + coinbaseBalanceSummary = "balance_summary" + coinbasePositions = "positions" + coinbaseSweeps = "sweeps" + coinbaseSchedule = "schedule" + coinbaseIntx = "intx" + coinbaseAllocate = "allocate" + coinbasePortfolio = "portfolio" + coinbaseTransactionSummary = "transaction_summary" + coinbaseConvert = "convert" + coinbaseQuote = "quote" + coinbaseTrade = "trade" + coinbasePaymentMethods = "payment_methods" + coinbaseV2 = "/v2/" + coinbaseNotifications = "notifications" + coinbaseUser = "user" + coinbaseAddresses = "addresses" + coinbaseTransactions = "transactions" + coinbaseDeposits = "deposits" + coinbaseCommit = "commit" + coinbaseWithdrawals = "withdrawals" + coinbaseCurrencies = "currencies" + coinbaseCrypto = "crypto" + coinbaseExchangeRates = "exchange-rates" + coinbasePrices = "prices" + coinbaseTime = "time" + coinbaseVolumeSummary = "volume-summary" + coinbaseBook = "book" + coinbaseStats = "stats" + coinbaseTrades = "trades" + coinbaseWrappedAssets = "wrapped-assets" + coinbaseConversionRate = "conversion-rate" + coinbaseMarket = "market" + + granUnknown = "UNKNOWN_GRANULARITY" + granOneMin = "ONE_MINUTE" + granFiveMin = "FIVE_MINUTE" + granFifteenMin = "FIFTEEN_MINUTE" + granThirtyMin = "THIRTY_MINUTE" + granOneHour = "ONE_HOUR" + granTwoHour = "TWO_HOUR" + granSixHour = "SIX_HOUR" + granOneDay = "ONE_DAY" + startDateString = "start_date" + endDateString = "end_date" + + warnSequenceIssue = "Out of order sequence number. Received %v, expected %v" + warnAuth = "%v authenticated request failed, attempting unauthenticated" + + manyFills = 65535 + manyOrds = 2147483647 ) -// CoinbasePro is the overarching type across the coinbasepro package -type CoinbasePro struct { - exchange.Base -} +// Constants defining whether a transfer is a deposit or withdrawal, used to simplify interactions with a few endpoints +const ( + FiatDeposit FiatTransferType = false + FiatWithdrawal FiatTransferType = true +) + +// While the exchange's fee pages say the worst taker/maker fees are lower than the ones listed here, the data returned by the GetTransactionsSummary endpoint are consistent with these worst case scenarios. The best case scenarios are untested, and assumed to be in line with the fee pages +const ( + WorstCaseTakerFee = 0.012 + WorstCaseMakerFee = 0.006 + BestCaseTakerFee = 0.0005 + BestCaseMakerFee = 0 + StablePairMakerFee = 0 + WorstCaseStablePairTakerFee = 0.000045 +) -// GetProducts returns supported currency pairs on the exchange with specific -// information about the pair -func (c *CoinbasePro) GetProducts(ctx context.Context) ([]Product, error) { - var products []Product +var ( + errAccountIDEmpty = errors.New("account id cannot be empty") + errClientOrderIDEmpty = errors.New("client order id cannot be empty") + errProductIDEmpty = errors.New("product id cannot be empty") + errOrderIDEmpty = errors.New("order ids cannot be empty") + errCancelLimitExceeded = errors.New("100 order cancel limit exceeded") + errOpenPairWithOtherTypes = errors.New("cannot pair open orders with other order types") + errSizeAndPriceZero = errors.New("size and price cannot both be 0") + errCurrWalletConflict = errors.New("exactly one of walletID and currency must be specified") + errWalletIDEmpty = errors.New("wallet id cannot be empty") + errAddressIDEmpty = errors.New("address id cannot be empty") + errTransactionTypeEmpty = errors.New("transaction type cannot be empty") + errToEmpty = errors.New("to cannot be empty") + errTransactionIDEmpty = errors.New("transaction id cannot be empty") + errPaymentMethodEmpty = errors.New("payment method cannot be empty") + errDepositIDEmpty = errors.New("deposit id cannot be empty") + errInvalidPriceType = errors.New("price type must be spot, buy, or sell") + errInvalidOrderType = errors.New("order type must be market, limit, or stop") + errEndTimeInPast = errors.New("end time cannot be in the past") + errNoMatchingWallets = errors.New("no matching wallets returned") + errOrderModFailNoRet = errors.New("order modification failed but no error returned") + errNameEmpty = errors.New("name cannot be empty") + errPortfolioIDEmpty = errors.New("portfolio id cannot be empty") + errFeeTypeNotSupported = errors.New("fee type not supported") + errCantDecodePrivKey = errors.New("cannot decode private key") + errNoWalletForCurrency = errors.New("no wallet found for currency, address creation impossible") + errChannelNameUnknown = errors.New("unknown channel name") + errNoWalletsReturned = errors.New("no wallets returned") + errPayMethodNotFound = errors.New("payment method not found") + errUnknownL2DataType = errors.New("unknown l2update data type") + errOrderFailedToCancel = errors.New("failed to cancel order") + errUnrecognisedStatusType = errors.New("unrecognised status type") + errStringConvert = errors.New("unable to convert into string value") + errFloatConvert = errors.New("unable to convert into float64 value") + errWrappedAssetEmpty = errors.New("wrapped asset cannot be empty") + errUnrecognisedOrderType = errors.New("unrecognised order type") + errUnrecognisedAssetType = errors.New("unrecognised asset type") + errUnrecognisedStrategyType = errors.New("unrecognised strategy type") + errIntervalNotSupported = errors.New("interval not supported") + errEndpointPathInvalid = errors.New("endpoint path invalid, should start with https://") + + allowedGranularities = []string{granOneMin, granFiveMin, granFifteenMin, granThirtyMin, granOneHour, granTwoHour, granSixHour, granOneDay} + closedStatuses = []string{"FILLED", "CANCELLED", "EXPIRED", "FAILED"} + openStatus = []string{"OPEN"} +) - return products, c.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseproProducts, &products) +// GetAllAccounts returns information on all trading accounts associated with the API key +func (c *CoinbasePro) GetAllAccounts(ctx context.Context, limit uint8, cursor string) (*AllAccountsResponse, error) { + vals := url.Values{} + if limit != 0 { + vals.Set("limit", strconv.FormatUint(uint64(limit), 10)) + } + if cursor != "" { + vals.Set("cursor", cursor) + } + var resp AllAccountsResponse + return &resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseV3+coinbaseAccounts, vals, nil, true, &resp, nil) } -// GetOrderbook returns orderbook by currency pair and level -func (c *CoinbasePro) GetOrderbook(ctx context.Context, symbol string, level int) (interface{}, error) { - orderbook := OrderbookResponse{} +// GetAccountByID returns information for a single account +func (c *CoinbasePro) GetAccountByID(ctx context.Context, accountID string) (*Account, error) { + if accountID == "" { + return nil, errAccountIDEmpty + } + path := coinbaseV3 + coinbaseAccounts + "/" + accountID + resp := struct { + Account Account `json:"account"` + }{} + return &resp.Account, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) +} - path := fmt.Sprintf("%s/%s/%s", coinbaseproProducts, symbol, coinbaseproOrderbook) - if level > 0 { - levelStr := strconv.Itoa(level) - path = fmt.Sprintf("%s/%s/%s?level=%s", coinbaseproProducts, symbol, coinbaseproOrderbook, levelStr) +// GetBestBidAsk returns the best bid/ask for all products. Can be filtered to certain products by passing through additional strings +func (c *CoinbasePro) GetBestBidAsk(ctx context.Context, products []string) ([]ProductBook, error) { + vals := url.Values{} + for x := range products { + vals.Add("product_ids", products[x]) } + resp := struct { + Pricebooks []ProductBook `json:"pricebooks"` + }{} + return resp.Pricebooks, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseV3+coinbaseBestBidAsk, vals, nil, true, &resp, nil) +} - if err := c.SendHTTPRequest(ctx, exchange.RestSpot, path, &orderbook); err != nil { - return nil, err +// GetProductBookV3 returns a list of bids/asks for a single product +func (c *CoinbasePro) GetProductBookV3(ctx context.Context, productID currency.Pair, limit uint16, aggregationIncrement float64, authenticated bool) (*ProductBookResp, error) { + if productID.IsEmpty() { + return nil, errProductIDEmpty + } + vals := url.Values{} + vals.Set("product_id", productID.String()) + if limit != 0 { + vals.Set("limit", strconv.FormatInt(int64(limit), 10)) + } + if aggregationIncrement != 0 { + vals.Set("aggregation_price_increment", strconv.FormatFloat(aggregationIncrement, 'f', -1, 64)) } + var resp *ProductBookResp + if authenticated { + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseV3+coinbaseProductBook, vals, nil, true, &resp, nil) + } + path := coinbaseV3 + coinbaseMarket + "/" + coinbaseProductBook + return resp, c.SendHTTPRequest(ctx, exchange.RestSpot, path, vals, &resp) +} - if level == 3 { - ob := OrderbookL3{ - Sequence: orderbook.Sequence, - Bids: make([]OrderL3, len(orderbook.Bids)), - Asks: make([]OrderL3, len(orderbook.Asks)), - } - ob.Sequence = orderbook.Sequence - for x := range orderbook.Asks { - priceConv, ok := orderbook.Asks[x][0].(string) - if !ok { - return nil, errors.New("unable to type assert price") - } - price, err := strconv.ParseFloat(priceConv, 64) - if err != nil { - return nil, err - } - amountConv, ok := orderbook.Asks[x][1].(string) - if !ok { - return nil, errors.New("unable to type assert amount") - } - amount, err := strconv.ParseFloat(amountConv, 64) - if err != nil { - return nil, err - } - ordID, ok := orderbook.Asks[x][2].(string) - if !ok { - return nil, errors.New("unable to type assert order ID") - } - ob.Asks[x] = OrderL3{Price: price, Amount: amount, OrderID: ordID} - } - for x := range orderbook.Bids { - priceConv, ok := orderbook.Bids[x][0].(string) - if !ok { - return nil, errors.New("unable to type assert price") - } - price, err := strconv.ParseFloat(priceConv, 64) - if err != nil { - return nil, err - } - amountConv, ok := orderbook.Bids[x][1].(string) - if !ok { - return nil, errors.New("unable to type assert amount") - } - amount, err := strconv.ParseFloat(amountConv, 64) - if err != nil { - return nil, err - } - ordID, ok := orderbook.Bids[x][2].(string) - if !ok { - return nil, errors.New("unable to type assert order ID") - } - ob.Bids[x] = OrderL3{Price: price, Amount: amount, OrderID: ordID} - } - return ob, nil +// GetAllProducts returns information on all currency pairs that are available for trading +func (c *CoinbasePro) GetAllProducts(ctx context.Context, limit, offset int32, productType, contractExpiryType, expiringContractStatus string, productIDs []string, authenticated bool) (*AllProducts, error) { + vals := url.Values{} + vals.Set("limit", strconv.FormatInt(int64(limit), 10)) + if offset != 0 { + vals.Set("offset", strconv.FormatInt(int64(offset), 10)) } - ob := OrderbookL1L2{ - Sequence: orderbook.Sequence, - Bids: make([]OrderL1L2, len(orderbook.Bids)), - Asks: make([]OrderL1L2, len(orderbook.Asks)), + if productType != "" { + vals.Set("product_type", productType) } - for x := range orderbook.Asks { - priceConv, ok := orderbook.Asks[x][0].(string) - if !ok { - return nil, errors.New("unable to type assert price") - } - price, err := strconv.ParseFloat(priceConv, 64) - if err != nil { - return nil, err - } - amountConv, ok := orderbook.Asks[x][1].(string) - if !ok { - return nil, errors.New("unable to type assert amount") - } - amount, err := strconv.ParseFloat(amountConv, 64) - if err != nil { - return nil, err - } - numOrders, ok := orderbook.Asks[x][2].(float64) - if !ok { - return nil, errors.New("unable to type assert number of orders") - } - ob.Asks[x] = OrderL1L2{Price: price, Amount: amount, NumOrders: numOrders} + if contractExpiryType != "" { + vals.Set("contract_expiry_type", contractExpiryType) } - for x := range orderbook.Bids { - priceConv, ok := orderbook.Bids[x][0].(string) - if !ok { - return nil, errors.New("unable to type assert price") - } - price, err := strconv.ParseFloat(priceConv, 64) - if err != nil { - return nil, err - } - amountConv, ok := orderbook.Bids[x][1].(string) - if !ok { - return nil, errors.New("unable to type assert amount") - } - amount, err := strconv.ParseFloat(amountConv, 64) - if err != nil { - return nil, err - } - numOrders, ok := orderbook.Bids[x][2].(float64) - if !ok { - return nil, errors.New("unable to type assert number of orders") - } - ob.Bids[x] = OrderL1L2{Price: price, Amount: amount, NumOrders: numOrders} + if expiringContractStatus != "" { + vals.Set("expiring_contract_status", expiringContractStatus) } - return ob, nil + for x := range productIDs { + vals.Add("product_ids", productIDs[x]) + } + var resp AllProducts + if authenticated { + return &resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseV3+coinbaseProducts, vals, nil, true, &resp, nil) + } + path := coinbaseV3 + coinbaseMarket + "/" + coinbaseProducts + return &resp, c.SendHTTPRequest(ctx, exchange.RestSpot, path, vals, &resp) } -// GetTicker returns ticker by currency pair -// currencyPair - example "BTC-USD" -func (c *CoinbasePro) GetTicker(ctx context.Context, currencyPair string) (Ticker, error) { - tick := Ticker{} - path := fmt.Sprintf( - "%s/%s/%s", coinbaseproProducts, currencyPair, coinbaseproTicker) - return tick, c.SendHTTPRequest(ctx, exchange.RestSpot, path, &tick) +// GetProductByID returns information on a single specified currency pair +func (c *CoinbasePro) GetProductByID(ctx context.Context, productID string, authenticated bool) (*Product, error) { + if productID == "" { + return nil, errProductIDEmpty + } + var resp Product + if authenticated { + path := coinbaseV3 + coinbaseProducts + "/" + productID + return &resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) + } + path := coinbaseV3 + coinbaseMarket + "/" + coinbaseProducts + "/" + productID + return &resp, c.SendHTTPRequest(ctx, exchange.RestSpot, path, nil, &resp) } -// GetTrades listd the latest trades for a product -// currencyPair - example "BTC-USD" -func (c *CoinbasePro) GetTrades(ctx context.Context, currencyPair string) ([]Trade, error) { - var trades []Trade - path := fmt.Sprintf( - "%s/%s/%s", coinbaseproProducts, currencyPair, coinbaseproTrades) - return trades, c.SendHTTPRequest(ctx, exchange.RestSpot, path, &trades) +// GetHistoricRates returns historic rates for a product. Rates are returned in grouped buckets based on requested granularity. Requests that return more than 300 data points are rejected +func (c *CoinbasePro) GetHistoricRates(ctx context.Context, productID, granularity string, startDate, endDate time.Time, authenticated bool) ([]CandleStruct, error) { + if productID == "" { + return nil, errProductIDEmpty + } + validGran := common.StringSliceContains(allowedGranularities, granularity) + if !validGran { + return nil, fmt.Errorf("%w %v, allowed granularities are: %+v", kline.ErrUnsupportedInterval, granularity, allowedGranularities) + } + vals := url.Values{} + vals.Set("start", strconv.FormatInt(startDate.Unix(), 10)) + vals.Set("end", strconv.FormatInt(endDate.Unix(), 10)) + vals.Set("granularity", granularity) + resp := struct { + Candles []CandleStruct `json:"candles"` + }{} + if authenticated { + path := coinbaseV3 + coinbaseProducts + "/" + productID + "/" + coinbaseCandles + return resp.Candles, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp, nil) + } + path := coinbaseV3 + coinbaseMarket + "/" + coinbaseProducts + "/" + productID + "/" + coinbaseCandles + return resp.Candles, c.SendHTTPRequest(ctx, exchange.RestSpot, path, vals, &resp) } -// GetHistoricRates returns historic rates for a product. Rates are returned in -// grouped buckets based on requested granularity. -func (c *CoinbasePro) GetHistoricRates(ctx context.Context, currencyPair, start, end string, granularity int64) ([]History, error) { - values := url.Values{} +// GetTicker returns snapshot information about the last trades (ticks) and best bid/ask. Contrary to documentation, this does not tell you the 24h volume +func (c *CoinbasePro) GetTicker(ctx context.Context, productID string, limit uint16, startDate, endDate time.Time, authenticated bool) (*Ticker, error) { + if productID == "" { + return nil, errProductIDEmpty + } + vals := url.Values{} + vals.Set("limit", strconv.FormatInt(int64(limit), 10)) + if !startDate.IsZero() && !startDate.Equal(time.Time{}) { + vals.Set("start", strconv.FormatInt(startDate.Unix(), 10)) + } + if !endDate.IsZero() && !endDate.Equal(time.Time{}) { + vals.Set("end", strconv.FormatInt(endDate.Unix(), 10)) + } + var resp Ticker + if authenticated { + path := coinbaseV3 + coinbaseProducts + "/" + productID + "/" + coinbaseTicker + return &resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp, nil) + } + path := coinbaseV3 + coinbaseMarket + "/" + coinbaseProducts + "/" + productID + "/" + coinbaseTicker + return &resp, c.SendHTTPRequest(ctx, exchange.RestSpot, path, vals, &resp) +} - if start != "" { - values.Set("start", start) - } else { - values.Set("start", "") +// PlaceOrder places either a limit, market, or stop order +func (c *CoinbasePro) PlaceOrder(ctx context.Context, clientOID, productID, side, stopDirection, orderType, stpID, marginType, rpID string, amount, limitPrice, stopPrice, leverage float64, postOnly bool, endTime time.Time) (*PlaceOrderResp, error) { + if clientOID == "" { + return nil, errClientOrderIDEmpty + } + if productID == "" { + return nil, errProductIDEmpty + } + if amount <= 0 { + return nil, order.ErrAmountIsInvalid + } + orderConfig, err := prepareOrderConfig(orderType, side, stopDirection, amount, limitPrice, stopPrice, endTime, postOnly) + if err != nil { + return nil, err } + req := map[string]any{ + "client_order_id": clientOID, + "product_id": productID, + "side": side, + "order_configuration": orderConfig, + "self_trade_prevention_id": stpID, + "leverage": strconv.FormatFloat(leverage, 'f', -1, 64), + "retail_portfolio_id": rpID, + "margin_type": FormatMarginType(marginType)} + var resp PlaceOrderResp + return &resp, + c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseV3+coinbaseOrders, nil, req, true, &resp, nil) +} - if end != "" { - values.Set("end", end) - } else { - values.Set("end", "") +// CancelOrders cancels orders by orderID. Can only cancel 100 orders per request +func (c *CoinbasePro) CancelOrders(ctx context.Context, orderIDs []string) ([]OrderCancelDetail, error) { + if len(orderIDs) == 0 { + return nil, errOrderIDEmpty } + if len(orderIDs) > 100 { + return nil, errCancelLimitExceeded + } + path := coinbaseV3 + coinbaseOrders + "/" + coinbaseBatchCancel + req := map[string]any{"order_ids": orderIDs} + resp := struct { + Results []OrderCancelDetail `json:"results"` + }{} + return resp.Results, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp, nil) +} - allowedGranularities := []int64{60, 300, 900, 3600, 21600, 86400} - if !slices.Contains(allowedGranularities, granularity) { - return nil, errors.New("Invalid granularity value: " + strconv.FormatInt(granularity, 10) + ". Allowed values are {60, 300, 900, 3600, 21600, 86400}") +// EditOrder edits an order to a new size or price. Only limit orders with a good-till-cancelled time in force can be edited +func (c *CoinbasePro) EditOrder(ctx context.Context, orderID string, size, price float64) (bool, error) { + if orderID == "" { + return false, errOrderIDEmpty } - if granularity > 0 { - values.Set("granularity", strconv.FormatInt(granularity, 10)) + if size == 0 && price == 0 { + return false, errSizeAndPriceZero } + path := coinbaseV3 + coinbaseOrders + "/" + coinbaseEdit + req := map[string]any{ + "order_id": orderID, + "size": strconv.FormatFloat(size, 'f', -1, 64), + "price": strconv.FormatFloat(price, 'f', -1, 64)} + resp := struct { + Success bool `json:"success"` + }{} + return resp.Success, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp, nil) +} - var resp [][6]float64 - path := common.EncodeURLValues( - fmt.Sprintf("%s/%s/%s", coinbaseproProducts, currencyPair, coinbaseproHistory), - values) - if err := c.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp); err != nil { - return nil, err +// EditOrderPreview simulates an edit order request, to preview the result. Only limit orders with a good-till-cancelled time in force can be edited. +func (c *CoinbasePro) EditOrderPreview(ctx context.Context, orderID string, size, price float64) (*EditOrderPreviewResp, error) { + if orderID == "" { + return nil, errOrderIDEmpty + } + if size == 0 && price == 0 { + return nil, errSizeAndPriceZero } + path := coinbaseV3 + coinbaseOrders + "/" + coinbaseEditPreview + req := map[string]any{ + "order_id": orderID, + "size": strconv.FormatFloat(size, 'f', -1, 64), + "price": strconv.FormatFloat(price, 'f', -1, 64)} + var resp *EditOrderPreviewResp + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp, nil) +} - history := make([]History, len(resp)) - for x := range resp { - history[x] = History{ - Time: time.Unix(int64(resp[x][0]), 0), - Low: resp[x][1], - High: resp[x][2], - Open: resp[x][3], - Close: resp[x][4], - Volume: resp[x][5], +// GetAllOrders lists orders, filtered by their status +func (c *CoinbasePro) GetAllOrders(ctx context.Context, productID, userNativeCurrency, orderType, orderSide, cursor, productType, orderPlacementSource, contractExpiryType, retailPortfolioID string, orderStatus, assetFilters []string, limit int32, startDate, endDate time.Time) (*GetAllOrdersResp, error) { + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startDate, endDate, startDateString, endDateString) + if err != nil { + return nil, err + } + for x := range orderStatus { + if orderStatus[x] == "OPEN" && len(orderStatus) > 1 { + return nil, errOpenPairWithOtherTypes } + params.Values.Add("order_status", orderStatus[x]) + } + for x := range assetFilters { + params.Values.Add("asset_filters", assetFilters[x]) + } + if productID != "" { + params.Values.Set("product_id", productID) + } + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(int64(limit), 10)) + } + if cursor != "" { + params.Values.Set("cursor", cursor) + } + if userNativeCurrency != "" { + params.Values.Set("user_native_currency", userNativeCurrency) + } + if orderPlacementSource != "" { + params.Values.Set("order_placement_source", orderPlacementSource) + } + if productType != "" { + params.Values.Set("product_type", productType) + } + if orderSide != "" { + params.Values.Set("order_side", orderSide) + } + if contractExpiryType != "" { + params.Values.Set("contract_expiry_type", contractExpiryType) + } + // This functionality has been deprecated, and only works for legacy API keys + if retailPortfolioID != "" { + params.Values.Set("retail_portfolio_id", retailPortfolioID) + } + if orderType != "" { + params.Values.Set("order_type", orderType) } + path := coinbaseV3 + coinbaseOrders + "/" + coinbaseHistorical + "/" + coinbaseBatch + var resp GetAllOrdersResp + return &resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, params.Values, nil, true, &resp, nil) +} - return history, nil +// GetFills returns information of recent fills on the specified order +func (c *CoinbasePro) GetFills(ctx context.Context, orderID, productID, cursor string, startDate, endDate time.Time, limit uint16) (*FillResponse, error) { + var params Params + params.Values = url.Values{} + err := params.prepareDateString(startDate, endDate, "start_sequence_timestamp", "end_sequence_timestamp") + if err != nil { + return nil, err + } + if orderID != "" { + params.Values.Set("order_id", orderID) + } + if productID != "" { + params.Values.Set("product_id", productID) + } + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(int64(limit), 10)) + } + if cursor != "" { + params.Values.Set("cursor", cursor) + } + path := coinbaseV3 + coinbaseOrders + "/" + coinbaseHistorical + "/" + coinbaseFills + var resp FillResponse + return &resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, params.Values, nil, true, &resp, nil) } -// GetStats returns a 24 hr stat for the product. Volume is in base currency -// units. open, high, low are in quote currency units. -func (c *CoinbasePro) GetStats(ctx context.Context, currencyPair string) (Stats, error) { - stats := Stats{} - path := fmt.Sprintf( - "%s/%s/%s", coinbaseproProducts, currencyPair, coinbaseproStats) +// GetOrderByID returns a single order by order id. +func (c *CoinbasePro) GetOrderByID(ctx context.Context, orderID, clientOID, userNativeCurrency string) (*GetOrderResponse, error) { + if orderID == "" { + return nil, errOrderIDEmpty + } + resp := struct { + Order GetOrderResponse `json:"order"` + }{} + vals := url.Values{} + if clientOID != "" { + vals.Set("client_order_id", clientOID) + } + if userNativeCurrency != "" { + vals.Set("user_native_currency", userNativeCurrency) + } + path := coinbaseV3 + coinbaseOrders + "/" + coinbaseHistorical + "/" + orderID + return &resp.Order, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp, nil) +} - return stats, c.SendHTTPRequest(ctx, exchange.RestSpot, path, &stats) +// PreviewOrder simulates the results of an order request +func (c *CoinbasePro) PreviewOrder(ctx context.Context, productID, side, orderType, stopDirection, marginType string, commissionValue, amount, limitPrice, stopPrice, tradableBalance, leverage float64, postOnly, isMax, skipFCMRiskCheck bool, endTime time.Time) (*PreviewOrderResp, error) { + if amount == 0 { + return nil, order.ErrAmountIsInvalid + } + orderConfig, err := prepareOrderConfig(orderType, side, stopDirection, amount, limitPrice, stopPrice, endTime, postOnly) + if err != nil { + return nil, err + } + req := map[string]any{ + "product_id": productID, + "side": side, + "commission_rate": map[string]string{"value": strconv.FormatFloat(commissionValue, 'f', -1, 64)}, + "order_configuration": orderConfig, + "is_max": isMax, + "tradable_balance": strconv.FormatFloat(tradableBalance, 'f', -1, 64), + "skip_fcm_risk_check": skipFCMRiskCheck, + "leverage": strconv.FormatFloat(leverage, 'f', -1, 64), + } + if mt := FormatMarginType(marginType); mt != "" { + req["margin_type"] = mt + } + var resp *PreviewOrderResp + path := coinbaseV3 + coinbaseOrders + "/" + coinbasePreview + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp, nil) } -// GetCurrencies returns a list of supported currency on the exchange -// Warning: Not all currencies may be currently in use for tradinc. -func (c *CoinbasePro) GetCurrencies(ctx context.Context) ([]Currency, error) { - var currencies []Currency +// GetAllPortfolios returns a list of portfolios associated with the user +func (c *CoinbasePro) GetAllPortfolios(ctx context.Context, portfolioType string) ([]SimplePortfolioData, error) { + resp := struct { + Portfolios []SimplePortfolioData `json:"portfolios"` + }{} + vals := url.Values{} + if portfolioType != "" { + vals.Set("portfolio_type", portfolioType) + } + return resp.Portfolios, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseV3+coinbasePortfolios, vals, nil, true, &resp, nil) +} - return currencies, c.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseproCurrencies, ¤cies) +// CreatePortfolio creates a new portfolio +func (c *CoinbasePro) CreatePortfolio(ctx context.Context, name string) (*SimplePortfolioData, error) { + if name == "" { + return nil, errNameEmpty + } + req := map[string]any{"name": name} + resp := struct { + Portfolio SimplePortfolioData `json:"portfolio"` + }{} + return &resp.Portfolio, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseV3+coinbasePortfolios, nil, req, true, &resp, nil) } -// GetCurrentServerTime returns the API server time -func (c *CoinbasePro) GetCurrentServerTime(ctx context.Context) (ServerTime, error) { - serverTime := ServerTime{} - return serverTime, c.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseproTime, &serverTime) +// MovePortfolioFunds transfers funds between portfolios +func (c *CoinbasePro) MovePortfolioFunds(ctx context.Context, cur, from, to string, amount float64) (*MovePortfolioFundsResponse, error) { + if from == "" || to == "" { + return nil, errPortfolioIDEmpty + } + if cur == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + if amount == 0 { + return nil, order.ErrAmountIsInvalid + } + funds := FundsData{ + Value: strconv.FormatFloat(amount, 'f', -1, 64), + Currency: cur} + req := map[string]any{ + "source_portfolio_uuid": from, + "target_portfolio_uuid": to, + "funds": funds} + path := coinbaseV3 + coinbasePortfolios + "/" + coinbaseMoveFunds + var resp *MovePortfolioFundsResponse + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp, nil) } -// GetAccounts returns a list of trading accounts associated with the APIKEYS -func (c *CoinbasePro) GetAccounts(ctx context.Context) ([]AccountResponse, error) { - var resp []AccountResponse +// GetPortfolioByID provides detailed information on a single portfolio +func (c *CoinbasePro) GetPortfolioByID(ctx context.Context, portfolioID string) (*DetailedPortfolioResponse, error) { + if portfolioID == "" { + return nil, errPortfolioIDEmpty + } + path := coinbaseV3 + coinbasePortfolios + "/" + portfolioID + resp := struct { + Breakdown DetailedPortfolioResponse `json:"breakdown"` + }{} + return &resp.Breakdown, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) +} - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproAccounts, nil, &resp) +// DeletePortfolio deletes a portfolio +func (c *CoinbasePro) DeletePortfolio(ctx context.Context, portfolioID string) error { + if portfolioID == "" { + return errPortfolioIDEmpty + } + path := coinbaseV3 + coinbasePortfolios + "/" + portfolioID + return c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, nil, true, nil, nil) } -// GetAccount returns information for a single account. Use this endpoint when -// account_id is known -func (c *CoinbasePro) GetAccount(ctx context.Context, accountID string) (AccountResponse, error) { - resp := AccountResponse{} - path := fmt.Sprintf("%s/%s", coinbaseproAccounts, accountID) +// EditPortfolio edits the name of a portfolio +func (c *CoinbasePro) EditPortfolio(ctx context.Context, portfolioID, name string) (*SimplePortfolioData, error) { + if portfolioID == "" { + return nil, errPortfolioIDEmpty + } + if name == "" { + return nil, errNameEmpty + } + req := map[string]any{"name": name} + path := coinbaseV3 + coinbasePortfolios + "/" + portfolioID + resp := struct { + Portfolio SimplePortfolioData `json:"portfolio"` + }{} + return &resp.Portfolio, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPut, path, nil, req, true, &resp, nil) +} - return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp) +// GetFuturesBalanceSummary returns information on balances related to Coinbase Financial Markets futures trading +func (c *CoinbasePro) GetFuturesBalanceSummary(ctx context.Context) (*FuturesBalanceSummary, error) { + resp := struct { + BalanceSummary FuturesBalanceSummary `json:"balance_summary"` + }{} + path := coinbaseV3 + coinbaseCFM + "/" + coinbaseBalanceSummary + return &resp.BalanceSummary, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) } -// GetAccountHistory returns a list of account activity. Account activity either -// increases or decreases your account balance. Items are paginated and sorted -// latest first. -func (c *CoinbasePro) GetAccountHistory(ctx context.Context, accountID string) ([]AccountLedgerResponse, error) { - var resp []AccountLedgerResponse - path := fmt.Sprintf("%s/%s/%s", coinbaseproAccounts, accountID, coinbaseproLedger) +// GetAllFuturesPositions returns a list of all open positions in CFM futures products +func (c *CoinbasePro) GetAllFuturesPositions(ctx context.Context) ([]FuturesPosition, error) { + resp := struct { + Positions []FuturesPosition `json:"positions"` + }{} + path := coinbaseV3 + coinbaseCFM + "/" + coinbasePositions + return resp.Positions, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) +} - return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp) +// GetFuturesPositionByID returns information on a single open position in CFM futures products +func (c *CoinbasePro) GetFuturesPositionByID(ctx context.Context, productID string) (*FuturesPosition, error) { + if productID == "" { + return nil, errProductIDEmpty + } + path := coinbaseV3 + coinbaseCFM + "/" + coinbasePositions + "/" + productID + var resp *FuturesPosition + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) } -// GetHolds returns the holds that are placed on an account for any active -// orders or pending withdraw requests. As an order is filled, the hold amount -// is updated. If an order is canceled, any remaining hold is removed. For a -// withdraw, once it is completed, the hold is removed. -func (c *CoinbasePro) GetHolds(ctx context.Context, accountID string) ([]AccountHolds, error) { - var resp []AccountHolds - path := fmt.Sprintf("%s/%s/%s", coinbaseproAccounts, accountID, coinbaseproHolds) +// ScheduleFuturesSweep schedules a sweep of funds from a CFTC-regulated futures account to a Coinbase USD Spot wallet. Request submitted before 5 pm ET are processed the following business day, requests submitted after are processed in 2 business days. Only one sweep request can be pending at a time. Funds transferred depend on the excess available in the futures account. An amount of 0 will sweep all available excess funds +func (c *CoinbasePro) ScheduleFuturesSweep(ctx context.Context, amount float64) (bool, error) { + path := coinbaseV3 + coinbaseCFM + "/" + coinbaseSweeps + "/" + coinbaseSchedule + var req map[string]any + if amount != 0 { + req = map[string]any{"usd_amount": strconv.FormatFloat(amount, 'f', -1, 64)} + } + resp := struct { + Success bool `json:"success"` + }{} + return resp.Success, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp, nil) +} - return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp) +// ListFuturesSweeps returns information on pending and/or processing requests to sweep funds +func (c *CoinbasePro) ListFuturesSweeps(ctx context.Context) ([]SweepData, error) { + resp := struct { + Sweeps []SweepData `json:"sweeps"` + }{} + path := coinbaseV3 + coinbaseCFM + "/" + coinbaseSweeps + return resp.Sweeps, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) } -// PlaceLimitOrder places a new limit order. Orders can only be placed if the -// account has sufficient funds. Once an order is placed, account funds -// will be put on hold for the duration of the order. How much and which funds -// are put on hold depends on the order type and parameters specified. -// -// GENERAL PARAMS -// clientRef - [optional] Order ID selected by you to identify your order -// side - buy or sell -// productID - A valid product id -// stp - [optional] Self-trade prevention flag -// -// LIMIT ORDER PARAMS -// price - Price per bitcoin -// amount - Amount of BTC to buy or sell -// timeInforce - [optional] GTC, GTT, IOC, or FOK (default is GTC) -// cancelAfter - [optional] min, hour, day * Requires time_in_force to be GTT -// postOnly - [optional] Post only flag Invalid when time_in_force is IOC or FOK -func (c *CoinbasePro) PlaceLimitOrder(ctx context.Context, clientRef string, price, amount float64, side string, timeInforce RequestParamsTimeForceType, cancelAfter, productID, stp string, postOnly bool) (string, error) { - resp := GeneralizedOrderResponse{} - req := make(map[string]interface{}) - req["type"] = order.Limit.Lower() - req["price"] = strconv.FormatFloat(price, 'f', -1, 64) - req["size"] = strconv.FormatFloat(amount, 'f', -1, 64) - req["side"] = side - req["product_id"] = productID +// CancelPendingFuturesSweep cancels a pending sweep request +func (c *CoinbasePro) CancelPendingFuturesSweep(ctx context.Context) (bool, error) { + path := coinbaseV3 + coinbaseCFM + "/" + coinbaseSweeps + resp := struct { + Success bool `json:"success"` + }{} + return resp.Success, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, nil, true, &resp, nil) +} - if cancelAfter != "" { - req["cancel_after"] = cancelAfter +// AllocatePortfolio allocates funds to a position in your perpetuals portfolio +func (c *CoinbasePro) AllocatePortfolio(ctx context.Context, portfolioID, productID, cur string, amount float64) error { + if portfolioID == "" { + return errPortfolioIDEmpty } - if timeInforce != "" { - req["time_in_force"] = timeInforce + if productID == "" { + return errProductIDEmpty } - if clientRef != "" { - req["client_oid"] = clientRef + if cur == "" { + return currency.ErrCurrencyCodeEmpty } - if stp != "" { - req["stp"] = stp + if amount == 0 { + return order.ErrAmountIsInvalid } - if postOnly { - req["post_only"] = postOnly + req := map[string]any{ + "portfolio_uuid": portfolioID, + "symbol": productID, + "currency": cur, + "amount": strconv.FormatFloat(amount, 'f', -1, 64)} + path := coinbaseV3 + coinbaseIntx + "/" + coinbaseAllocate + return c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, nil, nil) +} + +// GetPerpetualsPortfolioSummary returns a summary of your perpetuals portfolio +func (c *CoinbasePro) GetPerpetualsPortfolioSummary(ctx context.Context, portfolioID string) (*PerpetualsPortfolioSummary, error) { + if portfolioID == "" { + return nil, errPortfolioIDEmpty } + path := coinbaseV3 + coinbaseIntx + "/" + coinbasePortfolio + "/" + portfolioID + resp := struct { + Summary PerpetualsPortfolioSummary `json:"summary"` + }{} + return &resp.Summary, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) +} - err := c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproOrders, req, &resp) - if err != nil { - return "", err +// GetAllPerpetualsPositions returns a list of all open positions in your perpetuals portfolio +func (c *CoinbasePro) GetAllPerpetualsPositions(ctx context.Context, portfolioID string) (*AllPerpPosResponse, error) { + if portfolioID == "" { + return nil, errPortfolioIDEmpty } + path := coinbaseV3 + coinbaseIntx + "/" + coinbasePositions + "/" + portfolioID + var resp *AllPerpPosResponse + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) +} - return resp.ID, nil +// GetPerpetualsPositionByID returns information on a single open position in your perpetuals portfolio +func (c *CoinbasePro) GetPerpetualsPositionByID(ctx context.Context, portfolioID, productID string) (*OnePerpPosResponse, error) { + if portfolioID == "" { + return nil, errPortfolioIDEmpty + } + if productID == "" { + return nil, errProductIDEmpty + } + path := coinbaseV3 + coinbaseIntx + "/" + coinbasePositions + "/" + portfolioID + "/" + productID + var resp *OnePerpPosResponse + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) } -// PlaceMarketOrder places a new market order. -// Orders can only be placed if the account has sufficient funds. Once an order -// is placed, account funds will be put on hold for the duration of the order. -// How much and which funds are put on hold depends on the order type and -// parameters specified. -// -// GENERAL PARAMS -// clientRef - [optional] Order ID selected by you to identify your order -// side - buy or sell -// productID - A valid product id -// stp - [optional] Self-trade prevention flag -// -// MARKET ORDER PARAMS -// size - [optional]* Desired amount in BTC -// funds [optional]* Desired amount of quote currency to use -// * One of size or funds is required. -func (c *CoinbasePro) PlaceMarketOrder(ctx context.Context, clientRef string, size, funds float64, side, productID, stp string) (string, error) { - resp := GeneralizedOrderResponse{} - req := make(map[string]interface{}) - req["side"] = side - req["product_id"] = productID - req["type"] = order.Market.Lower() +// GetTransactionSummary returns a summary of transactions with fee tiers, total volume, and fees +func (c *CoinbasePro) GetTransactionSummary(ctx context.Context, startDate, endDate time.Time, userNativeCurrency, productType, contractExpiryType string) (*TransactionSummary, error) { + var params Params + params.Values = url.Values{} + err := params.prepareDateString(startDate, endDate, startDateString, endDateString) + if err != nil { + return nil, err + } + if contractExpiryType != "" { + params.Values.Set("contract_expiry_type", contractExpiryType) + } + if productType != "" { + params.Values.Set("product_type", productType) + } + if userNativeCurrency != "" { + params.Values.Set("user_native_currency", userNativeCurrency) + } + var resp TransactionSummary + return &resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseV3+coinbaseTransactionSummary, params.Values, nil, true, &resp, nil) +} - if size != 0 { - req["size"] = strconv.FormatFloat(size, 'f', -1, 64) +// CreateConvertQuote creates a quote for a conversion between two currencies. The trade_id returned can be used to commit the trade, but that must be done within 10 minutes of the quote's creation +func (c *CoinbasePro) CreateConvertQuote(ctx context.Context, from, to, userIncentiveID, codeVal string, amount float64) (*ConvertResponse, error) { + if from == "" || to == "" { + return nil, errAccountIDEmpty } - if funds != 0 { - req["funds"] = strconv.FormatFloat(funds, 'f', -1, 64) + if amount == 0 { + return nil, order.ErrAmountIsInvalid } - if clientRef != "" { - req["client_oid"] = clientRef + path := coinbaseV3 + coinbaseConvert + "/" + coinbaseQuote + tIM := map[string]any{ + "user_incentive_id": userIncentiveID, + "code_val": codeVal} + req := map[string]any{ + "from_account": from, + "to_account": to, + "amount": strconv.FormatFloat(amount, 'f', -1, 64), + "trade_incentive_metadata": tIM} + resp := struct { + Trade ConvertResponse `json:"trade"` + }{} + return &resp.Trade, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp, nil) +} + +// CommitConvertTrade commits a conversion between two currencies, using the trade_id returned from CreateConvertQuote +func (c *CoinbasePro) CommitConvertTrade(ctx context.Context, tradeID, from, to string) (*ConvertResponse, error) { + if tradeID == "" { + return nil, errTransactionIDEmpty } - if stp != "" { - req["stp"] = stp + if from == "" || to == "" { + return nil, errAccountIDEmpty } + path := coinbaseV3 + coinbaseConvert + "/" + coinbaseTrade + "/" + tradeID + req := map[string]any{ + "from_account": from, + "to_account": to} + resp := struct { + Trade ConvertResponse `json:"trade"` + }{} + return &resp.Trade, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp, nil) +} - err := c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproOrders, req, &resp) - if err != nil { - return "", err +// GetConvertTradeByID returns information on a conversion between two currencies +func (c *CoinbasePro) GetConvertTradeByID(ctx context.Context, tradeID, from, to string) (*ConvertResponse, error) { + if tradeID == "" { + return nil, errTransactionIDEmpty + } + if from == "" || to == "" { + return nil, errAccountIDEmpty } + path := coinbaseV3 + coinbaseConvert + "/" + coinbaseTrade + "/" + tradeID + req := map[string]any{ + "from_account": from, + "to_account": to} + resp := struct { + Trade ConvertResponse `json:"trade"` + }{} + return &resp.Trade, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, req, true, &resp, nil) +} - return resp.ID, nil +// GetV3Time returns the current server time, calling V3 of the API +func (c *CoinbasePro) GetV3Time(ctx context.Context) (*ServerTimeV3, error) { + var resp *ServerTimeV3 + return resp, c.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseV3+coinbaseTime, nil, &resp) } -// PlaceMarginOrder places a new market order. -// Orders can only be placed if the account has sufficient funds. Once an order -// is placed, account funds will be put on hold for the duration of the order. -// How much and which funds are put on hold depends on the order type and -// parameters specified. -// -// GENERAL PARAMS -// clientRef - [optional] Order ID selected by you to identify your order -// side - buy or sell -// productID - A valid product id -// stp - [optional] Self-trade prevention flag -// -// MARGIN ORDER PARAMS -// size - [optional]* Desired amount in BTC -// funds - [optional]* Desired amount of quote currency to use -func (c *CoinbasePro) PlaceMarginOrder(ctx context.Context, clientRef string, size, funds float64, side, productID, stp string) (string, error) { - resp := GeneralizedOrderResponse{} - req := make(map[string]interface{}) - req["side"] = side - req["product_id"] = productID - req["type"] = "margin" +// GetAllPaymentMethods returns a list of all payment methods associated with the user's account +func (c *CoinbasePro) GetAllPaymentMethods(ctx context.Context) ([]PaymentMethodData, error) { + resp := struct { + PaymentMethods []PaymentMethodData `json:"payment_methods"` + }{} + req := map[string]any{"currency": "BTC"} + path := coinbaseV3 + coinbasePaymentMethods + return resp.PaymentMethods, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, req, true, &resp, nil) +} - if size != 0 { - req["size"] = strconv.FormatFloat(size, 'f', -1, 64) +// GetPaymentMethodByID returns information on a single payment method associated with the user's account +func (c *CoinbasePro) GetPaymentMethodByID(ctx context.Context, paymentMethodID string) (*PaymentMethodData, error) { + if paymentMethodID == "" { + return nil, errPaymentMethodEmpty } - if funds != 0 { - req["funds"] = strconv.FormatFloat(funds, 'f', -1, 64) + path := coinbaseV3 + coinbasePaymentMethods + "/" + paymentMethodID + resp := struct { + PaymentMethod PaymentMethodData `json:"payment_method"` + }{} + return &resp.PaymentMethod, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp, nil) +} + +// GetCurrentUser returns information about the user associated with the API key +func (c *CoinbasePro) GetCurrentUser(ctx context.Context) (*UserResponse, error) { + resp := struct { + Data UserResponse `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseV2+coinbaseUser, nil, nil, false, &resp, nil) +} + +// GetAllWallets lists all accounts associated with the API key +func (c *CoinbasePro) GetAllWallets(ctx context.Context, pag PaginationInp) (*GetAllWalletsResponse, error) { + var resp *GetAllWalletsResponse + var params Params + params.Values = url.Values{} + params.preparePagination(pag) + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseV2+coinbaseAccounts, params.Values, nil, false, &resp, nil) +} + +// GetWalletByID returns information about a single wallet. In lieu of a wallet ID, a currency can be provided to get the primary account for that currency +func (c *CoinbasePro) GetWalletByID(ctx context.Context, walletID, currency string) (*WalletData, error) { + if (walletID == "" && currency == "") || (walletID != "" && currency != "") { + return nil, errCurrWalletConflict } - if clientRef != "" { - req["client_oid"] = clientRef + var path string + if walletID != "" { + path = coinbaseV2 + coinbaseAccounts + "/" + walletID } - if stp != "" { - req["stp"] = stp + if currency != "" { + path = coinbaseV2 + coinbaseAccounts + "/" + currency } + resp := struct { + Data WalletData `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, false, &resp, nil) +} - err := c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproOrders, req, &resp) - if err != nil { - return "", err +// CreateAddress generates a crypto address for depositing to the specified wallet +func (c *CoinbasePro) CreateAddress(ctx context.Context, walletID, name string) (*AddressData, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + path := coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseAddresses + req := map[string]any{"name": name} + resp := struct { + Data AddressData `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, false, &resp, nil) +} + +// GetAllAddresses returns information on all addresses associated with a wallet +func (c *CoinbasePro) GetAllAddresses(ctx context.Context, walletID string, pag PaginationInp) (*GetAllAddrResponse, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + path := coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseAddresses + var params Params + params.Values = url.Values{} + params.preparePagination(pag) + var resp *GetAllAddrResponse + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, params.Values, nil, false, &resp, nil) +} + +// GetAddressByID returns information on a single address associated with the specified wallet +func (c *CoinbasePro) GetAddressByID(ctx context.Context, walletID, addressID string) (*AddressData, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + if addressID == "" { + return nil, errAddressIDEmpty } + path := coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseAddresses + "/" + addressID + resp := struct { + Data AddressData `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, false, &resp, nil) +} + +// GetAddressTransactions returns a list of transactions associated with the specified address +func (c *CoinbasePro) GetAddressTransactions(ctx context.Context, walletID, addressID string, pag PaginationInp) (*ManyTransactionsResp, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + if addressID == "" { + return nil, errAddressIDEmpty + } + path := coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseAddresses + "/" + addressID + "/" + coinbaseTransactions + var params Params + params.Values = url.Values{} + params.preparePagination(pag) + var resp *ManyTransactionsResp + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, params.Values, nil, false, &resp, nil) +} - return resp.ID, nil +// SendMoney can send funds to an email or cryptocurrency address (if "traType" is set to "send"), or to another one of the user's wallets or vaults (if "traType" is set to "transfer"). Coinbase may delay or cancel the transaction at their discretion. The "idem" parameter is an optional string for idempotency; a token with a max length of 100 characters, if a previous transaction included the same token as a parameter, the new transaction won't be processed, and information on the previous transaction will be returned instead +func (c *CoinbasePro) SendMoney(ctx context.Context, traType, walletID, to, cur, description, idem, financialInstitutionWebsite, destinationTag string, amount float64, skipNotifications, toFinancialInstitution bool) (*TransactionData, error) { + if traType == "" { + return nil, errTransactionTypeEmpty + } + if walletID == "" { + return nil, errWalletIDEmpty + } + if to == "" { + return nil, errToEmpty + } + if amount == 0 { + return nil, order.ErrAmountIsInvalid + } + if cur == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + path := coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseTransactions + req := map[string]any{ + "type": traType, + "to": to, + "amount": strconv.FormatFloat(amount, 'f', -1, 64), + "currency": cur, + "description": description, + "skip_notifications": skipNotifications, + "idem": idem, + "to_financial_institution": toFinancialInstitution, + "financial_institution_website": financialInstitutionWebsite, + "destination_tag": destinationTag} + resp := struct { + Data TransactionData `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, false, &resp, nil) } -// CancelExistingOrder cancels order by orderID -func (c *CoinbasePro) CancelExistingOrder(ctx context.Context, orderID string) error { - path := fmt.Sprintf("%s/%s", coinbaseproOrders, orderID) +// GetAllTransactions returns a list of transactions associated with the specified wallet +func (c *CoinbasePro) GetAllTransactions(ctx context.Context, walletID string, pag PaginationInp) (*ManyTransactionsResp, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + path := coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseTransactions + var params Params + params.Values = url.Values{} + params.preparePagination(pag) + var resp *ManyTransactionsResp + return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, params.Values, nil, false, &resp, nil) +} - return c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, nil) +// GetTransactionByID returns information on a single transaction associated with the specified wallet +func (c *CoinbasePro) GetTransactionByID(ctx context.Context, walletID, transactionID string) (*TransactionData, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + if transactionID == "" { + return nil, errTransactionIDEmpty + } + path := coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseTransactions + "/" + transactionID + resp := struct { + Data TransactionData `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, false, &resp, nil) } -// CancelAllExistingOrders cancels all open orders on the exchange and returns -// and array of order IDs -// currencyPair - [optional] all orders for a currencyPair string will be -// canceled -func (c *CoinbasePro) CancelAllExistingOrders(ctx context.Context, currencyPair string) ([]string, error) { - var resp []string - req := make(map[string]interface{}) +// FiatTransfer prepares and optionally processes a transfer of funds between the exchange and a fiat payment method. "Deposit" signifies funds going from exchange to bank, "withdraw" signifies funds going from bank to exchange +func (c *CoinbasePro) FiatTransfer(ctx context.Context, walletID, cur, paymentMethod string, amount float64, commit bool, transferType FiatTransferType) (*DeposWithdrData, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + if amount == 0 { + return nil, order.ErrAmountIsInvalid + } + if cur == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + if paymentMethod == "" { + return nil, errPaymentMethodEmpty + } + var path string + switch transferType { + case FiatDeposit: + path = coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseDeposits + case FiatWithdrawal: + path = coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseWithdrawals + } + req := map[string]any{ + "currency": cur, + "payment_method": paymentMethod, + "amount": strconv.FormatFloat(amount, 'f', -1, 64), + "commit": commit} + resp := struct { + Data DeposWithdrData `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, false, &resp, nil) +} - if currencyPair != "" { - req["product_id"] = currencyPair +// CommitTransfer processes a deposit/withdrawal that was created with the "commit" parameter set to false +func (c *CoinbasePro) CommitTransfer(ctx context.Context, walletID, depositID string, transferType FiatTransferType) (*DeposWithdrData, error) { + if walletID == "" { + return nil, errWalletIDEmpty } - return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, coinbaseproOrders, req, &resp) + if depositID == "" { + return nil, errDepositIDEmpty + } + var path string + switch transferType { + case FiatDeposit: + path = coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseDeposits + "/" + depositID + "/" + coinbaseCommit + case FiatWithdrawal: + path = coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseWithdrawals + "/" + depositID + "/" + coinbaseCommit + } + resp := struct { + Data DeposWithdrData `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, nil, false, &resp, nil) } -// GetOrders lists current open orders. Only open or un-settled orders are -// returned. As soon as an order is no longer open and settled, it will no -// longer appear in the default request. -// status - can be a range of "open", "pending", "done" or "active" -// currencyPair - [optional] for example "BTC-USD" -func (c *CoinbasePro) GetOrders(ctx context.Context, status []string, currencyPair string) ([]GeneralizedOrderResponse, error) { - var resp []GeneralizedOrderResponse - params := url.Values{} +// GetAllFiatTransfers returns a list of transfers either to or from fiat payment methods and the specified wallet +func (c *CoinbasePro) GetAllFiatTransfers(ctx context.Context, walletID string, pag PaginationInp, transferType FiatTransferType) (*ManyDeposWithdrResp, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + var path string + switch transferType { + case FiatDeposit: + path = coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseDeposits + case FiatWithdrawal: + path = coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseWithdrawals + } + var params Params + params.Values = url.Values{} + params.preparePagination(pag) + var resp *ManyDeposWithdrResp + err := c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, params.Values, nil, false, &resp, nil) + if err != nil { + return nil, err + } + for i := range resp.Data { + resp.Data[i].TransferType = transferType + } + return resp, nil +} - for _, individualStatus := range status { - params.Add("status", individualStatus) +// GetFiatTransferByID returns information on a single deposit/withdrawal associated with the specified wallet +func (c *CoinbasePro) GetFiatTransferByID(ctx context.Context, walletID, depositID string, transferType FiatTransferType) (*DeposWithdrData, error) { + if walletID == "" { + return nil, errWalletIDEmpty + } + if depositID == "" { + return nil, errDepositIDEmpty } - if currencyPair != "" { - params.Set("product_id", currencyPair) + var path string + switch transferType { + case FiatDeposit: + path = coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseDeposits + "/" + depositID + case FiatWithdrawal: + path = coinbaseV2 + coinbaseAccounts + "/" + walletID + "/" + coinbaseWithdrawals + "/" + depositID } + resp := struct { + Data DeposWithdrData `json:"data"` + }{} + return &resp.Data, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, false, &resp, nil) +} - path := common.EncodeURLValues(coinbaseproOrders, params) - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp) +// GetFiatCurrencies lists currencies that Coinbase knows about +func (c *CoinbasePro) GetFiatCurrencies(ctx context.Context) ([]FiatData, error) { + resp := struct { + Data []FiatData `json:"data"` + }{} + return resp.Data, c.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseV2+coinbaseCurrencies, nil, &resp) } -// GetOrder returns a single order by order id. -func (c *CoinbasePro) GetOrder(ctx context.Context, orderID string) (GeneralizedOrderResponse, error) { - resp := GeneralizedOrderResponse{} - path := fmt.Sprintf("%s/%s", coinbaseproOrders, orderID) +// GetCryptocurrencies lists cryptocurrencies that Coinbase knows about +func (c *CoinbasePro) GetCryptocurrencies(ctx context.Context) ([]CryptoData, error) { + resp := struct { + Data []CryptoData `json:"data"` + }{} + path := coinbaseV2 + coinbaseCurrencies + "/" + coinbaseCrypto + return resp.Data, c.SendHTTPRequest(ctx, exchange.RestSpot, path, nil, &resp) +} - return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp) +// GetExchangeRates returns exchange rates for the specified currency. If none is specified, it defaults to USD +func (c *CoinbasePro) GetExchangeRates(ctx context.Context, currency string) (*GetExchangeRatesResp, error) { + resp := struct { + Data GetExchangeRatesResp `json:"data"` + }{} + vals := url.Values{} + vals.Set("currency", currency) + return &resp.Data, c.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseV2+coinbaseExchangeRates, vals, &resp) } -// GetFills returns a list of recent fills -func (c *CoinbasePro) GetFills(ctx context.Context, orderID, currencyPair string) ([]FillResponse, error) { - var resp []FillResponse - params := url.Values{} +// GetPrice returns the price the spot/buy/sell price for the specified currency pair, including the standard Coinbase fee of 1%, but excluding any other fees +func (c *CoinbasePro) GetPrice(ctx context.Context, currencyPair, priceType string) (*GetPriceResp, error) { + var path string + switch priceType { + case "spot", "buy", "sell": + path = coinbaseV2 + coinbasePrices + "/" + currencyPair + "/" + priceType + default: + return nil, errInvalidPriceType + } + resp := struct { + Data GetPriceResp `json:"data"` + }{} + return &resp.Data, c.SendHTTPRequest(ctx, exchange.RestSpot, path, nil, &resp) +} - if orderID != "" { - params.Set("order_id", orderID) - } - if currencyPair != "" { - params.Set("product_id", currencyPair) - } - if params.Get("order_id") == "" && params.Get("product_id") == "" { - return resp, errors.New("no parameters set") - } - - path := common.EncodeURLValues(coinbaseproFills, params) - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp) -} - -// MarginTransfer sends funds between a standard/default profile and a margin -// profile. -// A deposit will transfer funds from the default profile into the margin -// profile. A withdraw will transfer funds from the margin profile to the -// default profile. Withdraws will fail if they would set your margin ratio -// below the initial margin ratio requirement. -// -// amount - the amount to transfer between the default and margin profile -// transferType - either "deposit" or "withdraw" -// profileID - The id of the margin profile to deposit or withdraw from -// currency - currency to transfer, currently on "BTC" or "USD" -func (c *CoinbasePro) MarginTransfer(ctx context.Context, amount float64, transferType, profileID, currency string) (MarginTransfer, error) { - resp := MarginTransfer{} - req := make(map[string]interface{}) - req["type"] = transferType - req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) - req["currency"] = currency - req["margin_profile_id"] = profileID - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproMarginTransfer, req, &resp) -} - -// GetPosition returns an overview of account profile. -func (c *CoinbasePro) GetPosition(ctx context.Context) (AccountOverview, error) { - resp := AccountOverview{} - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproPosition, nil, &resp) -} - -// ClosePosition closes a position and allowing you to repay position as well -// repayOnly - allows the position to be repaid -func (c *CoinbasePro) ClosePosition(ctx context.Context, repayOnly bool) (AccountOverview, error) { - resp := AccountOverview{} - req := make(map[string]interface{}) - req["repay_only"] = repayOnly - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproPositionClose, req, &resp) -} - -// GetPayMethods returns a full list of payment methods -func (c *CoinbasePro) GetPayMethods(ctx context.Context) ([]PaymentMethod, error) { - var resp []PaymentMethod - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproPaymentMethod, nil, &resp) -} - -// DepositViaPaymentMethod deposits funds from a payment method. See the Payment -// Methods section for retrieving your payment methods. -// -// amount - The amount to deposit -// currency - The type of currency -// paymentID - ID of the payment method -func (c *CoinbasePro) DepositViaPaymentMethod(ctx context.Context, amount float64, currency, paymentID string) (DepositWithdrawalInfo, error) { - resp := DepositWithdrawalInfo{} - req := make(map[string]interface{}) - req["amount"] = amount - req["currency"] = currency - req["payment_method_id"] = paymentID - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproPaymentMethodDeposit, req, &resp) -} - -// DepositViaCoinbase deposits funds from a coinbase account. Move funds between -// a Coinbase account and coinbasepro trading account within daily limits. Moving -// funds between Coinbase and coinbasepro is instant and free. See the Coinbase -// Accounts section for retrieving your Coinbase accounts. -// -// amount - The amount to deposit -// currency - The type of currency -// accountID - ID of the coinbase account -func (c *CoinbasePro) DepositViaCoinbase(ctx context.Context, amount float64, currency, accountID string) (DepositWithdrawalInfo, error) { - resp := DepositWithdrawalInfo{} - req := make(map[string]interface{}) - req["amount"] = amount - req["currency"] = currency - req["coinbase_account_id"] = accountID - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproDepositCoinbase, req, &resp) -} - -// WithdrawViaPaymentMethod withdraws funds to a payment method -// -// amount - The amount to withdraw -// currency - The type of currency -// paymentID - ID of the payment method -func (c *CoinbasePro) WithdrawViaPaymentMethod(ctx context.Context, amount float64, currency, paymentID string) (DepositWithdrawalInfo, error) { - resp := DepositWithdrawalInfo{} - req := make(map[string]interface{}) - req["amount"] = amount - req["currency"] = currency - req["payment_method_id"] = paymentID - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproWithdrawalPaymentMethod, req, &resp) -} - -// /////////////////////// NO ROUTE FOUND ERROR //////////////////////////////// -// WithdrawViaCoinbase withdraws funds to a coinbase account. -// -// amount - The amount to withdraw -// currency - The type of currency -// accountID - ID of the coinbase account -// func (c *CoinbasePro) WithdrawViaCoinbase(amount float64, currency, accountID string) (DepositWithdrawalInfo, error) { -// resp := DepositWithdrawalInfo{} -// req := make(map[string]interface{}) -// req["amount"] = amount -// req["currency"] = currency -// req["coinbase_account_id"] = accountID -// -// return resp, -// c.SendAuthenticatedHTTPRequest(ctx,http.MethodPost, coinbaseproWithdrawalCoinbase, req, &resp) -// } - -// WithdrawCrypto withdraws funds to a crypto address -// -// amount - The amount to withdraw -// currency - The type of currency -// cryptoAddress - A crypto address of the recipient -func (c *CoinbasePro) WithdrawCrypto(ctx context.Context, amount float64, currency, cryptoAddress string) (DepositWithdrawalInfo, error) { - resp := DepositWithdrawalInfo{} - req := make(map[string]interface{}) - req["amount"] = amount - req["currency"] = currency - req["crypto_address"] = cryptoAddress - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproWithdrawalCrypto, req, &resp) -} - -// GetCoinbaseAccounts returns a list of coinbase accounts -func (c *CoinbasePro) GetCoinbaseAccounts(ctx context.Context) ([]CoinbaseAccounts, error) { - var resp []CoinbaseAccounts - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproCoinbaseAccounts, nil, &resp) -} - -// GetReport returns batches of historic information about your account in -// various human and machine readable forms. -// -// reportType - "fills" or "account" -// startDate - Starting date for the report (inclusive) -// endDate - Ending date for the report (inclusive) -// currencyPair - ID of the product to generate a fills report for. -// E.c. BTC-USD. *Required* if type is fills -// accountID - ID of the account to generate an account report for. *Required* -// if type is account -// format - pdf or csv (default is pdf) -// email - [optional] Email address to send the report to -func (c *CoinbasePro) GetReport(ctx context.Context, reportType, startDate, endDate, currencyPair, accountID, format, email string) (Report, error) { - resp := Report{} - req := make(map[string]interface{}) - req["type"] = reportType - req["start_date"] = startDate - req["end_date"] = endDate - req["format"] = "pdf" - - if currencyPair != "" { - req["product_id"] = currencyPair - } - if accountID != "" { - req["account_id"] = accountID - } - if format == "csv" { - req["format"] = format - } - if email != "" { - req["email"] = email - } - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproReports, req, &resp) -} - -// GetReportStatus once a report request has been accepted for processing, the -// status is available by polling the report resource endpoint. -func (c *CoinbasePro) GetReportStatus(ctx context.Context, reportID string) (Report, error) { - resp := Report{} - path := fmt.Sprintf("%s/%s", coinbaseproReports, reportID) - - return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp) -} - -// GetTrailingVolume this request will return your 30-day trailing volume for -// all products. -func (c *CoinbasePro) GetTrailingVolume(ctx context.Context) ([]Volume, error) { - var resp []Volume - - return resp, - c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproTrailingVolume, nil, &resp) -} - -// GetTransfers returns a history of withdrawal and or deposit transactions -func (c *CoinbasePro) GetTransfers(ctx context.Context, profileID, transferType string, limit int64, start, end time.Time) ([]TransferHistory, error) { - if !start.IsZero() && !end.IsZero() { - err := common.StartEndTimeCheck(start, end) +// GetV2Time returns the current server time, calling V2 of the API +func (c *CoinbasePro) GetV2Time(ctx context.Context) (*ServerTimeV2, error) { + resp := struct { + Data ServerTimeV2 `json:"data"` + }{} + return &resp.Data, c.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseV2+coinbaseTime, nil, &resp) +} + +// GetAllCurrencies returns a list of all currencies that Coinbase knows about. These aren't necessarily tradable +func (c *CoinbasePro) GetAllCurrencies(ctx context.Context) ([]CurrencyData, error) { + var resp []CurrencyData + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, coinbaseCurrencies, nil, &resp) +} + +// GetACurrency returns information on a single currency specified by the user +func (c *CoinbasePro) GetACurrency(ctx context.Context, cur string) (*CurrencyData, error) { + if cur == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + var resp *CurrencyData + path := coinbaseCurrencies + "/" + cur + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp) +} + +// GetAllTradingPairs returns a list of currency pairs which are available for trading +func (c *CoinbasePro) GetAllTradingPairs(ctx context.Context, pairType string) ([]PairData, error) { + var resp []PairData + vals := url.Values{} + if pairType != "" { + vals.Set("type", pairType) + } + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, coinbaseProducts, vals, &resp) +} + +// GetAllPairVolumes returns a list of currency pairs and their associated volumes +func (c *CoinbasePro) GetAllPairVolumes(ctx context.Context) ([]PairVolumeData, error) { + var resp []PairVolumeData + path := coinbaseProducts + "/" + coinbaseVolumeSummary + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp) +} + +// GetPairDetails returns information on a single currency pair +func (c *CoinbasePro) GetPairDetails(ctx context.Context, pair string) (*PairData, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + var resp *PairData + path := coinbaseProducts + "/" + pair + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp) +} + +// GetProductBookV1 returns the order book for the specified currency pair. Level 1 only returns the best bids and asks, Level 2 returns the full order book with orders at the same price aggregated, Level 3 returns the full non-aggregated order book. +func (c *CoinbasePro) GetProductBookV1(ctx context.Context, pair string, level uint8) (*OrderBook, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + var resp OrderBookResp + vals := url.Values{} + vals.Set("level", strconv.FormatUint(uint64(level), 10)) + path := coinbaseProducts + "/" + pair + "/" + coinbaseBook + err := c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, vals, &resp) + if err != nil { + return nil, err + } + ob := &OrderBook{ + Sequence: resp.Sequence, + Bids: make([]Orders, len(resp.Bids)), + Asks: make([]Orders, len(resp.Asks)), + AuctionMode: resp.AuctionMode, + Auction: resp.Auction, + Time: resp.Time, + } + for i := range resp.Bids { + tempS1, ok := resp.Bids[i][0].(string) + if !ok { + return nil, fmt.Errorf("%w, %v", errStringConvert, resp.Bids[i][0]) + } + tempF1, err := strconv.ParseFloat(tempS1, 64) + if err != nil { + return nil, err + } + tempS2, ok := resp.Bids[i][1].(string) + if !ok { + return nil, fmt.Errorf("%w, %v", errStringConvert, resp.Bids[i][1]) + } + tempF2, err := strconv.ParseFloat(tempS2, 64) + if err != nil { + return nil, err + } + switch tempV := resp.Bids[i][2].(type) { + case string: + tempU, err := uuid.FromString(tempV) + if err != nil { + return nil, err + } + ob.Bids[i] = Orders{Price: tempF1, Size: tempF2, OrderCount: 1, OrderID: tempU} + case float64: + tempU := uint64(tempV) + ob.Bids[i] = Orders{Price: tempF1, Size: tempF2, OrderCount: tempU} + } + } + for i := range resp.Asks { + tempS1, ok := resp.Asks[i][0].(string) + if !ok { + return nil, fmt.Errorf("%w, %v", errStringConvert, resp.Asks[i][0]) + } + tempF1, err := strconv.ParseFloat(tempS1, 64) if err != nil { return nil, err } + tempS2, ok := resp.Asks[i][1].(string) + if !ok { + return nil, fmt.Errorf("%w, %v", errStringConvert, resp.Asks[i][1]) + } + tempF2, err := strconv.ParseFloat(tempS2, 64) + if err != nil { + return nil, err + } + switch tempV := resp.Asks[i][2].(type) { + case string: + tempU, err := uuid.FromString(tempV) + if err != nil { + return nil, err + } + ob.Asks[i] = Orders{Price: tempF1, Size: tempF2, OrderCount: 1, OrderID: tempU} + case float64: + tempU := uint64(tempV) + ob.Asks[i] = Orders{Price: tempF1, Size: tempF2, OrderCount: tempU} + } + } + return ob, nil +} + +// GetProductCandles returns historical market data for the specified currency pair. +func (c *CoinbasePro) GetProductCandles(ctx context.Context, pair string, granularity uint32, startTime, endTime time.Time) ([]Candle, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + var params Params + params.Values = url.Values{} + err := params.prepareDateString(startTime, endTime, "start", "end") + if err != nil { + return nil, err + } + if granularity != 0 { + params.Values.Set("granularity", strconv.FormatUint(uint64(granularity), 10)) + } + path := coinbaseProducts + "/" + pair + "/" + coinbaseCandles + var resp []RawCandles + err = c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, params.Values, &resp) + if err != nil { + return nil, err + } + candles := make([]Candle, len(resp)) + for i := range resp { + f1, ok := resp[i][0].(float64) + if !ok { + return nil, fmt.Errorf("%w, %v", errFloatConvert, resp[i][0]) + } + ti := int64(f1) + t := time.Unix(ti, 0) + f2, ok := resp[i][1].(float64) + if !ok { + return nil, fmt.Errorf("%w, %v", errFloatConvert, resp[i][1]) + } + f3, ok := resp[i][2].(float64) + if !ok { + return nil, fmt.Errorf("%w, %v", errFloatConvert, resp[i][2]) + } + f4, ok := resp[i][3].(float64) + if !ok { + return nil, fmt.Errorf("%w, %v", errFloatConvert, resp[i][3]) + } + f5, ok := resp[i][4].(float64) + if !ok { + return nil, fmt.Errorf("%w, %v", errFloatConvert, resp[i][4]) + } + f6, ok := resp[i][5].(float64) + if !ok { + return nil, fmt.Errorf("%w, %v", errFloatConvert, resp[i][5]) + } + candles[i] = Candle{ + Time: t, + Low: f2, + High: f3, + Open: f4, + Close: f5, + Volume: f6, + } } - req := make(map[string]interface{}) - if profileID != "" { - req["profile_id"] = profileID + return candles, nil +} + +// GetProductStats returns information on a specific pair's price and volume +func (c *CoinbasePro) GetProductStats(ctx context.Context, pair string) (*ProductStats, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty } - if !start.IsZero() { - req["before"] = start.Format(time.RFC3339) + path := coinbaseProducts + "/" + pair + "/" + coinbaseStats + var resp *ProductStats + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp) +} + +// GetProductTicker returns the ticker for the specified currency pair +func (c *CoinbasePro) GetProductTicker(ctx context.Context, pair string) (*ProductTicker, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty } - if !end.IsZero() { - req["after"] = end.Format(time.RFC3339) + path := coinbaseProducts + "/" + pair + "/" + coinbaseTicker + var resp *ProductTicker + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp) +} + +// GetProductTrades returns a list of the latest traides for a pair +func (c *CoinbasePro) GetProductTrades(ctx context.Context, pair, step, direction string, limit int64) ([]ProductTrades, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty } - if limit > 0 { - req["limit"] = limit + vals := url.Values{} + if step != "" { + vals.Set(direction, step) } - if transferType != "" { - req["type"] = transferType + vals.Set("limit", strconv.FormatInt(limit, 10)) + path := coinbaseProducts + "/" + pair + "/" + coinbaseTrades + var resp []ProductTrades + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, vals, &resp) +} + +// GetAllWrappedAssets returns a list of supported wrapped assets +func (c *CoinbasePro) GetAllWrappedAssets(ctx context.Context) (*AllWrappedAssets, error) { + var resp *AllWrappedAssets + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, coinbaseWrappedAssets, nil, &resp) +} + +// GetWrappedAssetDetails returns information on a single wrapped asset +func (c *CoinbasePro) GetWrappedAssetDetails(ctx context.Context, wrappedAsset string) (*WrappedAsset, error) { + if wrappedAsset == "" { + return nil, errWrappedAssetEmpty } - var resp []TransferHistory - return resp, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproTransfers, req, &resp) + var resp *WrappedAsset + path := coinbaseWrappedAssets + "/" + wrappedAsset + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp) +} + +// GetWrappedAssetConversionRate returns the conversion rate for the specified wrapped asset +func (c *CoinbasePro) GetWrappedAssetConversionRate(ctx context.Context, wrappedAsset string) (*WrappedAssetConversionRate, error) { + if wrappedAsset == "" { + return nil, errWrappedAssetEmpty + } + var resp *WrappedAssetConversionRate + path := coinbaseWrappedAssets + "/" + wrappedAsset + "/" + coinbaseConversionRate + return resp, c.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp) } // SendHTTPRequest sends an unauthenticated HTTP request -func (c *CoinbasePro) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result interface{}) error { +func (c *CoinbasePro) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, vals url.Values, result any) error { endpoint, err := c.API.Endpoints.GetURL(ep) if err != nil { return err } - + rLim := PubRate + if strings.Contains(path, coinbaseV2) { + rLim = V2Rate + } + path = common.EncodeURLValues(path, vals) item := &request.Item{ Method: http.MethodGet, Path: endpoint + path, @@ -766,139 +1392,313 @@ func (c *CoinbasePro) SendHTTPRequest(ctx context.Context, ep exchange.URL, path HTTPDebugging: c.HTTPDebugging, HTTPRecording: c.HTTPRecording, } - - return c.SendPayload(ctx, request.UnAuth, func() (*request.Item, error) { + return c.SendPayload(ctx, rLim, func() (*request.Item, error) { return item, nil }, request.UnauthenticatedRequest) } // SendAuthenticatedHTTPRequest sends an authenticated HTTP request -func (c *CoinbasePro) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, method, path string, params map[string]interface{}, result interface{}) (err error) { - creds, err := c.GetCredentials(ctx) - if err != nil { - return err - } +func (c *CoinbasePro) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, method, path string, queryParams url.Values, bodyParams map[string]any, isVersion3 bool, result any, returnHead *http.Header) (err error) { endpoint, err := c.API.Endpoints.GetURL(ep) if err != nil { return err } - + if len(endpoint) < 8 { + return errEndpointPathInvalid + } + queryString := common.EncodeURLValues("", queryParams) + interim := json.RawMessage{} newRequest := func() (*request.Item, error) { payload := []byte("") - if params != nil { - payload, err = json.Marshal(params) + if bodyParams != nil { + payload, err = json.Marshal(bodyParams) if err != nil { return nil, err } } - - n := strconv.FormatInt(time.Now().Unix(), 10) - message := n + method + "/" + path + string(payload) - - hmac, err := crypto.GetHMAC(crypto.HashSHA256, - []byte(message), - []byte(creds.Secret)) + var jwt string + jwt, _, err = c.GetJWT(ctx, method+" "+endpoint[8:]+path) if err != nil { return nil, err } - headers := make(map[string]string) - headers["CB-ACCESS-SIGN"] = crypto.Base64Encode(hmac) - headers["CB-ACCESS-TIMESTAMP"] = n - headers["CB-ACCESS-KEY"] = creds.Key - headers["CB-ACCESS-PASSPHRASE"] = creds.ClientID headers["Content-Type"] = "application/json" - + headers["CB-VERSION"] = "2024-11-27" + headers["Authorization"] = "Bearer " + jwt + path += queryString return &request.Item{ - Method: method, - Path: endpoint + path, - Headers: headers, - Body: bytes.NewBuffer(payload), - Result: result, - Verbose: c.Verbose, - HTTPDebugging: c.HTTPDebugging, - HTTPRecording: c.HTTPRecording, + Method: method, + Path: endpoint + path, + Headers: headers, + Body: bytes.NewBuffer(payload), + Result: &interim, + Verbose: c.Verbose, + HTTPDebugging: c.HTTPDebugging, + HTTPRecording: c.HTTPRecording, + HeaderResponse: returnHead, }, nil } - return c.SendPayload(ctx, request.Unset, newRequest, request.AuthenticatedRequest) + rateLim := V2Rate + if isVersion3 { + rateLim = V3Rate + } + err = c.SendPayload(ctx, rateLim, newRequest, request.AuthenticatedRequest) + // Doing this error handling because the docs indicate that errors can be returned even with a 200 status + // code, and that these errors can be buried in the JSON returned + if err != nil { + return err + } + singleErrCap := struct { + ErrorType string `json:"error"` + Message string `json:"message"` + ErrorDetails string `json:"error_details"` + EditFailureReason string `json:"edit_failure_reason"` + PreviewFailureReason string `json:"preview_failure_reason"` + NewOrderFailureReason string `json:"new_order_failure_reason"` + }{} + if err = json.Unmarshal(interim, &singleErrCap); err == nil { + if singleErrCap.Message != "" { + return fmt.Errorf("message: %s, error type: %s, error details: %s, edit failure reason: %s, preview failure reason: %s, new order failure reason: %s", singleErrCap.Message, singleErrCap.ErrorType, singleErrCap.ErrorDetails, singleErrCap.EditFailureReason, singleErrCap.PreviewFailureReason, singleErrCap.NewOrderFailureReason) + } + } + manyErrCap := struct { + Errors []struct { + Success bool `json:"success"` + FailureReason string `json:"failure_reason"` + OrderID string `json:"order_id"` + EditFailureReason string `json:"edit_failure_reason"` + PreviewFailureReason string `json:"preview_failure_reason"` + } + }{} + err = json.Unmarshal(interim, &manyErrCap) + if err == nil { + errMessage := "" + for i := range manyErrCap.Errors { + if !manyErrCap.Errors[i].Success || manyErrCap.Errors[i].EditFailureReason != "" || manyErrCap.Errors[i].PreviewFailureReason != "" { + errMessage += fmt.Sprintf("order id: %s, failure reason: %s, edit failure reason: %s, preview failure reason: %s", manyErrCap.Errors[i].OrderID, manyErrCap.Errors[i].FailureReason, manyErrCap.Errors[i].EditFailureReason, manyErrCap.Errors[i].PreviewFailureReason) + } + } + if errMessage != "" { + return errors.New(errMessage) + } + } + if result == nil { + return nil + } + return json.Unmarshal(interim, result) +} + +// GetJWT generates a new JWT +func (c *CoinbasePro) GetJWT(ctx context.Context, uri string) (string, time.Time, error) { + creds, err := c.GetCredentials(ctx) + if err != nil { + return "", time.Time{}, err + } + block, _ := pem.Decode([]byte(creds.Secret)) + if block == nil { + return "", time.Time{}, errCantDecodePrivKey + } + key, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return "", time.Time{}, err + } + nonce, err := common.GenerateRandomString(16, "1234567890ABCDEF") + if err != nil { + return "", time.Time{}, err + } + regTime := time.Now() + mapClaims := jwt.MapClaims{ + "iss": "cdp", + "nbf": regTime.Unix(), + "exp": regTime.Add(time.Minute * 2).Unix(), + "sub": creds.Key, + } + if uri != "" { + mapClaims["uri"] = uri + } + tok := jwt.NewWithClaims(jwt.SigningMethodES256, mapClaims) + tok.Header["kid"] = creds.Key + tok.Header["nonce"] = nonce + sign, err := tok.SignedString(key) + return sign, regTime.Add(time.Minute * 2), err + // The code below mostly works, but seems to lead to bad results on the signature step. Deferring until later + // head := map[string]any{"kid": creds.Key, "typ": "JWT", "alg": "ES256", "nonce": nonce} + // headJSON, err := json.Marshal(head) + // if err != nil { + // return "", time.Time{}, err + // } + // headEncode := base64URLEncode(headJSON) + // regTime := time.Now() + // body := map[string]any{"iss": "cdp", "nbf": regTime.Unix(), "exp": regTime.Add(time.Minute * 2).Unix(), "sub": creds.Key /*, "aud": "retail_rest_api_proxy"*/} + // if uri != "" { + // body["uri"] = uri + // } + // bodyJSON, err := json.Marshal(body) + // if err != nil { + // return "", time.Time{}, err + // } + // bodyEncode := base64URLEncode(bodyJSON) + // hash := sha256.Sum256([]byte(headEncode + "." + bodyEncode)) + // sig, err := ecdsa.SignASN1(rand.Reader, key, hash[:]) + // if err != nil { + // return "", time.Time{}, err + // } + // sigEncode := base64URLEncode(sig) + // return headEncode + "." + bodyEncode + "." + sigEncode, regTime.Add(time.Minute * 2), nil } // GetFee returns an estimate of fee based on type of transaction func (c *CoinbasePro) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { + if feeBuilder == nil { + return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) + } var fee float64 - switch feeBuilder.FeeType { - case exchange.CryptocurrencyTradeFee: - trailingVolume, err := c.GetTrailingVolume(ctx) + switch { + case !isStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.CryptocurrencyTradeFee: + fees, err := c.GetTransactionSummary(ctx, time.Now().Add(-time.Hour*24*30), time.Now(), "", "", "") if err != nil { return 0, err } - fee = c.calculateTradingFee(trailingVolume, - feeBuilder.Pair.Base, - feeBuilder.Pair.Quote, - feeBuilder.Pair.Delimiter, - feeBuilder.PurchasePrice, - feeBuilder.Amount, - feeBuilder.IsMaker) - case exchange.InternationalBankWithdrawalFee: - fee = getInternationalBankWithdrawalFee(feeBuilder.FiatCurrency) - case exchange.InternationalBankDepositFee: - fee = getInternationalBankDepositFee(feeBuilder.FiatCurrency) - case exchange.OfflineTradeFee: - fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount) - } - - if fee < 0 { - fee = 0 + if feeBuilder.IsMaker { + fee = fees.FeeTier.MakerFeeRate + } else { + fee = fees.FeeTier.TakerFeeRate + } + case feeBuilder.IsMaker && isStablePair(feeBuilder.Pair) && (feeBuilder.FeeType == exchange.CryptocurrencyTradeFee || feeBuilder.FeeType == exchange.OfflineTradeFee): + fee = StablePairMakerFee + case !feeBuilder.IsMaker && isStablePair(feeBuilder.Pair) && (feeBuilder.FeeType == exchange.CryptocurrencyTradeFee || feeBuilder.FeeType == exchange.OfflineTradeFee): + fee = WorstCaseStablePairTakerFee + case feeBuilder.IsMaker && !isStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.OfflineTradeFee: + fee = WorstCaseMakerFee + case !feeBuilder.IsMaker && !isStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.OfflineTradeFee: + fee = WorstCaseTakerFee + default: + return 0, errFeeTypeNotSupported } + return fee * feeBuilder.Amount * feeBuilder.PurchasePrice, nil +} - return fee, nil +var stableMap = map[key.PairAsset]bool{ + {Base: currency.USDT.Item, Quote: currency.USD.Item}: true, + {Base: currency.USDT.Item, Quote: currency.EUR.Item}: true, + {Base: currency.USDC.Item, Quote: currency.EUR.Item}: true, + {Base: currency.USDC.Item, Quote: currency.GBP.Item}: true, + {Base: currency.USDT.Item, Quote: currency.GBP.Item}: true, + {Base: currency.USDT.Item, Quote: currency.USDC.Item}: true, + {Base: currency.DAI.Item, Quote: currency.USD.Item}: true, + {Base: currency.CBETH.Item, Quote: currency.ETH.Item}: true, + {Base: currency.PYUSD.Item, Quote: currency.USD.Item}: true, + {Base: currency.EUROC.Item, Quote: currency.USD.Item}: true, + {Base: currency.GUSD.Item, Quote: currency.USD.Item}: true, + {Base: currency.EUROC.Item, Quote: currency.EUR.Item}: true, + {Base: currency.WBTC.Item, Quote: currency.BTC.Item}: true, + {Base: currency.LSETH.Item, Quote: currency.ETH.Item}: true, + {Base: currency.GYEN.Item, Quote: currency.USD.Item}: true, + {Base: currency.PAX.Item, Quote: currency.USD.Item}: true, } -// getOfflineTradeFee calculates the worst case-scenario trading fee -func getOfflineTradeFee(price, amount float64) float64 { - return 0.0025 * price * amount +// IsStablePair returns true if the currency pair is considered a "stable pair" by Coinbase +func isStablePair(pair currency.Pair) bool { + return stableMap[key.PairAsset{Base: pair.Base.Item, Quote: pair.Quote.Item}] } -func (c *CoinbasePro) calculateTradingFee(trailingVolume []Volume, base, quote currency.Code, delimiter string, purchasePrice, amount float64, isMaker bool) float64 { - var fee float64 - for _, i := range trailingVolume { - if strings.EqualFold(i.ProductID, base.String()+delimiter+quote.String()) { - switch { - case isMaker: - fee = 0 - case i.Volume <= 10000000: - fee = 0.003 - case i.Volume > 10000000 && i.Volume <= 100000000: - fee = 0.002 - case i.Volume > 100000000: - fee = 0.001 - } - break +// PrepareDateString encodes a set of parameters indicating start & end dates +func (p *Params) prepareDateString(startDate, endDate time.Time, labelStart, labelEnd string) error { + err := common.StartEndTimeCheck(startDate, endDate) + if err != nil { + if errors.Is(err, common.ErrDateUnset) { + return nil } + return err } - return fee * amount * purchasePrice + p.Values.Set(labelStart, startDate.Format(time.RFC3339)) + p.Values.Set(labelEnd, endDate.Format(time.RFC3339)) + return nil } -func getInternationalBankWithdrawalFee(c currency.Code) float64 { - var fee float64 - - if c.Equal(currency.USD) { - fee = 25 - } else if c.Equal(currency.EUR) { - fee = 0.15 +// PreparePagination formats pagination information in the way the exchange expects +func (p *Params) preparePagination(pag PaginationInp) { + if pag.Limit != 0 { + p.Values.Set("limit", strconv.FormatInt(int64(pag.Limit), 10)) + } + if pag.OrderAscend { + p.Values.Set("order", "asc") + } + if pag.StartingAfter != "" { + p.Values.Set("starting_after", pag.StartingAfter) + } + if pag.EndingBefore != "" { + p.Values.Set("ending_before", pag.EndingBefore) } - - return fee } -func getInternationalBankDepositFee(c currency.Code) float64 { - var fee float64 +// prepareOrderConfig populates the OrderConfiguration struct +func prepareOrderConfig(orderType, side, stopDirection string, amount, limitPrice, stopPrice float64, endTime time.Time, postOnly bool) (OrderConfiguration, error) { + var orderConfig OrderConfiguration + switch orderType { + case order.Market.String(), order.ImmediateOrCancel.String(): + orderConfig.MarketMarketIOC = &MarketMarketIOC{} + if side == order.Buy.String() { + orderConfig.MarketMarketIOC.QuoteSize = types.Number(amount) + } + if side == order.Sell.String() { + orderConfig.MarketMarketIOC.BaseSize = types.Number(amount) + } + case order.Limit.String(): + if endTime == (time.Time{}) { + orderConfig.LimitLimitGTC = &LimitLimitGTC{} + orderConfig.LimitLimitGTC.BaseSize = types.Number(amount) + orderConfig.LimitLimitGTC.LimitPrice = types.Number(limitPrice) + orderConfig.LimitLimitGTC.PostOnly = postOnly + } else { + if endTime.Before(time.Now()) { + return orderConfig, errEndTimeInPast + } + orderConfig.LimitLimitGTD = &LimitLimitGTD{} + orderConfig.LimitLimitGTD.BaseSize = types.Number(amount) + orderConfig.LimitLimitGTD.LimitPrice = types.Number(limitPrice) + orderConfig.LimitLimitGTD.PostOnly = postOnly + orderConfig.LimitLimitGTD.EndTime = endTime + } + case order.StopLimit.String(): + if endTime == (time.Time{}) { + orderConfig.StopLimitStopLimitGTC = &StopLimitStopLimitGTC{} + orderConfig.StopLimitStopLimitGTC.BaseSize = types.Number(amount) + orderConfig.StopLimitStopLimitGTC.LimitPrice = types.Number(limitPrice) + orderConfig.StopLimitStopLimitGTC.StopPrice = types.Number(stopPrice) + orderConfig.StopLimitStopLimitGTC.StopDirection = stopDirection + } else { + if endTime.Before(time.Now()) { + return orderConfig, errEndTimeInPast + } + orderConfig.StopLimitStopLimitGTD = &StopLimitStopLimitGTD{} + orderConfig.StopLimitStopLimitGTD.BaseSize = types.Number(amount) + orderConfig.StopLimitStopLimitGTD.LimitPrice = types.Number(limitPrice) + orderConfig.StopLimitStopLimitGTD.StopPrice = types.Number(stopPrice) + orderConfig.StopLimitStopLimitGTD.StopDirection = stopDirection + orderConfig.StopLimitStopLimitGTD.EndTime = endTime + } + default: + return orderConfig, errInvalidOrderType + } + return orderConfig, nil +} - if c.Equal(currency.USD) { - fee = 10 - } else if c.Equal(currency.EUR) { - fee = 0.15 +// FormatMarginType properly formats the margin type for the request +func FormatMarginType(marginType string) string { + if marginType == "ISOLATED" || marginType == "CROSS" { + return marginType + } + if marginType == "MULTI" { + return "CROSS" } + return "" +} - return fee +// String implements the stringer interface +func (f FiatTransferType) String() string { + if f { + return "withdrawal" + } + return "deposit" } diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index bf88705f5af..303ad47c45d 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -2,1074 +2,1935 @@ package coinbasepro import ( "context" - "errors" + "encoding/json" "log" "net/http" "os" + "strconv" "testing" "time" + "github.com/buger/jsonparser" + "github.com/gofrs/uuid" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" + "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange" - "github.com/thrasher-corp/gocryptotrader/portfolio/banking" + testsubs "github.com/thrasher-corp/gocryptotrader/internal/testing/subscriptions" + gctlog "github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) -var ( - c = &CoinbasePro{} - testPair = currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-") -) - // Please supply your APIKeys here for better testing const ( apiKey = "" apiSecret = "" - clientID = "" // passphrase you made at API CREATION canManipulateRealOrders = false + testingInSandbox = false +) + +var ( + c = &CoinbasePro{} + testCrypto = currency.BTC + testFiat = currency.USD + testStable = currency.USDC + testWrappedAsset = currency.CBETH + testPair = currency.NewPairWithDelimiter(testCrypto.String(), testFiat.String(), "-") +) + +// Constants used within tests +const ( + testAddress = "fake address" + testAmount = 1e-08 + testPrice = 1e+09 + + skipPayMethodNotFound = "no payment methods found, skipping" + skipInsufSuitableAccs = "insufficient suitable accounts for test, skipping" + skipInsufficientFunds = "insufficient funds for test, skipping" + skipInsufficientOrders = "insufficient orders for test, skipping" + skipInsufficientPortfolios = "insufficient portfolios for test, skipping" + skipInsufficientWallets = "insufficient wallets for test, skipping" + skipInsufficientFundsOrWallets = "insufficient funds or wallets for test, skipping" + skipInsufficientTransactions = "insufficient transactions for test, skipping" + + errExpectMismatch = "received: '%v' but expected: '%v'" + errExpectedNonEmpty = "expected non-empty response" + errPortfolioNameDuplicate = `CoinbasePro unsuccessful HTTP status code: 409 raw response: {"error":"CONFLICT","error_details":"A portfolio with this name already exists.","message":"A portfolio with this name already exists."}, authenticated request failed` + errPortTransferInsufFunds = `CoinbasePro unsuccessful HTTP status code: 429 raw response: {"error":"unknown","error_details":"[PORTFOLIO_ERROR_CODE_INSUFFICIENT_FUNDS] insufficient funds in source account","message":"[PORTFOLIO_ERROR_CODE_INSUFFICIENT_FUNDS] insufficient funds in source account"}, authenticated request failed` + errInvalidProductID = `CoinbasePro unsuccessful HTTP status code: 404 raw response: {"error":"NOT_FOUND","error_details":"valid product_id is required","message":"valid product_id is required"}` + errExpectedFeeRange = "expected fee range of %v and %v, received %v" + errOptionInvalid = `CoinbasePro unsuccessful HTTP status code: 400 raw response: {"error":"unknown","error_details":"parsing field \"product_type\": \"OPTIONS\" is not a valid value","message":"parsing field \"product_type\": \"OPTIONS\" is not a valid value"}` + + expectedTimestamp = "1970-01-01 00:20:34 +0000 UTC" ) func TestMain(m *testing.M) { c.SetDefaults() - cfg := config.GetConfig() - err := cfg.LoadConfig("../../testdata/configtest.json", true) + if testingInSandbox { + err := c.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ + exchange.RestSpot: coinbaseproSandboxAPIURL, + }) + if err != nil { + log.Fatal("failed to set sandbox endpoint", err) + } + } + err := exchangeBaseHelper(c) if err != nil { - log.Fatal("coinbasepro load config error", err) + log.Fatal(err) } - gdxConfig, err := cfg.GetExchangeConfig("CoinbasePro") + if apiKey != "" { + c.GetBase().API.AuthenticatedSupport = true + c.GetBase().API.AuthenticatedWebsocketSupport = true + } + err = gctlog.SetGlobalLogConfig(gctlog.GenDefaultSettings()) if err != nil { - log.Fatal("coinbasepro Setup() init error") + log.Fatal(err) } - gdxConfig.API.Credentials.Key = apiKey - gdxConfig.API.Credentials.Secret = apiSecret - gdxConfig.API.Credentials.ClientID = clientID - gdxConfig.API.AuthenticatedSupport = true - gdxConfig.API.AuthenticatedWebsocketSupport = true - c.Websocket = sharedtestvalues.NewTestWebsocket() - err = c.Setup(gdxConfig) + var dialer websocket.Dialer + err = c.Websocket.Conn.Dial(&dialer, http.Header{}) if err != nil { - log.Fatal("CoinbasePro setup error", err) + log.Fatal(err) } + go c.wsReadData() os.Exit(m.Run()) } -func TestGetProducts(t *testing.T) { - t.Skip("API is deprecated") +func TestSetup(t *testing.T) { + cfg, err := c.GetStandardConfig() + assert.NoError(t, err) + exch := &CoinbasePro{} + exch.SetDefaults() + err = exchangeBaseHelper(exch) + require.NoError(t, err) + cfg.ProxyAddress = string(rune(0x7f)) + err = exch.Setup(cfg) + assert.ErrorIs(t, err, exchange.ErrSettingProxyAddress) +} - _, err := c.GetProducts(context.Background()) - if err != nil { - t.Errorf("Coinbase, GetProducts() Error: %s", err) - } +func TestWsConnect(t *testing.T) { + exch := &CoinbasePro{} + exch.Websocket = sharedtestvalues.NewTestWebsocket() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + err := exch.Websocket.Disable() + assert.ErrorIs(t, err, stream.ErrAlreadyDisabled) + err = exch.WsConnect() + assert.ErrorIs(t, err, stream.ErrWebsocketNotEnabled) + exch.SetDefaults() + err = exchangeBaseHelper(exch) + require.NoError(t, err) + err = exch.Websocket.Enable() + assert.NoError(t, err) } -func TestGetOrderbook(t *testing.T) { - t.Skip("API is deprecated") +func TestGetAllAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err := c.GetAllAccounts(context.Background(), 50, "") + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - _, err := c.GetOrderbook(context.Background(), testPair.String(), 2) - if err != nil { - t.Error(err) +func TestGetAccountByID(t *testing.T) { + t.Parallel() + _, err := c.GetAccountByID(context.Background(), "") + assert.ErrorIs(t, err, errAccountIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + longResp, err := c.GetAllAccounts(context.Background(), 49, "") + assert.NoError(t, err) + if longResp == nil || len(longResp.Accounts) == 0 { + t.Fatal(errExpectedNonEmpty) } - _, err = c.GetOrderbook(context.Background(), testPair.String(), 3) - if err != nil { - t.Error(err) + shortResp, err := c.GetAccountByID(context.Background(), longResp.Accounts[0].UUID) + assert.NoError(t, err) + if *shortResp != longResp.Accounts[0] { + t.Errorf(errExpectMismatch, shortResp, longResp.Accounts[0]) } } -func TestGetTicker(t *testing.T) { - t.Skip("API is deprecated") +func TestGetBestBidAsk(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + testPairs := []string{testPair.String(), "ETH-USD"} + resp, err := c.GetBestBidAsk(context.Background(), testPairs) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - _, err := c.GetTicker(context.Background(), testPair.String()) - if err != nil { - t.Error("GetTicker() error", err) - } +func TestGetProductBookV3(t *testing.T) { + t.Parallel() + _, err := c.GetProductBookV3(context.Background(), currency.Pair{}, 0, 0, false) + assert.ErrorIs(t, err, errProductIDEmpty) + resp, err := c.GetProductBookV3(context.Background(), testPair, 4, -1, false) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err = c.GetProductBookV3(context.Background(), testPair, 4, -1, true) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestGetTrades(t *testing.T) { - _, err := c.GetTrades(context.Background(), testPair.String()) - if err != nil { - t.Error("GetTrades() error", err) - } +func TestGetAllProducts(t *testing.T) { + t.Parallel() + testPairs := []string{testPair.String(), "ETH-USD"} + resp, err := c.GetAllProducts(context.Background(), 30000, 1, "SPOT", "PERPETUAL", "STATUS_ALL", testPairs, false) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err = c.GetAllProducts(context.Background(), 0, 1, "SPOT", "PERPETUAL", "STATUS_ALL", nil, true) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestGetHistoricRatesGranularityCheck(t *testing.T) { - t.Skip("API is deprecated") +func TestGetProductByID(t *testing.T) { + t.Parallel() + _, err := c.GetProductByID(context.Background(), "", false) + assert.ErrorIs(t, err, errProductIDEmpty) + resp, err := c.GetProductByID(context.Background(), testPair.String(), false) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err = c.GetProductByID(context.Background(), testPair.String(), true) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - end := time.Now() - start := end.Add(-time.Hour * 2) - _, err := c.GetHistoricCandles(context.Background(), - testPair, asset.Spot, kline.OneHour, start, end) - if err != nil { - t.Fatal(err) - } +func TestGetHistoricRates(t *testing.T) { + t.Parallel() + _, err := c.GetHistoricRates(context.Background(), "", granUnknown, time.Time{}, time.Time{}, false) + assert.ErrorIs(t, err, errProductIDEmpty) + _, err = c.GetHistoricRates(context.Background(), testPair.String(), "blorbo", time.Time{}, time.Time{}, false) + assert.ErrorIs(t, err, kline.ErrUnsupportedInterval) + resp, err := c.GetHistoricRates(context.Background(), testPair.String(), granOneMin, time.Now().Add(-5*time.Minute), time.Now(), false) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err = c.GetHistoricRates(context.Background(), testPair.String(), granOneMin, time.Now().Add(-5*time.Minute), time.Now(), true) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestCoinbasePro_GetHistoricCandlesExtended(t *testing.T) { - t.Skip("API is deprecated") +func TestGetTicker(t *testing.T) { + t.Parallel() + _, err := c.GetTicker(context.Background(), "", 1, time.Time{}, time.Time{}, false) + assert.ErrorIs(t, err, errProductIDEmpty) + resp, err := c.GetTicker(context.Background(), testPair.String(), 5, time.Now().Add(-time.Minute*5), time.Now(), false) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err = c.GetTicker(context.Background(), testPair.String(), 5, time.Now().Add(-time.Minute*5), time.Now(), true) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - start := time.Unix(1546300800, 0) - end := time.Unix(1577836799, 0) +func TestPlaceOrder(t *testing.T) { + t.Parallel() + _, err := c.PlaceOrder(context.Background(), "", "", "", "", "", "", "", "", 0, 0, 0, 0, false, time.Time{}) + assert.ErrorIs(t, err, errClientOrderIDEmpty) + _, err = c.PlaceOrder(context.Background(), "meow", "", "", "", "", "", "", "", 0, 0, 0, 0, false, time.Time{}) + assert.ErrorIs(t, err, errProductIDEmpty) + _, err = c.PlaceOrder(context.Background(), "meow", testPair.String(), order.Sell.String(), "", "", "", "", "", 0, 0, 0, 0, false, time.Time{}) + assert.ErrorIs(t, err, order.ErrAmountIsInvalid) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + skipTestIfLowOnFunds(t) + id, err := uuid.NewV4() + assert.NoError(t, err) + resp, err := c.PlaceOrder(context.Background(), id.String(), testPair.String(), order.Sell.String(), "", order.Limit.String(), "", "CROSS", "", testAmount, testPrice, 0, 9999, false, time.Now().Add(time.Hour)) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + id, err = uuid.NewV4() + assert.NoError(t, err) + resp, err = c.PlaceOrder(context.Background(), id.String(), testPair.String(), order.Sell.String(), "", order.Limit.String(), "", "MULTI", "", testAmount, testPrice, 0, 9999, false, time.Now().Add(time.Hour)) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - _, err := c.GetHistoricCandlesExtended(context.Background(), - testPair, asset.Spot, kline.OneDay, start, end) - if err != nil { - t.Fatal(err) +func orderTestHelper(t *testing.T, orderSide string) *GetAllOrdersResp { + t.Helper() + ordIDs, err := c.GetAllOrders(context.Background(), "", "", "", orderSide, "", "", "", "", "", []string{}, []string{}, 1000, time.Time{}, time.Time{}) + assert.NoError(t, err) + if ordIDs == nil || len(ordIDs.Orders) == 0 { + t.Skip(skipInsufficientOrders) + } + for i := range ordIDs.Orders { + if ordIDs.Orders[i].Status == order.Open.String() { + ordIDs.Orders = ordIDs.Orders[i : i+1] + return ordIDs + } } + t.Skip(skipInsufficientOrders) + return nil } -func TestGetStats(t *testing.T) { - _, err := c.GetStats(context.Background(), testPair.String()) - if err != nil { - t.Error("GetStats() error", err) - } +func TestCancelOrders(t *testing.T) { + t.Parallel() + var orderSlice []string + _, err := c.CancelOrders(context.Background(), orderSlice) + assert.ErrorIs(t, err, errOrderIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + ordIDs := orderTestHelper(t, "") + orderSlice = append(orderSlice, ordIDs.Orders[0].OrderID) + resp, err := c.CancelOrders(context.Background(), orderSlice) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestGetCurrencies(t *testing.T) { - _, err := c.GetCurrencies(context.Background()) - if err != nil { - t.Error("GetCurrencies() error", err) - } +func TestEditOrder(t *testing.T) { + t.Parallel() + _, err := c.EditOrder(context.Background(), "", 0, 0) + assert.ErrorIs(t, err, errOrderIDEmpty) + _, err = c.EditOrder(context.Background(), "meow", 0, 0) + assert.ErrorIs(t, err, errSizeAndPriceZero) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + ordIDs := orderTestHelper(t, "SELL") + resp, err := c.EditOrder(context.Background(), ordIDs.Orders[0].OrderID, testAmount, testPrice*10) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestGetCurrentServerTime(t *testing.T) { - _, err := c.GetCurrentServerTime(context.Background()) - if err != nil { - t.Error("GetServerTime() error", err) - } +func TestEditOrderPreview(t *testing.T) { + t.Parallel() + _, err := c.EditOrderPreview(context.Background(), "", 0, 0) + assert.ErrorIs(t, err, errOrderIDEmpty) + _, err = c.EditOrderPreview(context.Background(), "meow", 0, 0) + assert.ErrorIs(t, err, errSizeAndPriceZero) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + ordIDs := orderTestHelper(t, "") + resp, err := c.EditOrderPreview(context.Background(), ordIDs.Orders[0].OrderID, testAmount, testPrice*10) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestWrapperGetServerTime(t *testing.T) { +func TestGetAllOrders(t *testing.T) { t.Parallel() - st, err := c.GetServerTime(context.Background(), asset.Spot) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } + assets := []string{testFiat.String()} + status := make([]string, 2) + _, err := c.GetAllOrders(context.Background(), "", "", "", "", "", "", "", "", "", status, assets, 0, time.Unix(2, 2), time.Unix(1, 1)) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + status[0] = "CANCELLED" + status[1] = "OPEN" + _, err = c.GetAllOrders(context.Background(), "", "", "", "", "", "", "", "", "", status, assets, 0, time.Time{}, time.Time{}) + assert.ErrorIs(t, err, errOpenPairWithOtherTypes) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + status = make([]string, 0) + assets = make([]string, 1) + assets[0] = testCrypto.String() + _, err = c.GetAllOrders(context.Background(), "", testFiat.String(), "LIMIT", "SELL", "", "SPOT", "RETAIL_ADVANCED", "UNKNOWN_CONTRACT_EXPIRY_TYPE", "", status, assets, 10, time.Time{}, time.Time{}) + assert.NoError(t, err) +} - if st.IsZero() { - t.Fatal("expected a time") - } +func TestGetFills(t *testing.T) { + t.Parallel() + _, err := c.GetFills(context.Background(), "", "", "", time.Unix(2, 2), time.Unix(1, 1), 0) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, err = c.GetFills(context.Background(), "", testPair.String(), "", time.Unix(1, 1), time.Now(), 5) + assert.NoError(t, err) + status := []string{"OPEN"} + ordID, err := c.GetAllOrders(context.Background(), "", "", "", "", "", "", "", "", "", status, nil, 3, time.Time{}, time.Time{}) + assert.NoError(t, err) + if ordID == nil || len(ordID.Orders) == 0 { + t.Skip(skipInsufficientOrders) + } + _, err = c.GetFills(context.Background(), ordID.Orders[0].OrderID, "", "", time.Time{}, time.Time{}, 5) + assert.NoError(t, err) } -func TestAuthRequests(t *testing.T) { +func TestGetOrderByID(t *testing.T) { t.Parallel() + _, err := c.GetOrderByID(context.Background(), "", "", "") + assert.ErrorIs(t, err, errOrderIDEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + ordID, err := c.GetAllOrders(context.Background(), "", "", "", "", "", "", "", "", "", nil, nil, 10, time.Time{}, time.Time{}) + assert.NoError(t, err) + if ordID == nil || len(ordID.Orders) == 0 { + t.Skip(skipInsufficientOrders) + } + resp, err := c.GetOrderByID(context.Background(), ordID.Orders[0].OrderID, ordID.Orders[0].ClientOID, testFiat.String()) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - _, err := c.GetAccounts(context.Background()) - if err != nil { - t.Error("GetAccounts() error", err) - } - accountResponse, err := c.GetAccount(context.Background(), - "13371337-1337-1337-1337-133713371337") - if accountResponse.ID != "" { - t.Error("Expecting no data returned") - } - if err == nil { - t.Error("Expecting error") - } - accountHistoryResponse, err := c.GetAccountHistory(context.Background(), - "13371337-1337-1337-1337-133713371337") - if len(accountHistoryResponse) > 0 { - t.Error("Expecting no data returned") - } - if err == nil { - t.Error("Expecting error") - } - getHoldsResponse, err := c.GetHolds(context.Background(), - "13371337-1337-1337-1337-133713371337") - if len(getHoldsResponse) > 0 { - t.Error("Expecting no data returned") - } - if err == nil { - t.Error("Expecting error") - } - orderResponse, err := c.PlaceLimitOrder(context.Background(), - "", 0.001, 0.001, - order.Buy.Lower(), "", "", testPair.String(), "", false) - if orderResponse != "" { - t.Error("Expecting no data returned") - } - if err == nil { - t.Error("Expecting error") - } - marketOrderResponse, err := c.PlaceMarketOrder(context.Background(), - "", 1, 0, - order.Buy.Lower(), testPair.String(), "") - if marketOrderResponse != "" { - t.Error("Expecting no data returned") - } - if err == nil { - t.Error("Expecting error") - } - fillsResponse, err := c.GetFills(context.Background(), - "1337", testPair.String()) - if len(fillsResponse) > 0 { - t.Error("Expecting no data returned") - } - if err == nil { - t.Error("Expecting error") - } - _, err = c.GetFills(context.Background(), "", "") - if err == nil { - t.Error("Expecting error") - } - marginTransferResponse, err := c.MarginTransfer(context.Background(), - 1, "withdraw", "13371337-1337-1337-1337-133713371337", "BTC") - if marginTransferResponse.ID != "" { - t.Error("Expecting no data returned") - } - if err == nil { - t.Error("Expecting error") - } - _, err = c.GetPosition(context.Background()) - if err == nil { - t.Error("Expecting error") - } - _, err = c.ClosePosition(context.Background(), false) - if err == nil { - t.Error("Expecting error") +func TestPreviewOrder(t *testing.T) { + t.Parallel() + _, err := c.PreviewOrder(context.Background(), "", "", "", "", "", 0, 0, 0, 0, 0, 0, false, false, false, time.Time{}) + assert.ErrorIs(t, err, order.ErrAmountIsInvalid) + _, err = c.PreviewOrder(context.Background(), "", "", "", "", "", 0, 1, 0, 0, 0, 0, false, false, false, time.Time{}) + assert.ErrorIs(t, err, errInvalidOrderType) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + skipTestIfLowOnFunds(t) + resp, err := c.PreviewOrder(context.Background(), testPair.String(), "BUY", "MARKET", "", "ISOLATED", 0, testAmount, 0, 0, 0, 0, false, false, false, time.Time{}) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetAllPortfolios(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err := c.GetAllPortfolios(context.Background(), "DEFAULT") + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestCreatePortfolio(t *testing.T) { + t.Parallel() + _, err := c.CreatePortfolio(context.Background(), "") + assert.ErrorIs(t, err, errNameEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + _, err = c.CreatePortfolio(context.Background(), "GCT Test Portfolio") + if err != nil && err.Error() != errPortfolioNameDuplicate { + t.Error(err) } - _, err = c.GetPayMethods(context.Background()) - if err != nil { - t.Error("GetPayMethods() error", err) +} + +func TestMovePortfolioFunds(t *testing.T) { + t.Parallel() + _, err := c.MovePortfolioFunds(context.Background(), "", "", "", 0) + assert.ErrorIs(t, err, errPortfolioIDEmpty) + _, err = c.MovePortfolioFunds(context.Background(), "", "meowPort", "woofPort", 0) + assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = c.MovePortfolioFunds(context.Background(), testCrypto.String(), "meowPort", "woofPort", 0) + assert.ErrorIs(t, err, order.ErrAmountIsInvalid) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + portID, err := c.GetAllPortfolios(context.Background(), "") + assert.NoError(t, err) + if len(portID) < 2 { + t.Skip(skipInsufficientPortfolios) + } + _, err = c.MovePortfolioFunds(context.Background(), testCrypto.String(), portID[0].UUID, portID[1].UUID, testAmount) + assert.NoError(t, err) +} + +func TestGetPortfolioByID(t *testing.T) { + t.Parallel() + _, err := c.GetPortfolioByID(context.Background(), "") + assert.ErrorIs(t, err, errPortfolioIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + portID, err := c.GetAllPortfolios(context.Background(), "") + assert.NoError(t, err) + if len(portID) == 0 { + t.Fatal(errExpectedNonEmpty) } - _, err = c.GetCoinbaseAccounts(context.Background()) - if err != nil { - t.Error("GetCoinbaseAccounts() error", err) + resp, err := c.GetPortfolioByID(context.Background(), portID[0].UUID) + assert.NoError(t, err) + if resp.Portfolio != portID[0] { + t.Errorf(errExpectMismatch, resp.Portfolio, portID[0]) } } -func setFeeBuilder() *exchange.FeeBuilder { - return &exchange.FeeBuilder{ - Amount: 1, - FeeType: exchange.CryptocurrencyTradeFee, - Pair: testPair, - PurchasePrice: 1, - } +func TestDeletePortfolio(t *testing.T) { + t.Parallel() + err := c.DeletePortfolio(context.Background(), "") + assert.ErrorIs(t, err, errPortfolioIDEmpty) + pID := portfolioIDFromName(t, "GCT Test Portfolio To-Delete") + err = c.DeletePortfolio(context.Background(), pID) + // The new JWT-based keys don't have permissions to delete portfolios they aren't assigned to, causing this to fail + assert.NoError(t, err) } -// TestGetFeeByTypeOfflineTradeFee logic test -func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { - var feeBuilder = setFeeBuilder() - _, err := c.GetFeeByType(context.Background(), feeBuilder) - if err != nil { - t.Fatal(err) +func TestEditPortfolio(t *testing.T) { + t.Parallel() + _, err := c.EditPortfolio(context.Background(), "", "") + assert.ErrorIs(t, err, errPortfolioIDEmpty) + _, err = c.EditPortfolio(context.Background(), "meow", "") + assert.ErrorIs(t, err, errNameEmpty) + pID := portfolioIDFromName(t, "GCT Test Portfolio To-Edit") + _, err = c.EditPortfolio(context.Background(), pID, "GCT Test Portfolio Edited") + // The new JWT-based keys don't have permissions to edit portfolios they aren't assigned to, causing this to fail + if err != nil && err.Error() != errPortfolioNameDuplicate { + t.Error(err) } - if !sharedtestvalues.AreAPICredentialsSet(c) { - if feeBuilder.FeeType != exchange.OfflineTradeFee { - t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) +} + +func TestGetFuturesBalanceSummary(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, err := c.GetFuturesBalanceSummary(context.Background()) + assert.NoError(t, err) +} + +func TestGetAllFuturesPositions(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, err := c.GetAllFuturesPositions(context.Background()) + assert.NoError(t, err) +} + +func TestGetFuturesPositionByID(t *testing.T) { + t.Parallel() + _, err := c.GetFuturesPositionByID(context.Background(), "") + assert.ErrorIs(t, err, errProductIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, err = c.GetFuturesPositionByID(context.Background(), "meow") + assert.NoError(t, err) +} + +func TestListFuturesSweeps(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, err := c.ListFuturesSweeps(context.Background()) + assert.NoError(t, err) +} + +func TestAllocatePortfolio(t *testing.T) { + t.Parallel() + err := c.AllocatePortfolio(context.Background(), "", "", "", 0) + assert.ErrorIs(t, err, errPortfolioIDEmpty) + err = c.AllocatePortfolio(context.Background(), "meow", "", "", 0) + assert.ErrorIs(t, err, errProductIDEmpty) + err = c.AllocatePortfolio(context.Background(), "meow", "bark", "", 0) + assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + pID := getINTXPortfolio(t) + err = c.AllocatePortfolio(context.Background(), pID, testCrypto.String(), testFiat.String(), 0.001337) + assert.NoError(t, err) +} + +func TestGetPerpetualsPortfolioSummary(t *testing.T) { + t.Parallel() + _, err := c.GetPerpetualsPortfolioSummary(context.Background(), "") + assert.ErrorIs(t, err, errPortfolioIDEmpty) + pID := getINTXPortfolio(t) + resp, err := c.GetPerpetualsPortfolioSummary(context.Background(), pID) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetAllPerpetualsPositions(t *testing.T) { + t.Parallel() + _, err := c.GetAllPerpetualsPositions(context.Background(), "") + assert.ErrorIs(t, err, errPortfolioIDEmpty) + pID := getINTXPortfolio(t) + _, err = c.GetAllPerpetualsPositions(context.Background(), pID) + assert.NoError(t, err) +} + +func TestGetPerpetualsPositionByID(t *testing.T) { + t.Parallel() + _, err := c.GetPerpetualsPositionByID(context.Background(), "", "") + assert.ErrorIs(t, err, errPortfolioIDEmpty) + _, err = c.GetPerpetualsPositionByID(context.Background(), "meow", "") + assert.ErrorIs(t, err, errProductIDEmpty) + pID := getINTXPortfolio(t) + _, err = c.GetPerpetualsPositionByID(context.Background(), pID, testPair.String()) + assert.NoError(t, err) +} + +func TestGetTransactionSummary(t *testing.T) { + t.Parallel() + _, err := c.GetTransactionSummary(context.Background(), time.Unix(2, 2), time.Unix(1, 1), "", "", "") + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err := c.GetTransactionSummary(context.Background(), time.Unix(1, 1), time.Now(), testFiat.String(), asset.Spot.Upper(), "UNKNOWN_CONTRACT_EXPIRY_TYPE") + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestCreateConvertQuote(t *testing.T) { + t.Parallel() + _, err := c.CreateConvertQuote(context.Background(), "", "", "", "", 0) + assert.ErrorIs(t, err, errAccountIDEmpty) + _, err = c.CreateConvertQuote(context.Background(), "meow", "123", "", "", 0) + assert.ErrorIs(t, err, order.ErrAmountIsInvalid) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + fromAccID, toAccID := convertTestHelper(t) + resp, err := c.CreateConvertQuote(context.Background(), fromAccID, toAccID, "", "", 0.01) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestCommitConvertTrade(t *testing.T) { + convertTestShared(t, c.CommitConvertTrade) +} + +func TestGetConvertTradeByID(t *testing.T) { + convertTestShared(t, c.GetConvertTradeByID) +} + +func TestGetV3Time(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + testGetNoArgs(t, c.GetV3Time) +} + +func TestGetAllPaymentMethods(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + testGetNoArgs(t, c.GetAllPaymentMethods) +} + +func TestGetPaymentMethodByID(t *testing.T) { + t.Parallel() + _, err := c.GetPaymentMethodByID(context.Background(), "") + assert.ErrorIs(t, err, errPaymentMethodEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + pmID, err := c.GetAllPaymentMethods(context.Background()) + assert.NoError(t, err) + if len(pmID) == 0 { + t.Skip(skipPayMethodNotFound) + } + resp, err := c.GetPaymentMethodByID(context.Background(), pmID[0].ID) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetCurrentUser(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + // This intermittently fails with the message "Unauthorized", for no clear reason + testGetNoArgs(t, c.GetCurrentUser) +} + +func TestGetAllWallets(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + pagIn := PaginationInp{Limit: 2} + resp, err := c.GetAllWallets(context.Background(), pagIn) + assert.NoError(t, err) + require.NotEmpty(t, resp, errExpectedNonEmpty) + if resp.Pagination.NextStartingAfter == "" { + t.Skip(skipInsufficientWallets) + } + pagIn.StartingAfter = resp.Pagination.NextStartingAfter + resp, err = c.GetAllWallets(context.Background(), pagIn) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetWalletByID(t *testing.T) { + t.Parallel() + _, err := c.GetWalletByID(context.Background(), "", "") + assert.ErrorIs(t, err, errCurrWalletConflict) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err := c.GetWalletByID(context.Background(), "", testCrypto.String()) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + resp, err = c.GetWalletByID(context.Background(), resp.ID, "") + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestCreateAddress(t *testing.T) { + t.Parallel() + _, err := c.CreateAddress(context.Background(), "", "") + assert.ErrorIs(t, err, errWalletIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + wID, err := c.GetWalletByID(context.Background(), "", testCrypto.String()) + assert.NoError(t, err) + assert.NotEmpty(t, wID, errExpectedNonEmpty) + resp, err := c.CreateAddress(context.Background(), wID.ID, "") + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetAllAddresses(t *testing.T) { + t.Parallel() + var pag PaginationInp + _, err := c.GetAllAddresses(context.Background(), "", pag) + assert.ErrorIs(t, err, errWalletIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + wID, err := c.GetWalletByID(context.Background(), "", testCrypto.String()) + assert.NoError(t, err) + assert.NotEmpty(t, wID, errExpectedNonEmpty) + resp, err := c.GetAllAddresses(context.Background(), wID.ID, pag) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetAddressByID(t *testing.T) { + t.Parallel() + _, err := c.GetAddressByID(context.Background(), "", "") + assert.ErrorIs(t, err, errWalletIDEmpty) + _, err = c.GetAddressByID(context.Background(), "123", "") + assert.ErrorIs(t, err, errAddressIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + wID, err := c.GetWalletByID(context.Background(), "", testCrypto.String()) + assert.NoError(t, err) + assert.NotEmpty(t, wID, errExpectedNonEmpty) + addID, err := c.GetAllAddresses(context.Background(), wID.ID, PaginationInp{}) + assert.NoError(t, err) + require.NotEmpty(t, addID, errExpectedNonEmpty) + resp, err := c.GetAddressByID(context.Background(), wID.ID, addID.Data[0].ID) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetAddressTransactions(t *testing.T) { + t.Parallel() + _, err := c.GetAddressTransactions(context.Background(), "", "", PaginationInp{}) + assert.ErrorIs(t, err, errWalletIDEmpty) + _, err = c.GetAddressTransactions(context.Background(), "123", "", PaginationInp{}) + assert.ErrorIs(t, err, errAddressIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + wID, err := c.GetWalletByID(context.Background(), "", testCrypto.String()) + assert.NoError(t, err) + assert.NotEmpty(t, wID, errExpectedNonEmpty) + addID, err := c.GetAllAddresses(context.Background(), wID.ID, PaginationInp{}) + assert.NoError(t, err) + require.NotEmpty(t, addID, errExpectedNonEmpty) + _, err = c.GetAddressTransactions(context.Background(), wID.ID, addID.Data[0].ID, PaginationInp{}) + assert.NoError(t, err) +} + +func TestSendMoney(t *testing.T) { + t.Parallel() + _, err := c.SendMoney(context.Background(), "", "", "", "", "", "", "", "", 0, false, false) + assert.ErrorIs(t, err, errTransactionTypeEmpty) + _, err = c.SendMoney(context.Background(), "123", "", "", "", "", "", "", "", 0, false, false) + assert.ErrorIs(t, err, errWalletIDEmpty) + _, err = c.SendMoney(context.Background(), "123", "123", "", "", "", "", "", "", 0, false, false) + assert.ErrorIs(t, err, errToEmpty) + _, err = c.SendMoney(context.Background(), "123", "123", "123", "", "", "", "", "", 0, false, false) + assert.ErrorIs(t, err, order.ErrAmountIsInvalid) + _, err = c.SendMoney(context.Background(), "123", "123", "123", "", "", "", "", "", 1, false, false) + assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + wID, err := c.GetAllWallets(context.Background(), PaginationInp{}) + assert.NoError(t, err) + if wID == nil || len(wID.Data) < 2 { + t.Skip(skipInsufficientWallets) + } + var ( + fromID string + toID string + ) + for i := range wID.Data { + if wID.Data[i].Currency.Name == testCrypto.String() { + if wID.Data[i].Balance.Amount > testAmount*100 { + fromID = wID.Data[i].ID + } else { + toID = wID.Data[i].ID + } } - } else { - if feeBuilder.FeeType != exchange.CryptocurrencyTradeFee { - t.Errorf("Expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) + if fromID != "" && toID != "" { + break } } + if fromID == "" || toID == "" { + t.Skip(skipInsufficientFundsOrWallets) + } + resp, err := c.SendMoney(context.Background(), "transfer", wID.Data[0].ID, wID.Data[1].ID, testCrypto.String(), "GCT Test", "123", "", "", testAmount, false, false) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestGetFee(t *testing.T) { - var feeBuilder = setFeeBuilder() +func TestGetAllTransactions(t *testing.T) { + t.Parallel() + var pag PaginationInp + _, err := c.GetAllTransactions(context.Background(), "", pag) + assert.ErrorIs(t, err, errWalletIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + wID, err := c.GetWalletByID(context.Background(), "", testCrypto.String()) + assert.NoError(t, err) + assert.NotEmpty(t, wID, errExpectedNonEmpty) + _, err = c.GetAllTransactions(context.Background(), wID.ID, pag) + assert.NoError(t, err) +} - if sharedtestvalues.AreAPICredentialsSet(c) { - // CryptocurrencyTradeFee Basic - if _, err := c.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } +func TestGetTransactionByID(t *testing.T) { + t.Parallel() + _, err := c.GetTransactionByID(context.Background(), "", "") + assert.ErrorIs(t, err, errWalletIDEmpty) + _, err = c.GetTransactionByID(context.Background(), "123", "") + assert.ErrorIs(t, err, errTransactionIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + wID, err := c.GetWalletByID(context.Background(), "", testCrypto.String()) + assert.NoError(t, err) + assert.NotEmpty(t, wID, errExpectedNonEmpty) + tID, err := c.GetAllTransactions(context.Background(), wID.ID, PaginationInp{}) + assert.NoError(t, err) + if tID == nil || len(tID.Data) == 0 { + t.Skip(skipInsufficientTransactions) + } + resp, err := c.GetTransactionByID(context.Background(), wID.ID, tID.Data[0].ID) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - // CryptocurrencyTradeFee High quantity - feeBuilder = setFeeBuilder() - feeBuilder.Amount = 1000 - feeBuilder.PurchasePrice = 1000 - if _, err := c.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } +func TestFiatTransfer(t *testing.T) { + t.Parallel() + _, err := c.FiatTransfer(context.Background(), "", "", "", 0, false, FiatDeposit) + assert.ErrorIs(t, err, errWalletIDEmpty) + _, err = c.FiatTransfer(context.Background(), "123", "", "", 0, false, FiatDeposit) + assert.ErrorIs(t, err, order.ErrAmountIsInvalid) + _, err = c.FiatTransfer(context.Background(), "123", "", "", 1, false, FiatDeposit) + assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = c.FiatTransfer(context.Background(), "123", "123", "", 1, false, FiatDeposit) + assert.ErrorIs(t, err, errPaymentMethodEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + wallets, err := c.GetAllWallets(context.Background(), PaginationInp{}) + assert.NoError(t, err) + assert.NotEmpty(t, wallets, errExpectedNonEmpty) + wID, pmID := transferTestHelper(t, wallets) + resp, err := c.FiatTransfer(context.Background(), wID, testFiat.String(), pmID, testAmount, false, FiatDeposit) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + resp, err = c.FiatTransfer(context.Background(), wID, testFiat.String(), pmID, testAmount, false, FiatWithdrawal) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - // CryptocurrencyTradeFee IsMaker - feeBuilder = setFeeBuilder() - feeBuilder.IsMaker = true - if _, err := c.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } +func TestCommitTransfer(t *testing.T) { + t.Parallel() + _, err := c.CommitTransfer(context.Background(), "", "", FiatDeposit) + assert.ErrorIs(t, err, errWalletIDEmpty) + _, err = c.CommitTransfer(context.Background(), "123", "", FiatDeposit) + assert.ErrorIs(t, err, errDepositIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + wallets, err := c.GetAllWallets(context.Background(), PaginationInp{}) + assert.NoError(t, err) + assert.NotEmpty(t, wallets, errExpectedNonEmpty) + wID, pmID := transferTestHelper(t, wallets) + depID, err := c.FiatTransfer(context.Background(), wID, testFiat.String(), pmID, testAmount, false, FiatDeposit) + require.NoError(t, err) + resp, err := c.CommitTransfer(context.Background(), wID, depID.ID, FiatDeposit) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + depID, err = c.FiatTransfer(context.Background(), wID, testFiat.String(), pmID, testAmount, false, FiatWithdrawal) + require.NoError(t, err) + resp, err = c.CommitTransfer(context.Background(), wID, depID.ID, FiatWithdrawal) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - // CryptocurrencyTradeFee Negative purchase price - feeBuilder = setFeeBuilder() - feeBuilder.PurchasePrice = -1000 - if _, err := c.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - } +func TestGetAllFiatTransfers(t *testing.T) { + t.Parallel() + var pag PaginationInp + _, err := c.GetAllFiatTransfers(context.Background(), "", pag, FiatDeposit) + assert.ErrorIs(t, err, errWalletIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + wID, err := c.GetWalletByID(context.Background(), "", "AUD") + require.NoError(t, err) + assert.NotEmpty(t, wID, errExpectedNonEmpty) + // Fiat deposits/withdrawals aren't accepted for fiat currencies for Australian business accounts; the error + // "id not found" possibly reflects this + _, err = c.GetAllFiatTransfers(context.Background(), wID.ID, pag, FiatDeposit) + assert.NoError(t, err) + _, err = c.GetAllFiatTransfers(context.Background(), wID.ID, pag, FiatWithdrawal) + assert.NoError(t, err) +} - // CryptocurrencyWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if _, err := c.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } +func TestGetFiatTransferByID(t *testing.T) { + t.Parallel() + _, err := c.GetFiatTransferByID(context.Background(), "", "", FiatDeposit) + assert.ErrorIs(t, err, errWalletIDEmpty) + _, err = c.GetFiatTransferByID(context.Background(), "123", "", FiatDeposit) + assert.ErrorIs(t, err, errDepositIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + wID, err := c.GetWalletByID(context.Background(), "", "AUD") + require.NoError(t, err) + assert.NotEmpty(t, wID, errExpectedNonEmpty) + // Fiat deposits/withdrawals aren't accepted for fiat currencies for Australian business accounts; the error + // "id not found" possibly reflects this + dID, err := c.GetAllFiatTransfers(context.Background(), wID.ID, PaginationInp{}, FiatDeposit) + assert.NoError(t, err) + if dID == nil || len(dID.Data) == 0 { + t.Skip(skipInsufficientTransactions) + } + resp, err := c.GetFiatTransferByID(context.Background(), wID.ID, dID.Data[0].ID, FiatDeposit) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + resp, err = c.GetFiatTransferByID(context.Background(), wID.ID, dID.Data[0].ID, FiatWithdrawal) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - // CryptocurrencyDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CryptocurrencyDepositFee - if _, err := c.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } +func TestGetFiatCurrencies(t *testing.T) { + t.Parallel() + testGetNoArgs(t, c.GetFiatCurrencies) +} - // InternationalBankDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankDepositFee - feeBuilder.FiatCurrency = currency.EUR - if _, err := c.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } +func TestGetCryptocurrencies(t *testing.T) { + t.Parallel() + testGetNoArgs(t, c.GetCryptocurrencies) +} - // InternationalBankWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee - feeBuilder.FiatCurrency = currency.USD - if _, err := c.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } +func TestGetExchangeRates(t *testing.T) { + t.Parallel() + resp, err := c.GetExchangeRates(context.Background(), "") + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestCalculateTradingFee(t *testing.T) { +func TestGetPrice(t *testing.T) { t.Parallel() - // uppercase - var volume = []Volume{ - { - ProductID: "BTC_USD", - Volume: 100, - }, - } + _, err := c.GetPrice(context.Background(), "", "") + assert.ErrorIs(t, err, errInvalidPriceType) + resp, err := c.GetPrice(context.Background(), testPair.String(), asset.Spot.String()) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.003) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) - } +func TestGetV2Time(t *testing.T) { + t.Parallel() + testGetNoArgs(t, c.GetV2Time) +} - // lowercase - volume = []Volume{ - { - ProductID: "btc_usd", - Volume: 100, - }, - } +func TestGetAllCurrencies(t *testing.T) { + t.Parallel() + testGetNoArgs(t, c.GetAllCurrencies) +} - if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.003) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) - } +func TestGetACurrency(t *testing.T) { + t.Parallel() + testGetOneArg(t, c.GetACurrency, testCrypto.String(), currency.ErrCurrencyCodeEmpty) +} - // mixedCase - volume = []Volume{ - { - ProductID: "btc_USD", - Volume: 100, - }, - } +func TestGetAllTradingPairs(t *testing.T) { + t.Parallel() + _, err := c.GetAllTradingPairs(context.Background(), "") + assert.NoError(t, err) +} - if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.003) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) - } +func TestGetAllPairVolumes(t *testing.T) { + t.Parallel() + testGetNoArgs(t, c.GetAllPairVolumes) +} + +func TestGetPairDetails(t *testing.T) { + t.Parallel() + testGetOneArg(t, c.GetPairDetails, testPair.String(), currency.ErrCurrencyPairEmpty) +} + +func TestGetProductBookV1(t *testing.T) { + t.Parallel() + _, err := c.GetProductBookV1(context.Background(), "", 0) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + resp, err := c.GetProductBookV1(context.Background(), testPair.String(), 2) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + resp, err = c.GetProductBookV1(context.Background(), testPair.String(), 3) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetProductCandles(t *testing.T) { + t.Parallel() + _, err := c.GetProductCandles(context.Background(), "", 0, time.Time{}, time.Time{}) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + resp, err := c.GetProductCandles(context.Background(), testPair.String(), 300, time.Time{}, time.Time{}) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetProductStats(t *testing.T) { + t.Parallel() + testGetOneArg(t, c.GetProductStats, testPair.String(), currency.ErrCurrencyPairEmpty) +} + +func TestGetProductTicker(t *testing.T) { + t.Parallel() + testGetOneArg(t, c.GetProductTicker, testPair.String(), currency.ErrCurrencyPairEmpty) +} + +func TestGetProductTrades(t *testing.T) { + t.Parallel() + _, err := c.GetProductTrades(context.Background(), "", "", "", 0) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + resp, err := c.GetProductTrades(context.Background(), testPair.String(), "1", "before", 0) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - // medium volume - volume = []Volume{ - { - ProductID: "btc_USD", - Volume: 10000001, - }, - } +func TestGetAllWrappedAssets(t *testing.T) { + t.Parallel() + testGetNoArgs(t, c.GetAllWrappedAssets) +} - if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.002) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) - } +func TestGetWrappedAssetDetails(t *testing.T) { + t.Parallel() + testGetOneArg(t, c.GetWrappedAssetDetails, testWrappedAsset.String(), errWrappedAssetEmpty) +} - // high volume - volume = []Volume{ - { - ProductID: "btc_USD", - Volume: 100000010000, - }, - } +func TestGetWrappedAssetConversionRate(t *testing.T) { + t.Parallel() + testGetOneArg(t, c.GetWrappedAssetConversionRate, testWrappedAsset.String(), errWrappedAssetEmpty) +} - if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.001) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) - } +func TestSendHTTPRequest(t *testing.T) { + t.Parallel() + err := c.SendHTTPRequest(context.Background(), exchange.EdgeCase3, "", nil, nil) + assert.ErrorIs(t, err, exchange.ErrEndpointPathNotFound) +} - // no match - volume = []Volume{ - { - ProductID: "btc_beeteesee", - Volume: 100000010000, - }, - } +func TestSendAuthenticatedHTTPRequest(t *testing.T) { + t.Parallel() + err := c.SendAuthenticatedHTTPRequest(context.Background(), exchange.EdgeCase3, "", "", nil, nil, false, nil, nil) + assert.ErrorIs(t, err, exchange.ErrEndpointPathNotFound) + ch := make(chan struct{}) + body := map[string]any{"Unmarshalable": ch} + err = c.SendAuthenticatedHTTPRequest(context.Background(), exchange.RestSpot, "", "", nil, body, false, nil, nil) + var targetErr *json.UnsupportedTypeError + assert.ErrorAs(t, err, &targetErr) +} - if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) +func TestGetFee(t *testing.T) { + t.Parallel() + _, err := c.GetFee(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + feeBuilder := exchange.FeeBuilder{ + FeeType: exchange.OfflineTradeFee, + Amount: 1, + PurchasePrice: 1, } - - // taker - volume = []Volume{ - { - ProductID: "btc_USD", - Volume: 100000010000, - }, + resp, err := c.GetFee(context.Background(), &feeBuilder) + assert.NoError(t, err) + if resp != WorstCaseTakerFee { + t.Errorf(errExpectMismatch, resp, WorstCaseTakerFee) + } + feeBuilder.IsMaker = true + resp, err = c.GetFee(context.Background(), &feeBuilder) + assert.NoError(t, err) + if resp != WorstCaseMakerFee { + t.Errorf(errExpectMismatch, resp, WorstCaseMakerFee) + } + feeBuilder.Pair = currency.NewPair(currency.USDT, currency.USD) + resp, err = c.GetFee(context.Background(), &feeBuilder) + assert.NoError(t, err) + if resp != 0 { + t.Errorf(errExpectMismatch, resp, StablePairMakerFee) + } + feeBuilder.IsMaker = false + resp, err = c.GetFee(context.Background(), &feeBuilder) + assert.NoError(t, err) + if resp != WorstCaseStablePairTakerFee { + t.Errorf(errExpectMismatch, resp, WorstCaseStablePairTakerFee) } - - if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, true); resp != float64(0) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + _, err = c.GetFee(context.Background(), &feeBuilder) + assert.ErrorIs(t, err, errFeeTypeNotSupported) + feeBuilder.Pair = currency.Pair{} + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + feeBuilder.FeeType = exchange.CryptocurrencyTradeFee + resp, err = c.GetFee(context.Background(), &feeBuilder) + assert.NoError(t, err) + if !(resp <= WorstCaseTakerFee && resp >= BestCaseTakerFee) { + t.Errorf(errExpectedFeeRange, BestCaseTakerFee, WorstCaseTakerFee, resp) + } + feeBuilder.IsMaker = true + resp, err = c.GetFee(context.Background(), &feeBuilder) + assert.NoError(t, err) + if !(resp <= WorstCaseMakerFee && resp >= BestCaseMakerFee) { + t.Errorf(errExpectedFeeRange, BestCaseMakerFee, WorstCaseMakerFee, resp) } } -func TestFormatWithdrawPermissions(t *testing.T) { - expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.AutoWithdrawFiatWithAPIPermissionText - withdrawPermissions := c.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { - t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) - } +func TestFetchTradablePairs(t *testing.T) { + t.Parallel() + _, err := c.FetchTradablePairs(context.Background(), asset.Options) + assert.EqualValues(t, errOptionInvalid, err.Error()) + resp, err := c.FetchTradablePairs(context.Background(), asset.Spot) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + resp, err = c.FetchTradablePairs(context.Background(), asset.Futures) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestGetActiveOrders(t *testing.T) { - var getOrdersRequest = order.MultiOrderRequest{ - Type: order.AnyType, - AssetType: asset.Spot, - Pairs: []currency.Pair{testPair}, - Side: order.AnySide, - } - - _, err := c.GetActiveOrders(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Could not get open orders: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } +func TestUpdateTradablePairs(t *testing.T) { + t.Parallel() + err := c.UpdateTradablePairs(context.Background(), false) + assert.NoError(t, err) } -func TestGetOrderHistory(t *testing.T) { - var getOrdersRequest = order.MultiOrderRequest{ - Type: order.AnyType, - AssetType: asset.Spot, - Pairs: []currency.Pair{testPair}, - Side: order.AnySide, - } +func TestUpdateAccountInfo(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err := c.UpdateAccountInfo(context.Background(), asset.Spot) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - _, err := c.GetOrderHistory(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Could not get order history: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } +func TestFetchAccountInfo(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err := c.FetchAccountInfo(context.Background(), asset.Spot) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - getOrdersRequest.Pairs = []currency.Pair{} - _, err = c.GetOrderHistory(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Could not get order history: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } +func TestUpdateTicker(t *testing.T) { + t.Parallel() + _, err := c.UpdateTicker(context.Background(), currency.Pair{}, asset.Spot) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + resp, err := c.UpdateTicker(context.Background(), testPair, asset.Spot) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - getOrdersRequest.Pairs = nil - _, err = c.GetOrderHistory(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Could not get order history: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } +func TestFetchTicker(t *testing.T) { + t.Parallel() + _, err := c.FetchTicker(context.Background(), currency.Pair{}, asset.Spot) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + resp, err := c.FetchTicker(context.Background(), testPair, asset.Spot) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -// Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them -// ---------------------------------------------------------------------------------------------------------------------------- +func TestFetchOrderbook(t *testing.T) { + t.Parallel() + _, err := c.FetchOrderbook(context.Background(), currency.Pair{}, asset.Empty) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + resp, err := c.FetchOrderbook(context.Background(), testPair, asset.Spot) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} -func TestSubmitOrder(t *testing.T) { +func TestUpdateOrderbook(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, c, canManipulateRealOrders) + _, err := c.UpdateOrderbook(context.Background(), currency.Pair{}, asset.Empty) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + _, err = c.UpdateOrderbook(context.Background(), testPair, asset.Empty) + assert.ErrorIs(t, err, asset.ErrNotSupported) + _, err = c.UpdateOrderbook(context.Background(), currency.NewPairWithDelimiter("meow", "woof", "-"), asset.Spot) + assert.EqualValues(t, errInvalidProductID, err.Error()) + resp, err := c.UpdateOrderbook(context.Background(), testPair, asset.Futures) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - // limit order - var orderSubmission = &order.Submit{ - Exchange: c.Name, - Pair: currency.Pair{ - Delimiter: "-", - Base: currency.BTC, - Quote: currency.USD, - }, - Side: order.Buy, - Type: order.Limit, - Price: 1, - Amount: 0.001, - ClientID: "meowOrder", - AssetType: asset.Spot, - } - response, err := c.SubmitOrder(context.Background(), orderSubmission) - if sharedtestvalues.AreAPICredentialsSet(c) && (err != nil || response.Status != order.New) { - t.Errorf("Order failed to be placed: %v", err) - } else if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } - - // market order from amount - orderSubmission = &order.Submit{ - Exchange: c.Name, - Pair: currency.Pair{ - Delimiter: "-", - Base: currency.BTC, - Quote: currency.USD, - }, - Side: order.Buy, - Type: order.Market, - Amount: 0.001, - ClientID: "meowOrder", - AssetType: asset.Spot, - } - response, err = c.SubmitOrder(context.Background(), orderSubmission) - if sharedtestvalues.AreAPICredentialsSet(c) && (err != nil || response.Status != order.New) { - t.Errorf("Order failed to be placed: %v", err) - } else if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } - - // market order from quote amount - orderSubmission = &order.Submit{ - Exchange: c.Name, - Pair: currency.Pair{ - Delimiter: "-", - Base: currency.BTC, - Quote: currency.USD, - }, - Side: order.Buy, - Type: order.Market, - QuoteAmount: 1, - ClientID: "meowOrder", - AssetType: asset.Spot, - } - response, err = c.SubmitOrder(context.Background(), orderSubmission) - if sharedtestvalues.AreAPICredentialsSet(c) && (err != nil || response.Status != order.New) { - t.Errorf("Order failed to be placed: %v", err) - } else if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } +func TestGetAccountFundingHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, err := c.GetAccountFundingHistory(context.Background()) + assert.NoError(t, err) } -func TestCancelExchangeOrder(t *testing.T) { +func TestGetWithdrawalsHistory(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, c, canManipulateRealOrders) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, err := c.GetWithdrawalsHistory(context.Background(), currency.NewCode("meow"), asset.Spot) + assert.ErrorIs(t, err, errNoMatchingWallets) + _, err = c.GetWithdrawalsHistory(context.Background(), testCrypto, asset.Spot) + assert.NoError(t, err) +} - var orderCancellation = &order.Cancel{ - OrderID: "1", - WalletAddress: core.BitcoinDonationAddress, - AccountID: "1", +func TestSubmitOrder(t *testing.T) { + t.Parallel() + _, err := c.SubmitOrder(context.Background(), nil) + assert.ErrorIs(t, err, order.ErrSubmissionIsNil) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + skipTestIfLowOnFunds(t) + ord := order.Submit{ + Exchange: c.Name, Pair: testPair, AssetType: asset.Spot, - } + Side: order.Buy, + Type: order.Market, + StopDirection: order.StopUp, + Amount: testAmount, + Price: testPrice, + RetrieveFees: true, + ClientOrderID: strconv.FormatInt(time.Now().UnixMilli(), 18) + "GCTSubmitOrderTest", + } + resp, err := c.SubmitOrder(context.Background(), &ord) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + ord.StopDirection = order.StopDown + ord.Side = order.Buy + resp, err = c.SubmitOrder(context.Background(), &ord) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) + ord.Type = order.Market + ord.QuoteAmount = testAmount + resp, err = c.SubmitOrder(context.Background(), &ord) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - err := c.CancelOrder(context.Background(), orderCancellation) - if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } +func TestModifyOrder(t *testing.T) { + t.Parallel() + _, err := c.ModifyOrder(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + var ord order.Modify + _, err = c.ModifyOrder(context.Background(), &ord) + assert.ErrorIs(t, err, order.ErrPairIsEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + skipTestIfLowOnFunds(t) + ordIDs := orderTestHelper(t, "SELL") + ord.OrderID = ordIDs.Orders[0].OrderID + ord.Price = testPrice + 1 + resp2, err := c.ModifyOrder(context.Background(), &ord) + assert.NoError(t, err) + assert.NotEmpty(t, resp2, errExpectedNonEmpty) } -func TestCancelAllExchangeOrders(t *testing.T) { +func TestCancelOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, c, canManipulateRealOrders) + err := c.CancelOrder(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + var can order.Cancel + err = c.CancelOrder(context.Background(), &can) + assert.ErrorIs(t, err, order.ErrIDNotSet) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + can.OrderID = "0" + err = c.CancelOrder(context.Background(), &can) + assert.ErrorIs(t, err, errOrderFailedToCancel) + ordIDs := orderTestHelper(t, "") + can.OrderID = ordIDs.Orders[0].OrderID + err = c.CancelOrder(context.Background(), &can) + assert.NoError(t, err) +} - var orderCancellation = &order.Cancel{ - OrderID: "1", - WalletAddress: core.BitcoinDonationAddress, - AccountID: "1", - Pair: testPair, - AssetType: asset.Spot, - } +func TestCancelBatchOrders(t *testing.T) { + t.Parallel() + _, err := c.CancelBatchOrders(context.Background(), nil) + assert.ErrorIs(t, err, errOrderIDEmpty) + can := make([]order.Cancel, 1) + _, err = c.CancelBatchOrders(context.Background(), can) + assert.ErrorIs(t, err, order.ErrIDNotSet) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + ordIDs := orderTestHelper(t, "") + can[0].OrderID = ordIDs.Orders[0].OrderID + resp2, err := c.CancelBatchOrders(context.Background(), can) + assert.NoError(t, err) + assert.NotEmpty(t, resp2, errExpectedNonEmpty) +} - resp, err := c.CancelAllOrders(context.Background(), orderCancellation) +func TestGetOrderInfo(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + ordID, err := c.GetAllOrders(context.Background(), testPair.String(), "", "", "", "", asset.Spot.Upper(), "", "", "", nil, nil, 2, time.Time{}, time.Now()) + require.NoError(t, err) + if ordID == nil || len(ordID.Orders) == 0 { + t.Skip(skipInsufficientOrders) + } + resp, err := c.GetOrderInfo(context.Background(), ordID.Orders[0].OrderID, testPair, asset.Spot) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetDepositAddress(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, err := c.GetDepositAddress(context.Background(), currency.NewCode("fake currency that doesn't exist"), "", "") + assert.ErrorIs(t, err, errNoWalletForCurrency) + resp, err := c.GetDepositAddress(context.Background(), testCrypto, "", "") + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") +func TestWithdrawCryptocurrencyFunds(t *testing.T) { + t.Parallel() + req := withdraw.Request{} + _, err := c.WithdrawCryptocurrencyFunds(context.Background(), &req) + assert.ErrorIs(t, err, common.ErrExchangeNameUnset) + req.Exchange = c.Name + req.Currency = testCrypto + req.Amount = testAmount + req.Type = withdraw.Crypto + req.Crypto.Address = testAddress + _, err = c.WithdrawCryptocurrencyFunds(context.Background(), &req) + assert.ErrorIs(t, err, errWalletIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + wallets, err := c.GetAllWallets(context.Background(), PaginationInp{}) + assert.NoError(t, err) + if wallets == nil || len(wallets.Data) == 0 { + t.Fatal(errExpectedNonEmpty) + } + for i := range wallets.Data { + if wallets.Data[i].Currency.Name == testCrypto.String() && wallets.Data[i].Balance.Amount > testAmount*100 { + req.WalletID = wallets.Data[i].ID + break + } } - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Could not cancel orders: %v", err) + if req.WalletID == "" { + t.Skip(skipInsufficientFunds) } + resp, err := c.WithdrawCryptocurrencyFunds(context.Background(), &req) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestWithdrawFiatFunds(t *testing.T) { + withdrawFiatFundsHelper(t, c.WithdrawFiatFunds) +} - if len(resp.Status) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.Status)) +func TestWithdrawFiatFundsToInternationalBank(t *testing.T) { + withdrawFiatFundsHelper(t, c.WithdrawFiatFundsToInternationalBank) +} + +func TestGetFeeByType(t *testing.T) { + t.Parallel() + _, err := c.GetFeeByType(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + var feeBuilder exchange.FeeBuilder + feeBuilder.FeeType = exchange.OfflineTradeFee + feeBuilder.Amount = 1 + feeBuilder.PurchasePrice = 1 + resp, err := c.GetFeeByType(context.Background(), &feeBuilder) + assert.NoError(t, err) + if resp != WorstCaseTakerFee { + t.Errorf(errExpectMismatch, resp, WorstCaseTakerFee) } } -func TestModifyOrder(t *testing.T) { +func TestGetActiveOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, c, canManipulateRealOrders) + _, err := c.GetActiveOrders(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + var req order.MultiOrderRequest + _, err = c.GetActiveOrders(context.Background(), &req) + assert.ErrorIs(t, err, asset.ErrNotSupported) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + req.AssetType = asset.Spot + req.Side = order.AnySide + req.Type = order.AnyType + _, err = c.GetActiveOrders(context.Background(), &req) + assert.NoError(t, err) + req.Pairs = req.Pairs.Add(currency.NewPair(testCrypto, testFiat)) + _, err = c.GetActiveOrders(context.Background(), &req) + assert.NoError(t, err) +} - _, err := c.ModifyOrder(context.Background(), - &order.Modify{AssetType: asset.Spot}) - if err == nil { - t.Error("ModifyOrder() Expected error") - } +func TestGetOrderHistory(t *testing.T) { + t.Parallel() + _, err := c.GetOrderHistory(context.Background(), nil) + assert.ErrorIs(t, err, order.ErrGetOrdersRequestIsNil) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + var req order.MultiOrderRequest + req.AssetType = asset.Spot + req.Side = order.AnySide + req.Type = order.AnyType + _, err = c.GetOrderHistory(context.Background(), &req) + assert.NoError(t, err) + req.Pairs = req.Pairs.Add(testPair) + _, err = c.GetOrderHistory(context.Background(), &req) + assert.NoError(t, err) } -func TestWithdraw(t *testing.T) { +func TestGetHistoricCandles(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, c, canManipulateRealOrders) + _, err := c.GetHistoricCandles(context.Background(), currency.Pair{}, asset.Empty, kline.OneYear, time.Time{}, time.Time{}) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + resp, err := c.GetHistoricCandles(context.Background(), testPair, asset.Spot, kline.SixHour, time.Now().Add(-time.Hour*60), time.Now()) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - withdrawCryptoRequest := withdraw.Request{ - Exchange: c.Name, - Amount: -1, - Currency: currency.BTC, - Description: "WITHDRAW IT ALL", - Crypto: withdraw.CryptoRequest{ - Address: core.BitcoinDonationAddress, - }, - } +func TestGetHistoricCandlesExtended(t *testing.T) { + t.Parallel() + _, err := c.GetHistoricCandlesExtended(context.Background(), currency.Pair{}, asset.Empty, kline.OneYear, time.Time{}, time.Time{}) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + resp, err := c.GetHistoricCandlesExtended(context.Background(), testPair, asset.Spot, kline.OneMin, time.Now().Add(-time.Hour*9), time.Now()) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} - _, err := c.WithdrawCryptocurrencyFunds(context.Background(), - &withdrawCryptoRequest) - if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) - } +func TestValidateAPICredentials(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + err := c.ValidateAPICredentials(context.Background(), asset.Spot) + assert.NoError(t, err) } -func TestWithdrawFiat(t *testing.T) { +func TestGetServerTime(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, c, canManipulateRealOrders) + _, err := c.GetServerTime(context.Background(), 0) + assert.NoError(t, err) +} - var withdrawFiatRequest = withdraw.Request{ - Amount: 100, - Currency: currency.USD, - Fiat: withdraw.FiatRequest{ - Bank: banking.Account{ - BankName: "Federal Reserve Bank", - }, +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := c.GetLatestFundingRates(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + req := fundingrate.LatestRateRequest{Asset: asset.UpsideProfitContract} + _, err = c.GetLatestFundingRates(context.Background(), &req) + assert.ErrorIs(t, err, asset.ErrNotSupported) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + req.Asset = asset.Futures + resp, err := c.GetLatestFundingRates(context.Background(), &req) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestGetFuturesContractDetails(t *testing.T) { + t.Parallel() + _, err := c.GetFuturesContractDetails(context.Background(), asset.Empty) + assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) + _, err = c.GetFuturesContractDetails(context.Background(), asset.UpsideProfitContract) + assert.ErrorIs(t, err, asset.ErrNotSupported) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err := c.GetFuturesContractDetails(context.Background(), asset.Futures) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +func TestUpdateOrderExecutionLimits(t *testing.T) { + t.Parallel() + err := c.UpdateOrderExecutionLimits(context.Background(), asset.Options) + assert.EqualValues(t, errOptionInvalid, err.Error()) + err = c.UpdateOrderExecutionLimits(context.Background(), asset.Spot) + assert.NoError(t, err) +} + +func TestGetOrderRespToOrderDetail(t *testing.T) { + t.Parallel() + mockData := &GetOrderResponse{ + OrderConfiguration: OrderConfiguration{ + MarketMarketIOC: &MarketMarketIOC{}, + LimitLimitGTC: &LimitLimitGTC{}, + LimitLimitGTD: &LimitLimitGTD{}, + StopLimitStopLimitGTC: &StopLimitStopLimitGTC{}, + StopLimitStopLimitGTD: &StopLimitStopLimitGTD{}, }, - } + SizeInQuote: false, + Side: "BUY", + Status: "OPEN", + Settled: true, + EditHistory: []EditHistory{(EditHistory{})}, + } + resp := c.getOrderRespToOrderDetail(mockData, testPair, asset.Spot) + expected := &order.Detail{ImmediateOrCancel: true, Exchange: "CoinbasePro", Type: 0x40, Side: 0x2, Status: 0x8000, AssetType: 0x2, Date: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), CloseTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), LastUpdated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), Pair: testPair} + assert.EqualValues(t, expected, resp) + mockData.Side = "SELL" + mockData.Status = "FILLED" + resp = c.getOrderRespToOrderDetail(mockData, testPair, asset.Spot) + expected.Side = 0x4 + expected.Status = 0x80 + assert.EqualValues(t, expected, resp) + mockData.Status = "CANCELLED" + resp = c.getOrderRespToOrderDetail(mockData, testPair, asset.Spot) + expected.Status = 0x100 + assert.EqualValues(t, expected, resp) + mockData.Status = "EXPIRED" + resp = c.getOrderRespToOrderDetail(mockData, testPair, asset.Spot) + expected.Status = 0x2000 + assert.EqualValues(t, expected, resp) + mockData.Status = "FAILED" + resp = c.getOrderRespToOrderDetail(mockData, testPair, asset.Spot) + expected.Status = 0x1000 + assert.EqualValues(t, expected, resp) + mockData.Status = "UNKNOWN_ORDER_STATUS" + resp = c.getOrderRespToOrderDetail(mockData, testPair, asset.Spot) + expected.Status = 0x0 + assert.EqualValues(t, expected, resp) +} - _, err := c.WithdrawFiatFunds(context.Background(), &withdrawFiatRequest) - if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") +func TestFiatTransferTypeString(t *testing.T) { + t.Parallel() + var f FiatTransferType + if f.String() != "deposit" { + t.Errorf(errExpectMismatch, f.String(), "deposit") } - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) + f = FiatWithdrawal + if f.String() != "withdrawal" { + t.Errorf(errExpectMismatch, f.String(), "withdrawal") } } -func TestWithdrawInternationalBank(t *testing.T) { +func TestFormatExchangeKlineIntervalV3(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, c, canManipulateRealOrders) + testSequence := map[kline.Interval]string{ + kline.OneMin: granOneMin, + kline.FiveMin: granFiveMin, + kline.FifteenMin: granFifteenMin, + kline.ThirtyMin: granThirtyMin, + kline.TwoHour: granTwoHour, + kline.SixHour: granSixHour, + kline.OneDay: granOneDay, + kline.OneWeek: ""} + for k := range testSequence { + resp, err := FormatExchangeKlineIntervalV3(k) + if resp != testSequence[k] { + t.Errorf(errExpectMismatch, resp, testSequence[k]) + } + if resp == "" { + assert.ErrorIs(t, err, errIntervalNotSupported) + } + } +} - var withdrawFiatRequest = withdraw.Request{ - Amount: 100, - Currency: currency.USD, - Fiat: withdraw.FiatRequest{ - Bank: banking.Account{ - BankName: "Federal Reserve Bank", - }, - }, +func TestGetCurrencyTradeURL(t *testing.T) { + t.Parallel() + testexch.UpdatePairsOnce(t, c) + for _, a := range c.GetAssetTypes(false) { + pairs, err := c.CurrencyPairs.GetPairs(a, false) + require.NoError(t, err, "cannot get pairs for %s", a) + require.NotEmpty(t, pairs, "no pairs for %s", a) + resp, err := c.GetCurrencyTradeURL(context.Background(), a, pairs[0]) + require.NoError(t, err) + assert.NotEmpty(t, resp) } +} - _, err := c.WithdrawFiatFundsToInternationalBank(context.Background(), - &withdrawFiatRequest) - if !sharedtestvalues.AreAPICredentialsSet(c) && err == nil { - t.Error("Expecting an error when no keys are set") +func TestScheduleFuturesSweep(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + curSweeps, err := c.ListFuturesSweeps(context.Background()) + assert.NoError(t, err) + preCancel := false + if len(curSweeps) > 0 { + for i := range curSweeps { + if curSweeps[i].Status == "PENDING" { + preCancel = true + } + } } - if sharedtestvalues.AreAPICredentialsSet(c) && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) + if preCancel { + _, err = c.CancelPendingFuturesSweep(context.Background()) + assert.NoError(t, err) } + _, err = c.ScheduleFuturesSweep(context.Background(), 0.001337) + assert.NoError(t, err) } -func TestGetDepositAddress(t *testing.T) { - _, err := c.GetDepositAddress(context.Background(), currency.BTC, "", "") - if err == nil { - t.Error("GetDepositAddress() error", err) +func TestCancelPendingFuturesSweep(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + curSweeps, err := c.ListFuturesSweeps(context.Background()) + assert.NoError(t, err) + partialSkip := false + if len(curSweeps) > 0 { + for i := range curSweeps { + if curSweeps[i].Status == "PENDING" { + partialSkip = true + } + } + } + if !partialSkip { + _, err = c.ScheduleFuturesSweep(context.Background(), 0.001337) + assert.NoError(t, err) } + _, err = c.CancelPendingFuturesSweep(context.Background()) + assert.NoError(t, err) } // TestWsAuth dials websocket, sends login request. func TestWsAuth(t *testing.T) { - if !c.Websocket.IsEnabled() && !c.API.AuthenticatedWebsocketSupport || !sharedtestvalues.AreAPICredentialsSet(c) { + p := currency.Pairs{testPair} + if c.Websocket.IsEnabled() && !c.API.AuthenticatedWebsocketSupport || !sharedtestvalues.AreAPICredentialsSet(c) { t.Skip(stream.ErrWebsocketNotEnabled.Error()) } var dialer websocket.Dialer err := c.Websocket.Conn.Dial(&dialer, http.Header{}) - require.NoError(t, err, "Dial must not error") + require.NoError(t, err) go c.wsReadData() - - err = c.Subscribe(subscription.List{{Channel: "user", Pairs: currency.Pairs{testPair}}}) - require.NoError(t, err, "Subscribe must not error") + err = c.Subscribe(subscription.List{ + { + Channel: "myAccount", + Asset: asset.All, + Pairs: p, + Authenticated: true, + }, + }) + assert.NoError(t, err) timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) select { case badResponse := <-c.Websocket.DataHandler: - t.Error(badResponse) + assert.IsType(t, []order.Detail{}, badResponse) case <-timer.C: } timer.Stop() } -func TestWsSubscribe(t *testing.T) { - pressXToJSON := []byte(`{ - "type": "subscriptions", - "channels": [ - { - "name": "level2", - "product_ids": [ - "ETH-USD", - "ETH-EUR" - ] - }, - { - "name": "heartbeat", - "product_ids": [ - "ETH-USD", - "ETH-EUR" - ] - }, - { - "name": "ticker", - "product_ids": [ - "ETH-USD", - "ETH-EUR", - "ETH-BTC" - ] +func TestWsHandleData(t *testing.T) { + done := make(chan struct{}) + t.Cleanup(func() { + close(done) + }) + go func() { + for { + select { + case <-c.Websocket.DataHandler: + continue + case <-done: + return } - ] - }`) - err := c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } + } + }() + mockJSON := []byte(`{"type": "error"}`) + _, err := c.wsHandleData(mockJSON, 0) + assert.Error(t, err) + _, err = c.wsHandleData(nil, 0) + assert.ErrorIs(t, err, jsonparser.KeyPathNotFoundError) + mockJSON = []byte(`{"sequence_num": "l"}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorIs(t, err, strconv.ErrSyntax) + mockJSON = []byte(`{"sequence_num": 1, /\\/"""}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorIs(t, err, jsonparser.KeyPathNotFoundError) + mockJSON = []byte(`{"sequence_num": 0, "channel": "subscriptions"}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.NoError(t, err) + mockJSON = []byte(`{"sequence_num": 0, "channel": "", "events":}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorIs(t, err, jsonparser.UnknownValueTypeError) + mockJSON = []byte(`{"sequence_num": 0, "channel": "status", "events": ["type": 1234]}`) + _, err = c.wsHandleData(mockJSON, 0) + var targetErr *json.SyntaxError + assert.ErrorAs(t, err, &targetErr) + mockJSON = []byte(`{"sequence_num": 0, "channel": "status", "events": [{"type": "moo"}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.NoError(t, err) + mockJSON = []byte(`{"sequence_num": 0, "channel": "ticker", "events": ["type": ""}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorAs(t, err, &targetErr) + mockJSON = []byte(`{"sequence_num": 0, "channel": "ticker", "events": [{"type": "moo", "tickers": [{"price": "1.1"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorIs(t, err, jsonparser.KeyPathNotFoundError) + mockJSON = []byte(`{"sequence_num": 0, "channel": "ticker", "timestamp": "2006-01-02T15:04:05Z", "events": [{"type": "moo", "tickers": [{"price": "1.1"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.NoError(t, err) + mockJSON = []byte(`{"sequence_num": 0, "channel": "candles", "events": ["type": ""}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorAs(t, err, &targetErr) + mockJSON = []byte(`{"sequence_num": 0, "channel": "candles", "events": [{"type": "moo", "candles": [{"low": "1.1"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorIs(t, err, jsonparser.KeyPathNotFoundError) + mockJSON = []byte(`{"sequence_num": 0, "channel": "candles", "timestamp": "2006-01-02T15:04:05Z", "events": [{"type": "moo", "candles": [{"low": "1.1"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.NoError(t, err) + mockJSON = []byte(`{"sequence_num": 0, "channel": "market_trades", "events": ["type": ""}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorAs(t, err, &targetErr) + mockJSON = []byte(`{"sequence_num": 0, "channel": "market_trades", "events": [{"type": "moo", "trades": [{"price": "1.1"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.NoError(t, err) + mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "events": ["type": ""}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorAs(t, err, &targetErr) + mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "events": [{"type": "moo", "updates": [{"price_level": "1.1"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorIs(t, err, jsonparser.KeyPathNotFoundError) + mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "timestamp": "2006-01-02T15:04:05Z", "events": [{"type": "moo", "updates": [{"price_level": "1.1"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorIs(t, err, errUnknownL2DataType) + mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "timestamp": "2006-01-02T15:04:05Z", "events": [{"type": "snapshot", "product_id": "BTC-USD", "updates": [{"side": "bid", "price_level": "1.1", "new_quantity": "2.2"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.NoError(t, err) + mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "timestamp": "2006-01-02T15:04:05Z", "events": [{"type": "update", "product_id": "BTC-USD", "updates": [{"side": "bid", "price_level": "1.1", "new_quantity": "2.2"}]}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.NoError(t, err) + mockJSON = []byte(`{"sequence_num": 0, "channel": "user", "events": ["type": ""}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorAs(t, err, &targetErr) + mockJSON = []byte(`{"sequence_num": 0, "channel": "user", "events": [{"type": "moo", "orders": [{"limit_price": "2.2", "total_fees": "1.1"}], "positions": {"perpetual_futures_positions": [{"margin_type": "fakeMarginType"}], "expiring_futures_positions": [{}]}}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.NoError(t, err) + mockJSON = []byte(`{"sequence_num": 0, "channel": "fakechan", "events": ["type": ""}]}`) + _, err = c.wsHandleData(mockJSON, 0) + assert.ErrorIs(t, err, errChannelNameUnknown) } -func TestWsHeartbeat(t *testing.T) { - pressXToJSON := []byte(`{ - "type": "heartbeat", - "sequence": 90, - "last_trade_id": 20, - "product_id": "BTC-USD", - "time": "2014-11-07T08:19:28.464459Z" - }`) - err := c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } +func TestProcessSnapshotUpdate(t *testing.T) { + t.Parallel() + req := WebsocketOrderbookDataHolder{Changes: []WebsocketOrderbookData{{Side: "fakeside", PriceLevel: 1.1, NewQuantity: 2.2}}, ProductID: currency.NewBTCUSD()} + err := c.ProcessSnapshot(&req, time.Time{}) + assert.ErrorIs(t, err, order.ErrSideIsInvalid) + err = c.ProcessUpdate(&req, time.Time{}) + assert.ErrorIs(t, err, order.ErrSideIsInvalid) + req.Changes[0].Side = "offer" + err = c.ProcessSnapshot(&req, time.Now()) + assert.NoError(t, err) + err = c.ProcessUpdate(&req, time.Now()) + assert.NoError(t, err) } -func TestWsStatus(t *testing.T) { - pressXToJSON := []byte(`{ - "type": "status", - "products": [ - { - "id": "BTC-USD", - "base_currency": "BTC", - "quote_currency": "USD", - "base_min_size": "0.001", - "base_max_size": "70", - "base_increment": "0.00000001", - "quote_increment": "0.01", - "display_name": "BTC/USD", - "status": "online", - "status_message": null, - "min_market_funds": "10", - "max_market_funds": "1000000", - "post_only": false, - "limit_only": false, - "cancel_only": false - } - ], - "currencies": [ - { - "id": "USD", - "name": "United States Dollar", - "min_size": "0.01000000", - "status": "online", - "status_message": null, - "max_precision": "0.01", - "convertible_to": ["USDC"], "details": {} - }, - { - "id": "USDC", - "name": "USD Coin", - "min_size": "0.00000100", - "status": "online", - "status_message": null, - "max_precision": "0.000001", - "convertible_to": ["USD"], "details": {} - }, - { - "id": "BTC", - "name": "Bitcoin", - "min_size": "0.00000001", - "status": "online", - "status_message": null, - "max_precision": "0.00000001", - "convertible_to": [] - } - ] -}`) - err := c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) +func TestGenerateSubscriptions(t *testing.T) { + t.Parallel() + c := new(CoinbasePro) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + if err := testexch.Setup(c); err != nil { + log.Fatal(err) + } + c.Websocket.SetCanUseAuthenticatedEndpoints(true) + p1, err := c.GetEnabledPairs(asset.Spot) + require.NoError(t, err) + p2, err := c.GetEnabledPairs(asset.Futures) + require.NoError(t, err) + exp := subscription.List{} + for _, baseSub := range defaultSubscriptions.Enabled() { + s := baseSub.Clone() + s.QualifiedChannel = subscriptionNames[s.Channel] + switch s.Asset { + case asset.Spot: + s.Pairs = p1 + case asset.Futures: + s.Pairs = p2 + case asset.All: + s2 := s.Clone() + s2.Asset = asset.Futures + s2.Pairs = p2 + exp = append(exp, s2) + s.Asset = asset.Spot + s.Pairs = p1 + } + exp = append(exp, s) } + subs, err := c.generateSubscriptions() + require.NoError(t, err) + testsubs.EqualLists(t, exp, subs) + _, err = subscription.List{{Channel: "wibble"}}.ExpandTemplates(c) + assert.ErrorContains(t, err, "subscription channel not supported: wibble") } -func TestWsTicker(t *testing.T) { - pressXToJSON := []byte(`{ - "type": "ticker", - "trade_id": 20153558, - "sequence": 3262786978, - "time": "2017-09-02T17:05:49.250000Z", - "product_id": "BTC-USD", - "price": "4388.01000000", - "side": "buy", - "last_size": "0.03000000", - "best_bid": "4388", - "best_ask": "4388.01" -}`) - err := c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } +func TestSubscribeUnsubscribe(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + req := subscription.List{{Channel: "heartbeat", Asset: asset.Spot, Pairs: currency.Pairs{currency.NewPairWithDelimiter(testCrypto.String(), testFiat.String(), "-")}}} + err := c.Subscribe(req) + assert.NoError(t, err) + err = c.Unsubscribe(req) + assert.NoError(t, err) } -func TestWsOrderbook(t *testing.T) { - pressXToJSON := []byte(`{ - "type": "snapshot", - "product_id": "BTC-USD", - "bids": [["10101.10", "0.45054140"]], - "asks": [["10102.55", "0.57753524"]], - "time":"2023-08-15T06:46:55.376250Z" -}`) - err := c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) +func TestCheckSubscriptions(t *testing.T) { + t.Parallel() + c := &CoinbasePro{ //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + Base: exchange.Base{ + Config: &config.Exchange{ + Features: &config.FeaturesConfig{ + Subscriptions: subscription.List{ + {Enabled: true, Channel: "matches"}, + }, + }, + }, + Features: exchange.Features{}, + }, } + c.checkSubscriptions() + testsubs.EqualLists(t, defaultSubscriptions.Enabled(), c.Features.Subscriptions) + testsubs.EqualLists(t, defaultSubscriptions, c.Config.Features.Subscriptions) +} - pressXToJSON = []byte(`{ - "type": "l2update", - "product_id": "BTC-USD", - "time": "2023-08-15T06:46:57.933713Z", - "changes": [ - [ - "buy", - "10101.80000000", - "0.162567" - ] - ] -}`) - err = c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } +func TestGetJWT(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + _, _, err := c.GetJWT(context.Background(), "") + assert.NoError(t, err) } -func TestWsOrders(t *testing.T) { - pressXToJSON := []byte(`{ - "type": "received", - "time": "2014-11-07T08:19:27.028459Z", - "product_id": "BTC-USD", - "sequence": 10, - "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", - "size": "1.34", - "price": "502.1", - "side": "buy", - "order_type": "limit" -}`) - err := c.wsHandleData(pressXToJSON) +func exchangeBaseHelper(c *CoinbasePro) error { + cfg := config.GetConfig() + err := cfg.LoadConfig("../../testdata/configtest.json", true) if err != nil { - t.Error(err) + return err } - - pressXToJSON = []byte(`{ - "type": "received", - "time": "2014-11-09T08:19:27.028459Z", - "product_id": "BTC-USD", - "sequence": 12, - "order_id": "dddec984-77a8-460a-b958-66f114b0de9b", - "funds": "3000.234", - "side": "buy", - "order_type": "market" -}`) - err = c.wsHandleData(pressXToJSON) + gdxConfig, err := cfg.GetExchangeConfig("CoinbasePro") if err != nil { - t.Error(err) + return err } - - pressXToJSON = []byte(`{ - "type": "open", - "time": "2014-11-07T08:19:27.028459Z", - "product_id": "BTC-USD", - "sequence": 10, - "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", - "price": "200.2", - "remaining_size": "1.00", - "side": "sell" -}`) - err = c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) + if apiKey != "" { + gdxConfig.API.Credentials.Key = apiKey + gdxConfig.API.Credentials.Secret = apiSecret + gdxConfig.API.AuthenticatedSupport = true + gdxConfig.API.AuthenticatedWebsocketSupport = true } - - pressXToJSON = []byte(`{ - "type": "done", - "time": "2014-11-07T08:19:27.028459Z", - "product_id": "BTC-USD", - "sequence": 10, - "price": "200.2", - "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", - "reason": "filled", - "side": "sell", - "remaining_size": "0" -}`) - err = c.wsHandleData(pressXToJSON) + c.Websocket = sharedtestvalues.NewTestWebsocket() + err = c.Setup(gdxConfig) if err != nil { - t.Error(err) + return err } + return nil +} - pressXToJSON = []byte(`{ - "type": "match", - "trade_id": 10, - "sequence": 50, - "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", - "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1", - "time": "2014-11-07T08:19:27.028459Z", - "product_id": "BTC-USD", - "size": "5.23512", - "price": "400.23", - "side": "sell" -}`) - err = c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) +func skipTestIfLowOnFunds(t *testing.T) { + t.Helper() + accounts, err := c.GetAllAccounts(context.Background(), 250, "") + assert.NoError(t, err) + if accounts == nil || len(accounts.Accounts) == 0 { + t.Fatal(errExpectedNonEmpty) + } + var hasValidFunds bool + for i := range accounts.Accounts { + if accounts.Accounts[i].Currency == testCrypto.String() && + accounts.Accounts[i].AvailableBalance.Value > testAmount*100 { + hasValidFunds = true + } } + if !hasValidFunds { + t.Skip(skipInsufficientFunds) + } +} - pressXToJSON = []byte(`{ - "type": "change", - "time": "2014-11-07T08:19:27.028459Z", - "sequence": 80, - "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", - "product_id": "BTC-USD", - "new_size": "5.23512", - "old_size": "12.234412", - "price": "400.23", - "side": "sell" -}`) - err = c.wsHandleData(pressXToJSON) +func portfolioIDFromName(t *testing.T, targetName string) string { + t.Helper() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + createResp, err := c.CreatePortfolio(context.Background(), targetName) + var targetID string if err != nil { - t.Error(err) + assert.EqualValues(t, errPortfolioNameDuplicate, err.Error()) + getResp, err := c.GetAllPortfolios(context.Background(), "") + assert.NoError(t, err) + if len(getResp) == 0 { + t.Fatal(errExpectedNonEmpty) + } + for i := range getResp { + if getResp[i].Name == targetName { + targetID = getResp[i].UUID + break + } + } + } else { + targetID = createResp.UUID } - pressXToJSON = []byte(`{ - "type": "change", - "time": "2014-11-07T08:19:27.028459Z", - "sequence": 80, - "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", - "product_id": "BTC-USD", - "new_funds": "5.23512", - "old_funds": "12.234412", - "price": "400.23", - "side": "sell" -}`) - err = c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) + return targetID +} + +func getINTXPortfolio(t *testing.T) string { + t.Helper() + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + resp, err := c.GetAllPortfolios(context.Background(), "") + assert.NoError(t, err) + if len(resp) == 0 { + t.Skip(skipInsufficientPortfolios) + } + var targetID string + for i := range resp { + if resp[i].Type == "INTX" { + targetID = resp[i].UUID + break + } } - pressXToJSON = []byte(`{ - "type": "activate", - "product_id": "BTC-USD", - "timestamp": "1483736448.299000", - "user_id": "12", - "profile_id": "30000727-d308-cf50-7b1c-c06deb1934fc", - "order_id": "7b52009b-64fd-0a2a-49e6-d8a939753077", - "stop_type": "entry", - "side": "buy", - "stop_price": "80", - "size": "2", - "funds": "50", - "taker_fee_rate": "0.0025", - "private": true -}`) - err = c.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) + if targetID == "" { + t.Skip(skipInsufficientPortfolios) } + return targetID } -func TestStatusToStandardStatus(t *testing.T) { - type TestCases struct { - Case string - Result order.Status - } - testCases := []TestCases{ - {Case: "received", Result: order.New}, - {Case: "open", Result: order.Active}, - {Case: "done", Result: order.Filled}, - {Case: "match", Result: order.PartiallyFilled}, - {Case: "change", Result: order.Active}, - {Case: "activate", Result: order.Active}, - {Case: "LOL", Result: order.UnknownStatus}, +func convertTestHelper(t *testing.T) (fromAccID, toAccID string) { + t.Helper() + accIDs, err := c.GetAllAccounts(context.Background(), 250, "") + assert.NoError(t, err) + if accIDs == nil || len(accIDs.Accounts) == 0 { + t.Fatal(errExpectedNonEmpty) } - for i := range testCases { - result, _ := statusToStandardStatus(testCases[i].Case) - if result != testCases[i].Result { - t.Errorf("Expected: %v, received: %v", testCases[i].Result, result) + for x := range accIDs.Accounts { + if accIDs.Accounts[x].Currency == testStable.String() { + fromAccID = accIDs.Accounts[x].UUID + } + if accIDs.Accounts[x].Currency == testFiat.String() { + toAccID = accIDs.Accounts[x].UUID + } + if fromAccID != "" && toAccID != "" { + break } } + if fromAccID == "" || toAccID == "" { + t.Skip(skipInsufSuitableAccs) + } + return fromAccID, toAccID } -func TestParseTime(t *testing.T) { - // Rest examples use 2014-11-07T22:19:28.578544Z" and can be safely - // unmarhsalled into time.Time - - // All events except for activate use the above, in the below test - // we'll use their API docs example - r := convert.TimeFromUnixTimestampDecimal(1483736448.299000).UTC() - if r.Year() != 2017 || - r.Month().String() != "January" || - r.Day() != 6 { - t.Error("unexpected result") +func transferTestHelper(t *testing.T, wallets *GetAllWalletsResponse) (srcWalletID, tarWalletID string) { + t.Helper() + var hasValidFunds bool + for i := range wallets.Data { + if wallets.Data[i].Currency.Code == testFiat.String() && wallets.Data[i].Balance.Amount > 10 { + hasValidFunds = true + srcWalletID = wallets.Data[i].ID + } + } + if !hasValidFunds { + t.Skip(skipInsufficientFunds) } + pmID, err := c.GetAllPaymentMethods(context.Background()) + assert.NoError(t, err) + if len(pmID) == 0 { + t.Skip(skipPayMethodNotFound) + } + return srcWalletID, pmID[0].ID } -func TestGetRecentTrades(t *testing.T) { +type withdrawFiatFunc func(context.Context, *withdraw.Request) (*withdraw.ExchangeResponse, error) + +func withdrawFiatFundsHelper(t *testing.T, fn withdrawFiatFunc) { + t.Helper() t.Parallel() - _, err := c.GetRecentTrades(context.Background(), testPair, asset.Spot) - if err != nil { - t.Error(err) + req := withdraw.Request{} + _, err := fn(context.Background(), &req) + assert.ErrorIs(t, err, common.ErrExchangeNameUnset) + req.Exchange = c.Name + req.Currency = testFiat + req.Amount = 1 + req.Type = withdraw.Fiat + req.Fiat.Bank.Enabled = true + req.Fiat.Bank.SupportedExchanges = "CoinbasePro" + req.Fiat.Bank.SupportedCurrencies = testFiat.String() + req.Fiat.Bank.AccountNumber = "123" + req.Fiat.Bank.SWIFTCode = "456" + req.Fiat.Bank.BSBNumber = "789" + _, err = fn(context.Background(), &req) + assert.ErrorIs(t, err, errWalletIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + req.WalletID = "meow" + req.Fiat.Bank.BankName = "GCT's Officially Fake and Not Real Test Bank" + _, err = fn(context.Background(), &req) + assert.ErrorIs(t, err, errPayMethodNotFound) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + wallets, err := c.GetAllWallets(context.Background(), PaginationInp{}) + assert.NoError(t, err) + if wallets == nil || len(wallets.Data) == 0 { + t.Fatal(errExpectedNonEmpty) + } + req.WalletID = "" + for i := range wallets.Data { + if wallets.Data[i].Currency.Name == testFiat.String() && wallets.Data[i].Balance.Amount > testAmount*100 { + req.WalletID = wallets.Data[i].ID + break + } } + if req.WalletID == "" { + t.Skip(skipInsufficientFunds) + } + req.Fiat.Bank.BankName = "AUD Wallet" + resp, err := fn(context.Background(), &req) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestGetHistoricTrades(t *testing.T) { - t.Parallel() - _, err := c.GetHistoricTrades(context.Background(), - testPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) - if err != nil && err != common.ErrFunctionNotSupported { - t.Error(err) - } +type getNoArgsResp interface { + *ServerTimeV3 | []PaymentMethodData | *UserResponse | []FiatData | []CryptoData | *ServerTimeV2 | []CurrencyData | []PairVolumeData | *AllWrappedAssets } -func TestGetTransfers(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, c) - _, err := c.GetTransfers(context.Background(), "", "", 100, time.Time{}, time.Time{}) - if err != nil { - t.Error(err) - } +type getNoArgsAssertNotEmpty[G getNoArgsResp] func(context.Context) (G, error) + +func testGetNoArgs[G getNoArgsResp](t *testing.T, f getNoArgsAssertNotEmpty[G]) { + t.Helper() + resp, err := f(context.Background()) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } -func TestGetCurrencyTradeURL(t *testing.T) { +type genConvertTestFunc func(context.Context, string, string, string) (*ConvertResponse, error) + +func convertTestShared(t *testing.T, f genConvertTestFunc) { + t.Helper() t.Parallel() - testexch.UpdatePairsOnce(t, c) - for _, a := range c.GetAssetTypes(false) { - pairs, err := c.CurrencyPairs.GetPairs(a, false) - require.NoError(t, err, "cannot get pairs for %s", a) - require.NotEmpty(t, pairs, "no pairs for %s", a) - resp, err := c.GetCurrencyTradeURL(context.Background(), a, pairs[0]) - require.NoError(t, err) - assert.NotEmpty(t, resp) - } + _, err := f(context.Background(), "", "", "") + assert.ErrorIs(t, err, errTransactionIDEmpty) + _, err = f(context.Background(), "meow", "", "") + assert.ErrorIs(t, err, errAccountIDEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + fromAccID, toAccID := convertTestHelper(t) + resp, err := c.CreateConvertQuote(context.Background(), fromAccID, toAccID, "", "", 0.01) + assert.NoError(t, err) + require.NotNil(t, resp) + resp, err = f(context.Background(), resp.ID, fromAccID, toAccID) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) +} + +type getOneArgResp interface { + *CurrencyData | *PairData | *ProductStats | *ProductTicker | *WrappedAsset | *WrappedAssetConversionRate +} + +type getOneArgAssertNotEmpty[G getOneArgResp] func(context.Context, string) (G, error) + +func testGetOneArg[G getOneArgResp](t *testing.T, f getOneArgAssertNotEmpty[G], arg string, tarErr error) { + t.Helper() + _, err := f(context.Background(), "") + assert.ErrorIs(t, err, tarErr) + resp, err := f(context.Background(), arg) + assert.NoError(t, err) + assert.NotEmpty(t, resp, errExpectedNonEmpty) } diff --git a/exchanges/coinbasepro/coinbasepro_types.go b/exchanges/coinbasepro/coinbasepro_types.go index de2a37284a5..99b8c325344 100644 --- a/exchanges/coinbasepro/coinbasepro_types.go +++ b/exchanges/coinbasepro/coinbasepro_types.go @@ -1,518 +1,1488 @@ package coinbasepro import ( + "net/url" + "sync" "time" + "github.com/gofrs/uuid" "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/types" ) -// Product holds product information +// CoinbasePro is the overarching type across the coinbasepro package +type CoinbasePro struct { + exchange.Base + jwt string + jwtExpire time.Time + mut sync.RWMutex +} + +// Version is used for the niche cases where the Version of the API must be specified and passed around for proper functionality +type Version bool + +// FiatTransferType is used so that we don't need to duplicate the four fiat transfer-related endpoints under version 2 of the API +type FiatTransferType bool + +// ValueWithCurrency is a sub-struct used in the types Account, NativeAndRaw, DetailedPortfolioResponse, FuturesBalanceSummary, ListFuturesSweepsResponse, PerpetualsPortfolioSummary, PerpPositionDetail, FeeStruct, AmScale, and ConvertResponse +type ValueWithCurrency struct { + Value float64 `json:"value,string"` + Currency string `json:"currency"` +} + +// Account holds details for a trading account, returned by GetAccountByID and used as a sub-struct in the type AllAccountsResponse +type Account struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Currency string `json:"currency"` + AvailableBalance ValueWithCurrency `json:"available_balance"` + Default bool `json:"default"` + Active bool `json:"active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt time.Time `json:"deleted_at"` + Type string `json:"type"` + Ready bool `json:"ready"` + Hold ValueWithCurrency `json:"hold"` +} + +// AllAccountsResponse holds many Account structs, as well as pagination information, returned by GetAllAccounts +type AllAccountsResponse struct { + Accounts []Account `json:"accounts"` + HasNext bool `json:"has_next"` + Cursor string `json:"cursor"` + Size uint8 `json:"size"` +} + +// Params is used within functions to make the setting of parameters easier +type Params struct { + url.Values +} + +// PriceSize is a sub-struct used in the type ProductBook +type PriceSize struct { + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` +} + +// ProductBook holds bid and ask prices for a particular product, returned by GetBestBidAsk and used in ProductBookResp +type ProductBook struct { + ProductID currency.Pair `json:"product_id"` + Bids []PriceSize `json:"bids"` + Asks []PriceSize `json:"asks"` + Time time.Time `json:"time"` +} + +// ProductBookResp holds a ProductBook struct, and associated information, returned by GetProductBookV3 +type ProductBookResp struct { + Pricebook ProductBook `json:"pricebook"` + Last float64 `json:"last,string"` + MidMarket float64 `json:"mid_market,string"` + SpreadBPs float64 `json:"spread_bps,string"` + SpreadAbsolute float64 `json:"spread_absolute,string"` +} + +// FCMTradingSessionDetails is a sub-struct used in the type Product +type FCMTradingSessionDetails struct { + IsSessionOpen bool `json:"is_session_open"` + OpenTime time.Time `json:"open_time"` + CloseTime time.Time `json:"close_time"` + SessionState string `json:"session_state"` + AfterHoursOrderEntryDisabled bool `json:"after_hours_order_entry_disabled"` +} + +// PerpetualDetails is a sub-struct used in the type FutureProductDetails +type PerpetualDetails struct { + OpenInterest types.Number `json:"open_interest"` + FundingRate types.Number `json:"funding_rate"` + FundingTime time.Time `json:"funding_time"` + MaxLeverage types.Number `json:"max_leverage"` + BaseAssetUUID string `json:"base_asset_uuid"` +} + +// FutureProductDetails is a sub-struct used in the type Product +type FutureProductDetails struct { + Venue string `json:"venue"` + ContractCode string `json:"contract_code"` + ContractExpiry time.Time `json:"contract_expiry"` + ContractSize types.Number `json:"contract_size"` + ContractRootUnit string `json:"contract_root_unit"` + GroupDescription string `json:"group_description"` + ContractExpiryTimezone string `json:"contract_expiry_timezone"` + GroupShortDescription string `json:"group_short_description"` + RiskManagedBy string `json:"risk_managed_by"` + ContractExpiryType string `json:"contract_expiry_type"` + PerpetualDetails PerpetualDetails `json:"perpetual_details"` + ContractDisplayName string `json:"contract_display_name"` + TimeToExpiry time.Duration `json:"time_to_expiry_ms,string"` + NonCrypto bool `json:"non_crypto"` + ContractExpiryName string `json:"contract_expiry_name"` +} + +// Product holds product information, returned by GetProductByID, and used as a sub-struct in the type AllProducts type Product struct { - ID string `json:"id"` - BaseCurrency string `json:"base_currency"` - QuoteCurrency string `json:"quote_currency"` - QuoteIncrement float64 `json:"quote_increment,string"` - BaseIncrement float64 `json:"base_increment,string"` - DisplayName string `json:"display_name"` - MinimumMarketFunds float64 `json:"min_market_funds,string"` - MarginEnabled bool `json:"margin_enabled"` - PostOnly bool `json:"post_only"` - LimitOnly bool `json:"limit_only"` - CancelOnly bool `json:"cancel_only"` - Status string `json:"status"` - StatusMessage string `json:"status_message"` - TradingDisabled bool `json:"trading_disabled"` - ForeignExchangeStableCoin bool `json:"fx_stablecoin"` - MaxSlippagePercentage float64 `json:"max_slippage_percentage,string"` - AuctionMode bool `json:"auction_mode"` -} - -// Ticker holds basic ticker information + ID currency.Pair `json:"product_id"` + Price types.Number `json:"price"` + PricePercentageChange24H types.Number `json:"price_percentage_change_24h"` + Volume24H types.Number `json:"volume_24h"` + VolumePercentageChange24H types.Number `json:"volume_percentage_change_24h"` + BaseIncrement types.Number `json:"base_increment"` + QuoteIncrement types.Number `json:"quote_increment"` + QuoteMinSize types.Number `json:"quote_min_size"` + QuoteMaxSize types.Number `json:"quote_max_size"` + BaseMinSize types.Number `json:"base_min_size"` + BaseMaxSize types.Number `json:"base_max_size"` + BaseName string `json:"base_name"` + QuoteName string `json:"quote_name"` + Watched bool `json:"watched"` + IsDisabled bool `json:"is_disabled"` + New bool `json:"new"` + Status string `json:"status"` + CancelOnly bool `json:"cancel_only"` + LimitOnly bool `json:"limit_only"` + PostOnly bool `json:"post_only"` + TradingDisabled bool `json:"trading_disabled"` + AuctionMode bool `json:"auction_mode"` + ProductType string `json:"product_type"` + QuoteCurrencyID currency.Code `json:"quote_currency_id"` + BaseCurrencyID currency.Code `json:"base_currency_id"` + FCMTradingSessionDetails FCMTradingSessionDetails `json:"fcm_trading_session_details"` + MidMarketPrice types.Number `json:"mid_market_price"` + Alias string `json:"alias"` + AliasTo []string `json:"alias_to"` + BaseDisplaySymbol string `json:"base_display_symbol"` + QuoteDisplaySymbol string `json:"quote_display_symbol"` + ViewOnly bool `json:"view_only"` + PriceIncrement types.Number `json:"price_increment"` + DisplayName string `json:"display_name"` + ProductVenue string `json:"product_venue"` + ApproximateQuote24HVolume types.Number `json:"approximate_quote_24h_volume"` + FutureProductDetails FutureProductDetails `json:"future_product_details"` +} + +// AllProducts holds information on a lot of available currency pairs, returned by GetAllProducts +type AllProducts struct { + Products []Product `json:"products"` + NumProducts int32 `json:"num_products"` +} + +// CandleStruct holds historic trade information, returned by GetHistoricRates +type CandleStruct struct { + Start types.Time `json:"start"` + Low float64 `json:"low,string"` + High float64 `json:"high,string"` + Open float64 `json:"open,string"` + Close float64 `json:"close,string"` + Volume float64 `json:"volume,string"` +} + +// Trades is a sub-struct used in the type Ticker +type Trades struct { + TradeID string `json:"trade_id"` + ProductID currency.Pair `json:"product_id"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Time time.Time `json:"time"` + Side string `json:"side"` + Bid types.Number `json:"bid"` + Ask types.Number `json:"ask"` +} + +// Ticker holds basic ticker information, returned by GetTicker type Ticker struct { - TradeID int64 `json:"trade_id"` - Ask float64 `json:"ask,string"` - Bid float64 `json:"bid,string"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - Volume float64 `json:"volume,string"` - Time time.Time `json:"time"` + Trades []Trades `json:"trades"` + BestBid types.Number `json:"best_bid"` + BestAsk types.Number `json:"best_ask"` } -// Trade holds executed trade information -type Trade struct { - TradeID int64 `json:"trade_id"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - Time time.Time `json:"time"` - Side string `json:"side"` +// MarketMarketIOC is a sub-struct used in the type OrderConfiguration +type MarketMarketIOC struct { + QuoteSize types.Number `json:"quote_size,omitempty"` + BaseSize types.Number `json:"base_size,omitempty"` } -// History holds historic rate information -type History struct { - Time time.Time - Low float64 - High float64 - Open float64 - Close float64 - Volume float64 +// LimitLimitGTC is a sub-struct used in the type OrderConfiguration +type LimitLimitGTC struct { + BaseSize types.Number `json:"base_size"` + LimitPrice types.Number `json:"limit_price"` + PostOnly bool `json:"post_only"` +} + +// LimitLimitGTD is a sub-struct used in the type OrderConfiguration +type LimitLimitGTD struct { + BaseSize types.Number `json:"base_size"` + LimitPrice types.Number `json:"limit_price"` + EndTime time.Time `json:"end_time"` + PostOnly bool `json:"post_only"` +} + +// StopLimitStopLimitGTC is a sub-struct used in the type OrderConfiguration +type StopLimitStopLimitGTC struct { + BaseSize types.Number `json:"base_size"` + LimitPrice types.Number `json:"limit_price"` + StopPrice types.Number `json:"stop_price"` + StopDirection string `json:"stop_direction"` +} + +// StopLimitStopLimitGTD is a sub-struct used in the type OrderConfiguration +type StopLimitStopLimitGTD struct { + BaseSize types.Number `json:"base_size"` + LimitPrice types.Number `json:"limit_price"` + StopPrice types.Number `json:"stop_price"` + EndTime time.Time `json:"end_time"` + StopDirection string `json:"stop_direction"` +} + +// OrderConfiguration is a struct used in the formation of requests in PrepareOrderConfig, and is a sub-struct used in the types PlaceOrderResp and GetOrderResponse +type OrderConfiguration struct { + MarketMarketIOC *MarketMarketIOC `json:"market_market_ioc,omitempty"` + LimitLimitGTC *LimitLimitGTC `json:"limit_limit_gtc,omitempty"` + LimitLimitGTD *LimitLimitGTD `json:"limit_limit_gtd,omitempty"` + StopLimitStopLimitGTC *StopLimitStopLimitGTC `json:"stop_limit_stop_limit_gtc,omitempty"` + StopLimitStopLimitGTD *StopLimitStopLimitGTD `json:"stop_limit_stop_limit_gtd,omitempty"` +} + +// SuccessResponse is a sub-struct used in the type PlaceOrderResp +type SuccessResponse struct { + OrderID string `json:"order_id"` + ProductID currency.Pair `json:"product_id"` + Side string `json:"side"` + ClientOrderID string `json:"client_oid"` +} + +// PlaceOrderResp contains information on an order, returned by PlaceOrder +type PlaceOrderResp struct { + Success bool `json:"success"` + FailureReason string `json:"failure_reason"` + OrderID string `json:"order_id"` + SuccessResponse SuccessResponse `json:"success_response"` + OrderConfiguration OrderConfiguration `json:"order_configuration"` +} + +// OrderCancelDetail contains information on attempted order cancellations, returned by CancelOrders +type OrderCancelDetail struct { + Success bool `json:"success"` + FailureReason string `json:"failure_reason"` + OrderID string `json:"order_id"` +} + +// EditOrderPreviewResp contains information on the effects of editing an order, returned by EditOrderPreview +type EditOrderPreviewResp struct { + Slippage float64 `json:"slippage,string"` + OrderTotal float64 `json:"order_total,string"` + CommissionTotal float64 `json:"commission_total,string"` + QuoteSize float64 `json:"quote_size,string"` + BaseSize float64 `json:"base_size,string"` + BestBid float64 `json:"best_bid,string"` + BestAsk float64 `json:"best_ask,string"` + AverageFilledPrice float64 `json:"average_filled_price,string"` +} + +// EditHistory is a sub-struct used in the type GetOrderResponse +type EditHistory struct { + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + ReplaceAcceptTimestamp time.Time `json:"replace_accept_timestamp"` +} + +// GetOrderResponse contains information on an order, returned by GetOrderByID IterativeGetAllOrders, and used in GetAllOrdersResp +type GetOrderResponse struct { + OrderID string `json:"order_id"` + ProductID currency.Pair `json:"product_id"` + UserID string `json:"user_id"` + OrderConfiguration OrderConfiguration `json:"order_configuration"` + Side string `json:"side"` + ClientOID string `json:"client_order_id"` + Status string `json:"status"` + TimeInForce string `json:"time_in_force"` + CreatedTime time.Time `json:"created_time"` + CompletionPercentage float64 `json:"completion_percentage,string"` + FilledSize float64 `json:"filled_size,string"` + AverageFilledPrice float64 `json:"average_filled_price,string"` + Fee types.Number `json:"fee"` + NumberOfFills int64 `json:"num_fills,string"` + FilledValue float64 `json:"filled_value,string"` + PendingCancel bool `json:"pending_cancel"` + SizeInQuote bool `json:"size_in_quote"` + TotalFees float64 `json:"total_fees,string"` + SizeInclusiveOfFees bool `json:"size_inclusive_of_fees"` + TotalValueAfterFees float64 `json:"total_value_after_fees,string"` + TriggerStatus string `json:"trigger_status"` + OrderType string `json:"order_type"` + RejectReason string `json:"reject_reason"` + Settled bool `json:"settled"` + ProductType string `json:"product_type"` + RejectMessage string `json:"reject_message"` + CancelMessage string `json:"cancel_message"` + OrderPlacementSource string `json:"order_placement_source"` + OutstandingHoldAmount float64 `json:"outstanding_hold_amount,string"` + IsLiquidation bool `json:"is_liquidation"` + LastFillTime time.Time `json:"last_fill_time"` + EditHistory []EditHistory `json:"edit_history"` + Leverage types.Number `json:"leverage"` + MarginType string `json:"margin_type"` + RetailPortfolioID string `json:"retail_portfolio_id"` +} + +// Fills is a sub-struct used in the type FillResponse +type Fills struct { + EntryID string `json:"entry_id"` + TradeID string `json:"trade_id"` + OrderID string `json:"order_id"` + TradeTime time.Time `json:"trade_time"` + TradeType string `json:"trade_type"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Commission float64 `json:"commission,string"` + ProductID currency.Pair `json:"product_id"` + SequenceTimestamp time.Time `json:"sequence_timestamp"` + LiquidityIndicator string `json:"liquidity_indicator"` + SizeInQuote bool `json:"size_in_quote"` + UserID string `json:"user_id"` + Side string `json:"side"` +} + +// FillResponse contains fill information, returned by GetFills +type FillResponse struct { + Fills []Fills `json:"fills"` + Cursor string `json:"cursor"` +} + +// PreviewOrderResp contains information on the effects of placing an order, returned by PreviewOrder +type PreviewOrderResp struct { + OrderTotal float64 `json:"order_total,string"` + CommissionTotal float64 `json:"commission_total,string"` + Errs []string `json:"errs"` + Warning []string `json:"warning"` + QuoteSize float64 `json:"quote_size,string"` + BaseSize float64 `json:"base_size,string"` + BestBid float64 `json:"best_bid,string"` + BestAsk float64 `json:"best_ask,string"` + IsMax bool `json:"is_max"` + OrderMarginTotal float64 `json:"order_margin_total,string"` + Leverage float64 `json:"leverage,string"` + LongLeverage float64 `json:"long_leverage,string"` + ShortLeverage float64 `json:"short_leverage,string"` + Slippage float64 `json:"slippage,string"` +} + +// SimplePortfolioData is a sub-struct used in the type DetailedPortfolioResponse +type SimplePortfolioData struct { + Name string `json:"name"` + UUID string `json:"uuid"` + Type string `json:"type"` + Deleted bool `json:"deleted"` +} + +// MovePortfolioFundsResponse contains the UUIDs of the portfolios involved. Returned by MovePortfolioFunds +type MovePortfolioFundsResponse struct { + SourcePortfolioUUID string `json:"source_portfolio_uuid"` + TargetPortfolioUUID string `json:"target_portfolio_uuid"` +} + +// FundsData is used internally when preparing a request in MovePortfolioFunds +type FundsData struct { + Value string `json:"value"` + Currency string `json:"currency"` +} + +// NativeAndRaw is a sub-struct used in the type DetailedPortfolioResponse +type NativeAndRaw struct { + UserNativeCurrency ValueWithCurrency `json:"userNativeCurrency"` + RawCurrency ValueWithCurrency `json:"rawCurrency"` +} + +// PortfolioBalances is a sub-struct used in the type DetailedPortfolioResponse +type PortfolioBalances struct { + TotalBalance ValueWithCurrency `json:"total_balance"` + TotalFuturesBalance ValueWithCurrency `json:"total_futures_balance"` + TotalCashEquivalentBalance ValueWithCurrency `json:"total_cash_equivalent_balance"` + TotalCryptoBalance ValueWithCurrency `json:"total_crypto_balance"` + FuturesUnrealizedPNL ValueWithCurrency `json:"futures_unrealized_pnl"` + PerpUnrealizedPNL ValueWithCurrency `json:"perp_unrealized_pnl"` +} + +// SpotPositions is a sub-struct used in the type DetailedPortfolioResponse +type SpotPositions struct { + Asset string `json:"asset"` + AccountUUID string `json:"account_uuid"` + TotalBalanceFiat float64 `json:"total_balance_fiat"` + TotalBalanceCrypto float64 `json:"total_balance_crypto"` + AvailableToTreadeFiat float64 `json:"available_to_trade_fiat"` + Allocation float64 `json:"allocation"` + OneDayChange float64 `json:"one_day_change"` + CostBasis ValueWithCurrency `json:"cost_basis"` + AssetImgURL string `json:"asset_img_url"` + IsCash bool `json:"is_cash"` +} + +// PerpPositions is a sub-struct used in the type DetailedPortfolioResponse +type PerpPositions struct { + ProductID currency.Pair `json:"product_id"` + ProductUUID string `json:"product_uuid"` + Symbol string `json:"symbol"` + AssetImageURL string `json:"asset_image_url"` + VWAP NativeAndRaw `json:"vwap"` + PositionSide string `json:"position_side"` + NetSize float64 `json:"net_size,string"` + BuyOrderSize float64 `json:"buy_order_size,string"` + SellOrderSize float64 `json:"sell_order_size,string"` + IMContribution float64 `json:"im_contribution,string"` + UnrealizedPNL NativeAndRaw `json:"unrealized_pnl"` + MarkPrice NativeAndRaw `json:"mark_price"` + LiquidationPrice NativeAndRaw `json:"liquidation_price"` + Leverage float64 `json:"leverage,string"` + IMNotional NativeAndRaw `json:"im_notional"` + MMNotional NativeAndRaw `json:"mm_notional"` + PositionNotional NativeAndRaw `json:"position_notional"` + MarginType string `json:"margin_type"` + LiquidationBuffer float64 `json:"liquidation_buffer,string"` + LiquidationPercentage float64 `json:"liquidation_percentage,string"` +} + +// FuturesPositions is a sub-struct used in the type DetailedPortfolioResponse +type FuturesPositions []struct { + ProductID currency.Pair `json:"product_id"` + ContractSize float64 `json:"contract_size,string"` + Side string `json:"side"` + Amount float64 `json:"amount,string"` + AvgEntryPrice float64 `json:"avg_entry_price,string"` + CurrentPrice float64 `json:"current_price,string"` + UnrealizedPNL float64 `json:"unrealized_pnl,string"` + Expiry time.Time `json:"expiry"` + UnderlyingAsset string `json:"underlying_asset"` + AssetImgURL string `json:"asset_img_url"` + ProductName string `json:"product_name"` + Venue string `json:"venue"` + NotionalValue float64 `json:"notional_value,string"` +} + +// DetailedPortfolioResponse contains a great deal of information on a single portfolio. Returned by GetPortfolioByID +type DetailedPortfolioResponse struct { + Portfolio SimplePortfolioData `json:"portfolio"` + PortfolioBalances PortfolioBalances `json:"portfolio_balances"` + SpotPositions []SpotPositions `json:"spot_positions"` + PerpPositions []PerpPositions `json:"perp_positions"` + FuturesPositions []FuturesPositions `json:"futures_positions"` +} + +// FuturesBalanceSummary contains information on futures balances, returned by GetFuturesBalanceSummary +type FuturesBalanceSummary struct { + FuturesBuyingPower ValueWithCurrency `json:"futures_buying_power"` + TotalUSDBalance ValueWithCurrency `json:"total_usd_balance"` + CBIUSDBalance ValueWithCurrency `json:"cbi_usd_balance"` + CFMUSDBalance ValueWithCurrency `json:"cfm_usd_balance"` + TotalOpenOrdersHoldAmount ValueWithCurrency `json:"total_open_orders_hold_amount"` + UnrealizedPNL ValueWithCurrency `json:"unrealized_pnl"` + DailyRealizedPNL ValueWithCurrency `json:"daily_realized_pnl"` + InitialMargin ValueWithCurrency `json:"initial_margin"` + AvailableMargin ValueWithCurrency `json:"available_margin"` + LiquidationThreshold ValueWithCurrency `json:"liquidation_threshold"` + LiquidationBufferAmount ValueWithCurrency `json:"liquidation_buffer_amount"` + LiquidationBufferPercentage float64 `json:"liquidation_buffer_percentage,string"` +} + +// FuturesPosition contains information on a single futures position, returned by GetFuturesPositionByID +type FuturesPosition struct { + // This may belong in a struct of its own called "position", requiring a bit more abstraction, but for the moment I'll assume it doesn't + ProductID currency.Pair `json:"product_id"` + ExpirationTime time.Time `json:"expiration_time"` + Side string `json:"side"` + NumberOfContracts float64 `json:"number_of_contracts,string"` + CurrentPrice float64 `json:"current_price,string"` + AverageEntryPrice float64 `json:"avg_entry_price,string"` + UnrealizedPNL float64 `json:"unrealized_pnl,string"` + DailyRealizedPNL float64 `json:"daily_realized_pnl,string"` +} + +// SweepData contains information on pending and processing sweep requests, returned by ListFuturesSweeps +type SweepData struct { + ID string `json:"id"` + RequestedAmount ValueWithCurrency `json:"requested_amount"` + ShouldSweepAll bool `json:"should_sweep_all"` + Status string `json:"status"` + ScheduledTime time.Time `json:"scheduled_time"` +} + +// PerpetualsPortfolioSummary contains information on perpetuals portfolio balances, used as a sub-struct in the types PerpPositionDetail, AllPerpPosResponse, and OnePerpPosResponse +type PerpetualsPortfolioSummary struct { + PortfolioUUID string `json:"portfolio_uuid"` + Collateral float64 `json:"collateral,string"` + PositionNotional float64 `json:"position_notional,string"` + OpenPositionNotional float64 `json:"open_position_notional,string"` + PendingFees float64 `json:"pending_fees,string"` + Borrow float64 `json:"borrow,string"` + AccruedInterest float64 `json:"accrued_interest,string"` + RollingDebt float64 `json:"rolling_debt,string"` + PortfolioInitialMargin float64 `json:"portfolio_initial_margin,string"` + PortfolioIMNotional ValueWithCurrency `json:"portfolio_im_notional"` + PortfolioMaintenanceMargin float64 `json:"portfolio_maintenance_margin,string"` + PortfolioMMNotional ValueWithCurrency `json:"portfolio_mm_notional"` + LiquidationPercentage float64 `json:"liquidation_percentage,string"` + LiquidationBuffer float64 `json:"liquidation_buffer,string"` + MarginType string `json:"margin_type"` + MarginFlags string `json:"margin_flags"` + LiquidationStatus string `json:"liquidation_status"` + UnrealizedPNL ValueWithCurrency `json:"unrealized_pnl"` + BuyingPower ValueWithCurrency `json:"buying_power"` + TotalBalance ValueWithCurrency `json:"total_balance"` + MaxWithDrawal ValueWithCurrency `json:"max_withdrawal"` +} + +// PerpPositionDetail contains information on a single perpetuals position, used as a sub-struct in the types AllPerpPosResponse and OnePerpPosResponse +type PerpPositionDetail struct { + ProductID currency.Pair `json:"product_id"` + ProductUUID string `json:"product_uuid"` + Symbol string `json:"symbol"` + VWAP ValueWithCurrency `json:"vwap"` + PositionSide string `json:"position_side"` + NetSize float64 `json:"net_size,string"` + BuyOrderSize float64 `json:"buy_order_size,string"` + SellOrderSize float64 `json:"sell_order_size,string"` + IMContribution float64 `json:"im_contribution,string"` + UnrealizedPNL ValueWithCurrency `json:"unrealized_pnl"` + MarkPrice ValueWithCurrency `json:"mark_price"` + LiquidationPrice ValueWithCurrency `json:"liquidation_price"` + Leverage float64 `json:"leverage,string"` + IMNotional ValueWithCurrency `json:"im_notional"` + MMNotional ValueWithCurrency `json:"mm_notional"` + PositionNotional ValueWithCurrency `json:"position_notional"` + MarginType string `json:"margin_type"` + LiquidationBuffer float64 `json:"liquidation_buffer,string"` + LiquidationPercentage float64 `json:"liquidation_percentage,string"` + PortfolioSummary PerpetualsPortfolioSummary `json:"portfolio_summary"` +} + +// AllPerpPosResponse contains information on perpetuals positions, returned by GetAllPerpetualsPositions +type AllPerpPosResponse struct { + Positions []PerpPositionDetail `json:"positions"` + PortfolioSummary PerpetualsPortfolioSummary `json:"portfolio_summary"` +} + +// OnePerpPosResponse contains information on a single perpetuals position, returned by GetPerpetualsPositionByID +type OnePerpPosResponse struct { + Position PerpPositionDetail `json:"position"` + PortfolioSummary PerpetualsPortfolioSummary `json:"portfolio_summary"` +} + +// FeeTier is a sub-struct used in the type TransactionSummary +type FeeTier struct { + PricingTier string `json:"pricing_tier"` + USDFrom float64 `json:"usd_from,string"` + USDTo float64 `json:"usd_to,string"` + TakerFeeRate float64 `json:"taker_fee_rate,string"` + MakerFeeRate float64 `json:"maker_fee_rate,string"` + AOPFrom types.Number `json:"aop_from"` + AOPTo types.Number `json:"aop_to"` +} + +// MarginRate is a sub-struct used in the type TransactionSummary +type MarginRate struct { + Value float64 `json:"value,string"` +} + +// GoodsAndServicesTax is a sub-struct used in the type TransactionSummary +type GoodsAndServicesTax struct { + Rate float64 `json:"rate,string"` + Type string `json:"type"` +} + +// TransactionSummary contains a summary of transaction fees, volume, and the like. Returned by GetTransactionSummary +type TransactionSummary struct { + TotalVolume float64 `json:"total_volume"` + TotalFees float64 `json:"total_fees"` + FeeTier FeeTier `json:"fee_tier"` + MarginRate MarginRate `json:"margin_rate"` + GoodsAndServicesTax GoodsAndServicesTax `json:"goods_and_services_tax"` + AdvancedTradeOnlyVolume float64 `json:"advanced_trade_only_volume"` + AdvancedTradeOnlyFees float64 `json:"advanced_trade_only_fees"` + CoinbaseProVolume float64 `json:"coinbase_pro_volume"` + CoinbaseProFees float64 `json:"coinbase_pro_fees"` + TotalBalance types.Number `json:"total_balance"` + HasPromoFee bool `json:"has_promo_fee"` +} + +// GetAllOrdersResp contains information on a lot of orders, returned by GetAllOrders +type GetAllOrdersResp struct { + Orders []GetOrderResponse `json:"orders"` + Sequence int64 `json:"sequence,string"` + HasNext bool `json:"has_next"` + Cursor string `json:"cursor"` +} + +// LinkStruct is a sub-struct storing information on links, used in Disclosure and ConvertResponse +type LinkStruct struct { + Text string `json:"text"` + URL string `json:"url"` +} + +// Disclosure is a sub-struct used in FeeStruct +type Disclosure struct { + Title string `json:"title"` + Description string `json:"description"` + Link LinkStruct `json:"link"` +} + +// FeeStruct is a sub-struct storing information on fees, used in ConvertResponse +type FeeStruct struct { + Title string `json:"title"` + Description string `json:"description"` + Amount ValueWithCurrency `json:"amount"` + Label string `json:"label"` + Disclosure Disclosure `json:"disclosure"` +} + +// Owner is a sub-struct, used in LedgerAccount +type Owner struct { + ID string `json:"id"` + UUID string `json:"uuid"` + UserUUID string `json:"user_uuid"` + Type string `json:"type"` +} + +// LedgerAccount is a sub-struct, used in AccountStruct +type LedgerAccount struct { + AccountID string `json:"account_id"` + Currency string `json:"currency"` + Owner Owner `json:"owner"` +} + +// AccountStruct is a sub-struct storing information on accounts, used in ConvertResponse +type AccountStruct struct { + Type string `json:"type"` + Network string `json:"network"` + LedgerAccount LedgerAccount `json:"ledger_account"` +} + +// AmScale is a sub-struct storing information on amounts and scales, used in ConvertResponse +type AmScale struct { + Amount ValueWithCurrency `json:"amount"` + Scale int32 `json:"scale"` +} + +// UnitPrice is a sub-struct used in ConvertResponse +type UnitPrice struct { + TargetToFiat AmScale `json:"target_to_fiat"` + TargetToSource AmScale `json:"target_to_source"` + SourceToFiat AmScale `json:"source_to_fiat"` +} + +// Context is a sub-struct used in UserWarnings +type Context struct { + Details []string `json:"details"` + Title string `json:"title"` + LinkText string `json:"link_text"` +} + +// UserWarnings is a sub-struct used in ConvertResponse +type UserWarnings struct { + ID string `json:"id"` + Link LinkStruct `json:"link"` + Context Context `json:"context"` + Code string `json:"code"` + Message string `json:"message"` +} + +// CancellationReason is a sub-struct used in ConvertResponse +type CancellationReason struct { + Message string `json:"message"` + Code string `json:"code"` + ErrorCode string `json:"error_code"` + ErrorCTA string `json:"error_cta"` +} + +// TaxDetails is a sub-struct used in ConvertResponse +type TaxDetails struct { + Name string `json:"name"` + Amount ValueWithCurrency `json:"amount"` +} + +// TradeIncentiveInfo is a sub-struct used in ConvertResponse +type TradeIncentiveInfo struct { + AppliedIncentive bool `json:"applied_incentive"` + UserIncentiveID string `json:"user_incentive_id"` + CodeVal string `json:"code_val"` + EndsAt time.Time `json:"ends_at"` + FeeWithoutIncentive ValueWithCurrency `json:"fee_without_incentive"` + Redeemed bool `json:"redeemed"` +} + +// ConvertResponse contains information on a convert trade, returned by CreateConvertQuote, CommitConvertTrade, and GetConvertTradeByID +type ConvertResponse struct { + ID string `json:"id"` + Status string `json:"status"` + UserEnteredAmount ValueWithCurrency `json:"user_entered_amount"` + Amount ValueWithCurrency `json:"amount"` + Subtotal ValueWithCurrency `json:"subtotal"` + Total ValueWithCurrency `json:"total"` + Fees []FeeStruct `json:"fees"` + TotalFee FeeStruct `json:"total_fee"` + Source AccountStruct `json:"source"` + Target AccountStruct `json:"target"` + UnitPrice UnitPrice `json:"unit_price"` + UserWarnings []UserWarnings `json:"user_warnings"` + UserReference string `json:"user_reference"` + SourceCurrency string `json:"source_currency"` + TargetCurrency string `json:"target_currency"` + CancellationReason CancellationReason `json:"cancellation_reason"` + SourceID string `json:"source_id"` + TargetID string `json:"target_id"` + ExchangeRate ValueWithCurrency `json:"exchange_rate"` + TaxDetails []TaxDetails `json:"tax_details"` + TradeIncentiveInfo TradeIncentiveInfo `json:"trade_incentive_info"` + TotalFeeWithoutTax FeeStruct `json:"total_fee_without_tax"` + FiatDenotedTotal ValueWithCurrency `json:"fiat_denoted_total"` +} + +// ServerTimeV3 holds information on the server's time, returned by GetV3Time +type ServerTimeV3 struct { + Iso time.Time `json:"iso"` + EpochSeconds types.Time `json:"epochSeconds"` + EpochMilliseconds types.Time `json:"epochMillis"` +} + +// PaymentMethodData is a sub-type that holds information on a payment method +type PaymentMethodData struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Currency string `json:"currency"` + Verified bool `json:"verified"` + AllowBuy bool `json:"allow_buy"` + AllowSell bool `json:"allow_sell"` + AllowDeposit bool `json:"allow_deposit"` + AllowWithdraw bool `json:"allow_withdraw"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// IDResource holds an ID, resource type, and associated data, used in ListNotificationsResponse, TransactionData, DeposWithdrData, and PaymentMethodData +type IDResource struct { + ID string `json:"id"` + Resource string `json:"resource"` + ResourcePath string `json:"resource_path"` + Email string `json:"email"` +} + +// PaginationResp holds pagination information, used in ListNotificationsResponse, GetAllWalletsResponse, GetAllAddrResponse, ManyTransactionsResp, ManyDeposWithdrResp, and GetAllPaymentMethodsResp +type PaginationResp struct { + EndingBefore string `json:"ending_before"` + StartingAfter string `json:"starting_after"` + PreviousEndingBefore string `json:"previous_ending_before"` + NextStartingAfter string `json:"next_starting_after"` + Limit uint8 `json:"limit"` + Order string `json:"order"` + PreviousURI string `json:"previous_uri"` + NextURI string `json:"next_uri"` +} + +// PaginationInp holds information needed to engage in pagination with Sign in With Coinbase. Used in ListNotifications, GetAllWallets, GetAllAddresses, GetAddressTransactions, GetAllTransactions, GetAllFiatTransfers, GetAllPaymentMethods, and preparePagination +type PaginationInp struct { + Limit uint8 + OrderAscend bool + StartingAfter string + EndingBefore string +} + +// AmountWithCurrency is a sub-struct used in ListNotificationsSubData, WalletData, TransactionData, DeposWithdrData, and PaymentMethodData +type AmountWithCurrency struct { + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` +} + +// Fees is a sub-struct used in ListNotificationsSubData +type Fees []struct { + Type string `json:"type"` + Amount AmountWithCurrency `json:"amount"` +} + +// ListNotificationsSubData is a sub-struct used in ListNotificationsData +type ListNotificationsSubData struct { + ID string `json:"id"` + Address string `json:"address"` + Name string `json:"name"` + Status string `json:"status"` + PaymentMethod IDResource `json:"payment_method"` + Transaction IDResource `json:"transaction"` + Amount AmountWithCurrency `json:"amount"` + Total AmountWithCurrency `json:"total"` + Subtotal AmountWithCurrency `json:"subtotal"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Resource string `json:"resource"` + ResourcePath string `json:"resource_path"` + Committed bool `json:"committed"` + Instant bool `json:"instant"` + Fee AmountWithCurrency `json:"fee"` + Fees []Fees `json:"fees"` + PayoutAt time.Time `json:"payout_at"` +} + +// AdditionalData is a sub-struct used in ListNotificationsData +type AdditionalData struct { + Hash string `json:"hash"` + Amount AmountWithCurrency `json:"amount"` +} + +// ListNotificationsData is a sub-struct used in ListNotificationsResponse +type ListNotificationsData struct { + ID string `json:"id"` + Type string `json:"type"` + Data ListNotificationsSubData `json:"data"` + AdditionalData AdditionalData `json:"additional_data"` + User IDResource `json:"user"` + Account IDResource `json:"account"` + DeliveryAttempts int32 `json:"delivery_attempts"` + CreatedAt time.Time `json:"created_at"` + Resource string `json:"resource"` + ResourcePath string `json:"resource_path"` + Transaction IDResource `json:"transaction"` } -// Stats holds last 24 hr data for coinbasepro -type Stats struct { - Open float64 `json:"open,string"` - High float64 `json:"high,string"` - Low float64 `json:"low,string"` - Volume float64 `json:"volume,string"` - Last float64 `json:"last,string"` - Volume30Day float64 `json:"volume_30day,string"` +// ListNotificationsResponse holds information on notifications that the user is subscribed to. Returned by ListNotifications +type ListNotificationsResponse struct { + Pagination PaginationResp `json:"pagination"` + Data []ListNotificationsData `json:"data"` } -// Currency holds singular currency product information +// CodeName is a sub-struct holding a code and a name, used in UserResponse +type CodeName struct { + Code string `json:"code"` + Name string `json:"name"` +} + +// Country is a sub-struct, used in UserResponse +type Country struct { + Code string `json:"code"` + Name string `json:"name"` + IsInEurope bool `json:"is_in_europe"` +} + +// Tiers is a sub-struct, used in UserResponse +type Tiers struct { + CompletedDescription string `json:"completed_description"` + UpgradeButtonText string `json:"upgrade_button_text"` + Header string `json:"header"` + Body string `json:"body"` +} + +// ReferralMoney is a sub-struct, used in UserResponse +type ReferralMoney struct { + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + CurrencySymbol string `json:"currency_symbol"` + ReferralThreshold float64 `json:"referral_threshold,string"` +} + +// UserResponse holds information on a user, returned by GetCurrentUser +type UserResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + ProfileLocation string `json:"profile_location"` + ProfileBio string `json:"profile_bio"` + ProfileURL string `json:"profile_url"` + AvatarURL string `json:"avatar_url"` + Resource string `json:"resource"` + ResourcePath string `json:"resource_path"` + LegacyID string `json:"legacy_id"` + TimeZone string `json:"time_zone"` + NativeCurrency string `json:"native_currency"` + BitcoinUnit string `json:"bitcoin_unit"` + State string `json:"state"` + Country Country `json:"country"` + Nationality CodeName `json:"nationality"` + RegionSupportsFiatTransfers bool `json:"region_supports_fiat_transfers"` + RegionSupportsCryptoToCryptoTransfers bool `json:"region_supports_crypto_to_crypto_transfers"` + CreatedAt time.Time `json:"created_at"` + SupportsRewards bool `json:"supports_rewards"` + Tiers Tiers `json:"tiers"` + ReferralMoney ReferralMoney `json:"referral_money"` + HasBlockingBuyRestrictions bool `json:"has_blocking_buy_restrictions"` + HasMadeAPurchase bool `json:"has_made_a_purchase"` + HasBuyDepositPaymentMethods bool `json:"has_buy_deposit_payment_methods"` + HasUnverifiedBuyDepositPaymentMethods bool `json:"has_unverified_buy_deposit_payment_methods"` + NeedsKYCRemediation bool `json:"needs_kyc_remediation"` + ShowInstantAchUx bool `json:"show_instant_ach_ux"` + UserType string `json:"user_type"` + Email string `json:"email"` + SendsDisabled bool `json:"sends_disabled"` +} + +// Currency is a sub-struct holding information on a currency, used in WalletData type Currency struct { - ID string - Name string + Code string `json:"code"` + Name string `json:"name"` + Color string `json:"color"` + SortIndex int32 `json:"sort_index"` + Exponent int32 `json:"exponent"` + Type string `json:"type"` + AddressRegex string `json:"address_regex"` + AssetID string `json:"asset_id"` + DestinationTagName string `json:"destination_tag_name"` + DestinationTagRegex string `json:"destination_tag_regex"` + Slug string `json:"slug"` + Rewards any `json:"rewards"` +} + +// WalletData is a sub-struct holding wallet information, used in GetAllWalletsResponse +type WalletData struct { + ID string `json:"id"` + Name string `json:"name"` + Primary bool `json:"primary"` + Type string `json:"type"` + Currency Currency `json:"currency"` + Balance AmountWithCurrency `json:"balance"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Resource string `json:"resource"` + ResourcePath string `json:"resource_path"` + AllowDeposits bool `json:"allow_deposits"` + AllowWithdrawals bool `json:"allow_withdrawals"` +} + +// GetAllWalletsResponse holds information on many wallets, returned by GetAllWallets +type GetAllWalletsResponse struct { + Pagination *PaginationResp `json:"pagination"` + Data []WalletData `json:"data"` +} + +// AddressInfo holds an address and a destination tag, used in AddressData +type AddressInfo struct { + Address string `json:"address"` + DestinationTag string `json:"destination_tag"` +} + +// TitleSubtitle holds a title and a subtitle, used in AddressData and TransactionData +type TitleSubtitle struct { + Title string `json:"title"` + Subtitle string `json:"subtitle"` +} + +// Options is a sub-struct used in Warnings +type Options struct { + Text string `json:"text"` + Style string `json:"style"` + ID string `json:"id"` +} + +// Warnings is a sub-struct used in AddressData +type Warnings struct { + Type string `json:"type"` + Title string `json:"title"` + Details string `json:"details"` + ImageURL string `json:"image_url"` + Options []Options `json:"options"` +} + +// ShareAddressCopy is a sub-struct used in AddressData +type ShareAddressCopy struct { + Line1 string `json:"line1"` + Line2 string `json:"line2"` +} + +// InlineWarning is a sub-struct used in AddressData +type InlineWarning struct { + Text string `json:"text"` + Tooltip TitleSubtitle `json:"tooltip"` +} + +// AddressData holds address information, used in GetAllAddrResponse +type AddressData struct { + ID string `json:"id"` + Address string `json:"address"` + AddressInfo AddressInfo `json:"address_info"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Network string `json:"network"` + URIScheme string `json:"uri_scheme"` + Resource string `json:"resource"` + ResourcePath string `json:"resource_path"` + Warnings []Warnings `json:"warnings"` + QRCodeImageURL string `json:"qr_code_image_url"` + AddressLabel string `json:"address_label"` + DefaultReceive bool `json:"default_receive"` + DestinationTag string `json:"destination_tag"` + DepositURI string `json:"deposit_uri"` + CallbackURL string `json:"callback_url"` + ShareAddressCopy ShareAddressCopy `json:"share_address_copy"` + ReceiveSubtitle string `json:"receive_subtitle"` + InlineWarning InlineWarning `json:"inline_warning"` +} + +// GetAllAddrResponse holds information on many addresses, returned by GetAllAddresses +type GetAllAddrResponse struct { + Pagination PaginationResp `json:"pagination"` + Data []AddressData `json:"data"` +} + +// AdvancedTradeFill is a sub-struct used in TransactionData +type AdvancedTradeFill struct { + FillPrice float64 `json:"fill_price,string"` + ProductID currency.Pair `json:"product_id"` + OrderID string `json:"order_id"` + Commission float64 `json:"commission,string"` + OrderSide string `json:"order_side"` +} + +// Network is a sub-struct used in TransactionData +type Network struct { + Status string `json:"status"` + Hash string `json:"hash"` + Name string `json:"name"` +} + +// TransactionData is a sub-type that holds information on a transaction. Used in ManyTransactionsResp +type TransactionData struct { + ID string `json:"id"` + Type string `json:"type"` + Status string `json:"status"` + Amount AmountWithCurrency `json:"amount"` + NativeAmount AmountWithCurrency `json:"native_amount"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Resource string `json:"resource"` + ResourcePath string `json:"resource_path"` + Details TitleSubtitle `json:"details"` + Network Network `json:"network"` + To IDResource `json:"to"` + From IDResource `json:"from"` +} + +// ManyTransactionsResp holds information on many transactions. Returned by GetAddressTransactions and GetAllTransactions +type ManyTransactionsResp struct { + Pagination PaginationResp `json:"pagination"` + Data []TransactionData `json:"data"` +} + +// DeposWithdrData is a sub-type that holds information on a deposit/withdrawal. Used in ManyDeposWithdrResp +type DeposWithdrData struct { + ID string `json:"id"` + Status string `json:"status"` + PaymentMethod IDResource `json:"payment_method"` + Transaction IDResource `json:"transaction"` + Amount AmountWithCurrency `json:"amount"` + Subtotal AmountWithCurrency `json:"subtotal"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Resource string `json:"resource"` + ResourcePath string `json:"resource_path"` + Committed bool `json:"committed"` + Fee AmountWithCurrency `json:"fee"` + PayoutAt time.Time `json:"payout_at"` + TransferType FiatTransferType `json:"transfer_type"` +} + +// ManyDeposWithdrResp holds information on many deposits. Returned by GetAllFiatTransfers +type ManyDeposWithdrResp struct { + Pagination PaginationResp `json:"pagination"` + Data []DeposWithdrData `json:"data"` +} + +// FiatData holds information on fiat currencies. Returned by GetFiatCurrencies +type FiatData struct { + ID string `json:"id"` + Name string `json:"name"` MinSize float64 `json:"min_size,string"` } -// ServerTime holds current requested server time information -type ServerTime struct { +// CryptoData holds information on cryptocurrencies. Returned by GetCryptocurrencies +type CryptoData struct { + Code string `json:"code"` + Name string `json:"name"` + Color string `json:"color"` + SortIndex uint16 `json:"sort_index"` + Exponent uint8 `json:"exponent"` + Type string `json:"type"` + AddressRegex string `json:"address_regex"` + AssetID string `json:"asset_id"` +} + +// GetExchangeRatesResp holds information on exchange rates. Returned by GetExchangeRates +type GetExchangeRatesResp struct { + Currency string `json:"currency"` + Rates map[string]types.Number `json:"rates"` +} + +// GetPriceResp holds information on a price. Returned by GetPrice +type GetPriceResp struct { + Amount float64 `json:"amount,string"` + Base string `json:"base"` + Currency string `json:"currency"` +} + +// ServerTimeV2 holds current requested server time information, returned by GetV2Time +type ServerTimeV2 struct { ISO time.Time `json:"iso"` - Epoch float64 `json:"epoch"` -} - -// AccountResponse holds the details for the trading accounts -type AccountResponse struct { - ID string `json:"id"` - Currency string `json:"currency"` - Balance float64 `json:"balance,string"` - Available float64 `json:"available,string"` - Hold float64 `json:"hold,string"` - ProfileID string `json:"profile_id"` - MarginEnabled bool `json:"margin_enabled"` - FundedAmount float64 `json:"funded_amount,string"` - DefaultAmount float64 `json:"default_amount,string"` -} - -// AccountLedgerResponse holds account history information -type AccountLedgerResponse struct { - ID string `json:"id"` - CreatedAt time.Time `json:"created_at"` - Amount float64 `json:"amount,string"` - Balance float64 `json:"balance,string"` - Type string `json:"type"` - Details interface{} `json:"details"` -} - -// AccountHolds contains the hold information about an account -type AccountHolds struct { - ID string `json:"id"` - AccountID string `json:"account_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt string `json:"updated_at"` - Amount float64 `json:"amount,string"` - Type string `json:"type"` - Reference string `json:"ref"` -} - -// GeneralizedOrderResponse is the generalized return type across order -// placement and information collation -type GeneralizedOrderResponse struct { - ID string `json:"id"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - ProductID string `json:"product_id"` - Side string `json:"side"` - Stp string `json:"stp"` - Type string `json:"type"` - TimeInForce string `json:"time_in_force"` - PostOnly bool `json:"post_only"` - CreatedAt time.Time `json:"created_at"` - FillFees float64 `json:"fill_fees,string"` - FilledSize float64 `json:"filled_size,string"` - ExecutedValue float64 `json:"executed_value,string"` - Status string `json:"status"` - Settled bool `json:"settled"` - Funds float64 `json:"funds,string"` - SpecifiedFunds float64 `json:"specified_funds,string"` - DoneReason string `json:"done_reason"` - DoneAt time.Time `json:"done_at"` -} - -// Funding holds funding data -type Funding struct { - ID string `json:"id"` - OrderID string `json:"order_id"` - ProfileID string `json:"profile_id"` - Amount float64 `json:"amount,string"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - Currency string `json:"currency"` - RepaidAmount float64 `json:"repaid_amount"` - DefaultAmount float64 `json:"default_amount,string"` - RepaidDefault bool `json:"repaid_default"` -} - -// MarginTransfer holds margin transfer details -type MarginTransfer struct { - CreatedAt time.Time `json:"created_at"` - ID string `json:"id"` - UserID string `json:"user_id"` - ProfileID string `json:"profile_id"` - MarginProfileID string `json:"margin_profile_id"` - Type string `json:"type"` - Amount float64 `json:"amount,string"` - Currency string `json:"currency"` - AccountID string `json:"account_id"` - MarginAccountID string `json:"margin_account_id"` - MarginProductID string `json:"margin_product_id"` - Status string `json:"status"` - Nonce int `json:"nonce"` -} - -// AccountOverview holds account information returned from position -type AccountOverview struct { - Status string `json:"status"` - Funding struct { - MaxFundingValue float64 `json:"max_funding_value,string"` - FundingValue float64 `json:"funding_value,string"` - OldestOutstanding struct { - ID string `json:"id"` - OrderID string `json:"order_id"` - CreatedAt time.Time `json:"created_at"` - Currency string `json:"currency"` - AccountID string `json:"account_id"` - Amount float64 `json:"amount,string"` - } `json:"oldest_outstanding"` - } `json:"funding"` - Accounts struct { - LTC Account `json:"LTC"` - ETH Account `json:"ETH"` - USD Account `json:"USD"` - BTC Account `json:"BTC"` - } `json:"accounts"` - MarginCall struct { - Active bool `json:"active"` - Price float64 `json:"price,string"` - Side string `json:"side"` - Size float64 `json:"size,string"` - Funds float64 `json:"funds,string"` - } `json:"margin_call"` - UserID string `json:"user_id"` - ProfileID string `json:"profile_id"` - Position struct { - Type string `json:"type"` - Size float64 `json:"size,string"` - Complement float64 `json:"complement,string"` - MaxSize float64 `json:"max_size,string"` - } `json:"position"` - ProductID string `json:"product_id"` -} - -// Account is a sub-type for account overview -type Account struct { - ID string `json:"id"` - Balance float64 `json:"balance,string"` - Hold float64 `json:"hold,string"` - FundedAmount float64 `json:"funded_amount,string"` - DefaultAmount float64 `json:"default_amount,string"` -} - -// PaymentMethod holds payment method information -type PaymentMethod struct { - ID string `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Currency string `json:"currency"` - PrimaryBuy bool `json:"primary_buy"` - PrimarySell bool `json:"primary_sell"` - AllowBuy bool `json:"allow_buy"` - AllowSell bool `json:"allow_sell"` - AllowDeposits bool `json:"allow_deposits"` - AllowWithdraw bool `json:"allow_withdraw"` - Limits struct { - Buy []LimitInfo `json:"buy"` - InstantBuy []LimitInfo `json:"instant_buy"` - Sell []LimitInfo `json:"sell"` - Deposit []LimitInfo `json:"deposit"` - } `json:"limits"` -} - -// LimitInfo is a sub-type for payment method -type LimitInfo struct { - PeriodInDays int `json:"period_in_days"` - Total struct { - Amount float64 `json:"amount,string"` - Currency string `json:"currency"` - } `json:"total"` -} - -// DepositWithdrawalInfo holds returned deposit information -type DepositWithdrawalInfo struct { - ID string `json:"id"` - Amount float64 `json:"amount,string"` - Currency string `json:"currency"` - PayoutAt time.Time `json:"payout_at"` -} - -// CoinbaseAccounts holds coinbase account information -type CoinbaseAccounts struct { - ID string `json:"id"` - Name string `json:"name"` - Balance float64 `json:"balance,string"` - Currency string `json:"currency"` - Type string `json:"type"` - Primary bool `json:"primary"` - Active bool `json:"active"` - WireDepositInformation struct { - AccountNumber string `json:"account_number"` - RoutingNumber string `json:"routing_number"` - BankName string `json:"bank_name"` - BankAddress string `json:"bank_address"` - BankCountry struct { - Code string `json:"code"` - Name string `json:"name"` - } `json:"bank_country"` - AccountName string `json:"account_name"` - AccountAddress string `json:"account_address"` - Reference string `json:"reference"` - } `json:"wire_deposit_information"` - SepaDepositInformation struct { - Iban string `json:"iban"` - Swift string `json:"swift"` - BankName string `json:"bank_name"` - BankAddress string `json:"bank_address"` - BankCountryName string `json:"bank_country_name"` - AccountName string `json:"account_name"` - AccountAddress string `json:"account_address"` - Reference string `json:"reference"` - } `json:"sep_deposit_information"` -} - -// Report holds historical information -type Report struct { - ID string `json:"id"` - Type string `json:"type"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - CompletedAt time.Time `json:"completed_at"` - ExpiresAt time.Time `json:"expires_at"` - FileURL string `json:"file_url"` - Params struct { - StartDate time.Time `json:"start_date"` - EndDate time.Time `json:"end_date"` - } `json:"params"` -} - -// Volume type contains trailing volume information -type Volume struct { - ProductID string `json:"product_id"` - ExchangeVolume float64 `json:"exchange_volume,string"` - Volume float64 `json:"volume,string"` - RecordedAt string `json:"recorded_at"` -} - -// OrderL1L2 is a type used in layer conversion -type OrderL1L2 struct { - Price float64 - Amount float64 - NumOrders float64 -} - -// OrderL3 is a type used in layer conversion -type OrderL3 struct { - Price float64 - Amount float64 - OrderID string -} - -// OrderbookL1L2 holds level 1 and 2 order book information -type OrderbookL1L2 struct { - Sequence int64 `json:"sequence"` - Bids []OrderL1L2 `json:"bids"` - Asks []OrderL1L2 `json:"asks"` -} - -// OrderbookL3 holds level 3 order book information -type OrderbookL3 struct { - Sequence int64 `json:"sequence"` - Bids []OrderL3 `json:"bids"` - Asks []OrderL3 `json:"asks"` -} - -// OrderbookResponse is a generalized response for order books -type OrderbookResponse struct { - Sequence int64 `json:"sequence"` - Bids [][3]interface{} `json:"bids"` - Asks [][3]interface{} `json:"asks"` -} - -// FillResponse contains fill information from the exchange -type FillResponse struct { - TradeID int64 `json:"trade_id"` - ProductID string `json:"product_id"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - OrderID string `json:"order_id"` - CreatedAt time.Time `json:"created_at"` - Liquidity string `json:"liquidity"` - Fee float64 `json:"fee,string"` - Settled bool `json:"settled"` - Side string `json:"side"` -} - -// WebsocketSubscribe takes in subscription information -type WebsocketSubscribe struct { - Type string `json:"type"` - ProductIDs []string `json:"product_ids,omitempty"` - Channels []any `json:"channels,omitempty"` - Signature string `json:"signature,omitempty"` - Key string `json:"key,omitempty"` - Passphrase string `json:"passphrase,omitempty"` - Timestamp string `json:"timestamp,omitempty"` -} - -// WsChannel defines a websocket subscription channel -type WsChannel struct { - Name string `json:"name"` - ProductIDs []string `json:"product_ids,omitempty"` -} - -// wsOrderReceived holds websocket received values -type wsOrderReceived struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - OrderType string `json:"order_type"` - Size float64 `json:"size,string"` - Price float64 `json:"price,omitempty,string"` - Funds float64 `json:"funds,omitempty,string"` - Side string `json:"side"` - ClientOID string `json:"client_oid"` - ProductID string `json:"product_id"` - Sequence int64 `json:"sequence"` - Time time.Time `json:"time"` - RemainingSize float64 `json:"remaining_size,string"` - NewSize float64 `json:"new_size,string"` - OldSize float64 `json:"old_size,string"` - Reason string `json:"reason"` - Timestamp float64 `json:"timestamp,string"` - UserID string `json:"user_id"` - ProfileID string `json:"profile_id"` - StopType string `json:"stop_type"` - StopPrice float64 `json:"stop_price,string"` - TakerFeeRate float64 `json:"taker_fee_rate,string"` - Private bool `json:"private"` - TradeID int64 `json:"trade_id"` - MakerOrderID string `json:"maker_order_id"` - TakerOrderID string `json:"taker_order_id"` - TakerUserID string `json:"taker_user_id"` -} - -// WebsocketHeartBeat defines JSON response for a heart beat message -type WebsocketHeartBeat struct { - Type string `json:"type"` - Sequence int64 `json:"sequence"` - LastTradeID int64 `json:"last_trade_id"` - ProductID string `json:"product_id"` - Time string `json:"time"` -} - -// WebsocketTicker defines ticker websocket response + Epoch uint64 `json:"epoch"` +} + +// WebsocketRequest is an aspect of constructing a request to the websocket server, used in sendRequest +type WebsocketRequest struct { + Type string `json:"type"` + ProductIDs []currency.Pair `json:"product_ids,omitempty"` + Channel string `json:"channel,omitempty"` + Signature string `json:"signature,omitempty"` + Key string `json:"api_key,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + JWT string `json:"jwt,omitempty"` +} + +// WebsocketTicker defines a ticker websocket response, used in WebsocketTickerHolder type WebsocketTicker struct { - Type string `json:"type"` - Sequence int64 `json:"sequence"` + Type string `json:"type"` + ProductID currency.Pair `json:"product_id"` + Price float64 `json:"price,string"` + Volume24H float64 `json:"volume_24_h,string"` + Low24H float64 `json:"low_24_h,string"` + High24H float64 `json:"high_24_h,string"` + Low52W float64 `json:"low_52_w,string"` + High52W float64 `json:"high_52_w,string"` + PricePercentageChange24H float64 `json:"price_percent_chg_24_h,string"` +} + +// WebsocketTickerHolder holds a variety of ticker responses, used when wsHandleData processes tickers +type WebsocketTickerHolder struct { + Type string `json:"type"` + Tickers []WebsocketTicker `json:"tickers"` +} + +// WebsocketCandle defines a candle websocket response, used in WebsocketCandleHolder +type WebsocketCandle struct { + Start types.Time `json:"start"` + Low float64 `json:"low,string"` + High float64 `json:"high,string"` + Open float64 `json:"open,string"` + Close float64 `json:"close,string"` + Volume float64 `json:"volume,string"` + ProductID currency.Pair `json:"product_id"` +} + +// WebsocketCandleHolder holds a variety of candle responses, used when wsHandleData processes candles +type WebsocketCandleHolder struct { + Type string `json:"type"` + Candles []WebsocketCandle `json:"candles"` +} + +// WebsocketMarketTrade defines a market trade websocket response, used in WebsocketMarketTradeHolder +type WebsocketMarketTrade struct { + TradeID string `json:"trade_id"` ProductID currency.Pair `json:"product_id"` Price float64 `json:"price,string"` - Open24H float64 `json:"open_24h,string"` - Volume24H float64 `json:"volume_24h,string"` - Low24H float64 `json:"low_24h,string"` - High24H float64 `json:"high_24h,string"` - Volume30D float64 `json:"volume_30d,string"` - BestBid float64 `json:"best_bid,string"` - BestAsk float64 `json:"best_ask,string"` - Side string `json:"side"` + Size float64 `json:"size,string"` + Side order.Side `json:"side"` Time time.Time `json:"time"` - TradeID int64 `json:"trade_id"` - LastSize float64 `json:"last_size,string"` -} - -// WebsocketOrderbookSnapshot defines a snapshot response -type WebsocketOrderbookSnapshot struct { - ProductID string `json:"product_id"` - Type string `json:"type"` - Bids [][2]string `json:"bids"` - Asks [][2]string `json:"asks"` - Time time.Time `json:"time"` -} - -// WebsocketL2Update defines an update on the L2 orderbooks -type WebsocketL2Update struct { - Type string `json:"type"` - ProductID string `json:"product_id"` - Time time.Time `json:"time"` - Changes [][3]string `json:"changes"` -} - -type wsMsgType struct { - Type string `json:"type"` - Sequence int64 `json:"sequence"` - ProductID string `json:"product_id"` -} - -type wsStatus struct { - Currencies []struct { - ConvertibleTo []string `json:"convertible_to"` - Details struct{} `json:"details"` - ID string `json:"id"` - MaxPrecision float64 `json:"max_precision,string"` - MinSize float64 `json:"min_size,string"` - Name string `json:"name"` - Status string `json:"status"` - StatusMessage interface{} `json:"status_message"` - } `json:"currencies"` - Products []struct { - BaseCurrency string `json:"base_currency"` - BaseIncrement float64 `json:"base_increment,string"` - BaseMaxSize float64 `json:"base_max_size,string"` - BaseMinSize float64 `json:"base_min_size,string"` - CancelOnly bool `json:"cancel_only"` - DisplayName string `json:"display_name"` - ID string `json:"id"` - LimitOnly bool `json:"limit_only"` - MaxMarketFunds float64 `json:"max_market_funds,string"` - MinMarketFunds float64 `json:"min_market_funds,string"` - PostOnly bool `json:"post_only"` - QuoteCurrency string `json:"quote_currency"` - QuoteIncrement float64 `json:"quote_increment,string"` - Status string `json:"status"` - StatusMessage interface{} `json:"status_message"` - } `json:"products"` - Type string `json:"type"` -} - -// RequestParamsTimeForceType Time in force -type RequestParamsTimeForceType string - -var ( - // CoinbaseRequestParamsTimeGTC GTC - CoinbaseRequestParamsTimeGTC = RequestParamsTimeForceType("GTC") - - // CoinbaseRequestParamsTimeIOC IOC - CoinbaseRequestParamsTimeIOC = RequestParamsTimeForceType("IOC") -) +} + +// WebsocketMarketTradeHolder holds a variety of market trade responses, used when wsHandleData processes trades +type WebsocketMarketTradeHolder struct { + Type string `json:"type"` + Trades []WebsocketMarketTrade `json:"trades"` +} + +// WebsocketProduct defines a product websocket response, used in WebsocketProductHolder +type WebsocketProduct struct { + ProductType string `json:"product_type"` + ID currency.Pair `json:"id"` + BaseCurrency string `json:"base_currency"` + QuoteCurrency string `json:"quote_currency"` + BaseIncrement float64 `json:"base_increment,string"` + QuoteIncrement float64 `json:"quote_increment,string"` + DisplayName string `json:"display_name"` + Status string `json:"status"` + StatusMessage string `json:"status_message"` + MinMarketFunds float64 `json:"min_market_funds,string"` +} + +// WebsocketProductHolder holds a variety of product responses, used when wsHandleData processes an update on a product's status +type WebsocketProductHolder struct { + Type string `json:"type"` + Products []WebsocketProduct `json:"products"` +} + +// WebsocketOrderbookData defines a websocket orderbook response, used in WebsocketOrderbookDataHolder +type WebsocketOrderbookData struct { + Side string `json:"side"` + EventTime time.Time `json:"event_time"` + PriceLevel float64 `json:"price_level,string"` + NewQuantity float64 `json:"new_quantity,string"` +} + +// WebsocketOrderbookDataHolder holds a variety of orderbook responses, used when wsHandleData processes orderbooks, as well as under typical operation of ProcessSnapshot, ProcessUpdate, and processBidAskArray +type WebsocketOrderbookDataHolder struct { + Type string `json:"type"` + ProductID currency.Pair `json:"product_id"` + Changes []WebsocketOrderbookData `json:"updates"` +} + +// WebsocketOrderData defines a websocket order response, used in WebsocketOrderDataHolder +type WebsocketOrderData struct { + AveragePrice float64 `json:"avg_price,string"` + CancelReason string `json:"cancel_reason"` + ClientOrderID string `json:"client_order_id"` + CompletionPercentage float64 `json:"completion_percentage,string"` + ContractExpiryType string `json:"contract_expiry_type"` + CumulativeQuantity float64 `json:"cumulative_quantity,string"` + FilledValue float64 `json:"filled_value,string"` + LeavesQuantity float64 `json:"leaves_quantity,string"` + LimitPrice float64 `json:"limit_price,string"` + NumberOfFills int64 `json:"number_of_fills"` + OrderID string `json:"order_id"` + OrderSide string `json:"order_side"` + OrderType string `json:"order_type"` + OutstandingHoldAmount float64 `json:"outstanding_hold_amount,string"` + PostOnly bool `json:"post_only"` + ProductID currency.Pair `json:"product_id"` + ProductType string `json:"product_type"` + RejectReason string `json:"reject_reason"` + RetailPortfolioID string `json:"retail_portfolio_id"` + RiskManagedBy string `json:"risk_managed_by"` + Status string `json:"status"` + StopPrice float64 `json:"stop_price,string"` + TimeInForce string `json:"time_in_force"` + TotalFees float64 `json:"total_fees,string"` + TotalValueAfterFees float64 `json:"total_value_after_fees,string"` + TriggerStatus string `json:"trigger_status"` + CreationTime time.Time `json:"creation_time"` + EndTime time.Time `json:"end_time"` + StartTime time.Time `json:"start_time"` +} + +// WebsocketPerpData defines a websocket perpetual position response, used in WebsocketPositionStruct +type WebsocketPerpData struct { + ProductID currency.Pair `json:"product_id"` + PortfolioUUID string `json:"portfolio_uuid"` + VWAP float64 `json:"vwap,string"` + EntryVWAP float64 `json:"entry_vwap,string"` + PositionSide string `json:"position_side"` + MarginType string `json:"margin_type"` + NetSize float64 `json:"net_size,string"` + BuyOrderSize float64 `json:"buy_order_size,string"` + SellOrderSize float64 `json:"sell_order_size,string"` + Leverage float64 `json:"leverage,string"` + MarkPrice float64 `json:"mark_price,string"` + LiquidationPrice float64 `json:"liquidation_price,string"` + IMNotional float64 `json:"im_notional,string"` + MMNotional float64 `json:"mm_notional,string"` + PositionNotional float64 `json:"position_notional,string"` + UnrealizedPNL float64 `json:"unrealized_pnl,string"` + AggregatedPNL float64 `json:"aggregated_pnl,string"` +} + +// WebsocketExpData defines a websocket expiring position response, used in WebsocketPositionStruct +type WebsocketExpData struct { + ProductID currency.Pair `json:"product_id"` + Side string `json:"side"` + NumberOfContracts float64 `json:"number_of_contracts,string"` + RealizedPNL float64 `json:"realized_pnl,string"` + UnrealizedPNL float64 `json:"unrealized_pnl,string"` + EntryPrice float64 `json:"entry_price,string"` +} + +// WebsocketPositionStruct holds position data, used in WebsocketOrderDataHolder +type WebsocketPositionStruct struct { + PerpetualFuturesPositions []WebsocketPerpData `json:"perpetual_futures_positions"` + ExpiringFuturesPositions []WebsocketExpData `json:"expiring_futures_positions"` +} + +// WebsocketOrderDataHolder holds a variety of order responses, used when wsHandleData processes orders +type WebsocketOrderDataHolder struct { + Type string `json:"type"` + Orders []WebsocketOrderData `json:"orders"` + Positions WebsocketPositionStruct `json:"positions"` +} + +// Details is a sub-struct used in CurrencyData +type Details struct { + Type string `json:"type"` + Symbol string `json:"symbol"` + NetworkConfirmations int32 `json:"network_confirmations"` + SortOrder int32 `json:"sort_order"` + CryptoAddressLink string `json:"crypto_address_link"` + CryptoTransactionLink string `json:"crypto_transaction_link"` + PushPaymentMethods []string `json:"push_payment_methods"` + GroupTypes []string `json:"group_types"` + DisplayName string `json:"display_name"` + ProcessingTimeSeconds int64 `json:"processing_time_seconds"` + MinWithdrawalAmount float64 `json:"min_withdrawal_amount"` + MaxWithdrawalAmount float64 `json:"max_withdrawal_amount"` +} + +// SupportedNetworks is a sub-struct used in CurrencyData +type SupportedNetworks struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + ContractAddress string `json:"contract_address"` + CryptoAddressLink string `json:"crypto_address_link"` + CryptoTransactionLink string `json:"crypto_transaction_link"` + MinWithdrawalAmount float64 `json:"min_withdrawal_amount"` + MaxWithdrawalAmount float64 `json:"max_withdrawal_amount"` + NetworkConfirmations int32 `json:"network_confirmations"` + ProcessingTimeSeconds int64 `json:"processing_time_seconds"` +} + +// CurrencyData contains information on known currencies, used in GetAllCurrencies and GetACurrency +type CurrencyData struct { + ID string `json:"id"` + Name string `json:"name"` + MinSize string `json:"min_size"` + Status string `json:"status"` + Message string `json:"message"` + MaxPrecision float64 `json:"max_precision,string"` + ConvertibleTo []string `json:"convertible_to"` + Details Details `json:"details"` + DefaultNetwork string `json:"default_network"` + SupportedNetworks []SupportedNetworks `json:"supported_networks"` + DisplayName string `json:"display_name"` +} + +// PairData contains information on available trading pairs, used in GetAllTradingPairs +type PairData struct { + ID string `json:"id"` + BaseCurrency string `json:"base_currency"` + QuoteCurrency string `json:"quote_currency"` + QuoteIncrement float64 `json:"quote_increment,string"` + BaseIncrement float64 `json:"base_increment,string"` + DisplayName string `json:"display_name"` + MinMarketFunds float64 `json:"min_market_funds,string"` + MarginEnabled bool `json:"margin_enabled"` + PostOnly bool `json:"post_only"` + LimitOnly bool `json:"limit_only"` + CancelOnly bool `json:"cancel_only"` + Status string `json:"status"` + StatusMessage string `json:"status_message"` + TradingDisabled bool `json:"trading_disabled"` + FXStablecoin bool `json:"fx_stablecoin"` + MaxSlippagePercentage float64 `json:"max_slippage_percentage,string"` + AuctionMode bool `json:"auction_mode"` + HighBidLimitPercentage types.Number `json:"high_bid_limit_percentage"` +} + +// PairVolumeData contains information on trading pair volume, used in GetAllPairVolumes +type PairVolumeData struct { + ID string `json:"id"` + BaseCurrency string `json:"base_currency"` + QuoteCurrency string `json:"quote_currency"` + DisplayName string `json:"display_name"` + MarketTypes []string `json:"market_types"` + SpotVolume24Hour types.Number `json:"spot_volume_24hour"` + SpotVolume30Day types.Number `json:"spot_volume_30day"` + RFQVolume24Hour float64 `json:"rfq_volume_24hour,string"` + RFQVolume30Day float64 `json:"rfq_volume_30day,string"` + ConversionVolume24Hour float64 `json:"conversion_volume_24hour,string"` + ConversionVolume30Day float64 `json:"conversion_volume_30day,string"` +} + +// Auction holds information on an ongoing auction, used as a sub-struct in OrderBookResp and OrderBook +type Auction struct { + OpenPrice float64 `json:"open_price,string"` + OpenSize float64 `json:"open_size,string"` + BestBidPrice float64 `json:"best_bid_price,string"` + BestBidSize float64 `json:"best_bid_size,string"` + BestAskPrice float64 `json:"best_ask_price,string"` + BestAskSize float64 `json:"best_ask_size,string"` + AuctionState string `json:"auction_state"` + CanOpen string `json:"can_open"` + Time time.Time `json:"time"` +} + +// OrderBookResp holds information on bids and asks for a particular currency pair, used for unmarshalling in GetProductBookV1 +type OrderBookResp struct { + Bids [][3]any `json:"bids"` + Asks [][3]any `json:"asks"` + Sequence float64 `json:"sequence"` + AuctionMode bool `json:"auction_mode"` + Auction Auction `json:"auction"` + Time time.Time `json:"time"` +} + +// Orders holds information on orders, used as a sub-struct in OrderBook +type Orders struct { + Price float64 + Size float64 + OrderCount uint64 + OrderID uuid.UUID +} + +// OrderBook holds information on bids and asks for a particular currency pair, used in GetProductBookV1 +type OrderBook struct { + Bids []Orders + Asks []Orders + Sequence float64 + AuctionMode bool + Auction Auction + Time time.Time +} + +// RawCandles holds raw candle data, used in unmarshalling for GetProductCandles +type RawCandles [6]any + +// Candle holds properly formatted candle data, returned by GetProductCandles +type Candle struct { + Time time.Time + Low float64 + High float64 + Open float64 + Close float64 + Volume float64 +} + +// ProductStats holds information on a pair's price and volume, returned by GetProductStats +type ProductStats struct { + Open float64 `json:"open,string"` + High float64 `json:"high,string"` + Low float64 `json:"low,string"` + Last float64 `json:"last,string"` + Volume float64 `json:"volume,string"` + Volume30Day float64 `json:"volume_30day,string"` + RFQVolume24Hour float64 `json:"rfq_volume_24hour,string"` + RFQVolume30Day float64 `json:"rfq_volume_30day,string"` + ConversionsVolume24Hour float64 `json:"conversions_volume_24hour,string"` + ConversionsVolume30Day float64 `json:"conversions_volume_30day,string"` +} + +// ProductTicker holds information on a pair's price and volume, returned by GetProductTicker +type ProductTicker struct { + Ask float64 `json:"ask,string"` + Bid float64 `json:"bid,string"` + Volume float64 `json:"volume,string"` + TradeID int32 `json:"trade_id"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Time time.Time `json:"time"` + RFQVolume float64 `json:"rfq_volume,string"` + ConversionsVolume float64 `json:"conversions_volume,string"` +} + +// ProductTrades holds information on a pair's trades, returned by GetProductTrades +type ProductTrades struct { + TradeID int32 `json:"trade_id"` + Side string `json:"side"` + Size float64 `json:"size,string"` + Price float64 `json:"price,string"` + Time time.Time `json:"time"` +} + +// WrappedAsset holds information on a wrapped asset, used in AllWrappedAssets and returned by GetWrappedAssetDetails +type WrappedAsset struct { + ID string `json:"id"` + CirculatingSupply float64 `json:"circulating_supply,string"` + TotalSupply float64 `json:"total_supply,string"` + ConversionRate float64 `json:"conversion_rate,string"` + APY types.Number `json:"apy"` +} + +// AllWrappedAssets holds information on all wrapped assets, returned by GetAllWrappedAssets +type AllWrappedAssets struct { + WrappedAssets []WrappedAsset `json:"wrapped_assets"` +} -// TransferHistory returns wallet transfer history -type TransferHistory struct { - ID string `json:"id"` - Type string `json:"type"` - CreatedAt string `json:"created_at"` - CompletedAt string `json:"completed_at"` - CanceledAt time.Time `json:"canceled_at"` - ProcessedAt time.Time `json:"processed_at"` - UserNonce int64 `json:"user_nonce"` - Amount string `json:"amount"` - Details struct { - CoinbaseAccountID string `json:"coinbase_account_id"` - CoinbaseTransactionID string `json:"coinbase_transaction_id"` - CoinbasePaymentMethodID string `json:"coinbase_payment_method_id"` - } `json:"details"` +// WrappedAssetConversionRate holds information on a wrapped asset's conversion rate, returned by GetWrappedAssetConversionRate +type WrappedAssetConversionRate struct { + Amount float64 `json:"amount,string"` } diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index e18f52553f0..80ec72588b0 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -3,20 +3,22 @@ package coinbasepro import ( "context" "encoding/json" - "errors" "fmt" "net/http" "strconv" + "strings" + "text/template" "time" + "github.com/buger/jsonparser" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/pkg/errors" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" - "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/margin" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" @@ -24,9 +26,38 @@ import ( ) const ( - coinbaseproWebsocketURL = "wss://ws-feed.pro.coinbase.com" + coinbaseproWebsocketURL = "wss://advanced-trade-ws.coinbase.com" ) +var subscriptionNames = map[string]string{ + subscription.HeartbeatChannel: "heartbeats", + subscription.TickerChannel: "ticker", + subscription.CandlesChannel: "candles", + subscription.AllTradesChannel: "market_trades", + subscription.OrderbookChannel: "level2", + subscription.MyAccountChannel: "user", + "status": "status", + "ticker_batch": "ticker_batch", + /* Not Implemented: + "futures_balance_summary": "futures_balance_summary", + */ +} + +var defaultSubscriptions = subscription.List{ + {Enabled: true, Channel: subscription.HeartbeatChannel}, + // Subscriptions to status return an "authentication failure" error, despite the endpoint not being authenticated and other authenticated channels working fine. + {Enabled: false, Channel: "status"}, + {Enabled: true, Asset: asset.Spot, Channel: subscription.TickerChannel}, + {Enabled: true, Asset: asset.Spot, Channel: subscription.CandlesChannel}, + {Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel}, + {Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel}, + {Enabled: true, Asset: asset.All, Channel: subscription.MyAccountChannel, Authenticated: true}, + {Enabled: false, Asset: asset.Spot, Channel: "ticker_batch"}, + /* Not Implemented: + {Enabled: false, Asset: asset.Spot, Channel: "futures_balance_summary", Authenticated: true}, + */ +} + // WsConnect initiates a websocket connection func (c *CoinbasePro) WsConnect() error { if !c.Websocket.IsEnabled() || !c.IsEnabled() { @@ -37,7 +68,6 @@ func (c *CoinbasePro) WsConnect() error { if err != nil { return err } - c.Websocket.Wg.Add(1) go c.wsReadData() return nil @@ -46,423 +76,529 @@ func (c *CoinbasePro) WsConnect() error { // wsReadData receives and passes on websocket messages for processing func (c *CoinbasePro) wsReadData() { defer c.Websocket.Wg.Done() - + var seqCount uint64 for { resp := c.Websocket.Conn.ReadMessage() if resp.Raw == nil { return } - err := c.wsHandleData(resp.Raw) + warn, err := c.wsHandleData(resp.Raw, seqCount) if err != nil { c.Websocket.DataHandler <- err } + if warn != "" { + c.Websocket.DataHandler <- warn + tempStr := strings.SplitN(warn, "Out of order sequence number. Received ", 2)[1] + tempStr = strings.SplitN(tempStr, ", expected ", 2)[0] + tempNum, err := strconv.ParseUint(tempStr, 10, 64) + if err != nil { + c.Websocket.DataHandler <- err + } else { + seqCount = tempNum + } + } + seqCount++ } } -func (c *CoinbasePro) wsHandleData(respRaw []byte) error { - msgType := wsMsgType{} - err := json.Unmarshal(respRaw, &msgType) +// wsHandleData handles all the websocket data coming from the websocket connection +func (c *CoinbasePro) wsHandleData(respRaw []byte, seqCount uint64) (string, error) { + var warnString string + ertype, _, _, err := jsonparser.Get(respRaw, "type") + if err == nil && string(ertype) == "error" { + return warnString, errors.New(string(respRaw)) + } + seqData, _, _, err := jsonparser.Get(respRaw, "sequence_num") if err != nil { - return err + return warnString, err } - - if msgType.Type == "subscriptions" || msgType.Type == "heartbeat" { - return nil + seqNum, err := strconv.ParseUint(string(seqData), 10, 64) + if err != nil { + return warnString, err } - - switch msgType.Type { + if seqNum != seqCount { + warnString = fmt.Sprintf(warnSequenceIssue, seqNum, seqCount) + } + channelRaw, _, _, err := jsonparser.Get(respRaw, "channel") + if err != nil { + return warnString, err + } + channel := string(channelRaw) + if channel == "subscriptions" || channel == "heartbeats" { + return warnString, nil + } + data, _, _, err := jsonparser.Get(respRaw, "events") + if err != nil { + return warnString, err + } + switch channel { case "status": - var status wsStatus - err = json.Unmarshal(respRaw, &status) - if err != nil { - return err - } - c.Websocket.DataHandler <- status - case "error": - c.Websocket.DataHandler <- errors.New(string(respRaw)) - case "ticker": - wsTicker := WebsocketTicker{} - err := json.Unmarshal(respRaw, &wsTicker) - if err != nil { - return err - } - - c.Websocket.DataHandler <- &ticker.Price{ - LastUpdated: wsTicker.Time, - Pair: wsTicker.ProductID, - AssetType: asset.Spot, - ExchangeName: c.Name, - Open: wsTicker.Open24H, - High: wsTicker.High24H, - Low: wsTicker.Low24H, - Last: wsTicker.Price, - Volume: wsTicker.Volume24H, - Bid: wsTicker.BestBid, - Ask: wsTicker.BestAsk, - } - - case "snapshot": - var snapshot WebsocketOrderbookSnapshot - err := json.Unmarshal(respRaw, &snapshot) + wsStatus := []WebsocketProductHolder{} + err = json.Unmarshal(data, &wsStatus) if err != nil { - return err + return warnString, err } - - err = c.ProcessSnapshot(&snapshot) + c.Websocket.DataHandler <- wsStatus + case "ticker", "ticker_batch": + wsTicker := []WebsocketTickerHolder{} + err = json.Unmarshal(data, &wsTicker) if err != nil { - return err + return warnString, err } - case "l2update": - var update WebsocketL2Update - err := json.Unmarshal(respRaw, &update) + sliToSend := []ticker.Price{} + var timestamp time.Time + timestamp, err = getTimestamp(respRaw) if err != nil { - return err - } - - err = c.ProcessUpdate(&update) - if err != nil { - return err + return warnString, err + } + for i := range wsTicker { + for j := range wsTicker[i].Tickers { + sliToSend = append(sliToSend, ticker.Price{ + LastUpdated: timestamp, + Pair: wsTicker[i].Tickers[j].ProductID, + AssetType: asset.Spot, + ExchangeName: c.Name, + High: wsTicker[i].Tickers[j].High24H, + Low: wsTicker[i].Tickers[j].Low24H, + Last: wsTicker[i].Tickers[j].Price, + Volume: wsTicker[i].Tickers[j].Volume24H, + }) + } } - case "received", "open", "done", "change", "activate": - var wsOrder wsOrderReceived - err := json.Unmarshal(respRaw, &wsOrder) + c.Websocket.DataHandler <- sliToSend + case "candles": + wsCandles := []WebsocketCandleHolder{} + err = json.Unmarshal(data, &wsCandles) if err != nil { - return err + return warnString, err } - var oType order.Type - var oSide order.Side - var oStatus order.Status - oType, err = order.StringToOrderType(wsOrder.OrderType) + sliToSend := []stream.KlineData{} + var timestamp time.Time + timestamp, err = getTimestamp(respRaw) if err != nil { - c.Websocket.DataHandler <- order.ClassificationError{ - Exchange: c.Name, - OrderID: wsOrder.OrderID, - Err: err, + return warnString, err + } + for i := range wsCandles { + for j := range wsCandles[i].Candles { + sliToSend = append(sliToSend, stream.KlineData{ + Timestamp: timestamp, + Pair: wsCandles[i].Candles[j].ProductID, + AssetType: asset.Spot, + Exchange: c.Name, + StartTime: wsCandles[i].Candles[j].Start.Time(), + OpenPrice: wsCandles[i].Candles[j].Open, + ClosePrice: wsCandles[i].Candles[j].Close, + HighPrice: wsCandles[i].Candles[j].High, + LowPrice: wsCandles[i].Candles[j].Low, + Volume: wsCandles[i].Candles[j].Volume, + }) } } - oSide, err = order.StringToOrderSide(wsOrder.Side) + c.Websocket.DataHandler <- sliToSend + case "market_trades": + wsTrades := []WebsocketMarketTradeHolder{} + err = json.Unmarshal(data, &wsTrades) if err != nil { - c.Websocket.DataHandler <- order.ClassificationError{ - Exchange: c.Name, - OrderID: wsOrder.OrderID, - Err: err, + return warnString, err + } + sliToSend := []trade.Data{} + for i := range wsTrades { + for j := range wsTrades[i].Trades { + sliToSend = append(sliToSend, trade.Data{ + TID: wsTrades[i].Trades[j].TradeID, + Exchange: c.Name, + CurrencyPair: wsTrades[i].Trades[j].ProductID, + AssetType: asset.Spot, + Side: wsTrades[i].Trades[j].Side, + Price: wsTrades[i].Trades[j].Price, + Amount: wsTrades[i].Trades[j].Size, + Timestamp: wsTrades[i].Trades[j].Time, + }) } } - oStatus, err = statusToStandardStatus(wsOrder.Type) + c.Websocket.DataHandler <- sliToSend + case "l2_data": + var wsL2 []WebsocketOrderbookDataHolder + err := json.Unmarshal(data, &wsL2) if err != nil { - c.Websocket.DataHandler <- order.ClassificationError{ - Exchange: c.Name, - OrderID: wsOrder.OrderID, - Err: err, - } - } - if wsOrder.Reason == "canceled" { - oStatus = order.Cancelled + return warnString, err } - ts := wsOrder.Time - if wsOrder.Type == "activate" { - ts = convert.TimeFromUnixTimestampDecimal(wsOrder.Timestamp) - } - - creds, err := c.GetCredentials(context.TODO()) + timestamp, err := getTimestamp(respRaw) if err != nil { - c.Websocket.DataHandler <- order.ClassificationError{ - Exchange: c.Name, - OrderID: wsOrder.OrderID, - Err: err, + return warnString, err + } + for i := range wsL2 { + switch wsL2[i].Type { + case "snapshot": + err = c.ProcessSnapshot(&wsL2[i], timestamp) + case "update": + err = c.ProcessUpdate(&wsL2[i], timestamp) + default: + err = fmt.Errorf("%w %v", errUnknownL2DataType, wsL2[i].Type) } - } - - if wsOrder.UserID != "" { - var p currency.Pair - var a asset.Item - p, a, err = c.GetRequestFormattedPairAndAssetType(wsOrder.ProductID) if err != nil { - return err - } - c.Websocket.DataHandler <- &order.Detail{ - HiddenOrder: wsOrder.Private, - Price: wsOrder.Price, - Amount: wsOrder.Size, - TriggerPrice: wsOrder.StopPrice, - ExecutedAmount: wsOrder.Size - wsOrder.RemainingSize, - RemainingAmount: wsOrder.RemainingSize, - Fee: wsOrder.TakerFeeRate, - Exchange: c.Name, - OrderID: wsOrder.OrderID, - AccountID: wsOrder.ProfileID, - ClientID: creds.ClientID, - Type: oType, - Side: oSide, - Status: oStatus, - AssetType: a, - Date: ts, - Pair: p, + return warnString, err } } - case "match", "last_match": - var wsOrder wsOrderReceived - err := json.Unmarshal(respRaw, &wsOrder) - if err != nil { - return err - } - oSide, err := order.StringToOrderSide(wsOrder.Side) + case "user": + var wsUser []WebsocketOrderDataHolder + err := json.Unmarshal(data, &wsUser) if err != nil { - c.Websocket.DataHandler <- order.ClassificationError{ - Exchange: c.Name, - Err: err, + return warnString, err + } + var sliToSend []order.Detail + for i := range wsUser { + for j := range wsUser[i].Orders { + var oType order.Type + oType, err = stringToStandardType(wsUser[i].Orders[j].OrderType) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + var oSide order.Side + oSide, err = order.StringToOrderSide(wsUser[i].Orders[j].OrderSide) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + var oStatus order.Status + oStatus, err = statusToStandardStatus(wsUser[i].Orders[j].Status) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + price := wsUser[i].Orders[j].AveragePrice + if wsUser[i].Orders[j].LimitPrice != 0 { + price = wsUser[i].Orders[j].LimitPrice + } + var asset asset.Item + asset, err = stringToStandardAsset(wsUser[i].Orders[j].ProductType) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + var ioc, fok bool + ioc, fok, err = strategyDecoder(wsUser[i].Orders[j].TimeInForce) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + sliToSend = append(sliToSend, order.Detail{ + Price: price, + ClientOrderID: wsUser[i].Orders[j].ClientOrderID, + ExecutedAmount: wsUser[i].Orders[j].CumulativeQuantity, + RemainingAmount: wsUser[i].Orders[j].LeavesQuantity, + Amount: wsUser[i].Orders[j].CumulativeQuantity + wsUser[i].Orders[j].LeavesQuantity, + OrderID: wsUser[i].Orders[j].OrderID, + Side: oSide, + Type: oType, + PostOnly: wsUser[i].Orders[j].PostOnly, + Pair: wsUser[i].Orders[j].ProductID, + AssetType: asset, + Status: oStatus, + TriggerPrice: wsUser[i].Orders[j].StopPrice, + ImmediateOrCancel: ioc, + FillOrKill: fok, + Fee: wsUser[i].Orders[j].TotalFees, + Date: wsUser[i].Orders[j].CreationTime, + CloseTime: wsUser[i].Orders[j].EndTime, + Exchange: c.Name, + }) } - } - var p currency.Pair - var a asset.Item - p, a, err = c.GetRequestFormattedPairAndAssetType(wsOrder.ProductID) - if err != nil { - return err - } - - if wsOrder.UserID != "" { - c.Websocket.DataHandler <- &order.Detail{ - OrderID: wsOrder.OrderID, - Pair: p, - AssetType: a, - Trades: []order.TradeHistory{ - { - Price: wsOrder.Price, - Amount: wsOrder.Size, - Exchange: c.Name, - TID: strconv.FormatInt(wsOrder.TradeID, 10), - Side: oSide, - Timestamp: wsOrder.Time, - IsMaker: wsOrder.TakerUserID == "", - }, - }, + for j := range wsUser[i].Positions.PerpetualFuturesPositions { + var oSide order.Side + oSide, err = order.StringToOrderSide(wsUser[i].Positions.PerpetualFuturesPositions[j].PositionSide) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + var mType margin.Type + mType, err = margin.StringToMarginType(wsUser[i].Positions.PerpetualFuturesPositions[j].MarginType) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + sliToSend = append(sliToSend, order.Detail{ + Pair: wsUser[i].Positions.PerpetualFuturesPositions[j].ProductID, + Side: oSide, + MarginType: mType, + Amount: wsUser[i].Positions.PerpetualFuturesPositions[j].NetSize, + Leverage: wsUser[i].Positions.PerpetualFuturesPositions[j].Leverage, + AssetType: asset.Futures, + Exchange: c.Name, + }) } - } else { - if !c.IsSaveTradeDataEnabled() { - return nil + for j := range wsUser[i].Positions.ExpiringFuturesPositions { + var oSide order.Side + oSide, err = order.StringToOrderSide(wsUser[i].Positions.ExpiringFuturesPositions[j].Side) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + sliToSend = append(sliToSend, order.Detail{ + Pair: wsUser[i].Positions.ExpiringFuturesPositions[j].ProductID, + Side: oSide, + ContractAmount: wsUser[i].Positions.ExpiringFuturesPositions[j].NumberOfContracts, + Price: wsUser[i].Positions.ExpiringFuturesPositions[j].EntryPrice, + }) } - return trade.AddTradesToBuffer(c.Name, trade.Data{ - Timestamp: wsOrder.Time, - Exchange: c.Name, - CurrencyPair: p, - AssetType: a, - Price: wsOrder.Price, - Amount: wsOrder.Size, - Side: oSide, - TID: strconv.FormatInt(wsOrder.TradeID, 10), - }) } + c.Websocket.DataHandler <- sliToSend default: - c.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: c.Name + stream.UnhandledMessage + string(respRaw)} - return nil - } - return nil -} - -func statusToStandardStatus(stat string) (order.Status, error) { - switch stat { - case "received": - return order.New, nil - case "open": - return order.Active, nil - case "done": - return order.Filled, nil - case "match": - return order.PartiallyFilled, nil - case "change", "activate": - return order.Active, nil - default: - return order.UnknownStatus, fmt.Errorf("%s not recognised as status type", stat) + return warnString, errChannelNameUnknown } + return warnString, nil } // ProcessSnapshot processes the initial orderbook snap shot -func (c *CoinbasePro) ProcessSnapshot(snapshot *WebsocketOrderbookSnapshot) error { - pair, err := currency.NewPairFromString(snapshot.ProductID) +func (c *CoinbasePro) ProcessSnapshot(snapshot *WebsocketOrderbookDataHolder, timestamp time.Time) error { + bids, asks, err := processBidAskArray(snapshot) if err != nil { return err } - - base := orderbook.Base{ - Pair: pair, - Bids: make(orderbook.Tranches, len(snapshot.Bids)), - Asks: make(orderbook.Tranches, len(snapshot.Asks)), - } - - for i := range snapshot.Bids { - var price float64 - price, err = strconv.ParseFloat(snapshot.Bids[i][0], 64) - if err != nil { - return err - } - var amount float64 - amount, err = strconv.ParseFloat(snapshot.Bids[i][1], 64) - if err != nil { - return err - } - base.Bids[i] = orderbook.Tranche{Price: price, Amount: amount} - } - - for i := range snapshot.Asks { - var price float64 - price, err = strconv.ParseFloat(snapshot.Asks[i][0], 64) - if err != nil { - return err - } - var amount float64 - amount, err = strconv.ParseFloat(snapshot.Asks[i][1], 64) - if err != nil { - return err - } - base.Asks[i] = orderbook.Tranche{Price: price, Amount: amount} - } - - base.Asset = asset.Spot - base.Pair = pair - base.Exchange = c.Name - base.VerifyOrderbook = c.CanVerifyOrderbook - base.LastUpdated = snapshot.Time - return c.Websocket.Orderbook.LoadSnapshot(&base) + return c.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ + Bids: bids, + Asks: asks, + Exchange: c.Name, + Pair: snapshot.ProductID, + Asset: asset.Spot, + LastUpdated: timestamp, + VerifyOrderbook: c.CanVerifyOrderbook, + }) } // ProcessUpdate updates the orderbook local cache -func (c *CoinbasePro) ProcessUpdate(update *WebsocketL2Update) error { - if len(update.Changes) == 0 { - return errors.New("no data in websocket update") - } - - p, err := currency.NewPairFromString(update.ProductID) +func (c *CoinbasePro) ProcessUpdate(update *WebsocketOrderbookDataHolder, timestamp time.Time) error { + bids, asks, err := processBidAskArray(update) if err != nil { return err } - - asks := make(orderbook.Tranches, 0, len(update.Changes)) - bids := make(orderbook.Tranches, 0, len(update.Changes)) - - for i := range update.Changes { - price, err := strconv.ParseFloat(update.Changes[i][1], 64) - if err != nil { - return err - } - volume, err := strconv.ParseFloat(update.Changes[i][2], 64) - if err != nil { - return err - } - if update.Changes[i][0] == order.Buy.Lower() { - bids = append(bids, orderbook.Tranche{Price: price, Amount: volume}) - } else { - asks = append(asks, orderbook.Tranche{Price: price, Amount: volume}) - } - } - - return c.Websocket.Orderbook.Update(&orderbook.Update{ + obU := orderbook.Update{ Bids: bids, Asks: asks, - Pair: p, - UpdateTime: update.Time, + Pair: update.ProductID, + UpdateTime: timestamp, Asset: asset.Spot, - }) + } + return c.Websocket.Orderbook.Update(&obU) } -// generateSubscriptions returns a list of subscriptions from the configured subscriptions feature +// GenerateSubscriptions adds default subscriptions to websocket to be handled by ManageSubscriptions() func (c *CoinbasePro) generateSubscriptions() (subscription.List, error) { - pairs, err := c.GetEnabledPairs(asset.Spot) - if err != nil { - return nil, err - } - pairFmt, err := c.GetPairFormat(asset.Spot, true) - if err != nil { - return nil, err - } - pairs = pairs.Format(pairFmt) - authed := c.IsWebsocketAuthenticationSupported() - subs := make(subscription.List, 0, len(c.Features.Subscriptions)) - for _, baseSub := range c.Features.Subscriptions { - if !authed && baseSub.Authenticated { - continue - } + return c.Features.Subscriptions.ExpandTemplates(c) +} - s := baseSub.Clone() - s.Asset = asset.Spot - s.Pairs = pairs - subs = append(subs, s) - } - return subs, nil +// GetSubscriptionTemplate returns a subscription channel template +func (c *CoinbasePro) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) { + return template.New("master.tmpl").Funcs(template.FuncMap{"channelName": channelName}).Parse(subTplText) } -// Subscribe sends a websocket message to receive data from the channel +// Subscribe sends a websocket message to receive data from a list of channels func (c *CoinbasePro) Subscribe(subs subscription.List) error { - r := &WebsocketSubscribe{ - Type: "subscribe", - Channels: make([]any, 0, len(subs)), - } - // See if we have a consistent Pair list for all the subs that we can use globally - // If all the subs have the same pairs then we can use the top level ProductIDs field - // Otherwise each and every sub needs to have it's own list - for i, s := range subs { - if i == 0 { - r.ProductIDs = s.Pairs.Strings() - } else if !subs[0].Pairs.Equal(s.Pairs) { - r.ProductIDs = nil - break - } - } + return c.ParallelChanOp(subs, func(subs subscription.List) error { return c.manageSubs("subscribe", subs) }, 1) +} + +// Unsubscribe sends a websocket message to stop receiving data from a list of channels +func (c *CoinbasePro) Unsubscribe(subs subscription.List) error { + return c.ParallelChanOp(subs, func(subs subscription.List) error { return c.manageSubs("unsubscribe", subs) }, 1) +} + +// manageSub subscribes or unsubscribes from a list of websocket channels +func (c *CoinbasePro) manageSubs(op string, subs subscription.List) error { + var errs error + subs, errs = subs.ExpandTemplates(c) for _, s := range subs { - if s.Authenticated && r.Key == "" && c.IsWebsocketAuthenticationSupported() { - if err := c.authWsSubscibeReq(r); err != nil { - return err + r := &WebsocketRequest{ + Type: op, + ProductIDs: s.Pairs, + Channel: s.QualifiedChannel, + Timestamp: strconv.FormatInt(time.Now().Unix(), 10), + } + var err error + limitType := WSUnauthRate + if s.Authenticated { + limitType = WSAuthRate + err = c.signWsRequest(r) + if err != nil { + errs = common.AppendError(errs, err) + continue } } - if len(r.ProductIDs) == 0 { - r.Channels = append(r.Channels, WsChannel{ - Name: s.Channel, - ProductIDs: s.Pairs.Strings(), - }) - } else { - // Coinbase does not support using [WsChannel{Name:"x"}] unless each ProductIDs field is populated - // Therefore we have to use Channels as an array of strings - r.Channels = append(r.Channels, s.Channel) + if err = c.Websocket.Conn.SendJSONMessage(context.TODO(), limitType, r); err == nil { + switch op { + case "subscribe": + err = c.Websocket.AddSuccessfulSubscriptions(c.Websocket.Conn, s) + case "unsubscribe": + err = c.Websocket.RemoveSubscriptions(c.Websocket.Conn, s) + } } + errs = common.AppendError(errs, err) } - err := c.Websocket.Conn.SendJSONMessage(context.TODO(), request.Unset, r) - if err == nil { - err = c.Websocket.AddSuccessfulSubscriptions(c.Websocket.Conn, subs...) - } - return err + return errs } -func (c *CoinbasePro) authWsSubscibeReq(r *WebsocketSubscribe) error { - creds, err := c.GetCredentials(context.TODO()) +func (c *CoinbasePro) signWsRequest(r *WebsocketRequest) error { + jwt, err := c.GetWSJWT() if err != nil { return err } - r.Timestamp = strconv.FormatInt(time.Now().Unix(), 10) - message := r.Timestamp + http.MethodGet + "/users/self/verify" - hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(creds.Secret)) + r.JWT = jwt + return nil +} + +// GetWSJWT returns a JWT, using a stored one of it's provided, and generating a new one otherwise +func (c *CoinbasePro) GetWSJWT() (string, error) { + c.mut.RLock() + if c.jwtExpire.After(time.Now()) { + retStr := c.jwt + c.mut.RUnlock() + return retStr, nil + } + go c.mut.RUnlock() + c.mut.Lock() + defer c.mut.Unlock() + var err error + c.jwt, c.jwtExpire, err = c.GetJWT(context.Background(), "") + return c.jwt, err +} + +// getTimestamp is a helper function which pulls a RFC3339-formatted timestamp from a byte slice of JSON data +func getTimestamp(rawData []byte) (time.Time, error) { + data, _, _, err := jsonparser.Get(rawData, "timestamp") if err != nil { - return err + return time.Time{}, err } - r.Signature = crypto.Base64Encode(hmac) - r.Key = creds.Key - r.Passphrase = creds.ClientID - return nil + return time.Parse(time.RFC3339, string(data)) } -// Unsubscribe sends a websocket message to stop receiving data from the channel -func (c *CoinbasePro) Unsubscribe(subs subscription.List) error { - r := &WebsocketSubscribe{ - Type: "unsubscribe", - Channels: make([]any, 0, len(subs)), +// processBidAskArray is a helper function that turns WebsocketOrderbookDataHolder into arrays of bids and asks +func processBidAskArray(data *WebsocketOrderbookDataHolder) (bids, asks orderbook.Tranches, err error) { + bids = make(orderbook.Tranches, 0, len(data.Changes)) + asks = make(orderbook.Tranches, 0, len(data.Changes)) + for i := range data.Changes { + change := orderbook.Tranche{Price: data.Changes[i].PriceLevel, Amount: data.Changes[i].NewQuantity} + switch data.Changes[i].Side { + case "bid": + bids = append(bids, change) + case "offer": + asks = append(asks, change) + default: + return nil, nil, fmt.Errorf("%w %v", order.ErrSideIsInvalid, data.Changes[i].Side) + } } - for _, s := range subs { - r.Channels = append(r.Channels, WsChannel{ - Name: s.Channel, - ProductIDs: s.Pairs.Strings(), - }) + bids.SortBids() + asks.SortAsks() + return bids, asks, nil +} + +// statusToStandardStatus is a helper function that converts a Coinbase Pro status string to a standardised order.Status type +func statusToStandardStatus(stat string) (order.Status, error) { + switch stat { + case "PENDING": + return order.New, nil + case "OPEN": + return order.Active, nil + case "FILLED": + return order.Filled, nil + case "CANCELLED": + return order.Cancelled, nil + case "EXPIRED": + return order.Expired, nil + case "FAILED": + return order.Rejected, nil + default: + return order.UnknownStatus, fmt.Errorf("%w %v", errUnrecognisedStatusType, stat) + } +} + +// stringToStandardType is a helper function that converts a Coinbase Pro side string to a standardised order.Type type +func stringToStandardType(str string) (order.Type, error) { + switch str { + case "LIMIT_ORDER_TYPE": + return order.Limit, nil + case "MARKET_ORDER_TYPE": + return order.Market, nil + case "STOP_LIMIT_ORDER_TYPE": + return order.StopLimit, nil + default: + return order.UnknownType, fmt.Errorf("%w %v", errUnrecognisedOrderType, str) } - err := c.Websocket.Conn.SendJSONMessage(context.TODO(), request.Unset, r) - if err == nil { - err = c.Websocket.RemoveSubscriptions(c.Websocket.Conn, subs...) +} + +// stringToStandardAsset is a helper function that converts a Coinbase Pro asset string to a standardised asset.Item type +func stringToStandardAsset(str string) (asset.Item, error) { + switch str { + case "SPOT": + return asset.Spot, nil + case "FUTURE": + return asset.Futures, nil + default: + return asset.Empty, fmt.Errorf("%w %v", errUnrecognisedAssetType, str) + } +} + +// strategyDecoder is a helper function that converts a Coinbase Pro time in force string to a few standardised bools +func strategyDecoder(str string) (ioc, fok bool, err error) { + switch str { + case "IMMEDIATE_OR_CANCEL": + return true, false, nil + case "FILL_OR_KILL": + return false, true, nil + case "GOOD_UNTIL_CANCELLED", "GOOD_UNTIL_DATE_TIME": + return false, false, nil + default: + return false, false, fmt.Errorf("%w %v", errUnrecognisedStrategyType, str) + } +} + +// Base64URLEncode is a helper function that does some tweaks to standard Base64 encoding, in a way which JWT requires +func base64URLEncode(b []byte) string { + s := crypto.Base64Encode(b) + s = strings.Split(s, "=")[0] + s = strings.ReplaceAll(s, "+", "-") + s = strings.ReplaceAll(s, "/", "_") + return s +} + +// checkSubscriptions looks for incompatible subscriptions and if found replaces all with defaults +// This should be unnecessary and removable by mid-2025 +func (c *CoinbasePro) checkSubscriptions() { + for _, s := range c.Config.Features.Subscriptions { + switch s.Channel { + case "level2_batch", "matches": + c.Config.Features.Subscriptions = defaultSubscriptions.Clone() + c.Features.Subscriptions = c.Config.Features.Subscriptions.Enabled() + return + } } - return err } + +func channelName(s *subscription.Subscription) string { + if n, ok := subscriptionNames[s.Channel]; ok { + return n + } + panic(fmt.Errorf("%w: %s", subscription.ErrNotSupported, s.Channel)) +} + +const subTplText = ` +{{ range $asset, $pairs := $.AssetPairs }} + {{- channelName $.S -}} + {{- $.AssetSeparator }} +{{- end }} +` diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index d5d9173db78..2a82891f343 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -2,11 +2,13 @@ package coinbasepro import ( "context" + "errors" "fmt" - "sort" + "math" "strconv" "time" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" @@ -23,7 +25,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer" - "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/log" @@ -34,46 +35,42 @@ import ( func (c *CoinbasePro) SetDefaults() { c.Name = "CoinbasePro" c.Enabled = true - c.Verbose = true c.API.CredentialsValidator.RequiresKey = true c.API.CredentialsValidator.RequiresSecret = true - c.API.CredentialsValidator.RequiresClientID = true - c.API.CredentialsValidator.RequiresBase64DecodeSecret = true - + c.API.CredentialsValidator.RequiresBase64DecodeSecret = false requestFmt := ¤cy.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true} configFmt := ¤cy.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true} - err := c.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot) + err := c.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot, asset.Futures) if err != nil { log.Errorln(log.ExchangeSys, err) } - c.Features = exchange.Features{ Supports: exchange.FeaturesSupported{ REST: true, Websocket: true, RESTCapabilities: protocol.Features{ - TickerFetching: true, - KlineFetching: true, - TradeFetching: true, - OrderbookFetching: true, - AutoPairUpdates: true, - AccountInfo: true, - GetOrder: true, - GetOrders: true, - CancelOrders: true, - CancelOrder: true, - SubmitOrder: true, - DepositHistory: true, - WithdrawalHistory: true, - UserTradeHistory: true, - CryptoDeposit: true, - CryptoWithdrawal: true, - FiatDeposit: true, - FiatWithdraw: true, - TradeFee: true, - FiatDepositFee: true, - FiatWithdrawalFee: true, - CandleHistory: true, + AutoPairUpdates: true, + AccountBalance: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + FiatWithdraw: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + ModifyOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + FiatWithdrawalFee: true, + CryptoWithdrawalFee: true, + TickerFetching: true, + KlineFetching: true, + OrderbookFetching: true, + AccountInfo: true, + FiatDeposit: true, + FundingRateFetching: true, + HasAssetTypeAccountSegregation: true, }, WebsocketCapabilities: protocol.Features{ TickerFetching: true, @@ -99,33 +96,30 @@ func (c *CoinbasePro) SetDefaults() { kline.IntervalCapacity{Interval: kline.OneMin}, kline.IntervalCapacity{Interval: kline.FiveMin}, kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, kline.IntervalCapacity{Interval: kline.SixHour}, kline.IntervalCapacity{Interval: kline.OneDay}, ), GlobalResultLimit: 300, }, }, - Subscriptions: subscription.List{ - {Enabled: true, Channel: "heartbeat"}, - {Enabled: true, Channel: "level2_batch"}, // Other orderbook feeds require authentication; This is batched in 50ms lots - {Enabled: true, Channel: "ticker"}, - {Enabled: true, Channel: "user", Authenticated: true}, - {Enabled: true, Channel: "matches"}, + Subscriptions: defaultSubscriptions.Clone(), + TradingRequirements: protocol.TradingRequirements{ + SpotMarketOrderAmountPurchaseQuotationOnly: true, }, } - - c.Requester, err = request.New(c.Name, - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), - request.WithLimiter(GetRateLimit())) + c.Requester, err = request.New(c.Name, common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), request.WithLimiter(GetRateLimit())) if err != nil { log.Errorln(log.ExchangeSys, err) } c.API.Endpoints = c.NewEndpoints() err = c.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ - exchange.RestSpot: coinbaseproAPIURL, - exchange.RestSandbox: coinbaseproSandboxAPIURL, - exchange.WebsocketSpot: coinbaseproWebsocketURL, + exchange.RestSpot: coinbaseAPIURL, + exchange.RestSandbox: coinbaseproSandboxAPIURL, + exchange.WebsocketSpot: coinbaseproWebsocketURL, + exchange.RestSpotSupplementary: coinbaseV1APIURL, }) if err != nil { log.Errorln(log.ExchangeSys, err) @@ -150,12 +144,11 @@ func (c *CoinbasePro) Setup(exch *config.Exchange) error { if err != nil { return err } - + c.checkSubscriptions() wsRunningURL, err := c.API.Endpoints.GetURL(exchange.WebsocketSpot) if err != nil { return err } - err = c.Websocket.Setup(&stream.WebsocketSetup{ ExchangeConfig: exch, DefaultURL: coinbaseproWebsocketURL, @@ -170,7 +163,6 @@ func (c *CoinbasePro) Setup(exch *config.Exchange) error { }, }) if err != nil { - fmt.Println("COINBASE ISSUE") return err } @@ -181,69 +173,87 @@ func (c *CoinbasePro) Setup(exch *config.Exchange) error { } // FetchTradablePairs returns a list of the exchanges tradable pairs -func (c *CoinbasePro) FetchTradablePairs(ctx context.Context, _ asset.Item) (currency.Pairs, error) { - products, err := c.GetProducts(ctx) +func (c *CoinbasePro) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { + var products *AllProducts + verified, err := c.verificationCheck(ctx) if err != nil { return nil, err } - - pairs := make([]currency.Pair, 0, len(products)) - for x := range products { - if products[x].TradingDisabled { - continue + aString := FormatAssetOutbound(a) + if verified { + products, err = c.GetAllProducts(ctx, 0, 0, aString, "", "", nil, verified) + if err != nil { + log.Warnf(log.ExchangeSys, warnAuth, err) + verified = false } - var pair currency.Pair - pair, err = currency.NewPairDelimiter(products[x].ID, currency.DashDelimiter) + } + if !verified { + products, err = c.GetAllProducts(ctx, 0, 0, aString, "", "", nil, verified) if err != nil { return nil, err } - pairs = append(pairs, pair) + } + pairs := make([]currency.Pair, 0, len(products.Products)) + for x := range products.Products { + if products.Products[x].TradingDisabled { + continue + } + pairs = append(pairs, products.Products[x].ID) } return pairs, nil } -// UpdateTradablePairs updates the exchanges available pairs and stores -// them in the exchanges config +// UpdateTradablePairs updates the exchanges available pairs and stores them in the exchanges config func (c *CoinbasePro) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error { - pairs, err := c.FetchTradablePairs(ctx, asset.Spot) - if err != nil { - return err - } - err = c.UpdatePairs(pairs, asset.Spot, false, forceUpdate) - if err != nil { - return err + assets := c.GetAssetTypes(false) + for i := range assets { + pairs, err := c.FetchTradablePairs(ctx, assets[i]) + if err != nil { + return err + } + err = c.UpdatePairs(pairs, assets[i], false, forceUpdate) + if err != nil { + return err + } } return c.EnsureOnePairEnabled() } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// coinbasepro exchange +// UpdateAccountInfo retrieves balances for all enabled currencies for the coinbasepro exchange func (c *CoinbasePro) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var response account.Holdings + var ( + response account.Holdings + accountBalance []Account + done bool + err error + cursor string + accountResp *AllAccountsResponse + ) response.Exchange = c.Name - accountBalance, err := c.GetAccounts(ctx) - if err != nil { - return response, err + for !done { + accountResp, err = c.GetAllAccounts(ctx, 250, cursor) + if err != nil { + return response, err + } + accountBalance = append(accountBalance, accountResp.Accounts...) + done = !accountResp.HasNext + cursor = accountResp.Cursor } - accountCurrencies := make(map[string][]account.Balance) for i := range accountBalance { - profileID := accountBalance[i].ProfileID + profileID := accountBalance[i].UUID currencies := accountCurrencies[profileID] accountCurrencies[profileID] = append(currencies, account.Balance{ Currency: currency.NewCode(accountBalance[i].Currency), - Total: accountBalance[i].Balance, - Hold: accountBalance[i].Hold, - Free: accountBalance[i].Available, - AvailableWithoutBorrow: accountBalance[i].Available - accountBalance[i].FundedAmount, - Borrowed: accountBalance[i].FundedAmount, + Total: accountBalance[i].AvailableBalance.Value, + Hold: accountBalance[i].Hold.Value, + Free: accountBalance[i].AvailableBalance.Value - accountBalance[i].Hold.Value, + AvailableWithoutBorrow: accountBalance[i].AvailableBalance.Value, }) } - if response.Accounts, err = account.CollectBalances(accountCurrencies, assetType); err != nil { return account.Holdings{}, err } - creds, err := c.GetCredentials(ctx) if err != nil { return account.Holdings{}, err @@ -252,7 +262,6 @@ func (c *CoinbasePro) UpdateAccountInfo(ctx context.Context, assetType asset.Ite if err != nil { return account.Holdings{}, err } - return response, nil } @@ -269,50 +278,34 @@ func (c *CoinbasePro) FetchAccountInfo(ctx context.Context, assetType asset.Item return acc, nil } -// UpdateTickers updates the ticker for all currency pairs of a given asset type -func (c *CoinbasePro) UpdateTickers(_ context.Context, _ asset.Item) error { +// UpdateTickers updates all currency pairs of a given asset type +func (c *CoinbasePro) UpdateTickers(context.Context, asset.Item) error { return common.ErrFunctionNotSupported } // UpdateTicker updates and returns the ticker for a currency pair func (c *CoinbasePro) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) { - fPair, err := c.FormatExchangeCurrency(p, a) + verified, err := c.verificationCheck(ctx) if err != nil { return nil, err } - - tick, err := c.GetTicker(ctx, fPair.String()) + fPair, err := c.FormatExchangeCurrency(p, a) if err != nil { return nil, err } - stats, err := c.GetStats(ctx, fPair.String()) + err = c.tickerHelper(ctx, fPair.String(), a, verified) if err != nil { return nil, err } - - tickerPrice := &ticker.Price{ - Last: stats.Last, - High: stats.High, - Low: stats.Low, - Bid: tick.Bid, - Ask: tick.Ask, - Volume: tick.Volume, - Open: stats.Open, - Pair: p, - LastUpdated: tick.Time, - ExchangeName: c.Name, - AssetType: a} - - err = ticker.ProcessTicker(tickerPrice) - if err != nil { - return tickerPrice, err - } - return ticker.GetTicker(c.Name, p, a) } // FetchTicker returns the ticker for a currency pair func (c *CoinbasePro) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { + p, err := c.FormatExchangeCurrency(p, assetType) + if err != nil { + return nil, err + } tickerNew, err := ticker.GetTicker(c.Name, p, assetType) if err != nil { return c.UpdateTicker(ctx, p, assetType) @@ -322,6 +315,10 @@ func (c *CoinbasePro) FetchTicker(ctx context.Context, p currency.Pair, assetTyp // FetchOrderbook returns orderbook base on the currency pair func (c *CoinbasePro) FetchOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + p, err := c.FormatExchangeCurrency(p, assetType) + if err != nil { + return nil, err + } ob, err := orderbook.Get(c.Name, p, assetType) if err != nil { return c.UpdateOrderbook(ctx, p, assetType) @@ -331,10 +328,16 @@ func (c *CoinbasePro) FetchOrderbook(ctx context.Context, p currency.Pair, asset // UpdateOrderbook updates and returns the orderbook for a currency pair func (c *CoinbasePro) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - if p.IsEmpty() { - return nil, currency.ErrCurrencyPairEmpty + verified, err := c.verificationCheck(ctx) + if err != nil { + return nil, err + } + p, err = c.FormatExchangeCurrency(p, assetType) + if err != nil { + return nil, err } - if err := c.CurrencyPairs.IsAssetEnabled(assetType); err != nil { + err = c.CurrencyPairs.IsAssetEnabled(assetType) + if err != nil { return nil, err } book := &orderbook.Base{ @@ -343,34 +346,32 @@ func (c *CoinbasePro) UpdateOrderbook(ctx context.Context, p currency.Pair, asse Asset: assetType, VerifyOrderbook: c.CanVerifyOrderbook, } - fPair, err := c.FormatExchangeCurrency(p, assetType) - if err != nil { - return book, err - } - - orderbookNew, err := c.GetOrderbook(ctx, fPair.String(), 2) - if err != nil { - return book, err + var orderbookNew *ProductBookResp + if verified { + orderbookNew, err = c.GetProductBookV3(ctx, p, 1000, 0, true) + if err != nil { + log.Warnf(log.ExchangeSys, warnAuth, err) + verified = false + } } - - obNew, ok := orderbookNew.(OrderbookL1L2) - if !ok { - return book, common.GetTypeAssertError("OrderbookL1L2", orderbookNew) + if !verified { + orderbookNew, err = c.GetProductBookV3(ctx, p, 1000, 0, false) + if err != nil { + return book, err + } } - - book.Bids = make(orderbook.Tranches, len(obNew.Bids)) - for x := range obNew.Bids { + book.Bids = make(orderbook.Tranches, len(orderbookNew.Pricebook.Bids)) + for x := range orderbookNew.Pricebook.Bids { book.Bids[x] = orderbook.Tranche{ - Amount: obNew.Bids[x].Amount, - Price: obNew.Bids[x].Price, + Amount: orderbookNew.Pricebook.Bids[x].Size, + Price: orderbookNew.Pricebook.Bids[x].Price, } } - - book.Asks = make(orderbook.Tranches, len(obNew.Asks)) - for x := range obNew.Asks { + book.Asks = make(orderbook.Tranches, len(orderbookNew.Pricebook.Asks)) + for x := range orderbookNew.Pricebook.Asks { book.Asks[x] = orderbook.Tranche{ - Amount: obNew.Asks[x].Amount, - Price: obNew.Asks[x].Price, + Amount: orderbookNew.Pricebook.Asks[x].Size, + Price: orderbookNew.Pricebook.Asks[x].Price, } } err = book.Process() @@ -380,57 +381,106 @@ func (c *CoinbasePro) UpdateOrderbook(ctx context.Context, p currency.Pair, asse return orderbook.Get(c.Name, p, assetType) } -// GetAccountFundingHistory returns funding history, deposits and -// withdrawals -func (c *CoinbasePro) GetAccountFundingHistory(_ context.Context) ([]exchange.FundingHistory, error) { - return nil, common.ErrFunctionNotSupported -} - -// GetWithdrawalsHistory returns previous withdrawals data -func (c *CoinbasePro) GetWithdrawalsHistory(_ context.Context, _ currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) { - // while fetching withdrawal history is possible, the API response lacks any useful information - // like the currency withdrawn and thus is unsupported. If that position changes, use GetTransfers(...) - return nil, common.ErrFunctionNotSupported -} - -// GetRecentTrades returns the most recent trades for a currency and asset -func (c *CoinbasePro) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) { - var err error - p, err = c.FormatExchangeCurrency(p, assetType) +// GetAccountFundingHistory returns funding history, deposits and withdrawals +func (c *CoinbasePro) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) { + wallIDs, err := c.GetAllWallets(ctx, PaginationInp{}) if err != nil { return nil, err } - var tradeData []Trade - tradeData, err = c.GetTrades(ctx, p.String()) - if err != nil { - return nil, err + if len(wallIDs.Data) == 0 { + return nil, errNoWalletsReturned + } + var accHistory []DeposWithdrData + for i := range wallIDs.Data { + tempAccHist, err := c.GetAllFiatTransfers(ctx, wallIDs.Data[i].ID, PaginationInp{}, FiatDeposit) + if err != nil { + return nil, err + } + accHistory = append(accHistory, tempAccHist.Data...) + tempAccHist, err = c.GetAllFiatTransfers(ctx, wallIDs.Data[i].ID, PaginationInp{}, FiatWithdrawal) + if err != nil { + return nil, err + } + accHistory = append(accHistory, tempAccHist.Data...) } - resp := make([]trade.Data, len(tradeData)) - for i := range tradeData { - var side order.Side - side, err = order.StringToOrderSide(tradeData[i].Side) + var cryptoHistory []TransactionData + for i := range wallIDs.Data { + tempCryptoHist, err := c.GetAllTransactions(ctx, wallIDs.Data[i].ID, PaginationInp{}) if err != nil { return nil, err } - resp[i] = trade.Data{ - Exchange: c.Name, - TID: strconv.FormatInt(tradeData[i].TradeID, 10), - CurrencyPair: p, - AssetType: assetType, - Side: side, - Price: tradeData[i].Price, - Amount: tradeData[i].Size, - Timestamp: tradeData[i].Time, + for j := range tempCryptoHist.Data { + if tempCryptoHist.Data[j].Type == "receive" || tempCryptoHist.Data[j].Type == "send" { + cryptoHistory = append(cryptoHistory, tempCryptoHist.Data[j]) + } } } + fundingData := c.processFundingData(accHistory, cryptoHistory) + return fundingData, nil +} - err = c.AddTradesToBuffer(resp...) +// GetWithdrawalsHistory returns previous withdrawals data +func (c *CoinbasePro) GetWithdrawalsHistory(ctx context.Context, cur currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) { + tempWallIDs, err := c.GetAllWallets(ctx, PaginationInp{}) if err != nil { return nil, err } + if len(tempWallIDs.Data) == 0 { + return nil, errNoWalletsReturned + } + var wallIDs []string + for i := range tempWallIDs.Data { + if tempWallIDs.Data[i].Currency.Code == cur.String() { + wallIDs = append(wallIDs, tempWallIDs.Data[i].ID) + } + } + if len(wallIDs) == 0 { + return nil, errNoMatchingWallets + } + var accHistory []DeposWithdrData + for i := range wallIDs { + tempAccHist, err := c.GetAllFiatTransfers(ctx, wallIDs[i], PaginationInp{}, FiatWithdrawal) + if err != nil { + return nil, err + } + accHistory = append(accHistory, tempAccHist.Data...) + } + var cryptoHistory []TransactionData + for i := range wallIDs { + tempCryptoHist, err := c.GetAllTransactions(ctx, wallIDs[i], PaginationInp{}) + if err != nil { + return nil, err + } + for j := range tempCryptoHist.Data { + if tempCryptoHist.Data[j].Type == "send" { + cryptoHistory = append(cryptoHistory, tempCryptoHist.Data[j]) + } + } + } + tempFundingData := c.processFundingData(accHistory, cryptoHistory) + fundingData := make([]exchange.WithdrawalHistory, len(tempFundingData)) + for i := range tempFundingData { + fundingData[i] = exchange.WithdrawalHistory{ + Status: tempFundingData[i].Status, + TransferID: tempFundingData[i].TransferID, + Description: tempFundingData[i].Description, + Timestamp: tempFundingData[i].Timestamp, + Currency: tempFundingData[i].Currency, + Amount: tempFundingData[i].Amount, + Fee: tempFundingData[i].Fee, + TransferType: tempFundingData[i].TransferType, + CryptoToAddress: tempFundingData[i].CryptoToAddress, + CryptoTxID: tempFundingData[i].CryptoTxID, + CryptoChain: tempFundingData[i].CryptoChain, + BankTo: tempFundingData[i].BankTo, + } + } + return fundingData, nil +} - sort.Sort(trade.ByDate(resp)) - return resp, nil +// GetRecentTrades returns the most recent trades for a currency and asset +func (c *CoinbasePro) GetRecentTrades(context.Context, currency.Pair, asset.Item) ([]trade.Data, error) { + return nil, common.ErrFunctionNotSupported } // GetHistoricTrades returns historic trade data within the timeframe provided @@ -440,171 +490,230 @@ func (c *CoinbasePro) GetHistoricTrades(_ context.Context, _ currency.Pair, _ as // SubmitOrder submits a new order func (c *CoinbasePro) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(c.GetTradingRequirements()); err != nil { + err := s.Validate(c.GetTradingRequirements()) + if err != nil { return nil, err } - - fPair, err := c.FormatExchangeCurrency(s.Pair, asset.Spot) + fPair, err := c.FormatExchangeCurrency(s.Pair, s.AssetType) if err != nil { return nil, err } - - var orderID string - switch s.Type { - case order.Market: - orderID, err = c.PlaceMarketOrder(ctx, - "", - s.Amount, - s.QuoteAmount, - s.Side.Lower(), - fPair.String(), - "") - case order.Limit: - timeInForce := CoinbaseRequestParamsTimeGTC - if s.ImmediateOrCancel { - timeInForce = CoinbaseRequestParamsTimeIOC + var stopDir string + if s.Type == order.StopLimit { + switch s.StopDirection { + case order.StopUp: + stopDir = "STOP_DIRECTION_STOP_UP" + case order.StopDown: + stopDir = "STOP_DIRECTION_STOP_DOWN" } - orderID, err = c.PlaceLimitOrder(ctx, - "", - s.Price, - s.Amount, - s.Side.Lower(), - timeInForce, - "", - fPair.String(), - "", - false) - default: - err = fmt.Errorf("%w %v", order.ErrUnsupportedOrderType, s.Type) } + amount := s.Amount + if (s.Type == order.Market || s.Type == order.ImmediateOrCancel) && s.Side == order.Buy { + amount = s.QuoteAmount + } + resp, err := c.PlaceOrder(ctx, s.ClientOrderID, fPair.String(), s.Side.String(), stopDir, s.Type.String(), "", s.MarginType.Upper(), "", amount, s.Price, s.TriggerPrice, s.Leverage, s.PostOnly, s.EndTime) + if err != nil { + return nil, err + } + subResp, err := s.DeriveSubmitResponse(resp.OrderID) if err != nil { return nil, err } - return s.DeriveSubmitResponse(orderID) + if s.RetrieveFees { + time.Sleep(s.RetrieveFeeDelay) + feeResp, err := c.GetOrderByID(ctx, resp.OrderID, s.ClientOrderID, "") + if err != nil { + return nil, err + } + subResp.Fee = feeResp.TotalFees + } + return subResp, nil } -// ModifyOrder will allow of changing orderbook placement and limit to -// market conversion -func (c *CoinbasePro) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) { - return nil, common.ErrFunctionNotSupported +// ModifyOrder will allow of changing orderbook placement and limit to market conversion +func (c *CoinbasePro) ModifyOrder(ctx context.Context, m *order.Modify) (*order.ModifyResponse, error) { + if m == nil { + return nil, common.ErrNilPointer + } + err := m.Validate() + if err != nil { + return nil, err + } + success, err := c.EditOrder(ctx, m.OrderID, m.Amount, m.Price) + if err != nil { + return nil, err + } + if !success { + return nil, errOrderModFailNoRet + } + return m.DeriveModifyResponse() } // CancelOrder cancels an order by its corresponding ID number func (c *CoinbasePro) CancelOrder(ctx context.Context, o *order.Cancel) error { - if err := o.Validate(o.StandardCancel()); err != nil { + if o == nil { + return common.ErrNilPointer + } + err := o.Validate(o.StandardCancel()) + if err != nil { return err } - return c.CancelExistingOrder(ctx, o.OrderID) + canSlice := []order.Cancel{*o} + resp, err := c.CancelBatchOrders(ctx, canSlice) + if err != nil { + return err + } + if resp.Status[o.OrderID] != order.Cancelled.String() { + return fmt.Errorf("%w %v", errOrderFailedToCancel, o.OrderID) + } + return err } -// CancelBatchOrders cancels an orders by their corresponding ID numbers -func (c *CoinbasePro) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) { - return nil, common.ErrFunctionNotSupported +// CancelBatchOrders cancels orders by their corresponding ID numbers +func (c *CoinbasePro) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.CancelBatchResponse, error) { + var status order.CancelBatchResponse + ordToCancel := len(o) + if ordToCancel == 0 { + return nil, errOrderIDEmpty + } + status.Status = make(map[string]string) + ordIDSlice := make([]string, ordToCancel) + for i := range o { + err := o[i].Validate(o[i].StandardCancel()) + if err != nil { + return nil, err + } + ordIDSlice[i] = o[i].OrderID + status.Status[o[i].OrderID] = "Failed to cancel" + } + resp := struct { + Results []OrderCancelDetail `json:"results"` + }{} + for i := 0; i < ordToCancel; i += 100 { + var tempOrdIDSlice []string + if ordToCancel-i < 100 { + tempOrdIDSlice = ordIDSlice[i:] + } else { + tempOrdIDSlice = ordIDSlice[i : i+100] + } + tempResp, err := c.CancelOrders(ctx, tempOrdIDSlice) + if err != nil { + return nil, err + } + resp.Results = append(resp.Results, tempResp...) + } + for i := range resp.Results { + if resp.Results[i].Success { + status.Status[resp.Results[i].OrderID] = order.Cancelled.String() + } + } + return &status, nil } // CancelAllOrders cancels all orders associated with a currency pair -func (c *CoinbasePro) CancelAllOrders(ctx context.Context, _ *order.Cancel) (order.CancelAllResponse, error) { - // CancellAllExisting orders returns a list of successful cancellations, we're only interested in failures - _, err := c.CancelAllExistingOrders(ctx, "") - return order.CancelAllResponse{}, err +func (c *CoinbasePro) CancelAllOrders(context.Context, *order.Cancel) (order.CancelAllResponse, error) { + return order.CancelAllResponse{}, common.ErrFunctionNotSupported } // GetOrderInfo returns order information based on order ID -func (c *CoinbasePro) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair, _ asset.Item) (*order.Detail, error) { - genOrderDetail, err := c.GetOrder(ctx, orderID) - if err != nil { - return nil, fmt.Errorf("error retrieving order %s : %w", orderID, err) - } - orderStatus, err := order.StringToOrderStatus(genOrderDetail.Status) +func (c *CoinbasePro) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetItem asset.Item) (*order.Detail, error) { + genOrderDetail, err := c.GetOrderByID(ctx, orderID, "", "") if err != nil { - return nil, fmt.Errorf("error parsing order status: %w", err) - } - orderType, err := order.StringToOrderType(genOrderDetail.Type) - if err != nil { - return nil, fmt.Errorf("error parsing order type: %w", err) - } - orderSide, err := order.StringToOrderSide(genOrderDetail.Side) - if err != nil { - return nil, fmt.Errorf("error parsing order side: %w", err) + return nil, err } - pair, err := currency.NewPairDelimiter(genOrderDetail.ProductID, "-") + response := c.getOrderRespToOrderDetail(genOrderDetail, pair, assetItem) + fillData, err := c.GetFills(ctx, orderID, "", "", time.Time{}, time.Now(), manyFills) if err != nil { - return nil, fmt.Errorf("error parsing order pair: %w", err) + return nil, err } - - response := order.Detail{ - Exchange: c.GetName(), - OrderID: genOrderDetail.ID, - Pair: pair, - Side: orderSide, - Type: orderType, - Date: genOrderDetail.DoneAt, - Status: orderStatus, - Price: genOrderDetail.Price, - Amount: genOrderDetail.Size, - ExecutedAmount: genOrderDetail.FilledSize, - RemainingAmount: genOrderDetail.Size - genOrderDetail.FilledSize, - Fee: genOrderDetail.FillFees, - } - fillResponse, err := c.GetFills(ctx, orderID, genOrderDetail.ProductID) - if err != nil { - return nil, fmt.Errorf("error retrieving the order fills: %w", err) - } - for i := range fillResponse { - var fillSide order.Side - fillSide, err = order.StringToOrderSide(fillResponse[i].Side) + cursor := fillData.Cursor + for cursor != "" { + tempFillData, err := c.GetFills(ctx, orderID, "", cursor, time.Time{}, time.Now(), manyFills) if err != nil { - return nil, fmt.Errorf("error parsing order Side: %w", err) + return nil, err } - response.Trades = append(response.Trades, order.TradeHistory{ - Timestamp: fillResponse[i].CreatedAt, - TID: strconv.FormatInt(fillResponse[i].TradeID, 10), - Price: fillResponse[i].Price, - Amount: fillResponse[i].Size, + fillData.Fills = append(fillData.Fills, tempFillData.Fills...) + cursor = tempFillData.Cursor + } + response.Trades = make([]order.TradeHistory, len(fillData.Fills)) + var orderSide order.Side + switch response.Side { + case order.Buy: + orderSide = order.Sell + case order.Sell: + orderSide = order.Buy + } + for i := range fillData.Fills { + response.Trades[i] = order.TradeHistory{ + Price: fillData.Fills[i].Price, + Amount: fillData.Fills[i].Size, + Fee: fillData.Fills[i].Commission, Exchange: c.GetName(), - Type: orderType, - Side: fillSide, - Fee: fillResponse[i].Fee, - }) + TID: fillData.Fills[i].TradeID, + Side: orderSide, + Timestamp: fillData.Fills[i].TradeTime, + Total: fillData.Fills[i].Price * fillData.Fills[i].Size, + } } - return &response, nil + return response, nil } // GetDepositAddress returns a deposit address for a specified currency -func (c *CoinbasePro) GetDepositAddress(_ context.Context, _ currency.Code, _, _ string) (*deposit.Address, error) { - return nil, common.ErrFunctionNotSupported +func (c *CoinbasePro) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, _ string) (*deposit.Address, error) { + allWalResp, err := c.GetAllWallets(ctx, PaginationInp{}) + if err != nil { + return nil, err + } + var targetWalletID string + for i := range allWalResp.Data { + if allWalResp.Data[i].Currency.Code == cryptocurrency.String() { + targetWalletID = allWalResp.Data[i].ID + break + } + } + if targetWalletID == "" { + return nil, errNoWalletForCurrency + } + resp, err := c.CreateAddress(ctx, targetWalletID, "") + if err != nil { + return nil, err + } + return &deposit.Address{ + Address: resp.Address, + Tag: resp.Name, + Chain: resp.Network, + }, nil } -// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is -// submitted +// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is submitted func (c *CoinbasePro) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { if err := withdrawRequest.Validate(); err != nil { return nil, err } - resp, err := c.WithdrawCrypto(ctx, - withdrawRequest.Amount, - withdrawRequest.Currency.String(), - withdrawRequest.Crypto.Address) + if withdrawRequest.WalletID == "" { + return nil, errWalletIDEmpty + } + message := generateIdempotency(withdrawRequest.Amount) + resp, err := c.SendMoney(ctx, "send", withdrawRequest.WalletID, withdrawRequest.Crypto.Address, withdrawRequest.Currency.String(), withdrawRequest.Description, message, "", withdrawRequest.Crypto.AddressTag, withdrawRequest.Amount, false, false) if err != nil { return nil, err } - return &withdraw.ExchangeResponse{ - ID: resp.ID, - }, err + return &withdraw.ExchangeResponse{Name: resp.Network.Name, ID: resp.ID, Status: resp.Status}, nil } -// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is -// submitted +// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted func (c *CoinbasePro) WithdrawFiatFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { if err := withdrawRequest.Validate(); err != nil { return nil, err } - paymentMethods, err := c.GetPayMethods(ctx) + if withdrawRequest.WalletID == "" { + return nil, errWalletIDEmpty + } + paymentMethods, err := c.GetAllPaymentMethods(ctx) if err != nil { return nil, err } - - selectedWithdrawalMethod := PaymentMethod{} + selectedWithdrawalMethod := PaymentMethodData{} for i := range paymentMethods { if withdrawRequest.Fiat.Bank.BankName == paymentMethods[i].Name { selectedWithdrawalMethod = paymentMethods[i] @@ -612,36 +721,22 @@ func (c *CoinbasePro) WithdrawFiatFunds(ctx context.Context, withdrawRequest *wi } } if selectedWithdrawalMethod.ID == "" { - return nil, fmt.Errorf("could not find payment method '%v'. Check the name via the website and try again", withdrawRequest.Fiat.Bank.BankName) + return nil, fmt.Errorf("%w %v", errPayMethodNotFound, withdrawRequest.Fiat.Bank.BankName) } - - resp, err := c.WithdrawViaPaymentMethod(ctx, - withdrawRequest.Amount, - withdrawRequest.Currency.String(), - selectedWithdrawalMethod.ID) + resp, err := c.FiatTransfer(ctx, withdrawRequest.WalletID, withdrawRequest.Currency.String(), selectedWithdrawalMethod.ID, withdrawRequest.Amount, true, FiatWithdrawal) if err != nil { return nil, err } - return &withdraw.ExchangeResponse{ - Status: resp.ID, + Name: selectedWithdrawalMethod.Name, + ID: resp.ID, + Status: resp.Status, }, nil } -// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a -// withdrawal is submitted +// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted func (c *CoinbasePro) WithdrawFiatFundsToInternationalBank(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { - if err := withdrawRequest.Validate(); err != nil { - return nil, err - } - v, err := c.WithdrawFiatFunds(ctx, withdrawRequest) - if err != nil { - return nil, err - } - return &withdraw.ExchangeResponse{ - ID: v.ID, - Status: v.Status, - }, nil + return c.WithdrawFiatFunds(ctx, withdrawRequest) } // GetFeeByType returns an estimate of fee based on type of transaction @@ -649,8 +744,7 @@ func (c *CoinbasePro) GetFeeByType(ctx context.Context, feeBuilder *exchange.Fee if feeBuilder == nil { return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) } - if !c.AreCredentialsValid(ctx) && // Todo check connection status - feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { + if !c.AreCredentialsValid(ctx) && feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } return c.GetFee(ctx, feeBuilder) @@ -658,173 +752,84 @@ func (c *CoinbasePro) GetFeeByType(ctx context.Context, feeBuilder *exchange.Fee // GetActiveOrders retrieves any orders that are active/open func (c *CoinbasePro) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) { + if req == nil { + return nil, common.ErrNilPointer + } err := req.Validate() if err != nil { return nil, err } - var respOrders []GeneralizedOrderResponse - var fPair currency.Pair - for i := range req.Pairs { - fPair, err = c.FormatExchangeCurrency(req.Pairs[i], asset.Spot) - if err != nil { - return nil, err - } - - var resp []GeneralizedOrderResponse - resp, err = c.GetOrders(ctx, - []string{"open", "pending", "active"}, - fPair.String()) - if err != nil { - return nil, err - } - respOrders = append(respOrders, resp...) + var respOrders []GetOrderResponse + ordStatus := []string{"OPEN"} + pairIDs := req.Pairs.Strings() + if len(pairIDs) == 1 { + respOrders, err = c.iterativeGetAllOrders(ctx, pairIDs[0], req.Type.String(), req.Side.String(), req.AssetType.Upper(), ordStatus, 1000, req.StartTime, req.EndTime) + } else { + respOrders, err = c.iterativeGetAllOrders(ctx, "", req.Type.String(), req.Side.String(), req.AssetType.Upper(), ordStatus, 1000, req.StartTime, req.EndTime) } - - format, err := c.GetPairFormat(asset.Spot, false) if err != nil { return nil, err } - orders := make([]order.Detail, len(respOrders)) for i := range respOrders { - var curr currency.Pair - curr, err = currency.NewPairDelimiter(respOrders[i].ProductID, - format.Delimiter) - if err != nil { - return nil, err - } - var side order.Side - side, err = order.StringToOrderSide(respOrders[i].Side) - if err != nil { - return nil, err - } - var orderType order.Type - orderType, err = order.StringToOrderType(respOrders[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", c.Name, err) - } - orders[i] = order.Detail{ - OrderID: respOrders[i].ID, - Amount: respOrders[i].Size, - ExecutedAmount: respOrders[i].FilledSize, - Type: orderType, - Date: respOrders[i].CreatedAt, - Side: side, - Pair: curr, - Exchange: c.Name, - } + orderRec := c.getOrderRespToOrderDetail(&respOrders[i], respOrders[i].ProductID, asset.Spot) + orders[i] = *orderRec + } + if len(pairIDs) > 1 { + order.FilterOrdersByPairs(&orders, req.Pairs) } return req.Filter(c.Name, orders), nil } -// GetOrderHistory retrieves account order information -// Can Limit response to specific order status +// GetOrderHistory retrieves account order information. Can Limit response to specific order status func (c *CoinbasePro) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) { err := req.Validate() if err != nil { return nil, err } - var respOrders []GeneralizedOrderResponse - if len(req.Pairs) > 0 { - var fPair currency.Pair - var resp []GeneralizedOrderResponse - for i := range req.Pairs { - fPair, err = c.FormatExchangeCurrency(req.Pairs[i], asset.Spot) - if err != nil { - return nil, err - } - resp, err = c.GetOrders(ctx, []string{"done"}, fPair.String()) - if err != nil { - return nil, err - } - respOrders = append(respOrders, resp...) - } - } else { - respOrders, err = c.GetOrders(ctx, []string{"done"}, "") + var p string + if len(req.Pairs) == 1 { + req.Pairs[0], err = c.FormatExchangeCurrency(req.Pairs[0], req.AssetType) if err != nil { return nil, err } + p = req.Pairs[0].String() } - - format, err := c.GetPairFormat(asset.Spot, false) + var ord []GetOrderResponse + interOrd, err := c.iterativeGetAllOrders(ctx, p, req.Type.String(), req.Side.String(), req.AssetType.Upper(), closedStatuses, manyOrds, req.StartTime, req.EndTime) if err != nil { return nil, err } - - orders := make([]order.Detail, len(respOrders)) - for i := range respOrders { - var curr currency.Pair - curr, err = currency.NewPairDelimiter(respOrders[i].ProductID, - format.Delimiter) - if err != nil { - return nil, err - } - var side order.Side - side, err = order.StringToOrderSide(respOrders[i].Side) - if err != nil { - return nil, err - } - var orderStatus order.Status - orderStatus, err = order.StringToOrderStatus(respOrders[i].Status) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", c.Name, err) - } - var orderType order.Type - orderType, err = order.StringToOrderType(respOrders[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", c.Name, err) - } - detail := order.Detail{ - OrderID: respOrders[i].ID, - Amount: respOrders[i].Size, - ExecutedAmount: respOrders[i].FilledSize, - RemainingAmount: respOrders[i].Size - respOrders[i].FilledSize, - Cost: respOrders[i].ExecutedValue, - CostAsset: curr.Quote, - Type: orderType, - Date: respOrders[i].CreatedAt, - CloseTime: respOrders[i].DoneAt, - Fee: respOrders[i].FillFees, - FeeAsset: curr.Quote, - Side: side, - Status: orderStatus, - Pair: curr, - Price: respOrders[i].Price, - Exchange: c.Name, - } - detail.InferCostsAndTimes() - orders[i] = detail + ord = append(ord, interOrd...) + interOrd, err = c.iterativeGetAllOrders(ctx, p, req.Type.String(), req.Side.String(), req.AssetType.Upper(), openStatus, manyOrds, req.StartTime, req.EndTime) + if err != nil { + return nil, err + } + ord = append(ord, interOrd...) + orders := make([]order.Detail, len(ord)) + for i := range ord { + singleOrder := c.getOrderRespToOrderDetail(&ord[i], ord[i].ProductID, req.AssetType) + orders[i] = *singleOrder + } + if len(req.Pairs) > 1 { + order.FilterOrdersByPairs(&orders, req.Pairs) } return req.Filter(c.Name, orders), nil } -// GetHistoricCandles returns a set of candle between two time periods for a -// designated time period +// GetHistoricCandles returns a set of candle between two time periods for a designated time period func (c *CoinbasePro) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { req, err := c.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } - - history, err := c.GetHistoricRates(ctx, - req.RequestFormatted.String(), - start.Format(time.RFC3339), - end.Format(time.RFC3339), - int64(req.ExchangeInterval.Duration().Seconds())) + verified, err := c.verificationCheck(ctx) if err != nil { return nil, err } - - timeSeries := make([]kline.Candle, len(history)) - for x := range history { - timeSeries[x] = kline.Candle{ - Time: history[x].Time, - Low: history[x].Low, - High: history[x].High, - Open: history[x].Open, - Close: history[x].Close, - Volume: history[x].Volume, - } + timeSeries, err := c.candleHelper(ctx, req.RequestFormatted.String(), interval, start, end, verified) + if err != nil { + return nil, err } return req.ProcessResponse(timeSeries) } @@ -835,35 +840,22 @@ func (c *CoinbasePro) GetHistoricCandlesExtended(ctx context.Context, pair curre if err != nil { return nil, err } - - timeSeries := make([]kline.Candle, 0, req.Size()) + verified, err := c.verificationCheck(ctx) + if err != nil { + return nil, err + } + var timeSeries []kline.Candle for x := range req.RangeHolder.Ranges { - var history []History - history, err = c.GetHistoricRates(ctx, - req.RequestFormatted.String(), - req.RangeHolder.Ranges[x].Start.Time.Format(time.RFC3339), - req.RangeHolder.Ranges[x].End.Time.Format(time.RFC3339), - int64(req.ExchangeInterval.Duration().Seconds())) + hist, err := c.candleHelper(ctx, req.RequestFormatted.String(), interval, req.RangeHolder.Ranges[x].Start.Time.Add(-time.Nanosecond), req.RangeHolder.Ranges[x].End.Time.Add(-time.Nanosecond), verified) if err != nil { return nil, err } - - for i := range history { - timeSeries = append(timeSeries, kline.Candle{ - Time: history[i].Time, - Low: history[i].Low, - High: history[i].High, - Open: history[i].Open, - Close: history[i].Close, - Volume: history[i].Volume, - }) - } + timeSeries = append(timeSeries, hist...) } return req.ProcessResponse(timeSeries) } -// ValidateAPICredentials validates current credentials used for wrapper -// functionality +// ValidateAPICredentials validates current credentials used for wrapper functionality func (c *CoinbasePro) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error { _, err := c.UpdateAccountInfo(ctx, assetType) return c.CheckTransientError(err) @@ -871,7 +863,7 @@ func (c *CoinbasePro) ValidateAPICredentials(ctx context.Context, assetType asse // GetServerTime returns the current exchange server time. func (c *CoinbasePro) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) { - st, err := c.GetCurrentServerTime(ctx) + st, err := c.GetV2Time(ctx) if err != nil { return time.Time{}, err } @@ -879,18 +871,119 @@ func (c *CoinbasePro) GetServerTime(ctx context.Context, _ asset.Item) (time.Tim } // GetLatestFundingRates returns the latest funding rates data -func (c *CoinbasePro) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { - return nil, common.ErrFunctionNotSupported +func (c *CoinbasePro) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, common.ErrNilPointer + } + if !c.SupportsAsset(r.Asset) { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + verified, err := c.verificationCheck(ctx) + if err != nil { + return nil, err + } + products, perpStart, err := c.fetchFutures(ctx, verified) + if err != nil { + return nil, err + } + funding := make([]fundingrate.LatestRateResponse, len(products.Products)) + for i := perpStart; i < len(products.Products); i++ { + funRate := fundingrate.Rate{ + Time: products.Products[i].FutureProductDetails.PerpetualDetails.FundingTime, + Rate: decimal.NewFromFloat(products.Products[i].FutureProductDetails.PerpetualDetails.FundingRate.Float64()), + } + funding[i] = fundingrate.LatestRateResponse{ + Exchange: c.Name, + Asset: r.Asset, + Pair: products.Products[i].ID, + LatestRate: funRate, + TimeChecked: time.Now(), + } + } + return funding, nil } // GetFuturesContractDetails returns all contracts from the exchange by asset type -func (c *CoinbasePro) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { - return nil, common.ErrFunctionNotSupported +func (c *CoinbasePro) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) { + if !item.IsFutures() { + return nil, futures.ErrNotFuturesAsset + } + if !c.SupportsAsset(item) { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item) + } + verified, err := c.verificationCheck(ctx) + if err != nil { + return nil, err + } + products, perpStart, err := c.fetchFutures(ctx, verified) + if err != nil { + return nil, err + } + contracts := make([]futures.Contract, len(products.Products)) + for i := range products.Products { + funRate := fundingrate.Rate{ + Time: products.Products[i].FutureProductDetails.PerpetualDetails.FundingTime, + Rate: decimal.NewFromFloat(products.Products[i].FutureProductDetails.PerpetualDetails.FundingRate.Float64()), + } + contracts[i] = futures.Contract{ + Exchange: c.Name, + Name: products.Products[i].ID, + Asset: item, + EndDate: products.Products[i].FutureProductDetails.ContractExpiry, + IsActive: !products.Products[i].IsDisabled, + Status: products.Products[i].Status, + SettlementCurrencies: currency.Currencies{products.Products[i].QuoteCurrencyID}, + Multiplier: products.Products[i].BaseIncrement.Float64(), + LatestRate: funRate, + } + if i < perpStart { + contracts[i].Type = futures.LongDated + } else { + contracts[i].Type = futures.Perpetual + } + } + return contracts, nil } // UpdateOrderExecutionLimits updates order execution limits -func (c *CoinbasePro) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { - return common.ErrNotYetImplemented +func (c *CoinbasePro) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + var data *AllProducts + verified, err := c.verificationCheck(ctx) + if err != nil { + return err + } + aString := FormatAssetOutbound(a) + if verified { + data, err = c.GetAllProducts(ctx, 0, 0, aString, "", "", nil, true) + if err != nil { + log.Warnf(log.ExchangeSys, warnAuth, err) + verified = false + } + } + if !verified { + data, err = c.GetAllProducts(ctx, 0, 0, aString, "", "", nil, false) + if err != nil { + return err + } + } + limits := make([]order.MinMaxLevel, len(data.Products)) + for i := range data.Products { + limits[i] = order.MinMaxLevel{ + Pair: data.Products[i].ID, + Asset: a, + MinPrice: data.Products[i].QuoteMinSize.Float64(), + MaxPrice: data.Products[i].QuoteMaxSize.Float64(), + PriceStepIncrementSize: data.Products[i].PriceIncrement.Float64(), + MinimumBaseAmount: data.Products[i].BaseMinSize.Float64(), + MaximumBaseAmount: data.Products[i].BaseMaxSize.Float64(), + MinimumQuoteAmount: data.Products[i].QuoteMinSize.Float64(), + MaximumQuoteAmount: data.Products[i].QuoteMaxSize.Float64(), + AmountStepIncrementSize: data.Products[i].BaseIncrement.Float64(), + QuoteStepIncrementSize: data.Products[i].QuoteIncrement.Float64(), + MaxTotalOrders: 1000, + } + } + return c.LoadLimits(limits) } // GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair @@ -902,3 +995,293 @@ func (c *CoinbasePro) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp cu cp.Delimiter = currency.DashDelimiter return tradeBaseURL + cp.Upper().String(), nil } + +// fetchFutures is a helper function for FetchTradablePairs, GetLatestFundingRates, GetFuturesContractDetails, and UpdateOrderExecutionLimits that calls the List Products endpoint twice, to get both expiring futures and perpetual futures +func (c *CoinbasePro) fetchFutures(ctx context.Context, verified bool) (*AllProducts, int, error) { + products, err := c.GetAllProducts(ctx, 0, 0, "FUTURE", "", "", nil, verified) + if err != nil { + if verified { + return c.fetchFutures(ctx, false) + } + return nil, 0, err + } + products2, err := c.GetAllProducts(ctx, 0, 0, "FUTURE", "PERPETUAL", "", nil, verified) + if err != nil { + if verified { + return c.fetchFutures(ctx, false) + } + return nil, 0, err + } + perpStart := len(products.Products) + products.Products = append(products.Products, products2.Products...) + return products, perpStart, nil +} + +// processFundingData is a helper function for GetAccountFundingHistory and GetWithdrawalsHistory, transforming the data returned by the Coinbase API into a format suitable for the exchange package +func (c *CoinbasePro) processFundingData(accHistory []DeposWithdrData, cryptoHistory []TransactionData) []exchange.FundingHistory { + fundingData := make([]exchange.FundingHistory, len(accHistory)+len(cryptoHistory)) + for i := range accHistory { + fundingData[i] = exchange.FundingHistory{ + ExchangeName: c.Name, + Status: accHistory[i].Status, + TransferID: accHistory[i].ID, + Timestamp: accHistory[i].PayoutAt, + Currency: accHistory[i].Amount.Currency, + Amount: accHistory[i].Amount.Amount, + Fee: accHistory[i].Fee.Amount, + TransferType: accHistory[i].TransferType.String(), + } + } + for i := range cryptoHistory { + fundingData[i+len(accHistory)] = exchange.FundingHistory{ + ExchangeName: c.Name, + Status: cryptoHistory[i].Status, + TransferID: cryptoHistory[i].ID, + Description: cryptoHistory[i].Details.Title + cryptoHistory[i].Details.Subtitle, + Timestamp: cryptoHistory[i].CreatedAt, + Currency: cryptoHistory[i].Amount.Currency, + Amount: cryptoHistory[i].Amount.Amount, + CryptoChain: cryptoHistory[i].Network.Name, + } + if cryptoHistory[i].Type == "receive" { + fundingData[i+len(accHistory)].TransferType = "deposit" + fundingData[i+len(accHistory)].CryptoFromAddress = cryptoHistory[i].To.ID + } + if cryptoHistory[i].Type == "send" { + fundingData[i+len(accHistory)].TransferType = "withdrawal" + fundingData[i+len(accHistory)].CryptoToAddress = cryptoHistory[i].From.ID + } + } + return fundingData +} + +// iterativeGetAllOrders is a helper function used in GetActiveOrders and GetOrderHistory to repeatedly call GetAllOrders until all orders have been retrieved +func (c *CoinbasePro) iterativeGetAllOrders(ctx context.Context, productID, orderType, orderSide, productType string, orderStatus []string, limit int32, startDate, endDate time.Time) ([]GetOrderResponse, error) { + hasNext := true + var resp []GetOrderResponse + var cursor string + if orderSide == "ANY" { + orderSide = "" + } + if orderType == "ANY" { + orderType = "" + } + if productType == "FUTURES" { + productType = "FUTURE" + } + for hasNext { + interResp, err := c.GetAllOrders(ctx, productID, "", orderType, orderSide, cursor, productType, "", "", "", orderStatus, nil, limit, startDate, endDate) + if err != nil { + return nil, err + } + resp = append(resp, interResp.Orders...) + hasNext = interResp.HasNext + cursor = interResp.Cursor + } + return resp, nil +} + +// FormatExchangeKlineIntervalV3 is a helper function used in GetHistoricCandles and GetHistoricCandlesExtended to convert kline.Interval to the string format used by V3 of Coinbase's API +func FormatExchangeKlineIntervalV3(interval kline.Interval) (string, error) { + switch interval { + case kline.OneMin: + return granOneMin, nil + case kline.FiveMin: + return granFiveMin, nil + case kline.FifteenMin: + return granFifteenMin, nil + case kline.ThirtyMin: + return granThirtyMin, nil + case kline.OneHour: + return granOneHour, nil + case kline.TwoHour: + return granTwoHour, nil + case kline.SixHour: + return granSixHour, nil + case kline.OneDay: + return granOneDay, nil + } + return "", errIntervalNotSupported +} + +// getOrderRespToOrderDetail is a helper function used in GetOrderInfo, GetActiveOrders, and GetOrderHistory to convert data returned by the Coinbase API into a format suitable for the exchange package +func (c *CoinbasePro) getOrderRespToOrderDetail(genOrderDetail *GetOrderResponse, pair currency.Pair, assetItem asset.Item) *order.Detail { + var amount float64 + var quoteAmount float64 + var orderType order.Type + if genOrderDetail.OrderConfiguration.MarketMarketIOC != nil { + quoteAmount = genOrderDetail.OrderConfiguration.MarketMarketIOC.QuoteSize.Float64() + amount = genOrderDetail.OrderConfiguration.MarketMarketIOC.BaseSize.Float64() + orderType = order.Market + } + var price float64 + var postOnly bool + if genOrderDetail.OrderConfiguration.LimitLimitGTC != nil { + amount = genOrderDetail.OrderConfiguration.LimitLimitGTC.BaseSize.Float64() + price = genOrderDetail.OrderConfiguration.LimitLimitGTC.LimitPrice.Float64() + postOnly = genOrderDetail.OrderConfiguration.LimitLimitGTC.PostOnly + orderType = order.Limit + } + if genOrderDetail.OrderConfiguration.LimitLimitGTD != nil { + amount = genOrderDetail.OrderConfiguration.LimitLimitGTD.BaseSize.Float64() + price = genOrderDetail.OrderConfiguration.LimitLimitGTD.LimitPrice.Float64() + postOnly = genOrderDetail.OrderConfiguration.LimitLimitGTD.PostOnly + orderType = order.Limit + } + var triggerPrice float64 + if genOrderDetail.OrderConfiguration.StopLimitStopLimitGTC != nil { + amount = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTC.BaseSize.Float64() + price = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTC.LimitPrice.Float64() + triggerPrice = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTC.StopPrice.Float64() + orderType = order.StopLimit + } + if genOrderDetail.OrderConfiguration.StopLimitStopLimitGTD != nil { + amount = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTD.BaseSize.Float64() + price = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTD.LimitPrice.Float64() + triggerPrice = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTD.StopPrice.Float64() + orderType = order.StopLimit + } + var remainingAmount float64 + if !genOrderDetail.SizeInQuote { + remainingAmount = amount - genOrderDetail.FilledSize + } + var orderSide order.Side + switch genOrderDetail.Side { + case order.Buy.String(): + orderSide = order.Buy + case order.Sell.String(): + orderSide = order.Sell + } + var orderStatus order.Status + switch genOrderDetail.Status { + case order.Open.String(): + orderStatus = order.Open + case order.Filled.String(): + orderStatus = order.Filled + case order.Cancelled.String(): + orderStatus = order.Cancelled + case order.Expired.String(): + orderStatus = order.Expired + case "FAILED": + orderStatus = order.Rejected + case "UNKNOWN_ORDER_STATUS": + orderStatus = order.UnknownStatus + } + var closeTime time.Time + if genOrderDetail.Settled { + closeTime = genOrderDetail.LastFillTime + } + var lastUpdateTime time.Time + if len(genOrderDetail.EditHistory) > 0 { + lastUpdateTime = genOrderDetail.EditHistory[len(genOrderDetail.EditHistory)-1].ReplaceAcceptTimestamp + } + response := order.Detail{ + ImmediateOrCancel: genOrderDetail.OrderConfiguration.MarketMarketIOC != nil, + PostOnly: postOnly, + Price: price, + Amount: amount, + TriggerPrice: triggerPrice, + AverageExecutedPrice: genOrderDetail.AverageFilledPrice, + QuoteAmount: quoteAmount, + ExecutedAmount: genOrderDetail.FilledSize, + RemainingAmount: remainingAmount, + Cost: genOrderDetail.TotalValueAfterFees, + Fee: genOrderDetail.TotalFees, + Exchange: c.GetName(), + OrderID: genOrderDetail.OrderID, + ClientOrderID: genOrderDetail.ClientOID, + ClientID: genOrderDetail.UserID, + Type: orderType, + Side: orderSide, + Status: orderStatus, + AssetType: assetItem, + Date: genOrderDetail.CreatedTime, + CloseTime: closeTime, + LastUpdated: lastUpdateTime, + Pair: pair, + } + return &response +} + +// VerificationCheck returns whether authentication support is enabled or not +func (c *CoinbasePro) verificationCheck(ctx context.Context) (bool, error) { + _, err := c.GetCredentials(ctx) + if err != nil { + if errors.Is(err, exchange.ErrAuthenticationSupportNotEnabled) || errors.Is(err, exchange.ErrCredentialsAreEmpty) { + return false, nil + } + return false, err + } + return true, nil +} + +// TickerHelper fetches the ticker for a given currency pair, used by UpdateTickers and UpdateTicker +func (c *CoinbasePro) tickerHelper(ctx context.Context, name string, assetType asset.Item, verified bool) error { + pair, err := currency.NewPairDelimiter(name, currency.DashDelimiter) + if err != nil { + return err + } + newTick := &ticker.Price{ + Pair: pair, + ExchangeName: c.Name, + AssetType: assetType, + } + ticks, err := c.GetTicker(ctx, name, 1, time.Time{}, time.Time{}, verified) + if err != nil { + if verified { + return c.tickerHelper(ctx, name, assetType, false) + } + return err + } + var last float64 + if len(ticks.Trades) != 0 { + last = ticks.Trades[0].Price + } + newTick.Last = last + newTick.Bid = ticks.BestBid.Float64() + newTick.Ask = ticks.BestAsk.Float64() + return ticker.ProcessTicker(newTick) +} + +// CandleHelper handles calling the candle function, and doing preliminary work on the data +func (c *CoinbasePro) candleHelper(ctx context.Context, pair string, granularity kline.Interval, start, end time.Time, verified bool) ([]kline.Candle, error) { + granString, err := FormatExchangeKlineIntervalV3(granularity) + if err != nil { + return nil, err + } + history, err := c.GetHistoricRates(ctx, pair, granString, start, end, verified) + if err != nil { + if verified { + return c.candleHelper(ctx, pair, granularity, start, end, false) + } + return nil, err + } + timeSeries := make([]kline.Candle, len(history)) + for x := range history { + timeSeries[x] = kline.Candle{ + Time: history[x].Start.Time(), + Low: history[x].Low, + High: history[x].High, + Open: history[x].Open, + Close: history[x].Close, + Volume: history[x].Volume, + } + } + return timeSeries, nil +} + +// XOR's the current time with the amount to cheaply generate an idempotency token where unwanted collisions should be rare +func generateIdempotency(am float64) string { + t := time.Now().UnixNano() + u := math.Float64bits(am) + t ^= int64(u) + return strconv.FormatInt(t, 10) +} + +// FormatAssetOutbound formats asset items for outbound requests +func FormatAssetOutbound(a asset.Item) string { + if a == asset.Futures { + return "FUTURE" + } + return a.Upper() +} diff --git a/exchanges/coinbasepro/ratelimit.go b/exchanges/coinbasepro/ratelimit.go index e636f6660d9..ada1444c49c 100644 --- a/exchanges/coinbasepro/ratelimit.go +++ b/exchanges/coinbasepro/ratelimit.go @@ -6,17 +6,22 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) -// Coinbasepro rate limit conts +// Coinbase pro rate limits const ( - coinbaseproRateInterval = time.Second - coinbaseproAuthRate = 5 - coinbaseproUnauthRate = 2 + V2Rate request.EndpointLimit = iota + V3Rate + WSAuthRate + WSUnauthRate + PubRate ) // GetRateLimit returns the rate limit for the exchange func GetRateLimit() request.RateLimitDefinitions { return request.RateLimitDefinitions{ - request.Auth: request.NewRateLimitWithWeight(coinbaseproRateInterval, coinbaseproAuthRate, 1), - request.UnAuth: request.NewRateLimitWithWeight(coinbaseproRateInterval, coinbaseproUnauthRate, 1), + V2Rate: request.NewRateLimitWithWeight(time.Hour, 10000, 1), + V3Rate: request.NewRateLimitWithWeight(time.Second, 27, 1), + WSAuthRate: request.NewRateLimitWithWeight(time.Second, 750, 1), + WSUnauthRate: request.NewRateLimitWithWeight(time.Second, 8, 1), + PubRate: request.NewRateLimitWithWeight(time.Second, 10, 1), } } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 938923e2332..547fd63faa0 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -52,6 +52,8 @@ const ( // Public Errors var ( ErrExchangeNameIsEmpty = errors.New("exchange name is empty") + ErrSettingProxyAddress = errors.New("setting proxy address error") + ErrEndpointPathNotFound = errors.New("no endpoint path found for the given key") ErrSymbolCannotBeMatched = errors.New("symbol cannot be matched") ) @@ -79,8 +81,7 @@ func (b *Base) SetClientProxyAddress(addr string) error { } proxy, err := url.Parse(addr) if err != nil { - return fmt.Errorf("setting proxy address error %s", - err) + return fmt.Errorf("%w %w", ErrSettingProxyAddress, err) } err = b.Requester.SetProxy(proxy) @@ -202,7 +203,7 @@ func (b *Base) GetLastPairsUpdateTime() int64 { return b.CurrencyPairs.LastUpdated } -// GetAssetTypes returns the either the enabled or available asset types for an +// GetAssetTypes returns either the enabled or available asset types for an // individual exchange func (b *Base) GetAssetTypes(enabled bool) asset.Items { return b.CurrencyPairs.GetAssetTypes(enabled) @@ -713,6 +714,7 @@ func (b *Base) UpdatePairs(incoming currency.Pairs, a asset.Item, enabled, force diff.Remove) } } + // TODO: Add check for nil config etc. err = b.Config.CurrencyPairs.StorePairs(a, incoming, enabled) if err != nil { return err @@ -1316,7 +1318,7 @@ func (e *Endpoints) GetURL(key URL) (string, error) { defer e.mu.RUnlock() val, ok := e.defaults[key.String()] if !ok { - return "", fmt.Errorf("no endpoint path found for the given key: %v", key) + return "", fmt.Errorf("%w %v", ErrEndpointPathNotFound, key) } return val, nil } diff --git a/exchanges/lbank/lbank.go b/exchanges/lbank/lbank.go index b5e88d03686..f07ccd7d20d 100644 --- a/exchanges/lbank/lbank.go +++ b/exchanges/lbank/lbank.go @@ -495,7 +495,8 @@ func (l *Lbank) SendHTTPRequest(ctx context.Context, ep exchange.URL, path strin }, request.UnauthenticatedRequest) } -func (l *Lbank) loadPrivKey(ctx context.Context) error { +// LoadPrivKey loads the private key +func (l *Lbank) LoadPrivKey(ctx context.Context) error { creds, err := l.GetCredentials(ctx) if err != nil { return err diff --git a/exchanges/lbank/lbank_test.go b/exchanges/lbank/lbank_test.go index 72d53f62621..4792c85f539 100644 --- a/exchanges/lbank/lbank_test.go +++ b/exchanges/lbank/lbank_test.go @@ -288,14 +288,14 @@ func TestLoadPrivKey(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - err := l.loadPrivKey(context.Background()) + err := l.LoadPrivKey(context.Background()) if err != nil { t.Error(err) } ctx := account.DeployCredentialsToContext(context.Background(), &account.Credentials{Secret: "errortest"}) - err = l.loadPrivKey(ctx) + err = l.LoadPrivKey(ctx) if err == nil { t.Errorf("Expected error due to pemblock nil") } @@ -305,7 +305,7 @@ func TestSign(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - err := l.loadPrivKey(context.Background()) + err := l.LoadPrivKey(context.Background()) if err != nil { t.Fatal(err) } diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index 486f49bcae9..cf79f6c96be 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -122,7 +122,7 @@ func (l *Lbank) Setup(exch *config.Exchange) error { } if l.API.AuthenticatedSupport { - err = l.loadPrivKey(context.TODO()) + err = l.LoadPrivKey(context.TODO()) if err != nil { l.API.AuthenticatedSupport = false log.Errorf(log.ExchangeSys, "%s couldn't load private key, setting authenticated support to false", l.Name) diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index b01d62aeba9..4b30516c107 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -38,7 +38,7 @@ func TestSubmit_Validate(t *testing.T) { Submit: nil, }, // nil struct { - ExpectedErr: errExchangeNameUnset, + ExpectedErr: common.ErrExchangeNameUnset, Submit: &Submit{}, }, // empty exchange { @@ -949,7 +949,7 @@ func TestStringToOrderType(t *testing.T) { {"tRiGgEr", Trigger, nil}, {"conDitiOnal", ConditionalStop, nil}, {"oCo", OCO, nil}, - {"woahMan", UnknownType, errUnrecognisedOrderType}, + {"woahMan", UnknownType, ErrUnrecognisedOrderType}, } for i := range cases { testData := &cases[i] @@ -1412,8 +1412,8 @@ func TestValidationOnOrderTypes(t *testing.T) { getOrders.Side = AnySide err = getOrders.Validate() - if !errors.Is(err, errUnrecognisedOrderType) { - t.Fatalf("received: '%v' but expected: '%v'", err, errUnrecognisedOrderType) + if !errors.Is(err, ErrUnrecognisedOrderType) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrUnrecognisedOrderType) } var errTestError = errors.New("test error") diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 75193c1ccab..9e30e2a0d5b 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -92,6 +92,12 @@ type Submit struct { // Iceberg specifies whether or not only visible portions of orders are shown in iceberg orders Iceberg bool + + // EndTime is the moment which a good til date order is valid until + EndTime time.Time + + // StopDirection is the direction from which the stop order will trigger + StopDirection StopDirection } // SubmitResponse is what is returned after submitting an order to an exchange @@ -363,6 +369,26 @@ const ( ConditionalStop // One-way stop order ) +// AllOrderTypes collects all order types for easy and consistent comparisons +var AllOrderTypes = Limit | + Market | + PostOnly | + ImmediateOrCancel | + Stop | + StopLimit | + StopMarket | + TakeProfit | + TakeProfitMarket | + TrailingStop | + FillOrKill | + IOS | + AnyType | + Liquidation | + Trigger | + OptimalLimitIOC | + OCO | + ConditionalStop + // Side enforces a standard for order sides across the code base type Side uint32 @@ -417,6 +443,17 @@ type ClassificationError struct { // MultiOrderRequest. type FilteredOrders []Detail +// StopDirection is the direction from which the stop order will trigger; Up will have the order trigger +// when the last trade price goes above the TriggerPrice; Down will have the order trigger when the +// last trade price goes below the TriggerPrice +type StopDirection bool + +// StopDirection types +const ( + StopUp StopDirection = true + StopDown StopDirection = false +) + // RiskManagement represents a risk management detail information. type RiskManagement struct { Enabled bool diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index ad09ba5dfc8..ae4fafd4302 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -39,13 +39,13 @@ var ( ErrAmountMustBeSet = errors.New("amount must be set") ErrClientOrderIDMustBeSet = errors.New("client order ID must be set") ErrUnknownSubmissionAmountType = errors.New("unknown submission amount type") + ErrIDNotSet = errors.New("ID not set") + ErrUnrecognisedOrderType = errors.New("unrecognised order type") ) var ( errTimeInForceConflict = errors.New("multiple time in force options applied") - errUnrecognisedOrderType = errors.New("unrecognised order type") errUnrecognisedOrderStatus = errors.New("unrecognised order status") - errExchangeNameUnset = errors.New("exchange name unset") errOrderSubmitIsNil = errors.New("order submit is nil") errOrderSubmitResponseIsNil = errors.New("order submit response is nil") errOrderDetailIsNil = errors.New("order detail is nil") @@ -64,7 +64,7 @@ func (s *Submit) Validate(requirements protocol.TradingRequirements, opt ...vali } if s.Exchange == "" { - return errExchangeNameUnset + return common.ErrExchangeNameUnset } if s.Pair.IsEmpty() { @@ -83,7 +83,7 @@ func (s *Submit) Validate(requirements protocol.TradingRequirements, opt ...vali return fmt.Errorf("%w %v", ErrSideIsInvalid, s.Side) } - if s.Type != Market && s.Type != Limit { + if AllOrderTypes&s.Type != s.Type || s.Type == UnknownType { return ErrTypeIsInvalid } @@ -1156,7 +1156,7 @@ func StringToOrderType(oType string) (Type, error) { case ConditionalStop.String(): return ConditionalStop, nil default: - return UnknownType, fmt.Errorf("'%v' %w", oType, errUnrecognisedOrderType) + return UnknownType, fmt.Errorf("'%v' %w", oType, ErrUnrecognisedOrderType) } } @@ -1229,7 +1229,7 @@ func (o *ClassificationError) Error() string { func (c *Cancel) StandardCancel() validate.Checker { return validate.Check(func() error { if c.OrderID == "" { - return errors.New("ID not set") + return ErrIDNotSet } return nil }) @@ -1282,7 +1282,7 @@ func (g *MultiOrderRequest) Validate(opt ...validate.Checker) error { } if g.Type == UnknownType { - return errUnrecognisedOrderType + return ErrUnrecognisedOrderType } var errs error diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index 5773864ffcd..976f9773602 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" @@ -82,7 +83,7 @@ func (s *Service) Update(b *Base) error { // DeployDepth used for subsystem deployment creates a depth item in the struct then returns a ptr to that Depth item func (s *Service) DeployDepth(exchange string, p currency.Pair, a asset.Item) (*Depth, error) { if exchange == "" { - return nil, errExchangeNameUnset + return nil, common.ErrExchangeNameUnset } if p.IsEmpty() { return nil, errPairNotSet @@ -294,7 +295,7 @@ func checkAlignment(depth Tranches, fundingRate, priceDuplication, isIDAligned, // list func (b *Base) Process() error { if b.Exchange == "" { - return errExchangeNameUnset + return common.ErrExchangeNameUnset } if b.Pair.IsEmpty() { diff --git a/exchanges/orderbook/orderbook_test.go b/exchanges/orderbook/orderbook_test.go index 127393b600c..360df2a858e 100644 --- a/exchanges/orderbook/orderbook_test.go +++ b/exchanges/orderbook/orderbook_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -324,7 +325,7 @@ func TestDeployDepth(t *testing.T) { c, err := currency.NewPairFromStrings("BTC", "USD") require.NoError(t, err) _, err = DeployDepth("", c, asset.Spot) - require.ErrorIs(t, err, errExchangeNameUnset) + require.ErrorIs(t, err, common.ErrExchangeNameUnset) _, err = DeployDepth("test", currency.EMPTYPAIR, asset.Spot) require.ErrorIs(t, err, errPairNotSet) _, err = DeployDepth("test", c, asset.Empty) diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index dc1929544a3..e980100b4a5 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -21,7 +21,6 @@ const ( // Vars for the orderbook package var ( - errExchangeNameUnset = errors.New("orderbook exchange name not set") errPairNotSet = errors.New("orderbook currency pair not set") errAssetTypeNotSet = errors.New("orderbook asset type not set") errCannotFindOrderbook = errors.New("cannot find orderbook(s)") diff --git a/exchanges/protocol/features.go b/exchanges/protocol/features.go index d7a0a43d37d..8649d8d1ee9 100644 --- a/exchanges/protocol/features.go +++ b/exchanges/protocol/features.go @@ -3,6 +3,7 @@ package protocol // Features holds all variables for the exchanges supported features // for a protocol (e.g REST or Websocket) type Features struct { + // TickerBatching allows the REST endpoint to fetch the entire ticker list available to the exchange TickerBatching bool `json:"tickerBatching,omitempty"` AutoPairUpdates bool `json:"autoPairUpdates,omitempty"` AccountBalance bool `json:"accountBalance,omitempty"` diff --git a/exchanges/stream/websocket.go b/exchanges/stream/websocket.go index 309db9a79d4..d4af0d2f22b 100644 --- a/exchanges/stream/websocket.go +++ b/exchanges/stream/websocket.go @@ -21,14 +21,14 @@ const jobBuffer = 5000 // Public websocket errors var ( - ErrWebsocketNotEnabled = errors.New("websocket not enabled") - ErrSubscriptionFailure = errors.New("subscription failure") - ErrSubscriptionNotSupported = errors.New("subscription channel not supported ") - ErrUnsubscribeFailure = errors.New("unsubscribe failure") - ErrAlreadyDisabled = errors.New("websocket already disabled") - ErrNotConnected = errors.New("websocket is not connected") - ErrNoMessageListener = errors.New("websocket listener not found for message") - ErrSignatureTimeout = errors.New("websocket timeout waiting for response with signature") + ErrWebsocketNotEnabled = errors.New("websocket not enabled") + ErrSubscriptionFailure = errors.New("subscription failure") + ErrUnsubscribeFailure = errors.New("unsubscribe failure") + ErrAlreadyDisabled = errors.New("websocket already disabled") + ErrNotConnected = errors.New("websocket is not connected") + ErrWebsocketAlreadyEnabled = errors.New("websocket already enabled") + ErrNoMessageListener = errors.New("websocket listener not found for message") + ErrSignatureTimeout = errors.New("websocket timeout waiting for response with signature") ) // Private websocket errors @@ -37,7 +37,6 @@ var ( errWebsocketIsNil = errors.New("websocket is nil") errWebsocketSetupIsNil = errors.New("websocket setup is nil") errWebsocketAlreadyInitialised = errors.New("websocket already initialised") - errWebsocketAlreadyEnabled = errors.New("websocket already enabled") errWebsocketFeaturesIsUnset = errors.New("websocket features is unset") errConfigFeaturesIsNil = errors.New("exchange config features is nil") errDefaultURLIsEmpty = errors.New("default url is empty") @@ -492,7 +491,7 @@ func (w *Websocket) Disable() error { // Enable enables the exchange websocket protocol func (w *Websocket) Enable() error { if w.IsConnected() || w.IsEnabled() { - return fmt.Errorf("%s %w", w.exchangeName, errWebsocketAlreadyEnabled) + return fmt.Errorf("%s %w", w.exchangeName, ErrWebsocketAlreadyEnabled) } w.setEnabled(true) diff --git a/exchanges/stream/websocket_test.go b/exchanges/stream/websocket_test.go index 2904bccadee..664b9ff6370 100644 --- a/exchanges/stream/websocket_test.go +++ b/exchanges/stream/websocket_test.go @@ -1163,7 +1163,7 @@ func TestEnable(t *testing.T) { w.Unsubscriber = func(subscription.List) error { return nil } w.GenerateSubs = func() (subscription.List, error) { return nil, nil } require.NoError(t, w.Enable(), "Enable must not error") - assert.ErrorIs(t, w.Enable(), errWebsocketAlreadyEnabled, "Enable should error correctly") + assert.ErrorIs(t, w.Enable(), ErrWebsocketAlreadyEnabled, "Enable should error correctly") } func TestSetupNewConnection(t *testing.T) { diff --git a/go.mod b/go.mod index 5b822073a41..fb48d2e41ee 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/friendsofgo/errors v0.9.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect diff --git a/go.sum b/go.sum index 4158408e54f..eebdc55238b 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/portfolio/withdraw/validate.go b/portfolio/withdraw/validate.go index 297cfe8ba22..45d5b434aa7 100644 --- a/portfolio/withdraw/validate.go +++ b/portfolio/withdraw/validate.go @@ -4,6 +4,7 @@ import ( "errors" "strings" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/validate" ) @@ -15,7 +16,7 @@ func (r *Request) Validate(opt ...validate.Checker) (err error) { } if r.Exchange == "" { - return ErrExchangeNameUnset + return common.ErrExchangeNameUnset } var allErrors []string diff --git a/portfolio/withdraw/validate_test.go b/portfolio/withdraw/validate_test.go index e2a95698248..dab57be3ea9 100644 --- a/portfolio/withdraw/validate_test.go +++ b/portfolio/withdraw/validate_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/validate" @@ -151,7 +152,7 @@ func TestExchangeNameUnset(t *testing.T) { r := Request{} err := r.Validate() if err != nil { - if err != ErrExchangeNameUnset { + if err != common.ErrExchangeNameUnset { t.Fatal(err) } } diff --git a/portfolio/withdraw/withdraw_types.go b/portfolio/withdraw/withdraw_types.go index cc65fe41950..38603af9790 100644 --- a/portfolio/withdraw/withdraw_types.go +++ b/portfolio/withdraw/withdraw_types.go @@ -40,8 +40,6 @@ const ( var ( // ErrRequestCannotBeNil message to return when a request is nil ErrRequestCannotBeNil = errors.New("request cannot be nil") - // ErrExchangeNameUnset message to return when an exchange name is unset - ErrExchangeNameUnset = errors.New("exchange name unset") // ErrInvalidRequest message to return when a request type is invalid ErrInvalidRequest = errors.New("invalid request type") // ErrStrAddressNotWhiteListed occurs when a withdrawal attempts to withdraw from a non-whitelisted address @@ -94,6 +92,9 @@ type Request struct { // Used exclusively in Binance.US ClientOrderID string `json:"clientID"` + // Currently used exclusively in Coinbase + WalletID string `json:"walletID"` + // Used exclusively in OKX to classify internal represented by '3' or on chain represented by '4' InternalTransfer bool diff --git a/testdata/configtest.json b/testdata/configtest.json index 51de85a84c9..989e40ab303 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1254,12 +1254,17 @@ }, "useGlobalFormat": true, "assetTypes": [ - "spot" + "spot", + "futures" ], "pairs": { "spot": { "enabled": "BTC-USD", "available": "LTC-GBP,XLM-BTC,DASH-BTC,DAI-USDC,ZEC-USDC,XLM-EUR,ZRX-BTC,LTC-BTC,ETC-BTC,ETH-USD,XRP-EUR,BTC-USDC,REP-USD,EOS-BTC,ZEC-BTC,ETC-GBP,LINK-ETH,XRP-BTC,ZRX-USD,ETH-USDC,MANA-USDC,BTC-EUR,BCH-GBP,DNT-USDC,EOS-EUR,BCH-EUR,LTC-EUR,CVC-USDC,ETH-GBP,DASH-USD,ETH-EUR,XTZ-BTC,ZRX-EUR,BAT-ETH,BTC-GBP,ETC-USD,BAT-USDC,BCH-USD,GNT-USDC,ALGO-USD,LINK-USD,XLM-USD,ETH-BTC,EOS-USD,REP-BTC,ETH-DAI,XRP-USD,LTC-USD,ETC-EUR,BTC-USD,XTZ-USD,BCH-BTC,LOOM-USDC" + }, + "futures": { + "enabled": "BTC-PERP-INTX", + "available": "BTC-PERP-INTX" } } },