OrderbookTransferController.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <?php
  2. namespace Controllers;
  3. use Libs\ResponseLib;
  4. use Models\CprModel;
  5. use Models\OrderbookModel;
  6. use Models\OrderbookTransferModel;
  7. use Models\PaymentModel;
  8. use Models\TokenModel;
  9. use Models\WalletModel;
  10. use Psr\Http\Message\ServerRequestInterface;
  11. use Respect\Validation\Exceptions\ValidationException;
  12. use Respect\Validation\Validator as val;
  13. use Services\TokenTransferService;
  14. class OrderbookTransferController
  15. {
  16. private PaymentModel $paymentModel;
  17. private WalletModel $walletModel;
  18. private OrderbookTransferModel $orderbookTransferModel;
  19. private TokenTransferService $tokenTransferService;
  20. private TokenModel $tokenModel;
  21. private CprModel $cprModel;
  22. private OrderbookModel $orderbookModel;
  23. private \PDO $pdo;
  24. public function __construct()
  25. {
  26. if (!isset($GLOBALS['pdo']) || !$GLOBALS['pdo'] instanceof \PDO) {
  27. throw new \RuntimeException('Global PDO connection not initialized');
  28. }
  29. $this->pdo = $GLOBALS['pdo'];
  30. $this->paymentModel = new PaymentModel();
  31. $this->walletModel = new WalletModel();
  32. $this->orderbookTransferModel = new OrderbookTransferModel();
  33. $this->tokenTransferService = new TokenTransferService();
  34. $this->tokenModel = new TokenModel();
  35. $this->cprModel = new CprModel();
  36. $this->orderbookModel = new OrderbookModel();
  37. }
  38. public function __invoke(ServerRequestInterface $request)
  39. {
  40. $body = json_decode((string)$request->getBody(), true) ?? [];
  41. try {
  42. val::key('external_id', val::stringType()->notEmpty())
  43. ->key('token_external_id', val::stringType()->notEmpty())
  44. ->assert($body);
  45. } catch (ValidationException $e) {
  46. return ResponseLib::sendFail('Validation failed: ' . $e->getFullMessage(), [], 'E_VALIDATE')->withStatus(400);
  47. }
  48. $externalId = trim((string)$body['external_id']);
  49. $tokenExternalId = trim((string)$body['token_external_id']);
  50. $companyId = (int)($request->getAttribute('api_company_id') ?? 0);
  51. if ($companyId <= 0) {
  52. return ResponseLib::sendFail('Empresa autenticada não encontrada', [], 'E_VALIDATE')->withStatus(401);
  53. }
  54. try {
  55. $payment = $this->paymentModel->findByExternalId($externalId);
  56. } catch (\Throwable $e) {
  57. return ResponseLib::sendFail('Falha ao consultar pagamento: ' . $e->getMessage(), [], 'E_DATABASE')->withStatus(500);
  58. }
  59. if (!$payment) {
  60. return ResponseLib::sendFail('Pagamento não encontrado', ['external_id' => $externalId], 'E_NOT_FOUND')->withStatus(404);
  61. }
  62. if ((int)$payment['status_id'] !== PaymentModel::STATUS_COMPLETED) {
  63. return ResponseLib::sendFail('Pagamento ainda não concluído', ['external_id' => $externalId, 'status_id' => $payment['status_id']], 'E_PAYMENT_PENDING')->withStatus(409);
  64. }
  65. try {
  66. $orderbook = $this->orderbookTransferModel->getByTokenExternalId($tokenExternalId);
  67. } catch (\Throwable $e) {
  68. return ResponseLib::sendFail('Falha ao consultar orderbook: ' . $e->getMessage(), [], 'E_DATABASE')->withStatus(500);
  69. }
  70. if (!$orderbook) {
  71. return ResponseLib::sendFail('Orderbook não encontrado', ['token_external_id' => $tokenExternalId], 'E_NOT_FOUND')->withStatus(404);
  72. }
  73. if ((int)$orderbook['status_id'] !== OrderbookModel::STATUS_OPEN) {
  74. if ((int)$orderbook['status_id'] === OrderbookModel::STATUS_COMPLETED) {
  75. return ResponseLib::sendOk([
  76. 'orderbook_id' => (int)$orderbook['orderbook_id'],
  77. 'token_external_id' => $tokenExternalId,
  78. 'message' => 'Orderbook já transferido anteriormente',
  79. ], 'S_TOKEN_ALREADY_TRANSFERRED');
  80. }
  81. return ResponseLib::sendFail(
  82. 'Orderbook não está disponível para transferência',
  83. ['orderbook_id' => (int)$orderbook['orderbook_id'], 'status_id' => $orderbook['status_id']],
  84. 'E_ORDERBOOK_STATUS'
  85. )->withStatus(409);
  86. }
  87. $paymentFlag = trim((string)($payment['payment_flag'] ?? ''));
  88. $expectedFlag = sprintf('orderbook:%d;token:%s', (int)$orderbook['orderbook_id'], $tokenExternalId);
  89. if ($paymentFlag === '' || $paymentFlag !== $expectedFlag) {
  90. return ResponseLib::sendFail(
  91. 'Pagamento não vinculado ao orderbook informado',
  92. [
  93. 'external_id' => $externalId,
  94. 'payment_flag' => $paymentFlag,
  95. 'expected' => $expectedFlag,
  96. ],
  97. 'E_PAYMENT_MISMATCH'
  98. )->withStatus(409);
  99. }
  100. $tokenId = (int)($orderbook['token_id'] ?? 0);
  101. if ($tokenId <= 0) {
  102. return ResponseLib::sendFail('Orderbook sem token associado', [], 'E_ORDERBOOK_TOKEN')->withStatus(409);
  103. }
  104. try {
  105. $token = $this->tokenModel->findById($tokenId);
  106. } catch (\Throwable $e) {
  107. return ResponseLib::sendFail('Falha ao carregar token vinculado: ' . $e->getMessage(), [], 'E_DATABASE')->withStatus(500);
  108. }
  109. if (!$token) {
  110. return ResponseLib::sendFail('Token associado ao orderbook não encontrado', ['token_id' => $tokenId], 'E_TOKEN_NOT_FOUND')->withStatus(404);
  111. }
  112. $cprId = (int)($token['cpr_id'] ?? 0);
  113. if ($cprId <= 0) {
  114. return ResponseLib::sendFail('Token não está vinculado a uma CPR', ['token_id' => $tokenId], 'E_CPR_NOT_FOUND')->withStatus(409);
  115. }
  116. try {
  117. $wallet = $this->walletModel->getPrimaryWalletByCompanyId($companyId);
  118. } catch (\Throwable $e) {
  119. return ResponseLib::sendFail('Falha ao consultar carteira da empresa: ' . $e->getMessage(), [], 'E_DATABASE')->withStatus(500);
  120. }
  121. if (!$wallet || empty($wallet['wallet_address'])) {
  122. return ResponseLib::sendFail('Carteira da empresa não encontrada', [], 'E_WALLET_NOT_FOUND')->withStatus(404);
  123. }
  124. $serverAddress = $this->resolveServerAddress();
  125. try {
  126. $transferResult = $this->tokenTransferService->transferFrom($serverAddress, (string)$wallet['wallet_address'], $tokenExternalId);
  127. } catch (\Throwable $e) {
  128. return ResponseLib::sendFail('Falha ao transferir token: ' . $e->getMessage(), [], 'E_TRANSFER')->withStatus(500);
  129. }
  130. $txHash = $transferResult['tx_hash'] ?? null;
  131. if (!$txHash) {
  132. return ResponseLib::sendFail('Transferência não retornou hash válido', [], 'E_TRANSFER_HASH')->withStatus(502);
  133. }
  134. try {
  135. $this->pdo->beginTransaction();
  136. $this->orderbookModel->updateWalletId((int)$orderbook['orderbook_id'], (int)$wallet['wallet_id']);
  137. $this->orderbookTransferModel->markCompleted((int)$orderbook['orderbook_id']);
  138. $this->tokenModel->updateWalletId($tokenId, (int)$wallet['wallet_id']);
  139. $this->cprModel->updateCompanyId($cprId, $companyId);
  140. $this->pdo->commit();
  141. } catch (\Throwable $e) {
  142. if ($this->pdo->inTransaction()) {
  143. $this->pdo->rollBack();
  144. }
  145. return ResponseLib::sendFail(
  146. 'Falha ao atualizar registros após transferência: ' . $e->getMessage(),
  147. [],
  148. 'E_TRANSFER_PERSIST'
  149. )->withStatus(500);
  150. }
  151. return ResponseLib::sendOk([
  152. 'orderbook_id' => (int)$orderbook['orderbook_id'],
  153. 'token_external_id' => $tokenExternalId,
  154. 'destination_address' => (string)$wallet['wallet_address'],
  155. 'transfer_output' => $transferResult['output'] ?? '',
  156. 'transfer_error' => $transferResult['error'] ?? '',
  157. 'transaction_hash' => $txHash,
  158. ], 'S_TOKEN_TRANSFERRED');
  159. }
  160. private function resolveServerAddress(): string
  161. {
  162. $address = $_ENV['SERVER_WALLET_ADDRESS'] ?? $_ENV['EASY_ADMIM_PUBLIC_KEY'] ?? '';
  163. $trimmed = trim((string)$address);
  164. if ($trimmed === '') {
  165. throw new \RuntimeException('Endereço do servidor (SERVER_WALLET_ADDRESS) não configurado.');
  166. }
  167. return $trimmed;
  168. }
  169. }