/** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $authenticated = false; try { $user = $this->auth->user(); if (!$user) { // redirect return redirect('/'); } $authenticated = $user->hasPermission('platformAdmin'); } catch (Exception $e) { // something else went wrong EventLog::logError('error.platformAuth.unexpected', $e); $error_message = 'An unexpected error occurred'; $error_code = 500; } if (!$authenticated) { $error_code = isset($error_code) ? $error_code : 403; $error_message = isset($error_message) ? $error_message : "You do not have privileges to perform this operation"; EventLog::logError('error.platformAuth.unauthenticated', ['remoteIp' => $request->getClientIp()]); return new Response($error_message, $error_code); $response = new JsonResponse(['message' => $error_message, 'errors' => [$error_message]], $error_code); return $response; } return $next($request); }
public function fire($job, $data) { // build the event data $event_data = $this->event_builder->buildBlockEventData($data['hash']); // fire an event try { Log::debug("Begin xchain.block.received {$event_data['height']} ({$event_data['hash']})"); Event::fire('xchain.block.received', [$event_data]); Log::debug("End xchain.block.received {$event_data['height']} ({$event_data['hash']})"); // job successfully handled $job->delete(); } catch (Exception $e) { EventLog::logError('BTCBlockJob.failed', $e, $data); // this block had a problem // but it might be found if we try a few more times $attempts = $job->attempts(); if ($attempts > self::MAX_ATTEMPTS) { // we've already tried MAX_ATTEMPTS times - give up Log::debug("Block {$data['hash']} event failed after attempt " . $attempts . ". Giving up."); $job->delete(); } else { $release_time = 2; Log::debug("Block {$data['hash']} event failed after attempt " . $attempts . ". Trying again in " . self::RETRY_DELAY . " seconds."); $job->release(self::RETRY_DELAY); } } }
public function fire($job, $data) { // update the notification // jobData.return = { // result: success // err: err // timestamp: new Date().getTime() // } try { // attempt job $this->fireJob($job, $data); // job successfully handled $job->delete(); } catch (Exception $e) { EventLog::logError('job.failed', $e); if ($job->attempts() > 30) { // give up EventLog::logError('job.failed.permanent', $data['meta']['id']); $job->delete(); } else { if ($job->attempts() > 10) { // try a 30 second delay $job->release(30); } else { if ($job->attempts() > 1) { // try a 10 second delay $job->release(10); } } } } }
public function send(PaymentAddress $payment_address, $parsed_tx, $confirmations) { $is_confirmed = $confirmations >= self::SEND_CONFIRMATIONS_REQUIRED; // check for vins if (!isset($parsed_tx['bitcoinTx']['vin']) or !$parsed_tx['bitcoinTx']['vin']) { EventLog::logError('bitcoinTx.send.noVins', ['txid' => $parsed_tx['txid']]); return; } // get the sending account $account = AccountHandler::getAccount($payment_address); foreach ($parsed_tx['bitcoinTx']['vin'] as $vin) { // update the UTXO record $is_spendable = true; if ($is_spendable) { $spent_txid = isset($vin['txid']) ? $vin['txid'] : null; $spent_n = isset($vin['vout']) ? $vin['vout'] : null; if ($spent_txid and $spent_n !== null) { $type = $is_confirmed ? TXO::SENT : TXO::SENDING; $spent = true; // spend the utxo (updates an existing utxo) Log::debug("new send TXO: {$spent_txid}:{$spent_n}/" . CurrencyUtil::valueToSatoshis($vin['value']) . " " . TXO::typeIntegerToString($type) . " to " . $payment_address['uuid']); $this->txo_repository->updateOrCreate(['txid' => $spent_txid, 'n' => $spent_n, 'type' => $type, 'spent' => $spent, 'amount' => CurrencyUtil::valueToSatoshis($vin['value'])], $payment_address, $account); } } } }
/** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { // ------------------------------------------------------------------------ // Log Before call $begin_log_vars = ['method' => '', 'route' => '', 'uri' => '', 'parameters' => '', 'inputBodySize' => '']; // get the request details $method = $request->method(); $begin_log_vars['method'] = $method; $route = $request->route(); if ($route) { $route_uri = $route->getUri(); $route_name = $route->getName(); $route_params = $route->parameters(); } else { $route_name = '[unknown]'; $route_uri = '[unknown]'; $route_params = []; } $begin_log_vars['route'] = $route_name; $begin_log_vars['uri'] = $route_uri; $begin_log_vars['parameters'] = $route_params; $body_size = $request->header('Content-Length'); if (!$body_size) { $body_size = 0; } $begin_log_vars['inputBodySize'] = $body_size; EventLog::debug('apiCall.begin', $begin_log_vars); // ------------------------------------------------------------------------ // Execute the next closure $status_code = null; $caught_exception = null; try { $response = $next($request); $status_code = $response->getStatusCode(); } catch (Exception $e) { $caught_exception = $e; $status_code = 500; if ($e instanceof HttpException) { $status_code = $e->getStatusCode(); } } // ------------------------------------------------------------------------ // Log after call $end_log_vars = ['method' => $begin_log_vars['method'], 'route' => $begin_log_vars['route'], 'uri' => $begin_log_vars['uri'], 'parameters' => $begin_log_vars['parameters'], 'status' => $status_code]; if ($caught_exception !== null) { // log the exception and then throw the error again EventLog::logError('apiCall.end', $caught_exception, $end_log_vars); throw $caught_exception; } else { if ($response->isServerError() or $response->isClientError()) { EventLog::warning('apiCall.end', $end_log_vars); } else { EventLog::debug('apiCall.end', $end_log_vars); } } return $response; }
/** * Handle the command. * * @param CreateAccount $command * @return void */ public function handle(CreateAccount $command) { $payment_address = $command->payment_address; $create_vars = $command->attributes; $create_vars['payment_address_id'] = $payment_address['id']; $create_vars['user_id'] = $payment_address['user_id']; $account = $this->account_repository->create($create_vars); EventLog::log('account.created', json_decode(json_encode($account))); }
public function fire($job, $data) { try { $_debugLogTxTiming = Config::get('xchain.debugLogTxTiming'); // load from bitcoind if ($_debugLogTxTiming) { Log::debug("Begin {$data['txid']}"); } if ($_debugLogTxTiming) { PHP_Timer::start(); } $transaction_model = $this->bitcoin_transaction_store->getParsedTransactionFromBitcoind($data['txid']); $bitcoin_transaction_data = $transaction_model['parsed_tx']['bitcoinTx']; if ($_debugLogTxTiming) { Log::debug("[" . getmypid() . "] Time for getParsedTransactionFromBitcoind: " . PHP_Timer::secondsToTimeString(PHP_Timer::stop())); } // parse the transaction if ($_debugLogTxTiming) { PHP_Timer::start(); } $event_data = $this->transaction_data_builder->buildParsedTransactionData($bitcoin_transaction_data, $data['ts']); if ($_debugLogTxTiming) { Log::debug("[" . getmypid() . "] Time for buildParsedTransactionData: " . PHP_Timer::secondsToTimeString(PHP_Timer::stop())); } // fire an event if ($_debugLogTxTiming) { PHP_Timer::start(); } Event::fire('xchain.tx.received', [$event_data, 0, null, null]); if ($_debugLogTxTiming) { Log::debug("[" . getmypid() . "] Time for fire xchain.tx.received: " . PHP_Timer::secondsToTimeString(PHP_Timer::stop())); } // job successfully handled if ($_debugLogTxTiming) { PHP_Timer::start(); } $job->delete(); if ($_debugLogTxTiming) { Log::debug("[" . getmypid() . "] Time for job->delete(): " . PHP_Timer::secondsToTimeString(PHP_Timer::stop())); } } catch (Exception $e) { EventLog::logError('BTCTransactionJob.failed', $e, $data); // this transaction had a problem // but it might be found if we try a few more times $attempts = $job->attempts(); if ($attempts > self::MAX_ATTEMPTS) { // we've already tried MAX_ATTEMPTS times - give up Log::debug("Transaction {$data['txid']} event failed after attempt " . $attempts . ". Giving up."); $job->delete(); } else { $release_time = 2; Log::debug("Transaction {$data['txid']} event failed after attempt " . $attempts . ". Trying again in " . self::RETRY_DELAY . " seconds."); $job->release(self::RETRY_DELAY); } } }
/** * Store a newly created resource in storage. * * @return Response */ public function store(APIControllerHelper $helper, CreateMonitorRequest $request, MonitoredAddressRepository $address_respository, Guard $auth) { $user = $auth->getUser(); if (!$user) { throw new Exception("User not found", 1); } $attributes = $request->only(array_keys($request->rules())); $attributes['user_id'] = $user['id']; $address = $address_respository->create($attributes); EventLog::log('monitor.created', json_decode(json_encode($address))); return $helper->transformResourceForOutput($address); }
/** * Execute the console command. * * @return mixed */ public function fire() { $user_repository = $this->laravel->make('Tokenly\\LaravelApiProvider\\Contracts\\APIUserRepositoryContract'); $user_vars = ['email' => $this->input->getArgument('email'), 'password' => $this->input->getOption('password')]; $user_model = $user_repository->create($user_vars); // log EventLog::log('user.create.cli', $user_model, ['id', 'email', 'apisecretkey']); // show the new user $user = clone $user_model; $user['password'] = '******'; $this->line(json_encode($user, 192)); }
protected function failedValidation(Validator $validator) { $errors = $this->formatErrors($validator); $json_errors_list = []; foreach ($errors as $field => $errors_list) { $json_errors_list = array_merge($json_errors_list, $errors_list); } $json_response = ['message' => 'This request was not valid.', 'errors' => $json_errors_list]; $response = new JsonResponse($json_response, 400); EventLog::logError('error.api.invalid_request', ['errors' => $json_errors_list]); throw new HttpResponseException($response); }
/** * Execute the console command. * * @return mixed */ public function fire() { $user_repository = app('Tokenly\\LaravelApiProvider\\Contracts\\APIUserRepositoryContract'); Log::debug("\$user_repository is " . get_class($user_repository)); $user_vars = ['username' => $this->argument('username'), 'email' => $this->argument('email'), 'confirmed_email' => $this->argument('email'), 'password' => $this->argument('password'), 'privileges' => ['platformAdmin' => true]]; $user_model = $user_repository->create($user_vars); // log EventLog::log('user.platformAdmin.cli', $user_model, ['id', 'username', 'email']); // show the new user $user = clone $user_model; $user['password'] = '******'; $this->line(json_encode($user, 192)); $this->comment('Done.'); }
/** * Store a newly created resource in storage. * * @return Response */ public function store(APIControllerHelper $helper, CreatePaymentAddressRequest $request, PaymentAddressRepository $payment_address_respository, Guard $auth) { $user = $auth->getUser(); if (!$user) { throw new Exception("User not found", 1); } $attributes = $request->only(array_keys($request->rules())); $attributes['user_id'] = $user['id']; $address = $payment_address_respository->create($attributes); EventLog::log('paymentAddress.created', $address->toArray()); // create a default account AccountHandler::createDefaultAccount($address); return $helper->transformResourceForOutput($address); }
public function handleConfirmedTransaction($parsed_tx, $confirmations, $block_seq, Block $block, $block_event_context = null) { $transaction_handler = $this->network_handler_factory->buildTransactionHandler($parsed_tx['network']); if ($block_event_context === null) { $block_event_context = $transaction_handler->newBlockEventContext(); } $found_addresses = $transaction_handler->findMonitoredAndPaymentAddressesByParsedTransaction($parsed_tx); try { $transaction_handler->updateProvisionalTransaction($parsed_tx, $found_addresses, $confirmations); $transaction_handler->invalidateProvisionalTransactions($found_addresses, $parsed_tx, $confirmations, $block_seq, $block, $block_event_context); $transaction_handler->updateUTXOs($found_addresses, $parsed_tx, $confirmations, $block_seq, $block); $transaction_handler->updateAccountBalances($found_addresses, $parsed_tx, $confirmations, $block_seq, $block); $transaction_handler->sendNotifications($found_addresses, $parsed_tx, $confirmations, $block_seq, $block); } catch (Exception $e) { EventLog::logError('handleConfirmedTransaction.error', $e, ['txid' => $parsed_tx['txid']]); } return; }
/** * Execute the console command. * * @return mixed */ public function fire() { $backfill_max = $this->input->getOption('maximum-blocks'); $blockchain_store = $this->laravel->make('App\\Blockchain\\Block\\BlockChainStore'); $block_handler = $this->laravel->make('App\\Handlers\\XChain\\XChainBlockHandler'); $bitcoind = $this->laravel->make('Nbobtc\\Bitcoind\\Bitcoind'); $block_height = $bitcoind->getblockcount(); $best_block_hash = $bitcoind->getblockhash($block_height); $this->info('Current block is ' . $block_height . ' (' . $best_block_hash . ')'); // load the current block from bitcoind $first_missing_hash = $blockchain_store->findFirstMissingHash($best_block_hash, $backfill_max); if ($first_missing_hash) { // backfill any missing blocks $missing_block_events = $blockchain_store->loadMissingBlockEventsFromBitcoind($first_missing_hash, $backfill_max); // process missing blocks foreach ($missing_block_events as $missing_block_event) { EventLog::log('block.missing.cli', $missing_block_event, ['height', 'hash']); $block_handler->processBlock($missing_block_event); } } }
/** * Get all balances for an address * * @param int $id * @return Response */ public function show(Client $xcpd_client, BitcoinPayer $bitcoin_payer, Cache $asset_info_cache, $address) { if (!AddressValidator::isValid($address)) { $message = "The address {$address} was not valid"; EventLog::logError('error.getBalance', ['address' => $address, 'message' => $message]); return new JsonResponse(['message' => $message], 500); } $balances = $xcpd_client->get_balances(['filters' => ['field' => 'address', 'op' => '==', 'value' => $address]]); // and get BTC balance too $btc_float_balance = $bitcoin_payer->getBalance($address); $balances = array_merge([['asset' => 'BTC', 'quantity' => $btc_float_balance]], $balances); $out = ['balances' => [], 'balancesSat' => []]; foreach ($balances as $balance) { $asset_name = $balance['asset']; if ($asset_name == 'BTC') { // BTC quantity is a float $quantity_float = floatval($balance['quantity']); $quantity_sat = CurrencyUtil::valueToSatoshis($balance['quantity']); } else { // determine quantity based on asset info $is_divisible = $asset_info_cache->isDivisible($asset_name); if ($is_divisible) { $quantity_float = CurrencyUtil::satoshisToValue($balance['quantity']); $quantity_sat = intval($balance['quantity']); } else { // non-divisible assets don't use satoshis $quantity_float = floatval($balance['quantity']); $quantity_sat = CurrencyUtil::valueToSatoshis($balance['quantity']); } } $out['balances'][$asset_name] = $quantity_float; $out['balancesSat'][$asset_name] = $quantity_sat; } ksort($out['balances']); ksort($out['balancesSat']); return json_encode($out); }
public function buildBlockEventData($block_hash) { try { // get the full block data from bitcoind $block = $this->bitcoind->getblock($block_hash, true); if (!$block) { throw new Exception("Block not found for hash {$block_hash}", 1); } // convert to array $block = json_decode(json_encode($block), true); // create the event data $event_data = []; $event_data['network'] = 'bitcoin'; $event_data['hash'] = $block['hash']; $event_data['height'] = $block['height']; $event_data['previousblockhash'] = $block['previousblockhash']; $event_data['time'] = $block['time']; $event_data['tx'] = $block['tx']; return $event_data; } catch (Exception $e) { EventLog::logError('block', $e, ['hash' => $block_hash]); throw $e; } }
/** * Execute the console command. * * @return mixed */ public function fire() { $this->info('begin'); $consul = app('Tokenly\\ConsulHealthDaemon\\ConsulClient'); $my_service_id = Config::get('consul-health.health_service_id'); $sleep_delay = Config::get('consul-health.loop_delay'); while (true) { try { $consul->checkPass($my_service_id); } catch (Exception $e) { EventLog::logError('healthcheck.failed', $e); } try { // fire a check event // for other handlers Event::fire('consul-health.console.check'); } catch (Exception $e) { EventLog::logError('healthcheck.failed', $e); $consul->checkFail($my_service_id, $e->getMessage()); } sleep($sleep_delay); } $this->info('done'); }
protected function executeSend(APIControllerHelper $helper, Request $request, PaymentAddressRepository $payment_address_respository, SendRepository $send_respository, PaymentAddressSender $address_sender, Guard $auth, APICallRepository $api_call_repository, $id) { $user = $auth->getUser(); if (!$user) { throw new Exception("User not found", 1); } // get the address $payment_address = $payment_address_respository->findByUuid($id); if (!$payment_address) { return new JsonResponse(['message' => 'address not found'], 404); } // make sure this address belongs to this user if ($payment_address['user_id'] != $user['id']) { return new JsonResponse(['message' => 'Not authorized to send from this address'], 403); } // attributes $request_attributes = $request->only(array_keys($request->rules())); // determine if this is a multisend $is_multisend = (isset($request_attributes['destinations']) and $request_attributes['destinations']); $is_regular_send = !$is_multisend; // normalize destinations $destinations = $is_multisend ? $this->normalizeDestinations($request_attributes['destinations']) : ''; $destination = $is_regular_send ? $request_attributes['destination'] : ''; // determine variables $quantity_sat = CurrencyUtil::valueToSatoshis($is_multisend ? $this->sumMultisendQuantity($destinations) : $request_attributes['quantity']); $asset = $is_regular_send ? $request_attributes['asset'] : 'BTC'; $is_sweep = isset($request_attributes['sweep']) ? !!$request_attributes['sweep'] : false; $float_fee = isset($request_attributes['fee']) ? $request_attributes['fee'] : PaymentAddressSender::DEFAULT_FEE; $dust_size = isset($request_attributes['dust_size']) ? $request_attributes['dust_size'] : PaymentAddressSender::DEFAULT_REGULAR_DUST_SIZE; $request_id = isset($request_attributes['requestId']) ? $request_attributes['requestId'] : Uuid::uuid4()->toString(); // create attibutes $create_attributes = []; $create_attributes['user_id'] = $user['id']; $create_attributes['payment_address_id'] = $payment_address['id']; $create_attributes['destination'] = $destination; $create_attributes['quantity_sat'] = $quantity_sat; $create_attributes['asset'] = $asset; $create_attributes['is_sweep'] = $is_sweep; $create_attributes['fee'] = $float_fee; $create_attributes['dust_size'] = $dust_size; // for multisends $create_attributes['destinations'] = $destinations; // the transaction must be committed before the lock is release and not after // therefore we must release the lock after this closure completes $lock_must_be_released = false; $lock_must_be_released_with_delay = false; // create a send and lock it immediately $send_result = $send_respository->executeWithNewLockedSendByRequestID($request_id, $create_attributes, function ($locked_send) use($request_attributes, $create_attributes, $payment_address, $user, $helper, $send_respository, $address_sender, $api_call_repository, $request_id, $is_multisend, $is_regular_send, $quantity_sat, $asset, $destination, $destinations, $is_sweep, $float_fee, $dust_size, &$lock_must_be_released, &$lock_must_be_released_with_delay) { $api_call = $api_call_repository->create(['user_id' => $user['id'], 'details' => ['method' => 'api/v1/sends/' . $payment_address['uuid'], 'args' => $request_attributes]]); // if a send already exists by this request_id, just return it if (isset($locked_send['txid']) && strlen($locked_send['txid'])) { EventLog::log('send.alreadyFound', $locked_send); return $helper->transformResourceForOutput($locked_send); } $float_quantity = CurrencyUtil::satoshisToValue($quantity_sat); // send EventLog::log('send.requested', array_merge($request_attributes, $create_attributes)); if ($is_sweep) { try { // get lock $lock_acquired = AccountHandler::acquirePaymentAddressLock($payment_address); if ($lock_acquired) { $lock_must_be_released = true; } list($txid, $float_balance_sent) = $address_sender->sweepAllAssets($payment_address, $request_attributes['destination'], $float_fee); $quantity_sat_sent = CurrencyUtil::valueToSatoshis($float_balance_sent); // clear all balances from all accounts AccountHandler::zeroAllBalances($payment_address, $api_call); // release the account lock with a slight delay if ($lock_acquired) { $lock_must_be_released_with_delay = true; } } catch (PaymentException $e) { EventLog::logError('error.sweep', $e); return new JsonResponse(['message' => $e->getMessage()], 500); } catch (Exception $e) { EventLog::logError('error.sweep', $e); return new JsonResponse(['message' => 'Unable to complete this request'], 500); } } else { try { // get the account $account_name = (isset($request_attributes['account']) and strlen($request_attributes['account'])) ? $request_attributes['account'] : 'default'; $account = AccountHandler::getAccount($payment_address, $account_name); if (!$account) { EventLog::logError('error.send.accountMissing', ['address_id' => $payment_address['id'], 'account' => $account_name]); return new JsonResponse(['message' => "This account did not exist."], 404); } // Log::debug("\$account=".json_encode($account, 192)); // get lock $lock_acquired = AccountHandler::acquirePaymentAddressLock($payment_address); if ($lock_acquired) { $lock_must_be_released = true; } // whether to spend unconfirmed balances $allow_unconfirmed = isset($request_attributes['unconfirmed']) ? $request_attributes['unconfirmed'] : false; // Log::debug("\$allow_unconfirmed=".json_encode($allow_unconfirmed, 192)); // validate that the funds are available if ($allow_unconfirmed) { $has_enough_funds = AccountHandler::accountHasSufficientFunds($account, $float_quantity, $asset, $float_fee, $dust_size); } else { $has_enough_funds = AccountHandler::accountHasSufficientConfirmedFunds($account, $float_quantity, $asset, $float_fee, $dust_size); } if (!$has_enough_funds) { EventLog::logError('error.send.insufficient', ['address_id' => $payment_address['id'], 'account' => $account_name, 'quantity' => $float_quantity, 'asset' => $asset]); return new JsonResponse(['message' => "This account does not have sufficient" . ($allow_unconfirmed ? '' : ' confirmed') . " funds available."], 400); } // send the funds EventLog::log('send.begin', ['request_id' => $request_id, 'address_id' => $payment_address['id'], 'account' => $account_name, 'quantity' => $float_quantity, 'asset' => $asset, 'destination' => $is_multisend ? $destinations : $destination]); $txid = $address_sender->sendByRequestID($request_id, $payment_address, $is_multisend ? $destinations : $destination, $float_quantity, $asset, $float_fee, $dust_size); EventLog::log('send.complete', ['txid' => $txid, 'request_id' => $request_id, 'address_id' => $payment_address['id'], 'account' => $account_name, 'quantity' => $float_quantity, 'asset' => $asset, 'destination' => $is_multisend ? $destinations : $destination]); // tag funds as sent with the txid if ($allow_unconfirmed) { AccountHandler::markAccountFundsAsSending($account, $float_quantity, $asset, $float_fee, $dust_size, $txid); } else { AccountHandler::markConfirmedAccountFundsAsSending($account, $float_quantity, $asset, $float_fee, $dust_size, $txid); // Log::debug("After marking confirmed funds as sent, all accounts for ${account['name']}: ".json_encode(app('App\Repositories\LedgerEntryRepository')->accountBalancesByAsset($account, null), 192)); // Log::debug("After marking confirmed funds as sent, all accounts for default: ".json_encode(app('App\Repositories\LedgerEntryRepository')->accountBalancesByAsset(AccountHandler::getAccount($payment_address), null), 192)); } // release the account lock if ($lock_acquired) { $lock_must_be_released_with_delay = true; } } catch (AccountException $e) { EventLog::logError('error.pay', $e); return new JsonResponse(['message' => $e->getMessage(), 'errorName' => $e->getErrorName()], $e->getStatusCode()); } catch (PaymentException $e) { EventLog::logError('error.pay', $e); return new JsonResponse(['message' => $e->getMessage()], 500); } catch (Exception $e) { EventLog::logError('error.pay', $e); return new JsonResponse(['message' => 'Unable to complete this request'], 500); } } $attributes = []; $attributes['sent'] = time(); $attributes['txid'] = $txid; EventLog::log('send.complete', $attributes); // update and send response $send_respository->update($locked_send, $attributes); return $helper->buildJSONResponse($locked_send->serializeForAPI()); }, self::SEND_LOCK_TIMEOUT); // make sure to release the lock if ($lock_must_be_released_with_delay) { $this->releasePaymentAddressLockWithDelay($payment_address); } else { if ($lock_must_be_released) { AccountHandler::releasePaymentAddressLock($payment_address); } } return $send_result; }
protected function sendNotificationsForInvalidatedProvisionalTransaction($invalidated_parsed_tx, $replacing_parsed_tx, $found_addresses, $confirmations, $block_seq, $block) { // build sources and destinations $sources = $invalidated_parsed_tx['sources'] ? $invalidated_parsed_tx['sources'] : []; $destinations = $invalidated_parsed_tx['destinations'] ? $invalidated_parsed_tx['destinations'] : []; $matched_monitored_address_ids = []; // loop through all matched monitored addresses foreach ($found_addresses['matched_monitored_addresses'] as $monitored_address) { // build the notification $notification = $this->buildInvalidatedNotification($invalidated_parsed_tx, $replacing_parsed_tx, $sources, $destinations, $confirmations, $block_seq, $block, $monitored_address); $this->wlog("\$invalidated_parsed_tx['timestamp']={$invalidated_parsed_tx['timestamp']}"); // create a notification $notification_vars_for_model = $notification; unset($notification_vars_for_model['notificationId']); try { // Log::debug("creating notification: ".json_encode(['txid' => $invalidated_parsed_tx['txid'], 'confirmations' => $confirmations, 'block_id' => $block ? $block['id'] : null,], 192)); // Log::debug("sendNotificationsForInvalidatedProvisionalTransaction inserting new notification: ".json_encode(['txid' => $invalidated_parsed_tx['txid'], 'monitored_address_id' => $monitored_address['id'], 'confirmations' => $confirmations, 'event_type' => 'invalidation',], 192)); $notification_model = $this->notification_repository->createForMonitoredAddress($monitored_address, ['txid' => $invalidated_parsed_tx['txid'], 'confirmations' => $confirmations, 'notification' => $notification_vars_for_model, 'block_id' => $block ? $block['id'] : null, 'event_type' => 'invalidation']); } catch (QueryException $e) { if ($e->errorInfo[0] == 23000) { EventLog::logError('notification.duplicate.error', $e, ['txid' => $invalidated_parsed_tx['txid'], 'monitored_address_id' => $monitored_address['id'], 'confirmations' => $confirmations, 'event_type' => 'invalidation']); continue; } else { throw $e; } } // apply user API token and key $user = $this->userByID($monitored_address['user_id']); // update notification $notification['notificationId'] = $notification_model['uuid']; // put notification in the queue EventLog::log('notification.out', ['event' => $notification['event'], 'invalidTxid' => $notification['invalidTxid'], 'replacingTxid' => $notification['replacingTxid'], 'endpoint' => $user['webhook_endpoint'], 'user' => $user['id'], 'id' => $notification_model['uuid']]); $this->xcaller_client->sendWebhook($notification, $monitored_address['webhookEndpoint'], $notification_model['uuid'], $user['apitoken'], $user['apisecretkey']); } }
public function sendByRequestID($request_id, PaymentAddress $payment_address, $destination, $float_quantity, $asset, $float_fee = null, $float_btc_dust_size = null, $is_sweep = false) { $composed_transaction_model = $this->generateComposedTransactionModel($request_id, $payment_address, $destination, $float_quantity, $asset, $float_fee, $float_btc_dust_size, $is_sweep); if (!$composed_transaction_model) { return null; } $signed_transaction_hex = $composed_transaction_model['transaction']; $utxo_identifiers = $composed_transaction_model['utxos']; // push all signed transactions to the bitcoin network // some of these may fail $sent_tx_id = null; try { $sent_tx_id = $this->bitcoind->sendrawtransaction($signed_transaction_hex); } catch (Exception $e) { Log::debug("bitcoind returned exception: " . $e->getCode()); if (in_array($e->getCode(), [-25, -26, -27])) { // this transaction was rejected, remove it from the composed transaction repository // so it can be created again $this->composed_transaction_repository->deleteComposedTransactionsByRequestID($request_id); // unspend each spent TXO $this->txo_repository->updateByTXOIdentifiers($utxo_identifiers, ['spent' => 0]); // delete each new TXO $this->txo_repository->deleteByTXID($composed_transaction_model['txid']); $error_log_details = compact('request_id', 'txid', 'destination', 'float_quantity', 'asset'); $error_log_details['errorCode'] = $e->getCode(); $error_log_details['errorMsg'] = $e->getMessage(); EventLog::log('composedTransaction.removed', $error_log_details); } // throw the exception throw $e; } return $sent_tx_id; }
public function send(PaymentAddress $payment_address, $quantity, $asset, $parsed_tx, $confirmations) { // when migrating, we need to ignore the transactions already confirmed if ($confirmations > 0 and $parsed_tx['bitcoinTx']['blockheight'] < Config::get('xchain.accountsIgnoreBeforeBlockHeight')) { EventLog::log('account.receive.ignored', ['blockheight' => $parsed_tx['bitcoinTx']['blockheight'], 'confirmations' => $confirmations, 'ignoredBefore' => Config::get('xchain.accountsIgnoreBeforeBlockHeight')]); return; } return RecordLock::acquireAndExecute($payment_address['uuid'], function () use($payment_address, $quantity, $asset, $parsed_tx, $confirmations) { DB::transaction(function () use($payment_address, $quantity, $asset, $parsed_tx, $confirmations) { list($txid, $dust_size, $btc_fees) = $this->extractDataFromParsedTransaction($parsed_tx); // Log::debug("send: $txid, $dust_size, $btc_fees \$confirmations=$confirmations"); // Log::debug("send $quantity $asset \$txid=$txid \$confirmations=".json_encode($confirmations, 192)); if ($confirmations >= self::SEND_CONFIRMATIONS_REQUIRED) { // confirmed send // find any sending funds and debit them $any_sending_funds_found = false; $sent_balances_by_account_id = $this->ledger_entry_repository->accountBalancesByTXID($txid, LedgerEntry::SENDING); // Log::debug("\$sent_balances_by_account_id=".json_encode($sent_balances_by_account_id, 192)); foreach ($sent_balances_by_account_id as $account_id => $balances) { $any_sending_funds_found = true; $account = $this->account_repository->findByID($account_id); // this account must belong to the payment address if ($account['payment_address_id'] != $payment_address['id']) { continue; } foreach ($balances as $asset => $quantity) { if ($quantity > 0) { $this->ledger_entry_repository->addDebit($quantity, $asset, $account, LedgerEntry::SENDING, LedgerEntry::DIRECTION_SEND, $txid); } } } } else { // unconfirmed send // get the default account $default_account = $this->getAccount($payment_address); // if there are any entries for this txid and payment address and type already, then // don't add anything new $existing_ledger_entries = $this->ledger_entry_repository->findByTXID($txid, $payment_address['id'], null, LedgerEntry::DIRECTION_SEND); if (count($existing_ledger_entries) > 0) { if ($confirmations == 0) { EventLog::log('account.send.alreadyRecorded', ['txid' => $txid, 'existingLedgerEntries' => count($existing_ledger_entries)]); } return; } // change type foreach ($this->buildSendBalances($quantity, $asset, $btc_fees, $dust_size) as $asset_sent => $quantity_sent) { $this->ledger_entry_repository->changeType($quantity_sent, $asset_sent, $default_account, LedgerEntry::CONFIRMED, LedgerEntry::SENDING, LedgerEntry::DIRECTION_SEND, $txid); } } }); }, self::SEND_LOCK_TIMEOUT); }
public function generateAndSendNotifications($block_event, $block_confirmations, Block $current_block) { // send a new block notification $notification = $this->buildNotification($block_event); // send block notifications // create a block notification for each user $notification_vars_for_model = $notification; unset($notification_vars_for_model['notificationId']); foreach ($this->user_repository->findWithWebhookEndpoint() as $user) { try { $notification_model = $this->notification_repository->createForUser($user, ['txid' => $block_event['hash'], 'confirmations' => $block_confirmations, 'notification' => $notification_vars_for_model, 'block_id' => $current_block['id']]); // add the id $notification['notificationId'] = $notification_model['uuid']; // put notification in the queue EventLog::log('notification.out', ['event' => $notification['event'], 'height' => $notification['height'], 'hash' => $notification['hash'], 'endpoint' => $user['webhook_endpoint'], 'user' => $user['id'], 'id' => $notification_model['uuid']]); $this->xcaller_client->sendWebhook($notification, $user['webhook_endpoint'], $notification_model['uuid'], $user['apitoken'], $user['apisecretkey']); } catch (QueryException $e) { if ($e->errorInfo[0] == 23000) { EventLog::logError('blockNotification.duplicate.error', $e, ['id' => $notification_model['uuid'], 'height' => $notification['height'], 'hash' => $block_event['hash'], 'user' => $user['id']]); } else { throw $e; } } catch (Exception $e) { EventLog::logError('notification.error', $e); sleep(3); throw $e; } } // send transaction notifications // also update every transaction that needs a new confirmation sent // find all transactions in the last 6 blocks // and send out notifications $blocks = $this->blockchain_store->findAllAsOfHeightEndingWithBlockhash($block_event['height'] - (self::MAX_CONFIRMATIONS_TO_NOTIFY - 1), $block_event['hash']); $block_hashes = []; $blocks_by_hash = []; foreach ($blocks as $previous_block) { $block_hashes[] = $previous_block['hash']; $blocks_by_hash[$previous_block['hash']] = $previous_block; } if ($block_hashes) { $block_event_context = $this->block_event_context_factory->newBlockEventContext(); $_offset = 0; foreach ($this->transaction_repository->findAllTransactionsConfirmedInBlockHashes($block_hashes) as $transaction_model) { $confirmations = $this->confirmations_builder->getConfirmationsForBlockHashAsOfHeight($transaction_model['block_confirmed_hash'], $block_event['height']); if ($_offset % 50 === 1) { Log::debug("tx {$_offset} {$confirmations} confirmations"); } // the block height might have changed if the chain was reorganized $parsed_tx = $transaction_model['parsed_tx']; $confirmed_block = $blocks_by_hash[$transaction_model['block_confirmed_hash']]; if ($confirmed_block) { $parsed_tx['bitcoinTx']['blockheight'] = $confirmed_block['height']; try { $this->events->fire('xchain.tx.confirmed', [$parsed_tx, $confirmations, $transaction_model['block_seq'], $current_block, $block_event_context]); } catch (Exception $e) { Log::error("xchain.tx.confirmed FAILED for tx {$_offset} with txid {$transaction_model['txid']}. " . $e->getMessage()); throw $e; } } else { EventLog::logError('block.blockNotFound', ['hash' => $transaction_model['block_confirmed_hash'], 'txid' => $transaction_model['txid']]); } ++$_offset; } } else { EventLog::logError('block.noBlocksFound', ['height' => $block_event['height'], 'hash' => $block_event['hash'], 'previousblockhash' => $block_event['previousblockhash']]); } }
public function fire($job, $data) { Log::debug("ValidateConfirmedCounterpartydTxJob called.\ntxid=" . json_encode($data['tx']['txid'], 192)); // $data = [ // 'tx' => $parsed_tx, // 'confirmations' => $confirmations, // 'block_seq' => $block_seq, // 'block_id' => $block_id, // ]; // { // "destination": "1H42mKvwutzE4DAip57tkAc9KEKMGBD2bB", // "source": "1MFHQCPGtcSfNPXAS6NryWja3TbUN9239Y", // "quantity": 1050000000000, // "block_index": 355675, // "tx_hash": "5cbaf7995e7a8337861a30a65f5d751550127f63fccdb8a9b307efc26e6aa28b", // "tx_index": 234609, // "status": "valid", // "asset": "LTBCOIN" // } // validate from counterpartyd // xcp_command -c get_sends -p '{"filters": {"field": "tx_hash", "op": "==", "value": "address"}}' $parsed_tx = $data['tx']; $tx_hash = $parsed_tx['txid']; try { $sends = $this->xcpd_client->get_sends(['filters' => ['field' => 'tx_hash', 'op' => '==', 'value' => $tx_hash]]); // not valid by default $is_valid = false; // not found by default $was_found = false; } catch (Exception $e) { EventLog::logError('error.counterparty', $e); // received no result from counterparty $sends = null; $is_valid = null; } if ($sends) { $send = $sends[0]; if ($send) { $is_valid = true; $was_found = true; try { if ($send['destination'] != $parsed_tx['destinations'][0]) { throw new Exception("mismatched destination: {$send['destination']} (xcpd) != {$parsed_tx['destinations'][0]} (parsed)", 1); } $xcpd_quantity_sat = $send['quantity']; // if token is not divisible, adjust to satoshis $is_divisible = $this->asset_cache->isDivisible($send['asset']); if (!$is_divisible) { $xcpd_quantity_sat = CurrencyUtil::valueToSatoshis($xcpd_quantity_sat); } // compare send quantity $parsed_quantity_sat = CurrencyUtil::valueToSatoshis($parsed_tx['values'][$send['destination']]); if ($xcpd_quantity_sat != $parsed_quantity_sat) { throw new Exception("mismatched quantity: {$xcpd_quantity_sat} (xcpd) != {$parsed_quantity_sat} (parsed)", 1); } // check asset if ($send['asset'] != $parsed_tx['asset']) { throw new Exception("mismatched asset: {$send['asset']} (xcpd) != {$parsed_tx['asset']} (parsed)", 1); } Log::debug("Send {$tx_hash} was confirmed by counterpartyd. {$xcpd_quantity_sat} {$send['asset']} to {$send['destination']}"); } catch (Exception $e) { EventLog::logError('error.counterpartyConfirm', $e, ['txid' => $tx_hash]); $is_valid = false; } } } if ($is_valid === null) { // no response from conterpartyd if ($job->attempts() > 240) { // permanent failure EventLog::logError('job.failed.permanent', ['txid' => $tx_hash]); $job->delete(); } else { // no response - bury this task and try again $release_time = null; $attempts = $job->attempts(); if ($job->attempts() > 60) { $release_time = 60; } else { if ($job->attempts() > 30) { $release_time = 30; } else { if ($job->attempts() > 20) { $release_time = 20; } else { if ($job->attempts() > 10) { $release_time = 5; } else { $release_time = 2; } } } } // put it back in the queue Log::debug("Send {$tx_hash} was not confirmed by counterpartyd yet. putting it back in the queue for {$release_time} seconds. \$sends=" . json_encode($sends, 192)); $job->release($release_time); } } else { if ($is_valid === true) { // valid send - return it $data['tx']['counterpartyTx']['validated'] = true; // handle the parsed tx now $block = $this->block_repository->findByID($data['block_id']); if (!$block) { throw new Exception("Block not found: {$data['block_id']}", 1); } try { $this->events->fire('xchain.tx.confirmed', [$data['tx'], $data['confirmations'], $data['block_seq'], $block]); } catch (Exception $e) { EventLog::logError('error.confirmingTx', $e); usleep(500000); // sleep 0.5 seconds to prevent runaway errors throw $e; } // if all went well, delete the job $job->delete(); } else { if ($is_valid === false) { if ($was_found) { // this send was found, but it was not a valid send // delete it $job->delete(); } else { // this send wasn't found by counterpartyd at all // but it might be found if we try a few more times $attempts = $job->attempts(); if ($attempts >= 4) { // we've already tried 4 times - give up Log::debug("Send {$tx_hash} was not found by counterpartyd after attempt " . $attempts . ". Giving up."); $job->delete(); } else { $release_time = $attempts > 2 ? 10 : 2; Log::debug("Send {$tx_hash} was not found by counterpartyd after attempt " . $attempts . ". Trying again in {$release_time} seconds."); $job->release($release_time); } } } } } }