From 268669cbc67464671693f0aec2595299d159ad18 Mon Sep 17 00:00:00 2001 From: Marc MOREAU Date: Thu, 25 Mar 2021 19:24:20 +0100 Subject: [PATCH 1/4] websocket * change the way to define websocket and their subscribers --- src/CoinbaseFacade.php | 7 + ...SubscriberAuthenticationAwareInterface.php | 4 - src/Functional/Api/CoinbaseApi.php | 10 +- .../Websocket/AbstractSubscriber.php | 85 +++++ src/Functional/Websocket/Subscriber.php | 73 +--- .../Websocket/SubscriberAuthenticateAware.php | 65 ++-- src/Functional/Websocket/Websocket.php | 24 +- tests/func/Websocket/WebsocketTest.php | 351 ++++++++++++++---- 8 files changed, 417 insertions(+), 202 deletions(-) create mode 100644 src/Functional/Websocket/AbstractSubscriber.php diff --git a/src/CoinbaseFacade.php b/src/CoinbaseFacade.php index cf2a4ff..993876c 100644 --- a/src/CoinbaseFacade.php +++ b/src/CoinbaseFacade.php @@ -13,6 +13,8 @@ use MockingMagician\CoinbaseProSdk\Functional\Build\LimitOrderToPlace; use MockingMagician\CoinbaseProSdk\Functional\Build\MarketOrderToPlace; use MockingMagician\CoinbaseProSdk\Functional\Build\Pagination; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Websocket; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\WebsocketRunner; /** * @codeCoverageIgnore @@ -113,4 +115,9 @@ public static function createMarketOrderToPlace( $clientOrderId ); } + + public static function createUnauthenticatedWebsocket() + { + return new Websocket(new WebsocketRunner()); + } } diff --git a/src/Contracts/Websocket/SubscriberAuthenticationAwareInterface.php b/src/Contracts/Websocket/SubscriberAuthenticationAwareInterface.php index 1769563..f8a17f0 100644 --- a/src/Contracts/Websocket/SubscriberAuthenticationAwareInterface.php +++ b/src/Contracts/Websocket/SubscriberAuthenticationAwareInterface.php @@ -12,9 +12,5 @@ interface SubscriberAuthenticationAwareInterface extends SubscriberInterface { - public function getCoinbaseApi(): ApiInterface; - - public function runWithAuthentication(bool $bool, bool $useCoinbaseRemoteTime = false): void; - public function activateChannelUser(bool $activate, array $productIds = []): void; } diff --git a/src/Functional/Api/CoinbaseApi.php b/src/Functional/Api/CoinbaseApi.php index b620419..a9aab0d 100644 --- a/src/Functional/Api/CoinbaseApi.php +++ b/src/Functional/Api/CoinbaseApi.php @@ -154,7 +154,7 @@ public function __construct(CoinbaseConfig $config) $this->time = $config->getConnectivityConfig()->isTimeActivate() ? new Time($this->requestFactory) : null; $this->withdrawals = $config->getConnectivityConfig()->isWithdrawalsActivate() ? new Withdrawals($this->requestFactory) : null; - $this->websocket = new Websocket(new WebsocketRunner()); + $this->websocket = new Websocket(new WebsocketRunner(), $this, $config->isUseCoinbaseRemoteTime() ? new Time($this->requestFactory) : null); } public function accounts(): AccountsInterface @@ -310,13 +310,13 @@ public function time(): TimeInterface return $this->time; } - public function getRequestFactory(): RequestFactoryInterface + public function websocket(): WebsocketInterface { - return $this->requestFactory; + return $this->websocket; } - public function websocket(): WebsocketInterface + public function getRequestFactory(): RequestFactoryInterface { - return $this->websocket; + return $this->requestFactory; } } diff --git a/src/Functional/Websocket/AbstractSubscriber.php b/src/Functional/Websocket/AbstractSubscriber.php new file mode 100644 index 0000000..446badd --- /dev/null +++ b/src/Functional/Websocket/AbstractSubscriber.php @@ -0,0 +1,85 @@ + + * @license https://github.com/MockingMagician/coinbase-pro-sdk/blob/master/LICENSE.md MIT + * @link https://github.com/MockingMagician/coinbase-pro-sdk/blob/master/README.md + */ + +namespace MockingMagician\CoinbaseProSdk\Functional\Websocket; + +use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; +use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; +use MockingMagician\CoinbaseProSdk\Functional\Error\ApiError; + +abstract class AbstractSubscriber +{ + /** + * @var array + */ + protected $payloadTemplate = [ + 'type' => null, + 'product_ids' => [], + 'channels' => [], + ]; + + protected function activateChannel(string $channelKey, bool $activate, ?array $productIds): void + { + if ($activate) { + $this->payloadTemplate['channels'][$channelKey] = ['name' => $channelKey]; + if (!is_null($productIds) && !empty($productIds)) { + $this->payloadTemplate['channels'][$channelKey]['product_ids'] = $productIds; + } + + return; + } + + if (isset($this->payloadTemplate['channels'][$channelKey])) { + unset($this->payloadTemplate['channels'][$channelKey]); + } + } + + public function setProductIds(array $productIds): void + { + $this->payloadTemplate['product_ids'] = $productIds; + } + + public function activateChannelHeartbeat(bool $activate, array $productIds = []): void + { + $this->activateChannel('heartbeat', $activate, $productIds); + } + + public function activateChannelStatus(bool $activate): void + { + $this->activateChannel('status', $activate, null); + } + + public function activateChannelTicker(bool $activate, array $productIds = []): void + { + $this->activateChannel('ticker', $activate, $productIds); + } + + public function activateChannelLevel2(bool $activate, array $productIds = []): void + { + $this->activateChannel('level2', $activate, $productIds); + } + + public function activateChannelFull(bool $activate, array $productIds = []): void + { + $this->activateChannel('full', $activate, $productIds); + } + + public function activateChannelMatches(bool $activate, array $productIds = []): void + { + $this->activateChannel('matches', $activate, $productIds); + } + + public function getPayload(bool $unsubscribe = false): array + { + $payload = $this->payloadTemplate; + $payload['type'] = $unsubscribe ? 'unsubscribe' : 'subscribe'; + $payload['channels'] = array_values($this->payloadTemplate['channels']); + + return $payload; + } +} diff --git a/src/Functional/Websocket/Subscriber.php b/src/Functional/Websocket/Subscriber.php index 81ab9f0..1c13c1c 100644 --- a/src/Functional/Websocket/Subscriber.php +++ b/src/Functional/Websocket/Subscriber.php @@ -8,76 +8,13 @@ namespace MockingMagician\CoinbaseProSdk\Functional\Websocket; -use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberInterface; +use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; +use MockingMagician\CoinbaseProSdk\Functional\Error\ApiError; -class Subscriber implements SubscriberInterface +final class Subscriber extends AbstractSubscriber implements SubscriberAuthenticationAwareInterface { - /** - * @var array - */ - private $payloadTemplate = [ - 'type' => null, - 'product_ids' => [], - 'channels' => [], - ]; - - public function setProductIds(array $productIds): void - { - $this->payloadTemplate['product_ids'] = $productIds; - } - - public function activateChannelHeartbeat(bool $activate, array $productIds = []): void - { - $this->activateChannel('heartbeat', $activate, $productIds); - } - - public function activateChannelStatus(bool $activate): void - { - $this->activateChannel('status', $activate, null); - } - - public function activateChannelTicker(bool $activate, array $productIds = []): void - { - $this->activateChannel('ticker', $activate, $productIds); - } - - public function activateChannelLevel2(bool $activate, array $productIds = []): void - { - $this->activateChannel('level2', $activate, $productIds); - } - - public function activateChannelFull(bool $activate, array $productIds = []): void - { - $this->activateChannel('full', $activate, $productIds); - } - - public function activateChannelMatches(bool $activate, array $productIds = []): void - { - $this->activateChannel('matches', $activate, $productIds); - } - - public function getPayload(bool $unsubscribe = false): array + public function activateChannelUser(bool $activate, array $productIds = []): void { - $payload = $this->payloadTemplate; - $payload['type'] = $unsubscribe ? 'unsubscribe' : 'subscribe'; - $payload['channels'] = array_values($this->payloadTemplate['channels']); - - return $payload; - } - - protected function activateChannel(string $channelKey, bool $activate, ?array $productIds): void - { - if ($activate) { - $this->payloadTemplate['channels'][$channelKey] = ['name' => $channelKey]; - if (!is_null($productIds) && !empty($productIds)) { - $this->payloadTemplate['channels'][$channelKey]['product_ids'] = $productIds; - } - - return; - } - - if (isset($this->payloadTemplate['channels'][$channelKey])) { - unset($this->payloadTemplate['channels'][$channelKey]); - } + throw new ApiError(sprintf('you are running websocket outside of any authentication, %s is not available in this context', __METHOD__)); } } diff --git a/src/Functional/Websocket/SubscriberAuthenticateAware.php b/src/Functional/Websocket/SubscriberAuthenticateAware.php index 92ef989..c8272d2 100644 --- a/src/Functional/Websocket/SubscriberAuthenticateAware.php +++ b/src/Functional/Websocket/SubscriberAuthenticateAware.php @@ -9,49 +9,31 @@ namespace MockingMagician\CoinbaseProSdk\Functional\Websocket; use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; +use MockingMagician\CoinbaseProSdk\Contracts\Connectivity\TimeInterface; use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberInterface; -use MockingMagician\CoinbaseProSdk\Functional\Error\ApiError; use MockingMagician\CoinbaseProSdk\Functional\Misc\Signer; -class SubscriberAuthenticateAware extends Subscriber implements SubscriberAuthenticationAwareInterface +final class SubscriberAuthenticateAware extends AbstractSubscriber implements SubscriberAuthenticationAwareInterface { /** - * @var null|ApiInterface + * @var ApiInterface */ private $coinbaseApi; /** - * @var bool + * @var null|TimeInterface */ - private $useCoinbaseRemoteTime; - /** - * @var bool - */ - private $authenticate; + private $time; - public function __construct(ApiInterface $api) + public function __construct(AbstractSubscriber $subscriber, ApiInterface $api, ?TimeInterface $time) { + $this->payloadTemplate = $subscriber->payloadTemplate; $this->coinbaseApi = $api; - $this->authenticate = false; - $this->useCoinbaseRemoteTime = false; - } - - public function getCoinbaseApi(): ApiInterface - { - return $this->coinbaseApi; - } - - public function runWithAuthentication(bool $bool, bool $useCoinbaseRemoteTime = false): void - { - $this->authenticate = $bool; - $this->useCoinbaseRemoteTime = $useCoinbaseRemoteTime; + $this->time = $time; } public function activateChannelUser(bool $activate, array $productIds = []): void { - if (!$this->authenticate) { - throw new ApiError('Activate channel user require to be authenticated, first activate with runWithAuthentication method'); - } $this->activateChannel('user', $activate, $productIds); } @@ -59,22 +41,21 @@ public function getPayload(bool $unsubscribe = false): array { $payload = parent::getPayload($unsubscribe); - if ($this->authenticate) { - $params = $this->coinbaseApi->getRequestFactory()->getParams(); - $signData = Signer::sign( - $params->getKey(), - $params->getSecret(), - $params->getPassphrase(), - SubscriberInterface::AUTHENTICATION_LIKE_METHOD, - SubscriberInterface::AUTHENTICATION_LIKE_URI, - '', - $this->useCoinbaseRemoteTime ? $this->coinbaseApi->time()->getTime()->getEpoch() : time() - ); - $payload['signature'] = $signData->getSignature(); - $payload['key'] = $signData->getKey(); - $payload['passphrase'] = $signData->getPassphrase(); - $payload['timestamp'] = $signData->getTimestamp(); - } + $params = $this->coinbaseApi->getRequestFactory()->getParams(); + $signData = Signer::sign( + $params->getKey(), + $params->getSecret(), + $params->getPassphrase(), + SubscriberInterface::AUTHENTICATION_LIKE_METHOD, + SubscriberInterface::AUTHENTICATION_LIKE_URI, + '', + $this->time ? $this->time->getTime()->getEpoch() : time() + ); + + $payload['signature'] = $signData->getSignature(); + $payload['key'] = $signData->getKey(); + $payload['passphrase'] = $signData->getPassphrase(); + $payload['timestamp'] = $signData->getTimestamp(); return $payload; } diff --git a/src/Functional/Websocket/Websocket.php b/src/Functional/Websocket/Websocket.php index a0e0b47..e857ee6 100644 --- a/src/Functional/Websocket/Websocket.php +++ b/src/Functional/Websocket/Websocket.php @@ -8,6 +8,9 @@ namespace MockingMagician\CoinbaseProSdk\Functional\Websocket; +use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; +use MockingMagician\CoinbaseProSdk\Contracts\Connectivity\TimeInterface; +use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberInterface; use MockingMagician\CoinbaseProSdk\Contracts\Websocket\WebsocketInterface; @@ -17,10 +20,29 @@ class Websocket implements WebsocketInterface * @var WebsocketRunner */ private $runner; + /** + * @var ApiInterface|null + */ + private $api; + /** + * @var TimeInterface|null + */ + private $time; - public function __construct(WebsocketRunner $runner) + public function __construct(WebsocketRunner $runner, ?ApiInterface $api = null, ?TimeInterface $time = null) { $this->runner = $runner; + $this->api = $api; + $this->time = $time; + } + + public function newSubscriber(): SubscriberAuthenticationAwareInterface + { + if (isset($this->api)) { + return new SubscriberAuthenticateAware(new Subscriber(), $this->api, $this->time); + } + + return new Subscriber(); } public function run(SubscriberInterface $subscriber, callable $userFunc, ...$args): void diff --git a/tests/func/Websocket/WebsocketTest.php b/tests/func/Websocket/WebsocketTest.php index 7283eea..ae85f63 100644 --- a/tests/func/Websocket/WebsocketTest.php +++ b/tests/func/Websocket/WebsocketTest.php @@ -10,7 +10,9 @@ use Dotenv\Dotenv; use MockingMagician\CoinbaseProSdk\CoinbaseFacade; +use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; use MockingMagician\CoinbaseProSdk\Contracts\Websocket\WebsocketRunnerInterface; +use MockingMagician\CoinbaseProSdk\Functional\Api\CoinbaseApi; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ActivateMessage; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ChangeMessage; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\DoneMessage; @@ -26,8 +28,6 @@ use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\SubscriptionsMessage; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\TickerMessage; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\UnknownMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Subscriber; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\SubscriberAuthenticateAware; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Websocket; use MockingMagician\CoinbaseProSdk\Functional\Websocket\WebsocketRunner; use MockingMagician\CoinbaseProSdk\Tests\Func\Connectivity\AbstractTest; @@ -37,14 +37,19 @@ */ final class WebsocketTest extends AbstractTest { + private $websocket; + /** + * @var CoinbaseApi + */ + private $coinbaseApi; /** * @var Websocket */ - private $websocket; + private $simpleWebsocket; /** - * @var \MockingMagician\CoinbaseProSdk\Functional\Api\CoinbaseApi + * @var Websocket|null */ - private $coinbaseApi; + private $authenticatedWebsocket; public function setUp(): void { @@ -61,8 +66,8 @@ public function setUp(): void getenv('API_PASSPHRASE_REAL_FOR_WEBSOCKET'), ]; - if (in_array(false, $params)) { - $this->markTestSkipped('Functional tests for websocket require REAL(production) key, secret, passphrase.'); + if (!in_array(false, $params)) { + $this->authenticatedWebsocket = new Websocket(new WebsocketRunner(), $this->coinbaseApi); } $this->coinbaseApi = CoinbaseFacade::createDefaultCoinbaseApi( @@ -75,16 +80,19 @@ public function setUp(): void ini_set('xdebug.var_display_max_depth', '16'); ini_set('xdebug.var_display_max_children', '256'); ini_set('xdebug.var_display_max_data', '4096'); - $this->websocket = new Websocket(new WebsocketRunner()); + + $this->simpleWebsocket = new Websocket(new WebsocketRunner()); } - public function testSubscription() + public function testSubscribeChannelHeartbeat() { - $this->websocket->run($this->getSimpleSubscriber(), function ($runner) { + $subscriber = $this->simpleWebsocket->newSubscriber(); + $subscriber->activateChannelHeartbeat(true, ['BTC-EUR', 'BTC-USD']); + $this->simpleWebsocket->run($subscriber, function ($runner) { /** @var WebsocketRunnerInterface $runner */ $error = null; $subscriptionMessageFound = false; - $im = $i = 500; + $im = $i = 10; while ($i--) { $message = $runner->getMessage(); if ($message instanceof SubscriptionsMessage) { @@ -101,13 +109,15 @@ public function testSubscription() }); } - public function testSubscriptionAuthenticate() + public function testSubscribeChannelMatches() { - $this->websocket->run($this->getAuthenticateSubscriber(), function ($runner) { + $subscriber = $this->simpleWebsocket->newSubscriber(); + $subscriber->activateChannelMatches(true, ['BTC-EUR', 'BTC-USD']); + $this->simpleWebsocket->run($subscriber, function ($runner) { /** @var WebsocketRunnerInterface $runner */ $error = null; $subscriptionMessageFound = false; - $im = $i = 500; + $im = $i = 10; while ($i--) { $message = $runner->getMessage(); if ($message instanceof SubscriptionsMessage) { @@ -124,93 +134,270 @@ public function testSubscriptionAuthenticate() }); } - public function testMessagesLongRun() + public function testSubscribeChannelTicker() { - $method = __METHOD__; - $subscriber = $this->getSimpleSubscriber(); - $subscriber->activateChannelLevel2(false); - $subscriber->activateChannelStatus(false); - $subscriber->activateChannelTicker(false); - $subscriber->activateChannelMatches(false); - $subscriber->activateChannelHeartbeat(false); - $this->websocket->run($subscriber, function ($runner, $method) { + $subscriber = $this->simpleWebsocket->newSubscriber(); + $subscriber->activateChannelTicker(true, ['BTC-EUR', 'BTC-USD']); + $this->simpleWebsocket->run($subscriber, function ($runner) { /** @var WebsocketRunnerInterface $runner */ - $messagesTypeCounter = [ - ActivateMessage::class => 0, - ChangeMessage::class => 0, - DoneMessage::class => 0, - ErrorMessage::class => 0, - HeartbeatMessage::class => 0, - L2UpdateMessage::class => 0, - LastMatchMessage::class => 0, - MatchMessage::class => 0, - OpenMessage::class => 0, - ReceivedMessage::class => 0, - SnapshotMessage::class => 0, - StatusMessage::class => 0, - SubscriptionsMessage::class => 0, - TickerMessage::class => 0, - UnknownMessage::class => 0, - ]; - $im = $i = 10 ** 5; - fwrite(STDOUT, sprintf("\nExecuting %s", $method)); - fwrite(STDOUT, "\nHas {$im} messages to fetch, rest :\n\r"); + $error = null; + $subscriptionMessageFound = false; + $im = $i = 10; while ($i--) { - fwrite(STDOUT, "\r".preg_replace('/./', ' ', $im)."\r{$i}"); $message = $runner->getMessage(); - ++$messagesTypeCounter[get_class($message)]; + if ($message instanceof SubscriptionsMessage) { + $subscriptionMessageFound = true; + } + if ($message instanceof ErrorMessage) { + $error = $message->getMessage().'. '.$message->getReason(); + + break; + } } - $this->assertEquals(array_sum($messagesTypeCounter), $im); - }, $method); + self::assertNull($error, $error ?? ''); + self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); + }); } - private function getSimpleSubscriber(): Subscriber + public function testSubscribeChannelStatus() { - $subscriber = new Subscriber(); - $subscriber->setProductIds($this->getProductIds()); - $subscriber->activateChannelTicker(true); - $subscriber->activateChannelMatches(true); + $subscriber = $this->simpleWebsocket->newSubscriber(); $subscriber->activateChannelStatus(true); - $subscriber->activateChannelLevel2(true); - $subscriber->activateChannelHeartbeat(true); - $subscriber->activateChannelFull(true); + $this->simpleWebsocket->run($subscriber, function ($runner) { + /** @var WebsocketRunnerInterface $runner */ + $error = null; + $subscriptionMessageFound = false; + $im = $i = 3; + while ($i--) { + $message = $runner->getMessage(); + if ($message instanceof SubscriptionsMessage) { + $subscriptionMessageFound = true; + } + if ($message instanceof ErrorMessage) { + $error = $message->getMessage().'. '.$message->getReason(); - return $subscriber; + break; + } + } + self::assertNull($error, $error ?? ''); + self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); + }); } - private function getProductIds(): array + public function testSubscribeChannelLevel2() { - $products = $this->coinbaseApi->products()->getProducts(); - $productIds = []; - foreach ($products as $product) { - $productIds[] = $product->getId(); - } + $subscriber = $this->simpleWebsocket->newSubscriber(); + $subscriber->activateChannelLevel2(true, ['BTC-EUR', 'BTC-USD']); + $this->simpleWebsocket->run($subscriber, function ($runner) { + /** @var WebsocketRunnerInterface $runner */ + $error = null; + $subscriptionMessageFound = false; + $im = $i = 4; + while ($i--) { + $message = $runner->getMessage(); + if ($message instanceof SubscriptionsMessage) { + $subscriptionMessageFound = true; + } + if ($message instanceof ErrorMessage) { + $error = $message->getMessage().'. '.$message->getReason(); - return array_values(array_filter($productIds, function ($value) { - if ( - false === stripos($value, 'USDC') - && false === stripos($value, 'GBP') - && false === stripos($value, 'USD') - ) { - return true; + break; + } } + self::assertNull($error, $error ?? ''); + self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); + }); + } - return false; - })); + public function testSubscribeChannelFull() + { + $subscriber = $this->simpleWebsocket->newSubscriber(); + $subscriber->activateChannelFull(true, ['BTC-EUR', 'BTC-USD']); + $this->simpleWebsocket->run($subscriber, function ($runner) { + /** @var WebsocketRunnerInterface $runner */ + $error = null; + $subscriptionMessageFound = false; + $im = $i = 10; + while ($i--) { + $message = $runner->getMessage(); + if ($message instanceof SubscriptionsMessage) { + $subscriptionMessageFound = true; + } + if ($message instanceof ErrorMessage) { + $error = $message->getMessage().'. '.$message->getReason(); + + break; + } + } + self::assertNull($error, $error ?? ''); + self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); + }); } - private function getAuthenticateSubscriber(): Subscriber + public function testSubscribeChannelUser() { - $subscriber = new SubscriberAuthenticateAware($this->coinbaseApi); - $subscriber->setProductIds($this->getProductIds()); - $subscriber->runWithAuthentication(true); - $subscriber->activateChannelUser(true); - $subscriber->activateChannelTicker(true); - $subscriber->activateChannelMatches(true); - $subscriber->activateChannelStatus(true); - $subscriber->activateChannelLevel2(true); - $subscriber->activateChannelHeartbeat(true); + if (is_null($this->authenticatedWebsocket)) { + $this->markTestSkipped('Functional tests for websocket require REAL(production) key, secret, passphrase.'); + return; + } + $subscriber = $this->authenticatedWebsocket->newSubscriber(); + $subscriber->activateChannelUser(true, ['BTC-EUR', 'BTC-USD']); + $this->authenticatedWebsocket->run($subscriber, function ($runner) { + /** @var WebsocketRunnerInterface $runner */ + $error = null; + $subscriptionMessageFound = false; + $im = $i = 1; + while ($i--) { + $message = $runner->getMessage(); + if ($message instanceof SubscriptionsMessage) { + $subscriptionMessageFound = true; + } + if ($message instanceof ErrorMessage) { + $error = $message->getMessage().'. '.$message->getReason(); - return $subscriber; + break; + } + } + self::assertNull($error, $error ?? ''); + self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); + }); } } +// +// public function testSubscription() +// { +// dump('hello'); +// $this->simpleWebsocket->run($this->getSimpleSubscriber(), function ($runner) { +// /** @var WebsocketRunnerInterface $runner */ +// $error = null; +// $subscriptionMessageFound = false; +// $im = $i = 500; +// while ($i--) { +// dump($i); +// $message = $runner->getMessage(); +// if ($message instanceof SubscriptionsMessage) { +// $subscriptionMessageFound = true; +// } +// if ($message instanceof ErrorMessage) { +// $error = $message->getMessage().'. '.$message->getReason(); +// +// break; +// } +// } +// self::assertNull($error, $error ?? ''); +// self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); +// }); +// } +// +// public function testSubscriptionAuthenticate() +// { +// $this->authenticatedWebsocket->run($this->getAuthenticateSubscriber(), function ($runner) { +// /** @var WebsocketRunnerInterface $runner */ +// $error = null; +// $subscriptionMessageFound = false; +// $im = $i = 500; +// while ($i--) { +// $message = $runner->getMessage(); +// if ($message instanceof SubscriptionsMessage) { +// $subscriptionMessageFound = true; +// } +// if ($message instanceof ErrorMessage) { +// $error = $message->getMessage().'. '.$message->getReason(); +// +// break; +// } +// } +// self::assertNull($error, $error ?? ''); +// self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); +// }); +// } +// +// public function testMessagesLongRun() +// { +// $method = __METHOD__; +// $subscriber = $this->getSimpleSubscriber(); +// $subscriber->activateChannelLevel2(false); +// $subscriber->activateChannelStatus(false); +// $subscriber->activateChannelTicker(false); +// $subscriber->activateChannelMatches(false); +// $subscriber->activateChannelHeartbeat(false); +// $this->simpleWebsocket->run($subscriber, function ($runner, $method) { +// /** @var WebsocketRunnerInterface $runner */ +// $messagesTypeCounter = [ +// ActivateMessage::class => 0, +// ChangeMessage::class => 0, +// DoneMessage::class => 0, +// ErrorMessage::class => 0, +// HeartbeatMessage::class => 0, +// L2UpdateMessage::class => 0, +// LastMatchMessage::class => 0, +// MatchMessage::class => 0, +// OpenMessage::class => 0, +// ReceivedMessage::class => 0, +// SnapshotMessage::class => 0, +// StatusMessage::class => 0, +// SubscriptionsMessage::class => 0, +// TickerMessage::class => 0, +// UnknownMessage::class => 0, +// ]; +// $im = $i = 10 ** 5; +// fwrite(STDOUT, sprintf("\nExecuting %s", $method)); +// fwrite(STDOUT, "\nHas {$im} messages to fetch, rest :\n\r"); +// while ($i--) { +// fwrite(STDOUT, "\r".preg_replace('/./', ' ', $im)."\r{$i}"); +// $message = $runner->getMessage(); +// ++$messagesTypeCounter[get_class($message)]; +// } +// $this->assertEquals(array_sum($messagesTypeCounter), $im); +// }, $method); +// } +// +// private function getSimpleSubscriber(): SubscriberAuthenticationAwareInterface +// { +// $subscriber = $this->simpleWebsocket->newSubscriber(); +// $subscriber->setProductIds($this->getProductIds()); +// $subscriber->activateChannelTicker(true); +// $subscriber->activateChannelMatches(true); +// $subscriber->activateChannelStatus(true); +// $subscriber->activateChannelLevel2(true); +// $subscriber->activateChannelHeartbeat(true); +// $subscriber->activateChannelFull(true); +// +// return $subscriber; +// } +// +// private function getAuthenticateSubscriber(): SubscriberAuthenticationAwareInterface +// { +// $subscriber = $this->authenticatedWebsocket->newSubscriber(); +// $subscriber->setProductIds($this->getProductIds()); +// $subscriber->activateChannelUser(true); +// $subscriber->activateChannelTicker(true); +// $subscriber->activateChannelMatches(true); +// $subscriber->activateChannelStatus(true); +// $subscriber->activateChannelLevel2(true); +// $subscriber->activateChannelHeartbeat(true); +// +// return $subscriber; +// } +// +// private function getProductIds(): array +// { +// $products = $this->coinbaseApi->products()->getProducts(); +// $productIds = []; +// foreach ($products as $product) { +// $productIds[] = $product->getId(); +// } +// +// return array_values(array_filter($productIds, function ($value) { +// if ( +// false === stripos($value, 'USDC') +// && false === stripos($value, 'GBP') +// && false === stripos($value, 'USD') +// ) { +// return true; +// } +// +// return false; +// })); +// } +//} From 233725abc2c074eadfb65e86c94a9f5496e55f1c Mon Sep 17 00:00:00 2001 From: Marc MOREAU Date: Sat, 27 Mar 2021 00:23:19 +0100 Subject: [PATCH 2/4] websocket * normalizing websocket * create websocket without to be authenticate * update documentation --- docs/_sass/custom/custom.scss | 75 ++++++ docs/coinbase-facade.md | 31 ++- docs/feature/websocket.md | 418 ++++++++++++++++++++++++++++++---- 3 files changed, 472 insertions(+), 52 deletions(-) create mode 100644 docs/_sass/custom/custom.scss diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss new file mode 100644 index 0000000..c94f014 --- /dev/null +++ b/docs/_sass/custom/custom.scss @@ -0,0 +1,75 @@ +div.highlighter-rouge { background-color: #232525; } +code { background-color: #232525; line-height: unset; } +.highlight { color: #ffffff } +.highlight pre { background-color: #232525; } +.highlight .hll { background-color: #333333 } +.highlight .c { color: #6c9662; font-style: italic; background-color: #232525 } /* Comment */ +.highlight .cd { color: #6c9662; font-style: italic } +.highlight .err { color: #ffffff } /* Error */ +.highlight .g { color: #ffffff } /* Generic */ +.highlight .k { color: #ca7732; font-weight: bold } /* Keyword */ +.highlight .l { color: #ffffff } /* Literal */ +.highlight .n { color: #ffffff } /* Name */ +.highlight .o { color: #ffffff } /* Operator */ +.highlight .x { color: #ffffff } /* Other */ +.highlight .p { color: #ffffff } /* Punctuation */ +.highlight .cm { color: #6c9662; font-style: italic; background-color: #232525 } /* Comment.Multiline */ +.highlight .cp { color: #ff262d; font-weight: bold; font-style: italic; background-color: #232525 } /* Comment.Preproc */ +.highlight .c1 { color: #6c9662; font-style: italic; background-color: #232525 } /* Comment.Single */ +.highlight .cs { color: #6c9662; font-style: italic; background-color: #232525 } /* Comment.Special */ +.highlight .gd { color: #ffffff } /* Generic.Deleted */ +.highlight .ge { color: #ffffff } /* Generic.Emph */ +.highlight .gr { color: #ffffff } /* Generic.Error */ +.highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #ffffff } /* Generic.Inserted */ +.highlight .go { color: #444444; background-color: #222222 } /* Generic.Output */ +.highlight .gp { color: #ffffff } /* Generic.Prompt */ +.highlight .gs { color: #ffffff } /* Generic.Strong */ +.highlight .gu { color: #ffffff; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #ffffff } /* Generic.Traceback */ +.highlight .kc { color: #ca7732; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #ca7732; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #ca7732; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #ca7732 } /* Keyword.Pseudo */ +.highlight .kr { color: #ca7732; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #cdcaa9; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #ffffff } /* Literal.Date */ +.highlight .m { color: #3f99d1; font-weight: bold } /* Literal.Number */ +.highlight .s { color: #3f99d1 } /* Literal.String */ +.highlight .na { color: #ff80c4; font-weight: bold } /* Name.Attribute */ +.highlight .nb { color: #ffffff } /* Name.Builtin */ +.highlight .nc { color: #ffffff } /* Name.Class */ +.highlight .no { color: #3f99d1 } /* Name.Constant */ +.highlight .nd { color: #ffffff } /* Name.Decorator */ +.highlight .ni { color: #ffffff } /* Name.Entity */ +.highlight .ne { color: #ffffff } /* Name.Exception */ +.highlight .nf { color: #fdc161; font-weight: bold } /* Name.Function */ +.highlight .nl { color: #ffffff } /* Name.Label */ +.highlight .nn { color: #ffffff } /* Name.Namespace */ +.highlight .nx { color: #ffffff } /* Name.Other */ +.highlight .py { color: #ffffff } /* Name.Property */ +.highlight .nt { color: #ca7732; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #9775a9 +} /* Name.Variable */ +.highlight .ow { color: #ffffff } /* Operator.Word */ +.highlight .w { color: #888888 } /* Text.Whitespace */ +.highlight .mf { color: #3f99d1; font-weight: bold } /* Literal.Number.Float */ +.highlight .mh { color: #3f99d1; font-weight: bold } /* Literal.Number.Hex */ +.highlight .mi { color: #3f99d1; font-weight: bold } /* Literal.Number.Integer */ +.highlight .mo { color: #3f99d1; font-weight: bold } /* Literal.Number.Oct */ +.highlight .sb { color: #3f99d1 } /* Literal.String.Backtick */ +.highlight .sc { color: #3f99d1 } /* Literal.String.Char */ +.highlight .sd { color: #3f99d1 } /* Literal.String.Doc */ +.highlight .s2 { color: #3f99d1 } /* Literal.String.Double */ +.highlight .se { color: #3f99d1 } /* Literal.String.Escape */ +.highlight .sh { color: #3f99d1 } /* Literal.String.Heredoc */ +.highlight .si { color: #3f99d1 } /* Literal.String.Interpol */ +.highlight .sx { color: #3f99d1 } /* Literal.String.Other */ +.highlight .sr { color: #3f99d1 } /* Literal.String.Regex */ +.highlight .s1 { color: #3f99d1 } /* Literal.String.Single */ +.highlight .ss { color: #3f99d1 } /* Literal.String.Symbol */ +.highlight .bp { color: #ffffff } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #ca7732 } /* Name.Variable.Class */ +.highlight .vg { color: #ca7732 } /* Name.Variable.Global */ +.highlight .vi { color: #ca7732 } /* Name.Variable.Instance */ +.highlight .il { color: #3f99d1; font-weight: bold } /* Literal.Number.Integer.Long */ diff --git a/docs/coinbase-facade.md b/docs/coinbase-facade.md index 33c5ec3..e5c8cc2 100644 --- a/docs/coinbase-facade.md +++ b/docs/coinbase-facade.md @@ -223,28 +223,22 @@ manage_rate_limits: false # pass false here to disable rate limit managing ```php use MockingMagician\CoinbaseProSdk\CoinbaseFacade; -use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; use MockingMagician\CoinbaseProSdk\Functional\Build\MarketOrderToPlace; -/** @var ApiInterface $api */ - $marketOrder = CoinbaseFacade::createMarketOrderToPlace( MarketOrderToPlace::SIDE_BUY, 'BTC-USD', 0.0001 ); ``` -More information about orders can be found in [Orders feature](./feature/orders.md) +More information about [Orders](./feature/orders.md) ### Limit order ```php use MockingMagician\CoinbaseProSdk\CoinbaseFacade; -use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; use MockingMagician\CoinbaseProSdk\Functional\Build\LimitOrderToPlace; -/** @var ApiInterface $api */ - $limitOrder = CoinbaseFacade::createLimitOrderToPlace( LimitOrderToPlace::SIDE_BUY, 'BTC-USD', @@ -252,16 +246,29 @@ $limitOrder = CoinbaseFacade::createLimitOrderToPlace( 0.0001 ); ``` -More information about orders can be found in [Orders feature](./feature/orders.md) +More information about [Orders](./feature/orders.md) ### Pagination ```php use MockingMagician\CoinbaseProSdk\CoinbaseFacade; -use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; - -/** @var ApiInterface $api */ $pagination = CoinbaseFacade::createPagination(); ``` -More information about pagination can be found in [Pagination](./pagination.md) +More information about [Pagination](./pagination.md) + +### Websocket + +Websocket is not part of the Coinbase REST api, it is real-time market data updates provided by coinbase. + +***It is not necessary to be authenticated*** to take advantage of it, so a method is directly defined in CoinbaseFacade to take advantage of this feature. + +```php +use MockingMagician\CoinbaseProSdk\CoinbaseFacade; + +$websocket = CoinbaseFacade::createUnauthenticatedWebsocket(); +``` + +***It is also possible to take advantage of it in an authenticated way*** in order to obtain more detailed information about the operations that concern the authenticated user. In this case it is necessary to use the websocket provided with the api. + +More information about [Websocket](./feature/websocket.md) diff --git a/docs/feature/websocket.md b/docs/feature/websocket.md index 6aaef3f..87ea588 100644 --- a/docs/feature/websocket.md +++ b/docs/feature/websocket.md @@ -14,67 +14,114 @@ Real-time market data updates provide the fastest insight into order flow and tr The websocket feed is publicly available, but connections to it are rate-limited to 1 per 4 seconds per IP, messages sent by client on each connection are rate-limited to 100 per second per IP. +Because the websocket is public, an access method without any connection parameters is available in [CoinbaseFacade](../coinbase-facade.md#websocket) + ## Websocket usage -Simple example : +Unauthenticated example : ```php -use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; +use MockingMagician\CoinbaseProSdk\CoinbaseFacade; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ErrorMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Subscriber; use MockingMagician\CoinbaseProSdk\Functional\Websocket\WebsocketRunner; -/** @var ApiInterface $api */ +$websocket = CoinbaseFacade::createUnauthenticatedWebsocket(); -$subscriber = new Subscriber(); +$subscriber = $websocket->newSubscriber(); $subscriber->activateChannelLevel2(true, [ 'BTC-EUR', 'XLM-EUR', ]); -$api->websocket()->run($subscriber, function ($runner) { +$websocket->run($subscriber, function ($runner) { /** @var WebsocketRunner $runner */ while ($runner->isConnected()) { $message = $runner->getMessage(); if ($message instanceof ErrorMessage) { - throw new Exception($message->getMessage()); // or break or what you want + throw new Exception($message->getMessage()); + // or break or what you want } // do something with your message } }); ``` -## Websocket subscriber -There are two types of subscriber, the simple subscriber and the authenticated subscriber, the latter gives more information about the messages that can be linked to the authenticated user. - -AuthenticateSubscriber example +Authenticated example : ```php use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ErrorMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\SubscriberAuthenticateAware;use MockingMagician\CoinbaseProSdk\Functional\Websocket\WebsocketRunner; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\WebsocketRunner; /** @var ApiInterface $api */ -$subscriber = new SubscriberAuthenticateAware($api); -$subscriber->runWithAuthentication(true); -$subscriber->activateChannelUser(true, [ +$websocket = $api->websocket(); + +$subscriber = $websocket->newSubscriber(); +$subscriber->activateChannelLevel2(true, [ 'BTC-EUR', 'XLM-EUR', ]); -$api->websocket()->run($subscriber, function ($runner) { +$subscriber->activateChannelUser(true, [ + 'BTC-EUR', + 'XLM-EUR', +]); // Using this method in any other context than in an authenticated manner will result in an error + +$websocket->run($subscriber, function ($runner) { /** @var WebsocketRunner $runner */ while ($runner->isConnected()) { $message = $runner->getMessage(); if ($message instanceof ErrorMessage) { - throw new Exception($message->getMessage()); // or break or what you want + throw new Exception($message->getMessage()); + // or break or what you want } // do something with your message } }); ``` -Accordind to documentation : + +## Websocket subscriber + +```php +use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; + +/** @var SubscriberAuthenticationAwareInterface $subscriber */ + +$subscriber->setProductIds([ + 'BTC-EUR', + 'XLM-EUR', +]); // Global productIds, activate productIds list for all activated channels + +$subscriber->activateChannelLevel2( + true, + ['BTC-EUR'] // optional productsIds to listen for this channel +); +$subscriber->activateChannelFull( + true, + ['BTC-EUR'] // optional productsIds to listen for this channel +); +$subscriber->activateChannelHeartbeat( + true, + ['BTC-EUR'] // optional productsIds to listen for this channel +); +$subscriber->activateChannelMatches( + true, + ['BTC-EUR'] // optional productsIds to listen for this channel +); +$subscriber->activateChannelTicker( + true, + ['BTC-EUR'] // optional productsIds to listen for this channel +); +$subscriber->activateChannelStatus(true); + +$subscriber->activateChannelUser( // only in authenticated context + true, + ['BTC-EUR'] // optional productsIds to listen for this channel +); +``` + +According to documentation : > The user channel is a version of the full channel that only contains messages that include the authenticated user. Consequently, you need to be authenticated to receive any messages. @@ -100,30 +147,321 @@ Accordind to documentation : [Link to documentation](https://docs.pro.coinbase.com/#the-full-channel) - -## Websocket subscriber in details +## Websocket messages ```php -use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\SubscriberAuthenticateAware; - -/** @var ApiInterface $api */ - -$subscriber = new SubscriberAuthenticateAware($api); - -$subscriber->runWithAuthentication(true); // specific to SubscriberAuthenticateAware -$subscriber->activateChannelUser(true, /* optional productsIds */ ['BTC-EUR']); // specific to SubscriberAuthenticateAware - -$subscriber->activateChannelLevel2(true, /* optional productsIds */ ['BTC-EUR']); -$subscriber->activateChannelFull(true, /* optional productsIds */ ['BTC-EUR']); -$subscriber->activateChannelHeartbeat(true, /* optional productsIds */ ['BTC-EUR']); -$subscriber->activateChannelMatches(true, /* optional productsIds */ ['BTC-EUR']); -$subscriber->activateChannelTicker(true, /* optional productsIds */ ['BTC-EUR']); +use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; +use MockingMagician\CoinbaseProSdk\Functional\Misc\Json; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ActivateMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ChangeMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\DoneMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ErrorMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\HeartbeatMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\L2UpdateMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\LastMatchMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\MatchMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\OpenMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ReceivedMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\SnapshotMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\StatusMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\SubscriptionsMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\TickerMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\UnknownMessage; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Websocket; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\WebsocketRunner; -$subscriber->activateChannelStatus(true); +/** @var Websocket $websocket */ +/** @var SubscriberAuthenticationAwareInterface $subscriber */ -$subscriber->setProductIds([ - 'BTC-EUR', - 'XLM-EUR', -]); // Global productIds, activate productIds for all active channels +$websocket->run($subscriber, function ($runner) { + /** @var WebsocketRunner $runner */ + while ($runner->isConnected()) { + $message = $runner->getMessage(); + + if ($message instanceof ErrorMessage) { + $message_ = $message->getMessage(); + $reason = $message->getReason(); + + throw new Exception("$message_. $reason"); + } + + if ($message instanceof ActivateMessage) { + $time = $message->getTime(); + $side = $message->getSide(); + $productId = $message->getProductId(); + $orderId = $message->getOrderId(); + $isPrivate = $message->isPrivate(); + $userId = $message->getUserId(); + $funds = $message->getFunds(); + $profileId = $message->getProfileId(); + $size = $message->getSize(); + $stopPrice = $message->getStopPrice(); + + echo sprintf( + 'At %s, user %s has activate a stop order with id %s', + $time, + $userId, + $orderId + ); + + continue; + } + + if ($message instanceof ChangeMessage) { + $newFunds = $message->getNewFunds(); + $newSize = $message->getNewSize(); + $oldFunds = $message->getOldFunds(); + $oldSize = $message->getOldSize(); + $orderId = $message->getOrderId(); + $price = $message->getPrice(); + $sequence = $message->getSequence(); + $productId = $message->getProductId(); + $side = $message->getSide(); + $time = $message->getTime(); + + echo sprintf( + 'At %s, order %s has changed', + $time, + $orderId + ); + + continue; + } + + if ($message instanceof DoneMessage) { + $orderId = $message->getOrderId(); + $price = $message->getPrice(); + $sequence = $message->getSequence(); + $productId = $message->getProductId(); + $side = $message->getSide(); + $time = $message->getTime(); + $reason = $message->getReason(); + $remainingSize = $message->getRemainingSize(); + + echo sprintf( + 'At %s, order %s has done cause %s', + $time, + $orderId, + $reason + ); + + continue; + } + + if ($message instanceof HeartbeatMessage) { + $sequence = $message->getSequence(); + $productId = $message->getProductId(); + $time = $message->getTime(); + $lastTradeId = $message->getLastTradeId(); + + echo sprintf( + 'At %s, product %s last trade id was %s', + $time, + $productId, + $lastTradeId + ); + + continue; + } + + if ($message instanceof L2UpdateMessage) { + $productId = $message->getProductId(); + $time = $message->getTime(); + $changes = $message->getChanges(); + + foreach ($changes as $change) { + $size = $change->getSize(); + $side = $change->getSide(); + $price = $change->getPrice(); + + echo sprintf( + 'At %s, product %s has changed at size %s in side %s, price is %s', + $time, + $productId, + $size, + $side, + $price + ); + } + + continue; + } + + if ($message instanceof LastMatchMessage) { + $productId = $message->getProductId(); + $time = $message->getTime(); + $side = $message->getSide(); + $makerOrderId = $message->getMakerOrderId(); + $takerOrderId = $message->getTakerOrderId(); + $tradeId = $message->getTradeId(); + $size = $message->getSize(); + + echo sprintf( + 'Last match for product %s at %s was trade id %s', + $productId, + $time, + $tradeId + ); + + continue; + } + + if ($message instanceof MatchMessage) { + $productId = $message->getProductId(); + $time = $message->getTime(); + $side = $message->getSide(); + $makerOrderId = $message->getMakerOrderId(); + $takerOrderId = $message->getTakerOrderId(); + $tradeId = $message->getTradeId(); + $size = $message->getSize(); + + echo sprintf( + 'Product %s has made a trade match at %s, trade id is %s', + $productId, + $time, + $tradeId + ); + + continue; + } + + if ($message instanceof OpenMessage) { + $productId = $message->getProductId(); + $time = $message->getTime(); + $side = $message->getSide(); + $orderId = $message->getOrderId(); + $price = $message->getPrice(); + $remainingSize = $message->getRemainingSize(); + $sequence = $message->getSequence(); + + echo sprintf( + 'At %s, an order with id %s was open on product %s', + $time, + $orderId, + $productId + ); + + continue; + } + + if ($message instanceof ReceivedMessage) { + $productId = $message->getProductId(); + $time = $message->getTime(); + $side = $message->getSide(); + $orderId = $message->getOrderId(); + $price = $message->getPrice(); + $sequence = $message->getSequence(); + $size = $message->getSize(); + $orderType = $message->getOrderType(); + $clientOrderId = $message->getClientOrderId(); + + echo sprintf( + 'At %s, an order with id %s was received on product %s', + $time, + $orderId, + $productId + ); + + continue; + } + + if ($message instanceof SnapshotMessage) { + $productId = $message->getProductId(); + $asks = $message->getAsks(); + $bids = $message->getBids(); + + foreach ($asks as $ask) { + $size = $ask->getSize(); + $price = $ask->getPrice(); + + echo sprintf( + 'Product %s has ask size % at price %s', + $productId, + $size, + $price + ); + } + + foreach ($bids as $bid) { + $size = $bid->getSize(); + $price = $bid->getPrice(); + + echo sprintf( + 'Product %s has bid size % at price %s', + $productId, + $size, + $price + ); + } + + continue; + } + + if ($message instanceof StatusMessage) { + $currencies = $message->getCurrencies(); + $products = $message->getProducts(); + + foreach ($currencies as $currency) { + // each currency is \MockingMagician\CoinbaseProSdk\Functional\DTO\CurrencyData + } + + foreach ($products as $product) { + // each currency is \MockingMagician\CoinbaseProSdk\Functional\DTO\ProductData + } + + continue; + } + + if ($message instanceof SubscriptionsMessage) { + $channels = $message->getChannels(); + + foreach ($channels as $channel) { + $name = $channel->getName(); + $productIds = $channel->getProductIds(); + + echo sprintf( + 'You have subscribe to channel %s with product ids : %s', + $name, + implode(', ', $productIds) + ); + } + + continue; + } + + if ($message instanceof TickerMessage) { + $message->getPrice(); + $message->getBestAsk(); + $message->getBestBid(); + $message->getHigh24h(); + $message->getLastSize(); + $message->getLow24h(); + $message->getOpen24h(); + $message->getVolume24h(); + $message->getVolume30d(); + $message->getTradeId(); + $message->getSide(); + $message->getProductId(); + $message->getTime(); + $message->getSequence(); + $message->getTime(); + + continue; + } + + if ($message instanceof UnknownMessage) { + $payload = $message->getPayload(); + $type = $payload['type']; + Json::encode($payload); + + echo sprintf( + 'This message of type %s is not yet implemented, payload looks as is : %s', + $type, + $payload + ); + + continue; + } + } +}); ``` From f4d53f9c70aa4ae007327126da20c3f426418863 Mon Sep 17 00:00:00 2001 From: Marc MOREAU Date: Sat, 27 Mar 2021 00:24:07 +0100 Subject: [PATCH 3/4] websocket * normalizing websocket * create websocket without to be authenticate * update documentation --- src/Contracts/DTO/ChannelDataInterface.php | 2 +- ...SubscriberAuthenticationAwareInterface.php | 2 - .../Websocket/WebsocketInterface.php | 2 + .../DTO/AbstractSnapshotAskBidInterface.php | 10 + src/Functional/DTO/ChannelData.php | 10 +- src/Functional/DTO/ProductData.php | 8 +- .../Websocket/AbstractSubscriber.php | 36 ++- src/Functional/Websocket/MessageHandler.php | 14 ++ src/Functional/Websocket/Websocket.php | 4 +- tests/func/Websocket/WebsocketTest.php | 223 +++++------------- tests/unit/Api/CoinbaseApiTest.php | 9 + 11 files changed, 128 insertions(+), 192 deletions(-) diff --git a/src/Contracts/DTO/ChannelDataInterface.php b/src/Contracts/DTO/ChannelDataInterface.php index ec74c54..530b410 100644 --- a/src/Contracts/DTO/ChannelDataInterface.php +++ b/src/Contracts/DTO/ChannelDataInterface.php @@ -12,5 +12,5 @@ interface ChannelDataInterface { public function getName(): string; - public function getProductsIds(): array; + public function getProductIds(): array; } diff --git a/src/Contracts/Websocket/SubscriberAuthenticationAwareInterface.php b/src/Contracts/Websocket/SubscriberAuthenticationAwareInterface.php index f8a17f0..46c6070 100644 --- a/src/Contracts/Websocket/SubscriberAuthenticationAwareInterface.php +++ b/src/Contracts/Websocket/SubscriberAuthenticationAwareInterface.php @@ -8,8 +8,6 @@ namespace MockingMagician\CoinbaseProSdk\Contracts\Websocket; -use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; - interface SubscriberAuthenticationAwareInterface extends SubscriberInterface { public function activateChannelUser(bool $activate, array $productIds = []): void; diff --git a/src/Contracts/Websocket/WebsocketInterface.php b/src/Contracts/Websocket/WebsocketInterface.php index 8d4e45d..1d74f4b 100644 --- a/src/Contracts/Websocket/WebsocketInterface.php +++ b/src/Contracts/Websocket/WebsocketInterface.php @@ -10,6 +10,8 @@ interface WebsocketInterface { + public function newSubscriber(): SubscriberAuthenticationAwareInterface; + /** * @param callable $userFunc (WebsocketRunnerInterface $websocket, ...$args):void * @param mixed ...$args diff --git a/src/Functional/DTO/AbstractSnapshotAskBidInterface.php b/src/Functional/DTO/AbstractSnapshotAskBidInterface.php index 91d99f0..021e802 100644 --- a/src/Functional/DTO/AbstractSnapshotAskBidInterface.php +++ b/src/Functional/DTO/AbstractSnapshotAskBidInterface.php @@ -19,6 +19,16 @@ abstract class AbstractSnapshotAskBidInterface extends AbstractCreator */ private $size; + public function getPrice(): float + { + return $this->price; + } + + public function getSize(): float + { + return $this->size; + } + public function __construct(float $price, float $size) { $this->price = $price; diff --git a/src/Functional/DTO/ChannelData.php b/src/Functional/DTO/ChannelData.php index f0af2bf..c0fe840 100644 --- a/src/Functional/DTO/ChannelData.php +++ b/src/Functional/DTO/ChannelData.php @@ -20,12 +20,12 @@ class ChannelData extends AbstractCreator implements ChannelDataInterface /** * @var string[] */ - private $productsIds; + private $productIds; - public function __construct(string $name, array $productsIds) + public function __construct(string $name, array $productIds) { $this->name = $name; - $this->productsIds = $productsIds; + $this->productIds = $productIds; } public function getName(): string @@ -36,9 +36,9 @@ public function getName(): string /** * @return string[] */ - public function getProductsIds(): array + public function getProductIds(): array { - return $this->productsIds; + return $this->productIds; } public static function createFromArray(array $array, ...$extraData) diff --git a/src/Functional/DTO/ProductData.php b/src/Functional/DTO/ProductData.php index bc0344f..fe0752a 100644 --- a/src/Functional/DTO/ProductData.php +++ b/src/Functional/DTO/ProductData.php @@ -215,10 +215,10 @@ public function isTradingFullyOperational(): bool { return !( - $this->isCancelOnly() || - $this->isLimitOnly() || - $this->isPostOnly() || - $this->isTradingDisabled() + $this->isCancelOnly() + || $this->isLimitOnly() + || $this->isPostOnly() + || $this->isTradingDisabled() ) ; } diff --git a/src/Functional/Websocket/AbstractSubscriber.php b/src/Functional/Websocket/AbstractSubscriber.php index 446badd..746f1f5 100644 --- a/src/Functional/Websocket/AbstractSubscriber.php +++ b/src/Functional/Websocket/AbstractSubscriber.php @@ -8,10 +8,6 @@ namespace MockingMagician\CoinbaseProSdk\Functional\Websocket; -use MockingMagician\CoinbaseProSdk\Contracts\Api\ApiInterface; -use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; -use MockingMagician\CoinbaseProSdk\Functional\Error\ApiError; - abstract class AbstractSubscriber { /** @@ -23,22 +19,6 @@ abstract class AbstractSubscriber 'channels' => [], ]; - protected function activateChannel(string $channelKey, bool $activate, ?array $productIds): void - { - if ($activate) { - $this->payloadTemplate['channels'][$channelKey] = ['name' => $channelKey]; - if (!is_null($productIds) && !empty($productIds)) { - $this->payloadTemplate['channels'][$channelKey]['product_ids'] = $productIds; - } - - return; - } - - if (isset($this->payloadTemplate['channels'][$channelKey])) { - unset($this->payloadTemplate['channels'][$channelKey]); - } - } - public function setProductIds(array $productIds): void { $this->payloadTemplate['product_ids'] = $productIds; @@ -82,4 +62,20 @@ public function getPayload(bool $unsubscribe = false): array return $payload; } + + protected function activateChannel(string $channelKey, bool $activate, ?array $productIds): void + { + if ($activate) { + $this->payloadTemplate['channels'][$channelKey] = ['name' => $channelKey]; + if (!is_null($productIds) && !empty($productIds)) { + $this->payloadTemplate['channels'][$channelKey]['product_ids'] = $productIds; + } + + return; + } + + if (isset($this->payloadTemplate['channels'][$channelKey])) { + unset($this->payloadTemplate['channels'][$channelKey]); + } + } } diff --git a/src/Functional/Websocket/MessageHandler.php b/src/Functional/Websocket/MessageHandler.php index 2938699..40c662e 100644 --- a/src/Functional/Websocket/MessageHandler.php +++ b/src/Functional/Websocket/MessageHandler.php @@ -32,32 +32,46 @@ public static function handle(array $payload): ?MessageInterface switch ($payload['type']) { case 'error': return new ErrorMessage($payload); + case 'subscriptions': return new SubscriptionsMessage($payload); + case 'status': return new StatusMessage($payload); + case 'snapshot': return new SnapshotMessage($payload); + case 'ticker': return new TickerMessage($payload); + case 'l2update': return new L2UpdateMessage($payload); + case 'heartbeat': return new HeartbeatMessage($payload); + case 'received': return new ReceivedMessage($payload); + case 'open': return new OpenMessage($payload); + case 'done': return new DoneMessage($payload); + case 'match': return new MatchMessage($payload); + case 'last_match': return new LastMatchMessage($payload); + case 'activate': return new ActivateMessage($payload); + case 'change': return new ChangeMessage($payload); + default: return new UnknownMessage($payload); } diff --git a/src/Functional/Websocket/Websocket.php b/src/Functional/Websocket/Websocket.php index e857ee6..adab935 100644 --- a/src/Functional/Websocket/Websocket.php +++ b/src/Functional/Websocket/Websocket.php @@ -21,11 +21,11 @@ class Websocket implements WebsocketInterface */ private $runner; /** - * @var ApiInterface|null + * @var null|ApiInterface */ private $api; /** - * @var TimeInterface|null + * @var null|TimeInterface */ private $time; diff --git a/tests/func/Websocket/WebsocketTest.php b/tests/func/Websocket/WebsocketTest.php index ae85f63..32c6ebb 100644 --- a/tests/func/Websocket/WebsocketTest.php +++ b/tests/func/Websocket/WebsocketTest.php @@ -10,24 +10,10 @@ use Dotenv\Dotenv; use MockingMagician\CoinbaseProSdk\CoinbaseFacade; -use MockingMagician\CoinbaseProSdk\Contracts\Websocket\SubscriberAuthenticationAwareInterface; use MockingMagician\CoinbaseProSdk\Contracts\Websocket\WebsocketRunnerInterface; use MockingMagician\CoinbaseProSdk\Functional\Api\CoinbaseApi; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ActivateMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ChangeMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\DoneMessage; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ErrorMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\HeartbeatMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\L2UpdateMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\LastMatchMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\MatchMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\OpenMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\ReceivedMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\SnapshotMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\StatusMessage; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\SubscriptionsMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\TickerMessage; -use MockingMagician\CoinbaseProSdk\Functional\Websocket\Message\UnknownMessage; use MockingMagician\CoinbaseProSdk\Functional\Websocket\Websocket; use MockingMagician\CoinbaseProSdk\Functional\Websocket\WebsocketRunner; use MockingMagician\CoinbaseProSdk\Tests\Func\Connectivity\AbstractTest; @@ -37,7 +23,6 @@ */ final class WebsocketTest extends AbstractTest { - private $websocket; /** * @var CoinbaseApi */ @@ -47,7 +32,7 @@ final class WebsocketTest extends AbstractTest */ private $simpleWebsocket; /** - * @var Websocket|null + * @var null|Websocket */ private $authenticatedWebsocket; @@ -66,14 +51,15 @@ public function setUp(): void getenv('API_PASSPHRASE_REAL_FOR_WEBSOCKET'), ]; - if (!in_array(false, $params)) { - $this->authenticatedWebsocket = new Websocket(new WebsocketRunner(), $this->coinbaseApi); - } - $this->coinbaseApi = CoinbaseFacade::createDefaultCoinbaseApi( 'https://api.pro.coinbase.com', ...$params ); + + if (!in_array(false, $params)) { + $this->authenticatedWebsocket = new Websocket(new WebsocketRunner(), $this->coinbaseApi); + } + if (!$this->retryHasInternetConnection(3, 1)) { $this->markTestSkipped('Functional tests require an internet connection.'); } @@ -238,6 +224,7 @@ public function testSubscribeChannelUser() { if (is_null($this->authenticatedWebsocket)) { $this->markTestSkipped('Functional tests for websocket require REAL(production) key, secret, passphrase.'); + return; } $subscriber = $this->authenticatedWebsocket->newSubscriber(); @@ -262,142 +249,62 @@ public function testSubscribeChannelUser() self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); }); } + + public function testSubscribeAllChannels() + { + $subscriber = $this->simpleWebsocket->newSubscriber(); + $subscriber->setProductIds(['BTC-EUR', 'BTC-USD']); + $subscriber->activateChannelFull(true); + $subscriber->activateChannelLevel2(true); + $subscriber->activateChannelStatus(true); + $subscriber->activateChannelTicker(true); + $subscriber->activateChannelMatches(true); + $subscriber->activateChannelHeartbeat(true); + $this->simpleWebsocket->run($subscriber, function ($runner) { + /** @var WebsocketRunnerInterface $runner */ + $error = null; + $subscriptionMessageFound = false; + $im = $i = 10; + while ($i--) { + $message = $runner->getMessage(); + if ($message instanceof SubscriptionsMessage) { + $subscriptionMessageFound = true; + } + if ($message instanceof ErrorMessage) { + $error = $message->getMessage().'. '.$message->getReason(); + + break; + } + } + self::assertNull($error, $error ?? ''); + self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); + }); + } + + public function testMessagesLongRun() + { + $method = __METHOD__; + $subscriber = $this->simpleWebsocket->newSubscriber(); + $subscriber->setProductIds(['BTC-EUR', 'BTC-USD']); + $subscriber->activateChannelLevel2(true); + $subscriber->activateChannelStatus(true); + $subscriber->activateChannelTicker(true); + $subscriber->activateChannelMatches(true); + $subscriber->activateChannelHeartbeat(true); + $subscriber->activateChannelFull(true); + $this->simpleWebsocket->run($subscriber, function ($runner, $method) { + /** @var WebsocketRunnerInterface $runner */ + $messagesTypeCounter = []; + $im = $i = 10 ** 5; + fwrite(STDOUT, sprintf("\nExecuting %s", $method)); + fwrite(STDOUT, "\nHas {$im} messages to fetch, rest :\n\r"); + while ($i--) { + fwrite(STDOUT, "\r".preg_replace('/./', ' ', $im)."\r{$i}"); + $message = $runner->getMessage(); + @++$messagesTypeCounter[get_class($message)]; + } + $this->assertEquals(array_sum($messagesTypeCounter), $im); + $this->assertArrayNotHasKey(ErrorMessage::class, $messagesTypeCounter); + }, $method); + } } -// -// public function testSubscription() -// { -// dump('hello'); -// $this->simpleWebsocket->run($this->getSimpleSubscriber(), function ($runner) { -// /** @var WebsocketRunnerInterface $runner */ -// $error = null; -// $subscriptionMessageFound = false; -// $im = $i = 500; -// while ($i--) { -// dump($i); -// $message = $runner->getMessage(); -// if ($message instanceof SubscriptionsMessage) { -// $subscriptionMessageFound = true; -// } -// if ($message instanceof ErrorMessage) { -// $error = $message->getMessage().'. '.$message->getReason(); -// -// break; -// } -// } -// self::assertNull($error, $error ?? ''); -// self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); -// }); -// } -// -// public function testSubscriptionAuthenticate() -// { -// $this->authenticatedWebsocket->run($this->getAuthenticateSubscriber(), function ($runner) { -// /** @var WebsocketRunnerInterface $runner */ -// $error = null; -// $subscriptionMessageFound = false; -// $im = $i = 500; -// while ($i--) { -// $message = $runner->getMessage(); -// if ($message instanceof SubscriptionsMessage) { -// $subscriptionMessageFound = true; -// } -// if ($message instanceof ErrorMessage) { -// $error = $message->getMessage().'. '.$message->getReason(); -// -// break; -// } -// } -// self::assertNull($error, $error ?? ''); -// self::assertTrue($subscriptionMessageFound, sprintf('No subscription message found in %s first messages received', $im)); -// }); -// } -// -// public function testMessagesLongRun() -// { -// $method = __METHOD__; -// $subscriber = $this->getSimpleSubscriber(); -// $subscriber->activateChannelLevel2(false); -// $subscriber->activateChannelStatus(false); -// $subscriber->activateChannelTicker(false); -// $subscriber->activateChannelMatches(false); -// $subscriber->activateChannelHeartbeat(false); -// $this->simpleWebsocket->run($subscriber, function ($runner, $method) { -// /** @var WebsocketRunnerInterface $runner */ -// $messagesTypeCounter = [ -// ActivateMessage::class => 0, -// ChangeMessage::class => 0, -// DoneMessage::class => 0, -// ErrorMessage::class => 0, -// HeartbeatMessage::class => 0, -// L2UpdateMessage::class => 0, -// LastMatchMessage::class => 0, -// MatchMessage::class => 0, -// OpenMessage::class => 0, -// ReceivedMessage::class => 0, -// SnapshotMessage::class => 0, -// StatusMessage::class => 0, -// SubscriptionsMessage::class => 0, -// TickerMessage::class => 0, -// UnknownMessage::class => 0, -// ]; -// $im = $i = 10 ** 5; -// fwrite(STDOUT, sprintf("\nExecuting %s", $method)); -// fwrite(STDOUT, "\nHas {$im} messages to fetch, rest :\n\r"); -// while ($i--) { -// fwrite(STDOUT, "\r".preg_replace('/./', ' ', $im)."\r{$i}"); -// $message = $runner->getMessage(); -// ++$messagesTypeCounter[get_class($message)]; -// } -// $this->assertEquals(array_sum($messagesTypeCounter), $im); -// }, $method); -// } -// -// private function getSimpleSubscriber(): SubscriberAuthenticationAwareInterface -// { -// $subscriber = $this->simpleWebsocket->newSubscriber(); -// $subscriber->setProductIds($this->getProductIds()); -// $subscriber->activateChannelTicker(true); -// $subscriber->activateChannelMatches(true); -// $subscriber->activateChannelStatus(true); -// $subscriber->activateChannelLevel2(true); -// $subscriber->activateChannelHeartbeat(true); -// $subscriber->activateChannelFull(true); -// -// return $subscriber; -// } -// -// private function getAuthenticateSubscriber(): SubscriberAuthenticationAwareInterface -// { -// $subscriber = $this->authenticatedWebsocket->newSubscriber(); -// $subscriber->setProductIds($this->getProductIds()); -// $subscriber->activateChannelUser(true); -// $subscriber->activateChannelTicker(true); -// $subscriber->activateChannelMatches(true); -// $subscriber->activateChannelStatus(true); -// $subscriber->activateChannelLevel2(true); -// $subscriber->activateChannelHeartbeat(true); -// -// return $subscriber; -// } -// -// private function getProductIds(): array -// { -// $products = $this->coinbaseApi->products()->getProducts(); -// $productIds = []; -// foreach ($products as $product) { -// $productIds[] = $product->getId(); -// } -// -// return array_values(array_filter($productIds, function ($value) { -// if ( -// false === stripos($value, 'USDC') -// && false === stripos($value, 'GBP') -// && false === stripos($value, 'USD') -// ) { -// return true; -// } -// -// return false; -// })); -// } -//} diff --git a/tests/unit/Api/CoinbaseApiTest.php b/tests/unit/Api/CoinbaseApiTest.php index 9e8d810..b985c9c 100644 --- a/tests/unit/Api/CoinbaseApiTest.php +++ b/tests/unit/Api/CoinbaseApiTest.php @@ -8,9 +8,13 @@ namespace MockingMagician\CoinbaseProSdk\Tests\Unit\Api; +use MockingMagician\CoinbaseProSdk\CoinbaseFacade; use MockingMagician\CoinbaseProSdk\Functional\Api\CoinbaseApi; use MockingMagician\CoinbaseProSdk\Functional\Api\Config\CoinbaseConfig; use MockingMagician\CoinbaseProSdk\Functional\Connectivity\AbstractConnectivity; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Subscriber; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\SubscriberAuthenticateAware; +use MockingMagician\CoinbaseProSdk\Functional\Websocket\Websocket; use PHPUnit\Framework\TestCase; use Throwable; @@ -42,6 +46,11 @@ public function testWithAllConnectivityEnabled() self::assertInstanceOf(AbstractConnectivity::class, $api->products()); self::assertInstanceOf(AbstractConnectivity::class, $api->currencies()); self::assertInstanceOf(AbstractConnectivity::class, $api->time()); + + self::assertInstanceOf(Websocket::class, $api->websocket()); + self::assertInstanceOf(SubscriberAuthenticateAware::class, $api->websocket()->newSubscriber()); + + self::assertInstanceOf(Subscriber::class, CoinbaseFacade::createUnauthenticatedWebsocket()->newSubscriber()); } public function testWithAllConnectivityDisabled() From aa554045bc22afd5a69d55ad04b62bd233a31694 Mon Sep 17 00:00:00 2001 From: Marc MOREAU Date: Sat, 27 Mar 2021 00:27:20 +0100 Subject: [PATCH 4/4] websocket * phpcs * phpstan --- src/CoinbaseFacade.php | 2 +- .../DTO/AbstractSnapshotAskBidInterface.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CoinbaseFacade.php b/src/CoinbaseFacade.php index 993876c..baf4aef 100644 --- a/src/CoinbaseFacade.php +++ b/src/CoinbaseFacade.php @@ -116,7 +116,7 @@ public static function createMarketOrderToPlace( ); } - public static function createUnauthenticatedWebsocket() + public static function createUnauthenticatedWebsocket(): Websocket { return new Websocket(new WebsocketRunner()); } diff --git a/src/Functional/DTO/AbstractSnapshotAskBidInterface.php b/src/Functional/DTO/AbstractSnapshotAskBidInterface.php index 021e802..67d4ec8 100644 --- a/src/Functional/DTO/AbstractSnapshotAskBidInterface.php +++ b/src/Functional/DTO/AbstractSnapshotAskBidInterface.php @@ -19,6 +19,12 @@ abstract class AbstractSnapshotAskBidInterface extends AbstractCreator */ private $size; + public function __construct(float $price, float $size) + { + $this->price = $price; + $this->size = $size; + } + public function getPrice(): float { return $this->price; @@ -29,12 +35,6 @@ public function getSize(): float return $this->size; } - public function __construct(float $price, float $size) - { - $this->price = $price; - $this->size = $size; - } - public static function createFromArray(array $array, ...$extraData) { return new static(