<?php namespace Coinbase\Wallet; use Coinbase\Wallet\Enum\ResourceType; use Coinbase\Wallet\Exception\LogicException; use Coinbase\Wallet\Exception\RuntimeException; use Coinbase\Wallet\Resource\Account; use Coinbase\Wallet\Resource\Address; use Coinbase\Wallet\Resource\Application; use Coinbase\Wallet\Resource\BitcoinAddress; use Coinbase\Wallet\Resource\BitcoinCashAddress; use Coinbase\Wallet\Resource\Buy; use Coinbase\Wallet\Resource\Checkout; use Coinbase\Wallet\Resource\CurrentUser; use Coinbase\Wallet\Resource\Deposit; use Coinbase\Wallet\Resource\Email; use Coinbase\Wallet\Resource\EthereumNetwork; use Coinbase\Wallet\Resource\EthrereumAddress; use Coinbase\Wallet\Resource\LitecoinAddress; use Coinbase\Wallet\Resource\LitecoinNetwork; use Coinbase\Wallet\Resource\Merchant; use Coinbase\Wallet\Resource\Order; use Coinbase\Wallet\Resource\PaymentMethod; use Coinbase\Wallet\Resource\Resource; use Coinbase\Wallet\Resource\ResourceCollection; use Coinbase\Wallet\Resource\Sell; use Coinbase\Wallet\Resource\Transaction; use Coinbase\Wallet\Resource\User; use Coinbase\Wallet\Resource\Withdrawal; use Coinbase\Wallet\Resource\Notification; use Coinbase\Wallet\Resource\BitcoinNetwork; use Coinbase\Wallet\Resource\BitcoinCashNetwork; use Coinbase\Wallet\Value\Fee; use Coinbase\Wallet\Value\Money; use Coinbase\Wallet\Value\Network; use Psr\Http\Message\ResponseInterface; class Mapper { private $reflection = []; // users /** @return User */ public function toUser(ResponseInterface $response, User $user = null) { return $this->injectUser($this->decode($response)['data'], $user); } /** @return array */ public function fromCurrentUser(CurrentUser $user) { return array_intersect_key( $this->extractData($user), array_flip(['name', 'time_zone', 'native_currency']) ); } // accounts /** @return ResourceCollection */ public function toAccounts(ResponseInterface $response) { return $this->toCollection($response, 'injectAccount'); } /** @return Account */ public function toAccount(ResponseInterface $response, Account $account = null) { return $this->injectAccount($this->decode($response)['data'], $account); } /** @return array */ public function fromAccount(Account $account) { return array_intersect_key( $this->extractData($account), array_flip(['name']) ); } // addresses /** @return ResourceCollection */ public function toAddresses(ResponseInterface $response) { return $this->toCollection($response, 'injectAddress'); } /** @return Address */ public function toAddress(ResponseInterface $response, Address $address = null) { return $this->injectAddress($this->decode($response)['data'], $address); } /** @return array */ public function fromAddress(Address $address) { return array_intersect_key( $this->extractData($address), array_flip(['name', 'callback_url']) ); } // transactions /** @return ResourceCollection */ public function toTransactions(ResponseInterface $response) { return $this->toCollection($response, 'injectTransaction'); } /** @return Transaction */ public function toTransaction(ResponseInterface $response, Transaction $transaction = null) { return $this->injectTransaction($this->decode($response)['data'], $transaction); } /** @return array */ public function fromTransaction(Transaction $transaction) { // validate $to = $transaction->getTo(); if ($to && !$to instanceof Email && !$to instanceof BitcoinAddress && !$to instanceof LitecoinAddress && !$to instanceof EthrereumAddress && !$to instanceof BitcoinCashAddress && !$to instanceof Account) { throw new LogicException( 'The Coinbase API only accepts transactions to an account, email, bitcoin address, bitcoin cash address, litecoin address, or ethereum address' ); } // filter $data = array_intersect_key( $this->extractData($transaction), array_flip(['type', 'to', 'amount', 'description', 'fee']) ); // to if (isset($data['to']['address'])) { $data['to'] = $data['to']['address']; } elseif (isset($data['to']['email'])) { $data['to'] = $data['to']['email']; } elseif (isset($data['to']['id'])) { $data['to'] = $data['to']['id']; } // currency if (isset($data['amount']['currency'])) { $data['currency'] = $data['amount']['currency']; } // amount if (isset($data['amount']['amount'])) { $data['amount'] = $data['amount']['amount']; } return $data; } // buys /** @return ResourceCollection */ public function toBuys(ResponseInterface $response) { return $this->toCollection($response, 'injectBuy'); } /** @return Buy */ public function toBuy(ResponseInterface $response, Buy $buy = null) { return $this->injectBuy($this->decode($response)['data'], $buy); } /** @return array */ public function fromBuy(Buy $buy) { // validate if ($buy->getAmount() && $buy->getTotal()) { throw new LogicException( 'The Coinbase API accepts buys with either an amount or a total, but not both' ); } // filter $data = array_intersect_key( $this->extractData($buy), array_flip(['amount', 'total', 'payment_method']) ); // currency if (isset($data['amount']['currency'])) { $data['currency'] = $data['amount']['currency']; } elseif (isset($data['total']['currency'])) { $data['currency'] = $data['total']['currency']; } // amount if (isset($data['amount']['amount'])) { $data['amount'] = $data['amount']['amount']; } // total if (isset($data['total']['amount'])) { $data['total'] = $data['total']['amount']; } // payment method if (isset($data['payment_method']['id'])) { $data['payment_method'] = $data['payment_method']['id']; } return $data; } // sells /** @return ResourceCollection */ public function toSells(ResponseInterface $response) { return $this->toCollection($response, 'injectSell'); } /** @return Sell */ public function toSell(ResponseInterface $response, Sell $sell = null) { return $this->injectSell($this->decode($response)['data'], $sell); } /** @return array */ public function fromSell(Sell $sell) { // validate if ($sell->getAmount() && $sell->getTotal()) { throw new LogicException( 'The Coinbase API accepts sells with either an amount or a total, but not both' ); } // filter $data = array_intersect_key( $this->extractData($sell), array_flip(['amount', 'total', 'payment_method']) ); // currency if (isset($data['amount']['currency'])) { $data['currency'] = $data['amount']['currency']; } elseif (isset($data['total']['currency'])) { $data['currency'] = $data['total']['currency']; } // amount if (isset($data['amount']['amount'])) { $data['amount'] = $data['amount']['amount']; } // total if (isset($data['total']['amount'])) { $data['total'] = $data['total']['amount']; } // payment method if (isset($data['payment_method']['id'])) { $data['payment_method'] = $data['payment_method']['id']; } return $data; } // deposits /** @return ResourceCollection */ public function toDeposits(ResponseInterface $response) { return $this->toCollection($response, 'injectDeposit'); } /** @return Deposit */ public function toDeposit(ResponseInterface $response, Deposit $deposit = null) { return $this->injectDeposit($this->decode($response)['data'], $deposit); } /** @return array */ public function fromDeposit(Deposit $deposit) { // filter $data = array_intersect_key( $this->extractData($deposit), array_flip(['amount', 'payment_method']) ); // currency if (isset($data['amount']['currency'])) { $data['currency'] = $data['amount']['currency']; } // amount if (isset($data['amount']['amount'])) { $data['amount'] = $data['amount']['amount']; } // payment method if (isset($data['payment_method']['id'])) { $data['payment_method'] = $data['payment_method']['id']; } return $data; } // withdrawals /** @return ResourceCollection */ public function toWithdrawals(ResponseInterface $response) { return $this->toCollection($response, 'injectWithdrawal'); } /** @return Withdrawal */ public function toWithdrawal(ResponseInterface $response, Withdrawal $withdrawal = null) { return $this->injectWithdrawal($this->decode($response)['data'], $withdrawal); } /** @return array */ public function fromWithdrawal(Withdrawal $withdrawal) { // filter $data = array_intersect_key( $this->extractData($withdrawal), array_flip(['amount', 'payment_method']) ); // currency if (isset($data['amount']['currency'])) { $data['currency'] = $data['amount']['currency']; } // amount if (isset($data['amount']['amount'])) { $data['amount'] = $data['amount']['amount']; } // payment method if (isset($data['payment_method']['id'])) { $data['payment_method'] = $data['payment_method']['id']; } return $data; } // payment methods /** @return ResourceCollection */ public function toPaymentMethods(ResponseInterface $response) { return $this->toCollection($response, 'injectPaymentMethod'); } /** @return PaymentMethod */ public function toPaymentMethod(ResponseInterface $response, PaymentMethod $paymentMethod = null) { return $this->injectPaymentMethod($this->decode($response)['data'], $paymentMethod); } // merchants /** @return Merchant */ public function toMerchant(ResponseInterface $response, Merchant $merchant = null) { return $this->injectMerchant($this->decode($response)['data'], $merchant); } // orders /** @return ResourceCollection */ public function toOrders(ResponseInterface $response) { return $this->toCollection($response, 'injectOrder'); } /** @return Order */ public function toOrder(ResponseInterface $response, Order $order = null) { return $this->injectOrder($this->decode($response)['data'], $order); } /** @return array */ public function fromOrder(Order $order) { // filter $data = array_intersect_key( $this->extractData($order), array_flip(['amount', 'name', 'description', 'notifications_url', 'metadata']) ); // currency if (isset($data['amount']['currency'])) { $data['currency'] = $data['amount']['currency']; } // amount if (isset($data['amount']['amount'])) { $data['amount'] = $data['amount']['amount']; } return $data; } // checkouts /** @return ResourceCollection */ public function toCheckouts(ResponseInterface $response) { return $this->toCollection($response, 'injectCheckout'); } /** @return Checkout */ public function toCheckout(ResponseInterface $response, Checkout $checkout = null) { return $this->injectCheckout($this->decode($response)['data'], $checkout); } /** @return array */ public function fromCheckout(Checkout $checkout) { $keys = [ 'amount', 'name', 'description', 'type', 'style', 'customer_defined_amount', 'amount_presets', 'notifications_url', 'success_url', 'cancel_url', 'auto_redirect', 'collect_shipping_address', 'collect_email', 'collect_phone_number', 'collect_country', 'metadata', ]; // filter $data = array_intersect_key( $this->extractData($checkout), array_flip($keys) ); // currency if (isset($data['amount']['currency'])) { $data['currency'] = $data['amount']['currency']; } // amount if (isset($data['amount']['amount'])) { $data['amount'] = $data['amount']['amount']; } return $data; } // notifications /** @return ResourceCollection */ public function toNotifications(ResponseInterface $response) { return $this->toCollection($response, 'injectNotification'); } /** @return Notification */ public function toNotification(ResponseInterface $response, Notification $notification = null) { return $this->injectNotification($this->decode($response)['data'], $notification); } // misc /** @return array */ public function toData(ResponseInterface $response) { return $this->decode($response)['data']; } /** @return Money|null */ public function toMoney(ResponseInterface $response) { $data = $this->decode($response)['data']; return new Money($data['amount'], $data['currency']); } /** @return array */ public function decode(ResponseInterface $response) { return json_decode($response->getBody(), true); } // private private function toCollection(ResponseInterface $response, $method) { $data = $this->decode($response); if (isset($data['pagination'])) { $coll = new ResourceCollection( $data['pagination']['previous_uri'], $data['pagination']['next_uri'] ); } else { $coll = new ResourceCollection(); } foreach ($data['data'] as $resource) { $coll->add($this->$method($resource)); } return $coll; } private function injectUser(array $data, User $user = null) { return $this->injectResource($data, $user ?: new User()); } private function injectAccount(array $data, Account $account = null) { return $this->injectResource($data, $account ?: new Account()); } private function injectAddress(array $data, Address $address = null) { return $this->injectResource($data, $address ?: new Address()); } private function injectApplication(array $data, Application $application = null) { return $this->injectResource($data, $application ?: new Application()); } private function injectTransaction(array $data, Transaction $transaction = null) { return $this->injectResource($data, $transaction ?: new Transaction()); } private function injectBuy(array $data, Buy $buy = null) { return $this->injectResource($data, $buy ?: new Buy()); } private function injectSell(array $data, Sell $sell = null) { return $this->injectResource($data, $sell ?: new Sell()); } private function injectDeposit(array $data, Deposit $deposit = null) { return $this->injectResource($data, $deposit ?: new Deposit()); } private function injectWithdrawal(array $data, Withdrawal $withdrawal = null) { return $this->injectResource($data, $withdrawal ?: new Withdrawal()); } private function injectPaymentMethod(array $data, PaymentMethod $paymentMethod = null) { return $this->injectResource($data, $paymentMethod ?: new PaymentMethod()); } private function injectMerchant(array $data, Merchant $merchant = null) { return $this->injectResource($data, $merchant ?: new Merchant()); } private function injectOrder(array $data, Order $order = null) { return $this->injectResource($data, $order ?: new Order()); } private function injectCheckout(array $data, Checkout $checkout = null) { return $this->injectResource($data, $checkout ?: new Checkout()); } public function injectNotification(array $data, Notification $notification = null) { return $this->injectResource($data, $notification ?: new Notification()); } private function injectResource(array $data, Resource $resource) { $properties = $this->getReflectionProperties($resource); // add raw data to object $properties['raw_data']->setValue($resource, $data); foreach ($properties as $key => $property) { if (isset($data[$key])) { $property->setValue($resource, $this->toPhp($key, $data[$key])); } } return $resource; } private function extractData(Resource $resource) { $data = []; foreach ($this->getReflectionProperties($resource) as $key => $property) { if (null !== $value = $this->fromPhp($property->getValue($resource))) { $data[$key] = $value; } } // remove raw data from array unset($data['raw_data']); return $data; } /** @return \ReflectionProperty[] */ private function getReflectionProperties(Resource $resource) { $type = $resource->getResourceType(); if (isset($this->reflection[$type])) { return $this->reflection[$type]; } $class = new \ReflectionObject($resource); $properties = []; do { foreach ($class->getProperties() as $property) { $property->setAccessible(true); $properties[self::snakeCase($property->getName())] = $property; } } while ($class = $class->getParentClass()); return $this->reflection[$type] = $properties; } private function toPhp($key, $value) { if ('_at' === substr($key, -3)) { // timestamp return new \DateTime($value); } if (is_scalar($value)) { // misc return $value; } if (is_integer(key($value))) { // list $list = []; foreach ($value as $k => $v) { $list[$k] = $this->toPhp($k, $v); } return $list; } if (isset($value['resource'])) { // resource return $this->createResource($value['resource'], $value); } if (isset($value['amount']) && isset($value['currency'])) { // money return new Money($value['amount'], $value['currency']); } if ('network' === $key && isset($value['status'])) { // network return new Network($value['status'], isset($value['hash']) ? $value['hash'] : null, isset($value['transaction_fee']) ? $value['transaction_fee'] : null); } if (isset($value['type']) && isset($value['amount']) && isset($value['amount']['amount']) && isset($value['amount']['currency'])) { // fee return new Fee($value['type'], new Money($value['amount']['amount'], $value['amount']['currency'])); } return $value; } private function fromPhp($value) { if (is_scalar($value)) { // misc return $value; } if (is_array($value)) { // list $list = []; foreach ($value as $k => $v) { $list[$k] = $this->fromPhp($v); } return $list; } if ($value instanceof \DateTime) { // timestamp return $value->format(\DateTime::ISO8601); } if ($value instanceof Email) { // email return [ 'resource' => ResourceType::EMAIL, 'email' => $value->getEmail(), ]; } if ($value instanceof BitcoinAddress) { // bitcoin address return [ 'resource' => ResourceType::BITCOIN_ADDRESS, 'address' => $value->getAddress(), ]; } if($value instanceof BitcoinCashAddress){ // bitcoin-cash address return [ 'resource' => ResourceType::BITCOIN_CASH_ADDRESS, 'address' => $value->getAddress(), ]; } if($value instanceof LitecoinAddress){ // litecoin address return [ 'resource' => ResourceType::LITECOIN_ADDRESS, 'address' => $value->getAddress(), ]; } if($value instanceof EthrereumAddress){ // ethereum address return [ 'resource' => ResourceType::ETHEREUM_ADDRESS, 'address' => $value->getAddress(), ]; } if ($value instanceof Resource) { // resource return [ 'id' => $value->getId(), 'resource' => $value->getResourceType(), 'resource_path' => $value->getResourcePath(), ]; } if ($value instanceof Money) { // money return [ 'amount' => $value->getAmount(), 'currency' => $value->getCurrency(), ]; } if ($value instanceof Network) { // network $data = ['status' => $value->getStatus()]; if ($hash = $value->getHash()) { $data['hash'] = $hash; } return $data; } if ($value instanceof Fee) { // fee return [ 'type' => $value->getType(), 'amount' => [ 'amount' => $value->getAmount()->getAmount(), 'currency' => $value->getAmount()->getCurrency(), ], ]; } // fail quietly return $value; } private static function snakeCase($word) { // copied from doctrine/inflector return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $word)); } private function createResource($type, array $data) { $expanded = $this->isExpanded($data); switch ($type) { case ResourceType::ACCOUNT: return $expanded ? $this->injectAccount($data) : new Account($data['resource_path']); case ResourceType::ADDRESS: return $expanded ? $this->injectAddress($data) : new Address($data['resource_path']); case ResourceType::APPLICATION: return $expanded ? $this->injectApplication($data) : new Application($data['resource_path']); case ResourceType::BITCOIN_ADDRESS: return new BitcoinAddress($data['address']); case ResourceType::BUY: return $expanded ? $this->injectBuy($data) : new Buy($data['resource_path']); case ResourceType::CHECKOUT: return $expanded ? $this->injectCheckout($data) : new Checkout($data['resource_path']); case ResourceType::DEPOSIT: return $expanded ? $this->injectDeposit($data) : new Deposit($data['resource_path']); case ResourceType::EMAIL: return new Email($data['email']); case ResourceType::MERCHANT: return $expanded ? $this->injectMerchant($data) : new Merchant($data['resource_path']); case ResourceType::ORDER: return $expanded ? $this->injectOrder($data) : new Order($data['resource_path']); case ResourceType::PAYMENT_METHOD: return $expanded ? $this->injectPaymentMethod($data) : new PaymentMethod($data['resource_path']); case ResourceType::SELL: return $expanded ? $this->injectSell($data) : new Sell($data['resource_path']); case ResourceType::TRANSACTION: return $expanded ? $this->injectTransaction($data) : new Transaction(null, $data['resource_path']); case ResourceType::USER: return $expanded ? $this->injectUser($data) : new User($data['resource_path']); case ResourceType::WITHDRAWAL: return $expanded ? $this->injectWithdrawal($data) : new Withdrawal($data['resource_path']); case ResourceType::NOTIFICATION: return $expanded ? $this->injectNotification($data) : new Notification($data['resource_path']); case ResourceType::BITCOIN_NETWORK: return new BitcoinNetwork(); case ResourceType::BITCOIN_CASH_NETWORK: return new BitcoinCashNetwork(); case ResourceType::LITECOIN_NETWORK: return new LitecoinNetwork(); case ResourceType::ETHEREUM_NETWORK: return new EthereumNetwork(); case ResourceType::LITECOIN_ADDRESS: return $expanded ? $this->injectAddress($data) : new Address($data['resource_path']); case ResourceType::ETHEREUM_ADDRESS: return $expanded ? $this->injectAddress($data) : new Address($data['resource_path']); case ResourceType::BITCOIN_CASH_ADDRESS: return $expanded ? $this->injectAddress($data) : new Address($data['resource_path']); default: throw new RuntimeException('Unrecognized resource type: '.$type); } } /** * Checks if a data array represents an expanded resource. * * @return Boolean Whether the data array represents a complete resource */ private function isExpanded(array $data) { return (Boolean) array_diff(array_keys($data), ['id', 'resource', 'resource_path']); } }