diff --git a/src/Client.php b/src/Client.php index debd270..b07c4da 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,14 +10,19 @@ use Butschster\Kraken\Responses\{AccountBalanceResponse, AddOrderResponse, AssetInfoResponse, + BoolStatusResponse, CancelOrderResponse, CancelOrdersAfterTimeoutResponse, ClosedOrdersResponse, DepositAddressesResponse, DepositMethodsResponse, + EarnAllocationsResponse, + EarnStrategiesResponse, Entities\AddOrder\OrderAdded, Entities\CancelOrdersAfterTimeout, Entities\DepositMethods, + Entities\Earn\Allocation\Allocations, + Entities\Earn\EarnStrategies, Entities\Orders\ClosedOrders, Entities\ServerTime, Entities\SystemStatus, @@ -38,6 +43,7 @@ use Butschster\Kraken\ValueObjects\{ AssetClass, AssetPair, TradableInfo }; +use Butschster\Kraken\Requests\EarnAllocationRequest; use DateTimeInterface; use Illuminate\Support\Str; use JMS\Serializer\SerializerInterface; @@ -307,6 +313,56 @@ public function getWithdrawalInformation(string $asset, string $key, BigDecimal )->result; } + public function getEarnStrategies(?string $asset = null, array $lockType = ['flex', 'bonded', 'instant'], bool $ascending = false): EarnStrategies + { + return $this->request( + method: 'private/Earn/Strategies', + responsePayload: EarnStrategiesResponse::class, + parameters: [ + 'asset' => $asset, + 'lock_type' => $lockType, + 'ascending' => $ascending ? 'true' : 'false', + // 'cursor' => '10', // not yet implemented + // 'limit' => 10, // not yet implemented + ], + )->result; + } + + public function getEarnAllocations(string $convertedAsset = 'USD', bool $hideZeroAllocations = false, bool $ascending = false): Allocations + { + return $this->request( + method: 'private/Earn/Allocations', + responsePayload: EarnAllocationsResponse::class, + parameters: [ + 'ascending' => $ascending ? 'true' : 'false', + 'converted_asset' => $convertedAsset, + 'hide_zero_allocations' => $hideZeroAllocations ? 'true' : 'false', + ] + )->result; + } + + public function allocateEarnFunds(BigDecimal $amount, string $strategyId): bool + { + $request = new EarnAllocationRequest($amount, $strategyId); + + return $this->request( + method: 'private/Earn/Allocate', + responsePayload: BoolStatusResponse::class, + parameters: $request->toArray(), + )->result; + } + + public function deallocateEarnFunds(BigDecimal $amount, string $strategyId): bool + { + $request = new EarnAllocationRequest($amount, $strategyId); + + return $this->request( + method: 'private/Earn/Deallocate', + responsePayload: BoolStatusResponse::class, + parameters: $request->toArray(), + )->result; + } + /** * Make request * @@ -350,7 +406,7 @@ public function request( $responseObject = $this->serializer->deserialize( - $response->getBody()->getContents(), + (string) $response->getBody(), $responsePayload, 'json', ); diff --git a/src/Contracts/Client.php b/src/Contracts/Client.php index 0eee94a..950fe4c 100644 --- a/src/Contracts/Client.php +++ b/src/Contracts/Client.php @@ -9,6 +9,8 @@ use Butschster\Kraken\Responses\Entities\CancelOrdersAfterTimeout; use Butschster\Kraken\Responses\Entities\DepositAddresses; use Butschster\Kraken\Responses\Entities\DepositMethods; +use Butschster\Kraken\Responses\Entities\Earn\Allocation\Allocations; +use Butschster\Kraken\Responses\Entities\Earn\EarnStrategies; use Butschster\Kraken\Responses\Entities\OrderBook\Orders; use Butschster\Kraken\Responses\Entities\Orders\ClosedOrders; use Butschster\Kraken\Responses\Entities\Orders\Order; @@ -23,6 +25,7 @@ use Butschster\Kraken\ValueObjects\AssetPair; use Butschster\Kraken\ValueObjects\TradableInfo; use DateTimeInterface; +use GuzzleHttp\Exception\GuzzleException; interface Client { @@ -208,4 +211,123 @@ public function getDepositAddresses(string $asset, string $method, bool $new = f * @return WithdrawalInformation */ public function getWithdrawalInformation(string $asset, string $key, BigDecimal $amount): WithdrawalInformation; + + /** + * List earn strategies along with their parameters. + * + * Requires a valid API key but not specific permission is required. + * + * Returns only strategies that are available to the user based on geographic region. + * + * When the user does not meet the tier restriction: + * - `can_allocate` will be false + * - `allocation_restriction_info` indicates `Tier` as the restriction reason + * + * Earn products generally require Intermediate tier. Get your account verified to access earn. + * + * A note about `lock_type`: + * - `instant`: can be deallocated without an unbonding period. This is called flexible in the UI. + * - `bonded`: has an unbonding period. Deallocation will not happen until this period has passed. + * - `flex`: "Kraken rewards". This is earning on your spot balances where eligible. It's turned on account wide from the UI and you cannot manually allocate to these strategies. + * + * Paging isn't yet implemented, so the endpoint always returns all data in the first page. + * + * @see https://docs.kraken.com/api/docs/rest-api/list-strategies + * @param string|null $asset + * @param array $lockType + * @param bool $ascending + * @return EarnStrategies + */ + public function getEarnStrategies(?string $asset = null, array $lockType = ['flex', 'bonded', 'instant'], bool $ascending = false): EarnStrategies; + + /** + * List all allocations for the user. + * + * Requires the `Query Funds` API key permission. + * + * By default, all allocations are returned, even for strategies that have been used in the past and have zero balance now. + * This allows the user to see how much was earned with a given strategy in the past. The `hide_zero_allocations` parameter + * can be used to remove zero balance entries from the output. Paging hasn't been implemented for this method as we don't + * expect the result for a particular user to be overwhelmingly large. + * + * All amounts in the output can be denominated in a currency of the user's choice (the `converted_asset` parameter). + * + * Information about when the next reward will be paid to the client is also provided in the output. + * + * Allocated funds can be in up to 4 states: + * - bonding + * - allocated + * - exit_queue (ETH only) + * - unbonding + * + * Any funds in `total` not in `bonding`/`unbonding` are simply allocated and earning rewards. Depending on the strategy, funds + * in the other 3 states can also be earning rewards. Consult the output of `/Earn/Strategies` to know whether `bonding`/`unbonding` + * earn rewards. `ETH` in `exit_queue` still earns rewards. + * + * Note that for `ETH`, when the funds are in the `exit_queue` state, the `expires` time given is the time when the funds will have + * finished unbonding, not when they go from exit queue to unbonding. + * + * (Un)bonding time estimate can be inaccurate right after having (de)allocated the funds. Wait 1-2 minutes after (de)allocating + * to get an accurate result. + * + * @see https://docs.kraken.com/api/docs/rest-api/list-allocations + * @param string $convertedAsset The currency to which amounts should be converted. Default is 'USD'. + * @param bool $hideZeroAllocations Whether to hide allocations with zero balance. Default is false. + * @param bool $ascending Whether to sort the results in ascending order. Default is false. + * @return Allocations The list of allocations. + */ + public function getEarnAllocations(string $convertedAsset = 'USD', bool $hideZeroAllocations = false, bool $ascending = false): Allocations; + + /** + * Allocate funds to the Strategy. + * + * Requires the `Earn Funds` API key permission. The amount must always be defined. + * + * This method is asynchronous. A couple of preflight checks are performed synchronously on behalf of the + * method before it is dispatched further. The client is required to poll the result using the + * `/0/private/Earn/AllocateStatus` endpoint. + * + * There can be only one (de)allocation request in progress for a given user and strategy at any time. While the operation is in progress: + * - `pending` attribute in `/Earn/Allocations` response for the strategy indicates that funds are being allocated, + * - `pending` attribute in `/Earn/AllocateStatus` response will be true. + * + * Following specific errors within `Earnings` class can be returned by this method: + * - Minimum allocation: `EEarnings:Below min:(De)allocation operation amount less than minimum` + * - Allocation in progress: `EEarnings:Busy:Another (de)allocation for the same strategy is in progress` + * - Service temporarily unavailable: `EEarnings:Busy`. Try again in a few minutes. + * - User tier verification: `EEarnings:Permission denied:The user's tier is not high enough` + * - Strategy not found: `EGeneral:Invalid arguments:Invalid strategy ID` + * + * @see https://docs.kraken.com/api/docs/rest-api/allocate-strategy + * @param BigDecimal $amount + * @param string $strategyId + * @return bool + */ + public function allocateEarnFunds(BigDecimal $amount, string $strategyId): bool; + + + /** + * Deallocate funds from a strategy. + * + * Requires the `Earn Funds` API key permission. The amount must always be defined. + * + * This method is asynchronous. A couple of preflight checks are performed synchronously on behalf of the + * method before it is dispatched further. If the method returns HTTP 202 code, the client is required to poll + * the result using the `/Earn/DeallocateStatus` endpoint. + * + * There can be only one (de)allocation request in progress for a given user and strategy. While the operation is in progress: + * - `pending` attribute in `Allocations` response for the strategy will hold the amount that is being deallocated (negative amount) + * - `pending` attribute in `DeallocateStatus` response will be true. + * + * Following specific errors within `Earnings` class can be returned by this method: + * - Minimum allocation: `EEarnings:Below min:(De)allocation operation amount less than minimum allowed` + * - Allocation in progress: `EEarnings:Busy:Another (de)allocation for the same strategy is in progress` + * - Strategy not found: `EGeneral:Invalid arguments:Invalid strategy ID` + * + * @see https://docs.kraken.com/api/docs/rest-api/deallocate-strategy + * @param BigDecimal $amount + * @param string $strategyId + * @return bool + */ + public function deallocateEarnFunds(BigDecimal $amount, string $strategyId): bool; } diff --git a/src/Contracts/EarnAllocationRequest.php b/src/Contracts/EarnAllocationRequest.php new file mode 100644 index 0000000..e4faae5 --- /dev/null +++ b/src/Contracts/EarnAllocationRequest.php @@ -0,0 +1,19 @@ +amount = $amount; + + return $this; + } + + public function setStrategyId(?string $strategy_id): self + { + $this->strategy_id = $strategy_id; + + return $this; + } + + public function amount(): ?BigDecimal + { + return $this->amount; + } + + public function strategy_id(): string + { + return $this->strategy_id; + } + + public function toArray(): array + { + return [ + 'amount' => (string) $this->amount(), + 'strategy_id' => $this->strategy_id(), + ]; + } +} diff --git a/src/Responses/BoolStatusResponse.php b/src/Responses/BoolStatusResponse.php new file mode 100644 index 0000000..ab4828d --- /dev/null +++ b/src/Responses/BoolStatusResponse.php @@ -0,0 +1,11 @@ +")] + public array $items = []; +} diff --git a/src/Responses/Entities/Earn/Allocation/AmountAllocated.php b/src/Responses/Entities/Earn/Allocation/AmountAllocated.php new file mode 100644 index 0000000..0dd63b1 --- /dev/null +++ b/src/Responses/Entities/Earn/Allocation/AmountAllocated.php @@ -0,0 +1,11 @@ +")] + public array $items = []; + + #[Type('string')] + public ?string $next_cursor = null; +} diff --git a/src/Responses/Entities/Earn/EarnStrategy.php b/src/Responses/Entities/Earn/EarnStrategy.php new file mode 100644 index 0000000..8c9bd6b --- /dev/null +++ b/src/Responses/Entities/Earn/EarnStrategy.php @@ -0,0 +1,47 @@ +type; + } +} diff --git a/src/Responses/Entities/Earn/LockTypeBonded.php b/src/Responses/Entities/Earn/LockTypeBonded.php new file mode 100644 index 0000000..32f86d5 --- /dev/null +++ b/src/Responses/Entities/Earn/LockTypeBonded.php @@ -0,0 +1,32 @@ + GraphNavigatorInterface::DIRECTION_DESERIALIZATION, + 'format' => 'json', + 'type' => 'EarnLockType', + 'method' => 'deserialize', + ], + ]; + } + + public function deserialize(JsonDeserializationVisitor $visitor, $data, array $type, Context $context) + { + $navigator = $context->getNavigator(); + switch ($data['type']) { + case 'instant': + return $navigator->accept($data, ['name' => Earn\LockTypeInstant::class], $context); + case 'flex': + return $navigator->accept($data, ['name' => Earn\LockTypeFlex::class], $context); + case 'bonded': + return $navigator->accept($data, ['name' => Earn\LockTypeBonded::class], $context); + default: + throw new \RuntimeException('Unknown lock type: '.$data['type']); + } + } +} diff --git a/src/Serializer/SerializerFactory.php b/src/Serializer/SerializerFactory.php index 351dfed..701c1f9 100644 --- a/src/Serializer/SerializerFactory.php +++ b/src/Serializer/SerializerFactory.php @@ -23,6 +23,7 @@ public function build() $registry->registerSubscribingHandler(new BigDecimalHandler()); $registry->registerSubscribingHandler(new TimestampHandler()); $registry->registerSubscribingHandler(new ComaSeparatedHandler()); + $registry->registerSubscribingHandler(new EarnLockTypeHandler()); }); return $builder->build();