/**
  * 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);
 }
Пример #2
0
 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);
         }
     }
 }
Пример #3
0
 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);
                 }
             }
         }
     }
 }
Пример #4
0
 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);
             }
         }
     }
 }
Пример #5
0
 /**
  * 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;
 }
Пример #6
0
 /**
  * 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)));
 }
Пример #7
0
 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);
         }
     }
 }
Пример #8
0
 /**
  * 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));
 }
Пример #10
0
 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);
 }
Пример #11
0
 /**
  * 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.');
 }
Пример #12
0
 /**
  * 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);
 }
Пример #13
0
 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;
 }
Пример #14
0
 /**
  * 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);
         }
     }
 }
Пример #15
0
 /**
  * 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);
 }
Пример #16
0
 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');
 }
Пример #18
0
 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;
 }
Пример #19
0
 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']);
     }
 }
Пример #20
0
 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;
 }
Пример #21
0
 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);
 }
Пример #22
0
 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);
                     }
                 }
             }
         }
     }
 }