/** * example.com/api/<guid>/sendmany?to=xxx&amount=satoshis¬e=yyy&password=zzz&account=invoice&debug=1 */ public function sendmany($guid) { // because it can be a long process, set execution time to a lot more ini_set('max_execution_time', 600); ini_set('memory_limit', '512M'); if (!$this->attemptAuth()) { return Response::json(['error' => AUTHENTICATION_FAIL]); } $recipients_json = Input::get('recipients'); // $note = Input::get( 'note' ); $note = ''; $external_user_id = Input::get('external_user_id', null); $account = Input::get('account', ""); if (!$note) { $note = ''; } /* validate that recipients is JSON object with address:satoshi pairs */ $recipients = json_decode($recipients_json); $is_valid_recipients = $this->validateSendManyJson($recipients); if (!$is_valid_recipients) { return Response::json(['error' => '#sendmany: ' . ADDRESS_AMOUNT_NOT_SPECIFIED_SEND_MANY]); } Log::info('=== START PAYMENT to MANY'); foreach ($recipients as $btc_address => $satoshi_amount) { Log::info("Payment to {$btc_address}, note: {$note}, amount: " . self::satoshiToBtc($satoshi_amount)); } Log::info('=== END PAYMENT to MANY'); $total_satoshis = $this->getSendManyTotalAmount($recipients); DB::beginTransaction(); // begin DB transaction $user_balance = Balance::getBalance($this->user->id, $this->crypto_type_id); if ($user_balance->balance < $total_satoshis) { DB::rollback(); Log::error('#sendmany: ' . NO_FUNDS); return Response::json(['error' => '#payment: ' . NO_FUNDS]); } $total_satoshis = abs($total_satoshis); // make it sure its positive $new_balance = bcsub($user_balance->balance, $total_satoshis); Log::info('User initial balance: ' . self::satoshiToBtc($user_balance->balance) . ' BTC, new balance: ' . self::satoshiToBtc($new_balance)); Balance::setNewUserBalance($user_balance, $new_balance); $sent = false; try { // convert satoshis to btc $recipients_copy = clone $recipients; // need to copy to another array, since satoshis are converted to bitcoin denomination by reference $this->bitcoin_core->setRpcConnection($this->user->rpc_connection); $addedExtraOutputs = false; /* check if that functionality is there */ if (BitcoinHelper::isMonitoringOutputsEnabled()) { // if anything fails in here, just continue sending try { /* Check here if there are enough confirmed UTXOs and whether more need to be added * Check of outputs were not checked recently, otherwise query for unspents */ $checkedOutputsRecently = Cache::get(self::OUTPUTS_CACHE_KEY); if (!$checkedOutputsRecently) { $outputs = $this->bitcoin_core->listunspent(1); $outputsResponse = new UnspentOutputsResponse($outputs); $total = $outputsResponse->getTotal(); $outputsThreshold = BitcoinHelper::getOutputsThreshold(); Log::info('Initiating checking outputs, total outputs: ' . $total . '. Outputs threshold: ' . $outputsThreshold); // if total less than 150, create 125 more if ($total < $outputsThreshold) { // send to own addresses some 0.06 or something // get 125 more addresses and create pairs for them $recipients_copy = BitcoinHelper::addOutputsToChangeAddresses($recipients_copy, $this->user->id); // sending email of new transaction hash to email at the end of this method $addedExtraOutputs = true; } else { // not added, just email about it MailHelper::sendAdminEmail(['subject' => 'Enough outputs exists', 'text' => 'No need to add extra. Number of outputs: ' . $outputsResponse->getTotal()]); } // set cache for 45 minutes until next check for outputs $outputsCacheDuration = BitcoinHelper::getOutputsCacheDuration(); Cache::put(self::OUTPUTS_CACHE_KEY, 1, $outputsCacheDuration); } } catch (Exception $e) { MailHelper::sendAdminEmail(['subject' => 'Failed in adding more outputs', 'text' => "Message\n" . $e->getMessage() . "\nTrace\n" . $e]); // replace back only original recipients $recipients_copy = clone $recipients; } } $recipients_bitcoin_denomination_obj = $this->convertSendManySatoshisToBtc($recipients_copy, false); $tx_id = $this->bitcoin_core->sendmany($account, $recipients_bitcoin_denomination_obj, 0, $note); $sent = true; if ($tx_id) { // if it fails here on inserting new transaction, then this transaction will be rolled back - user balance not updated, but jsonrpcclient will send out. // think of a clever way on which step it failed and accordingly let know if balance was updated or not foreach ($recipients as $address => $amount_satoshi) { $new_transaction = Transaction::insertNewTransaction(['tx_id' => $tx_id, 'user_id' => $this->user->id, 'transaction_type' => TX_SEND, 'crypto_amount' => $amount_satoshi, 'crypto_type_id' => $this->crypto_type_id, 'address_to' => $address, 'note' => $note, 'external_user_id' => $external_user_id]); } } } catch (Exception $e) { DB::rollback(); Log::error("#sendmany: send to address exception: " . $e->getMessage()); foreach ($recipients as $address => $amount_satoshi) { // create identical data first $tx_data = ['user_id' => $this->user->id, 'address_to' => $address, 'crypto_amount' => $amount_satoshi, 'error' => $e->getMessage(), 'user_note' => $note, 'sent_to_network' => $sent, 'transaction_type' => TX_SEND, 'external_user_id' => $external_user_id]; // because transaction was sent to network, decrease API user balance and also insert transaction hash if ($sent) { Balance::setNewUserBalance($user_balance, $new_balance); // also decrease balance $tx_data['tx_id'] = $tx_id; // because was sent to network, we know tx_id TransactionFailed::insertTransaction($tx_data); return Response::json(['message' => "#sendmany: send to address exception: " . $e->getMessage(), 'tx_hash' => $tx_id]); } else { TransactionFailed::insertTransaction($tx_data); } } // send email MailHelper::sendAdminEmail(['subject' => 'Failed in sendmany', 'text' => "Message\n" . $e->getMessage() . "\nTrace\n" . $e]); return Response::json(['error' => "#sendmany: send to address exception: " . $e->getMessage()]); } DB::commit(); if (isset($new_transaction)) { $this->saveFee($new_transaction); if ($addedExtraOutputs) { // send email about extra outputs with tx hash MailHelper::sendAdminEmail(['subject' => 'Added extra outputs', 'text' => "Tx id: {$tx_id}"]); } } return Response::json(['message' => 'Sent To Multiple Recipients', 'tx_hash' => $tx_id]); }