public function getValidatorInstance() { $validator = parent::getValidatorInstance(); $validator->after(function () use($validator) { // validate destinations $destinations = $this->json('destinations'); if ($destinations) { if (is_array($destinations)) { $offset = 0; foreach ($destinations as $destination) { if (!isset($destination['address']) or !isset($destination['amount'])) { $validator->errors()->add('destinations', 'Missing address or amount for destination ' . ($offset + 1) . '.'); continue; } if (!AddressValidator::isValid($destination['address'])) { $validator->errors()->add('destinations', 'The address for destination ' . ($offset + 1) . ' was invalid.'); } if (!is_numeric($destination['amount']) or CurrencyUtil::valueToSatoshis($destination['amount']) <= 0) { $validator->errors()->add('destinations', 'The amount for destination ' . ($offset + 1) . ' was invalid.'); } ++$offset; } } else { $validator->errors()->add('destinations', 'The destinations were invalid.'); } } }); return $validator; }
public function testMultiplePaymentAddressSendsWithSameClientGUID() { // mock the xcp sender $mock_calls = app('CounterpartySenderMockBuilder')->installMockCounterpartySenderDependencies($this->app, $this); $user = app('\\UserHelper')->createSampleUser(); $payment_address = app('\\PaymentAddressHelper')->createSamplePaymentAddress($user); // create a send with a client guid $api_tester = $this->getAPITester(); $posted_vars = $this->sendHelper()->samplePostVars(); $posted_vars['requestId'] = 'request001'; $expected_created_resource = ['id' => '{{response.id}}', 'destination' => '{{response.destination}}', 'destinations' => '', 'asset' => 'TOKENLY', 'sweep' => '{{response.sweep}}', 'quantity' => '{{response.quantity}}', 'txid' => '{{response.txid}}', 'requestId' => 'request001']; $loaded_resource_model = $api_tester->testAddResource($posted_vars, $expected_created_resource, $payment_address['uuid']); // get_asset_info followed by the send PHPUnit::assertCount(1, $mock_calls['btcd']); // validate that a mock send was triggered $send_details = app('TransactionComposerHelper')->parseCounterpartyTransaction($mock_calls['btcd'][0]['args'][0]); PHPUnit::assertEquals('1JztLWos5K7LsqW5E78EASgiVBaCe6f7cD', $send_details['destination']); PHPUnit::assertEquals(CurrencyUtil::valueToSatoshis(100), $send_details['quantity']); PHPUnit::assertEquals('TOKENLY', $send_details['asset']); // try the send again with the same request_id $expected_resource = $loaded_resource_model; $posted_vars = $this->sendHelper()->samplePostVars(); $posted_vars['requestId'] = 'request001'; $loaded_resource_model_2 = $api_tester->testAddResource($posted_vars, $expected_created_resource, $payment_address['uuid']); // does not send again PHPUnit::assertCount(1, $mock_calls['btcd']); // second send resource is the same as the first PHPUnit::assertEquals($loaded_resource_model, $loaded_resource_model_2); }
protected function buildTransaction($send_amount, $source, $destinations, $sample_txid_offset = 100, $filename = 'sample_btc_parsed_01.json') { $tx_helper = app('SampleTransactionsHelper'); $parsed_tx = $tx_helper->loadSampleTransaction($filename, ['txid' => str_repeat('4', 60) . sprintf('%04d', $sample_txid_offset)]); // fix vin $parsed_tx['bitcoinTx']['vin'][0]['addr'] = $source; $parsed_tx['bitcoinTx']['vin'][0]['value'] = $send_amount; $parsed_tx['bitcoinTx']['vin'][0]['valueSat'] = CurrencyUtil::valueToSatoshis($send_amount); $parsed_tx['sources'] = [$destinations[0][0]]; $tx_destinations = []; $values = []; $total_sent = 0; foreach ($destinations as $destination_pair) { $dest_address = $destination_pair[0]; $dest_amount = $destination_pair[1]; $total_sent += $dest_amount; if ($dest_address == $source) { continue; } if (!isset($values[$dest_address])) { $values[$dest_address] = 0; } $values[$dest_address] += $dest_amount; } $parsed_tx['destinations'] = $tx_destinations; $parsed_tx['values'] = $values; $parsed_tx['fees'] = $send_amount - $total_sent; $parsed_tx['bitcoinTx']['fees'] = $send_amount - $total_sent; return $parsed_tx; }
public function addUTXOToPaymentAddress($float_btc_balance, $payment_address, $account_name = 'default', $txid = 'SAMPLE01') { // add UTXOs for BTC balance $sample_txos = []; if ($float_btc_balance) { $txid = $this->txo_helper->nextTXID(); $sample_txos[] = $this->txo_helper->createSampleTXO($payment_address, ['txid' => $txid, 'amount' => CurrencyUtil::valueToSatoshis($float_btc_balance), 'n' => 0]); } return $sample_txos; }
protected function buildValues($name, $base, $target, $ask, $bid, $last, $timestamp = null) { $out = []; $out['name'] = $name; $out['base'] = $base; $out['target'] = $target; $out['ask'] = floatval($ask); $out['askSat'] = CurrencyUtil::valueToSatoshis($ask); $out['bid'] = floatval($bid); $out['bidSat'] = CurrencyUtil::valueToSatoshis($bid); $out['last'] = floatval($last); $out['lastSat'] = CurrencyUtil::valueToSatoshis($last); $out['timestamp'] = $timestamp === null ? time() : intval($timestamp); return $out; }
public function sampleVars($override_vars = []) { // apply sample post vars $override_vars = $this->samplePostVars($override_vars); $vars = array_merge(['txid' => 'SAMPLETXID000000000000000000000000000000000000000000000000000001'], $override_vars); if (isset($vars['quantity'])) { $vars['quantity_sat'] = CurrencyUtil::valueToSatoshis($vars['quantity']); unset($vars['quantity']); } if (isset($vars['requestId'])) { $vars['request_id'] = CurrencyUtil::valueToSatoshis($vars['requestId']); unset($vars['requestId']); } return $vars; }
protected function enhanceVin($vin, $n) { if (!isset($vin['scriptSig']) or !isset($vin['scriptSig']['hex'])) { return $vin; } // add n $vin['n'] = $n; // extract the address $address = $this->addressFromScriptHex($vin['scriptSig']['hex']); $vin['addr'] = $address; // build value $value = $this->buildValueFromTXO($vin['txid'], $vin['vout']); $vin['value'] = $value; $vin['valueSat'] = CurrencyUtil::valueToSatoshis($value); return $vin; }
public function testCurrentUtilsConversions() { // float value to satoshis PHPUnit::assertEquals(12300000000, CurrencyUtil::valueToSatoshis(123)); PHPUnit::assertEquals(2100000000000000, CurrencyUtil::valueToSatoshis(21000000)); PHPUnit::assertEquals(12311111119, CurrencyUtil::valueToSatoshis(123.111111189)); // satoshis to float value PHPUnit::assertEquals(123, CurrencyUtil::satoshisToValue(12300000000)); PHPUnit::assertEquals(21000000, CurrencyUtil::satoshisToValue(2100000000000000)); PHPUnit::assertEquals(123.11111119, CurrencyUtil::satoshisToValue(12311111119)); // satoshisToFormattedString PHPUnit::assertEquals('123', CurrencyUtil::satoshisToFormattedString(12300000000)); PHPUnit::assertEquals('123.4', CurrencyUtil::satoshisToFormattedString(12340000000)); PHPUnit::assertEquals('1,235.6', CurrencyUtil::satoshisToFormattedString(123560000000)); // valueToFormattedString PHPUnit::assertEquals('1,234.5', CurrencyUtil::valueToFormattedString(1234.5)); }
/** * 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 testAPIAddMultisend() { // mock the xcp sender $mock_calls = $this->app->make('CounterpartySenderMockBuilder')->installMockCounterpartySenderDependencies($this->app, $this); $user = $this->app->make('\\UserHelper')->createSampleUser(); $payment_address = $this->app->make('\\PaymentAddressHelper')->createSamplePaymentAddress($user); $api_tester = $this->getAPITester('/api/v1/multisends'); $posted_vars = $this->sendHelper()->sampleMultisendPostVars(); $expected_created_resource = ['id' => '{{response.id}}', 'destination' => '', 'destinations' => '{{response.destinations}}', 'asset' => 'BTC', 'txid' => '{{response.txid}}', 'requestId' => '{{response.requestId}}', 'sweep' => '{{response.sweep}}', 'quantity' => 0.006]; $api_response = $api_tester->testAddResource($posted_vars, $expected_created_resource, $payment_address['uuid']); // validate the send details $transaction_composer_helper = app('TransactionComposerHelper'); PHPUnit::assertCount(1, $mock_calls['btcd']); $send_details = $transaction_composer_helper->parseBTCTransaction($mock_calls['btcd'][0]['args'][0]); // primary send PHPUnit::assertEquals('1ATEST111XXXXXXXXXXXXXXXXXXXXwLHDB', $send_details['destination']); PHPUnit::assertEquals(CurrencyUtil::valueToSatoshis(0.001), $send_details['btc_amount']); // other change... PHPUnit::assertEquals('1ATEST222XXXXXXXXXXXXXXXXXXXYzLVeV', $send_details['change'][0][0]); PHPUnit::assertEquals(CurrencyUtil::valueToSatoshis(0.002), $send_details['change'][0][1]); PHPUnit::assertEquals('1ATEST333XXXXXXXXXXXXXXXXXXXatH8WE', $send_details['change'][1][0]); PHPUnit::assertEquals(CurrencyUtil::valueToSatoshis(0.003), $send_details['change'][1][1]); PHPUnit::assertEquals($payment_address['address'], $send_details['change'][2][0]); PHPUnit::assertEquals(CurrencyUtil::valueToSatoshis(1 - 0.006 - 0.0001), $send_details['change'][2][1]); }
protected function debugDumpUTXOs($utxos) { $out = ''; $out .= 'total utxos: ' . count($utxos) . "\n"; foreach ($utxos as $utxo) { $out .= ' ' . $utxo['txid'] . ':' . $utxo['n'] . ' (' . CurrencyUtil::satoshisToValue($utxo['amount']) . ')' . "\n"; } return rtrim($out); }
protected function assembleAccountBalancesWithTXID($results, $in_satoshis = false) { $sums = array_fill_keys(LedgerEntry::allTypeStrings(), []); foreach ($results as $result) { $txid = $result['txid']; if (!$txid) { $txid = 'none'; } if ($in_satoshis) { $sums[LedgerEntry::typeIntegerToString($result['type'])][$txid][$result['asset']] = $result['total_amount']; } else { $sums[LedgerEntry::typeIntegerToString($result['type'])][$txid][$result['asset']] = CurrencyUtil::satoshisToValue($result['total_amount']); } } return $sums; }
protected function buildDifferences($xchain_map, $daemon_map) { // $items_to_add = []; // $items_to_delete = []; $differences = []; $any_differences = false; foreach ($daemon_map as $daemon_map_key => $dum) { if (!isset($xchain_map[$daemon_map_key]) and $daemon_map[$daemon_map_key] != 0) { // $items_to_add[] = [$daemon_map_key => $daemon_map[$daemon_map_key]]; $differences[$daemon_map_key] = ['xchain' => '[NULL]', 'daemon' => $daemon_map[$daemon_map_key]]; $any_differences = true; } } foreach ($xchain_map as $xchain_map_key => $dum) { if (!isset($daemon_map[$xchain_map_key])) { // $items_to_delete[] = $xchain_map_key; $differences[$xchain_map_key] = ['xchain' => $xchain_map[$xchain_map_key], 'daemon' => '[NULL]']; $any_differences = true; } else { if (CurrencyUtil::valueToFormattedString($daemon_map[$xchain_map_key]) != CurrencyUtil::valueToFormattedString($xchain_map[$xchain_map_key])) { $differences[$xchain_map_key] = ['xchain' => $xchain_map[$xchain_map_key], 'daemon' => $daemon_map[$xchain_map_key]]; $any_differences = true; } } } // return ['any' => $any_differences, 'add' => $items_to_add, 'updates' => $differences, 'delete' => $items_to_delete]; return ['any' => $any_differences, 'differences' => $differences]; }
protected function showAccount(Account $account, $show_ledger = false) { $ledger = app('App\\Repositories\\LedgerEntryRepository'); $all_account_balances = $ledger->accountBalancesByAsset($account, null); $sep = str_repeat('-', 60) . "\n"; $out = ''; $out .= "{$sep}{$account['name']} ({$account['id']}, {$account['uuid']})\n{$sep}"; if ($show_ledger) { $out .= "\n"; $rows = []; $all_entries = $ledger->findByAccount($account); foreach ($all_entries as $entry) { $row = []; $row['date'] = $entry['created_at']->setTimezone('America/Chicago')->format('Y-m-d H:i:s T'); $row['amount'] = CurrencyUtil::satoshisToFormattedString($entry['amount']); $row['asset'] = $entry['asset']; $row['type'] = LedgerEntry::typeIntegerToString($entry['type']); $row['txid'] = $entry['txid']; $rows[] = $row; } $renderer = new ArrayToTextTable($rows); $renderer->showHeaders(true); $out .= $renderer->render(true) . "\n"; } // $out .= "BALANCES\n"; $out .= "\n"; foreach (LedgerEntry::allTypeStrings() as $type_string) { $out .= "{$type_string}:\n"; if (isset($all_account_balances[$type_string]) and $all_account_balances[$type_string]) { foreach ($all_account_balances[$type_string] as $asset => $balance) { $out .= " {$asset}: " . CurrencyUtil::valueToFormattedString($balance) . "\n"; } } else { $out .= " [empty]\n"; } } $out .= "\n{$sep}\n"; return $out; }
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; }
/** * Prime the address with UTXOs of a certain size * only if needed * * * @return Response */ public function primeAddress($address_uuid, Guard $auth, Request $request, APIControllerHelper $helper, PaymentAddressRepository $payment_address_repository, TXORepository $txo_repository, TXOChooser $txo_chooser, PaymentAddressSender $payment_address_sender) { try { $user = $auth->getUser(); if (!$user) { throw new Exception("User not found", 1); } $payment_address = $helper->requireResourceOwnedByUser($address_uuid, $user, $payment_address_repository); $size = floatval($request->input('size')); if ($size <= 0) { throw new Exception("Invalid size", 400); } $size_satoshis = CurrencyUtil::valueToSatoshis($size); $desired_prime_count = floatval($request->input('count')); if ($desired_prime_count <= 0) { throw new Exception("Invalid count", 400); } $fee = $request->input('fee'); if ($fee !== null) { $fee = floatval($fee); if ($fee <= 0) { throw new Exception("Invalid fee", 400); } } // get the UTXOs // [TXO::UNCONFIRMED, TXO::CONFIRMED] $txos = $this->filterGreenOrConfirmedUTXOs($txo_repository->findByPaymentAddress($payment_address, null, true)); // count the number that match the size $current_primed_count = 0; foreach ($txos as $txo) { if ($txo['amount'] == $size_satoshis) { ++$current_primed_count; } } $txid = null; $new_primed_count = $current_primed_count; if ($desired_prime_count > $current_primed_count) { // create a new priming transaction... $desired_new_primes_count_to_create = $desired_prime_count - $current_primed_count; $actual_new_primes_count_to_create = $this->findMaximumNewPrimeCountTXOs($txo_chooser, $payment_address, $desired_new_primes_count_to_create, $size, $fee); if ($actual_new_primes_count_to_create > 0) { $txid = $this->sendPrimingTransaction($payment_address_sender, $payment_address, $size, $actual_new_primes_count_to_create, $fee); $new_primed_count = $current_primed_count + $actual_new_primes_count_to_create; } } $output = ['oldPrimedCount' => $current_primed_count, 'newPrimedCount' => $new_primed_count, 'txid' => $txid, 'primed' => $txid ? true : false]; return $helper->buildJSONResponse($output); } catch (Exception $e) { if ($e->getCode() >= 400 and $e->getCode() < 500) { throw new HttpResponseException(new JsonResponse(['errors' => [$e->getMessage()]], 400)); } throw $e; } }
public function testAPICreatePrimes_2() { $this->app->make('CounterpartySenderMockBuilder')->installMockCounterpartySenderDependencies($this->app, $this); // setup list($payment_address, $sample_txos) = $this->setupPrimes(); // api tester $api_tester = $this->getAPITester(); $create_primes_vars = ['size' => CurrencyUtil::satoshisToValue(5430), 'count' => 2]; $response = $api_tester->callAPIWithAuthentication('POST', '/api/v1/primes/' . $payment_address['uuid'], $create_primes_vars); $api_data = json_decode($response->getContent(), true); PHPUnit::assertEquals(0, $api_data['oldPrimedCount']); PHPUnit::assertEquals(2, $api_data['newPrimedCount']); PHPUnit::assertEquals(true, $api_data['primed']); // check the composed transaction $composed_transaction_raw = DB::connection('untransacted')->table('composed_transactions')->first(); $send_data = app('TransactionComposerHelper')->parseBTCTransaction($composed_transaction_raw->transaction); $bitcoin_address = $payment_address['address']; PHPUnit::assertCount(2, $send_data['change']); PHPUnit::assertEquals($bitcoin_address, $send_data['destination']); PHPUnit::assertEquals(5430, $send_data['btc_amount']); PHPUnit::assertEquals($bitcoin_address, $send_data['change'][0][0]); PHPUnit::assertEquals(5430, $send_data['change'][0][1]); }
public function sampleData_get_balances_1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx($data, $address) { if (isset($this->balances_by_address) and isset($this->balances_by_address[$address])) { $float_balances = $this->balances_by_address[$address]; } else { $float_balances = ['BTC' => 0.01, 'LTBCOIN' => 1000, 'SOUP' => 5000]; } $satoshi_balances = []; foreach ($float_balances as $token => $float_balance) { $satoshi_balances[$token] = CurrencyUtil::valueToSatoshis($float_balance); } return ['balances' => $float_balances, 'balancesSat' => $satoshi_balances]; }
protected function assertFound($expected_offsets, $sample_entries, $chosen_entries) { $expected_entry_arrays = []; foreach ($expected_offsets as $expected_offset) { $expected_entry_arrays[] = $sample_entries[$expected_offset]->toArray(); } $actual_amounts = []; $chosen_entry_arrays = []; foreach ($chosen_entries as $chosen_entry) { $chosen_entry_arrays[] = $chosen_entry ? $chosen_entry->toArray() : null; $actual_amounts[] = CurrencyUtil::satoshisToFormattedString($chosen_entry['amount']) . ' ' . $chosen_entry['asset']; } PHPUnit::assertEquals($expected_entry_arrays, $chosen_entry_arrays, "Did not find the expected offsets of " . json_encode($expected_offsets) . '. Actual amounts were ' . json_encode($actual_amounts)); }
protected function normalizeBitcoindUTXORecord($unspent_txo_record) { return ['address' => $unspent_txo_record->destination_address, 'txid' => $unspent_txo_record->txid, 'vout' => $unspent_txo_record->n, 'script' => $unspent_txo_record->script, 'amount' => CurrencyUtil::satoshisToValue($unspent_txo_record->destination_value), 'amount_sat' => $unspent_txo_record->destination_value, 'confirmations' => $unspent_txo_record->confirmations, 'confirmed' => $unspent_txo_record->confirmations > 0]; }
protected function normalizeTransactionEvent($raw_transaction_event) { $meta = isset($raw_transaction_event['meta']) ? $raw_transaction_event['meta'] : []; if (isset($meta['baseFilename'])) { $sample_filename = $meta['baseFilename']; } else { $sample_filename = 'default_xcp_parsed_01.json'; } $default = json_decode(file_get_contents(base_path() . '/tests/fixtures/transactions/' . $sample_filename), true); $normalized_transaction_event = $default; if (isset($raw_transaction_event['txid'])) { $normalized_transaction_event['txid'] = $raw_transaction_event['txid']; $normalized_transaction_event['bitcoinTx']['txid'] = $raw_transaction_event['txid']; } if (isset($raw_transaction_event['sender'])) { $normalized_transaction_event['sources'] = [$raw_transaction_event['sender']]; } if (isset($raw_transaction_event['recipient'])) { $normalized_transaction_event['destinations'] = [$raw_transaction_event['recipient']]; // vouts if (isset($normalized_transaction_event['bitcoinTx']['vout'])) { $total_vouts = count($normalized_transaction_event['bitcoinTx']['vout']); foreach ($normalized_transaction_event['bitcoinTx']['vout'] as $offset => $vout) { if ($offset < $total_vouts - 1) { $new_recipient = $raw_transaction_event['recipient']; $vout['scriptPubKey']['addresses'] = [$new_recipient]; } $normalized_transaction_event['bitcoinTx']['vout'][$offset] = $vout; } } } // if (isset($raw_transaction_event['isCounterpartyTx'])) { // $normalized_transaction_event['isCounterpartyTx'] = $raw_transaction_event['isCounterpartyTx']; // if (!$raw_transaction_event['isCounterpartyTx']) { // $normalized_transaction_event['counterPartyTxType'] = false; // $normalized_transaction_event['counterpartyTx'] = []; // } // } if (isset($raw_transaction_event['asset'])) { $normalized_transaction_event['asset'] = $raw_transaction_event['asset']; } if (isset($raw_transaction_event['quantity'])) { // $normalized_transaction_event['quantity'] = $raw_transaction_event['quantity']; // $normalized_transaction_event['quantitySat'] = CurrencyUtil::valueToSatoshis($raw_transaction_event['quantity']); $normalized_transaction_event['values'] = [$normalized_transaction_event['destinations'][0] => $raw_transaction_event['quantity']]; if (isset($normalized_transaction_event['counterpartyTx']) and $normalized_transaction_event['counterpartyTx']) { $normalized_transaction_event['counterpartyTx']['quantity'] = $raw_transaction_event['quantity']; $normalized_transaction_event['counterpartyTx']['quantitySat'] = CurrencyUtil::valueToSatoshis($raw_transaction_event['quantity']); } else { // adjust utxos foreach ($normalized_transaction_event['bitcoinTx']['vout'] as $offset => $vout) { if ($offset < $total_vouts - 1) { $new_recipient = $raw_transaction_event['recipient']; $vout['value'] = $raw_transaction_event['quantity']; } $normalized_transaction_event['bitcoinTx']['vout'][$offset] = $vout; } } } // confirmations and blockhash if (isset($raw_transaction_event['confirmations'])) { $normalized_transaction_event['bitcoinTx']['confirmations'] = $raw_transaction_event['confirmations']; } if (isset($raw_transaction_event['blockhash'])) { $normalized_transaction_event['bitcoinTx']['blockhash'] = $raw_transaction_event['blockhash']; } if (isset($raw_transaction_event['blockId'])) { $normalized_transaction_event['bitcoinTx']['blockheight'] = $raw_transaction_event['blockId']; } // timestamp if (isset($raw_transaction_event['timestamp'])) { $normalized_transaction_event['timestamp'] = $raw_transaction_event['timestamp']; $normalized_transaction_event['bitcoinTx']['timestamp'] = $raw_transaction_event['timestamp']; } // vins if (isset($raw_transaction_event['vins'])) { foreach ($raw_transaction_event['vins'] as $offset => $vin) { $normalized_transaction_event['bitcoinTx']['vin'][$offset] = array_replace_recursive($normalized_transaction_event['bitcoinTx']['vin'][$offset], $vin); } } // timing if (isset($raw_transaction_event['mempool'])) { } if (isset($raw_transaction_event['blockId'])) { } // apply special transaction $normalized_transaction_event['txid'] = str_replace('%%last_send_txid%%', $this->last_send_txid, $normalized_transaction_event['txid']); return $normalized_transaction_event; }
/** * Execute the console command. * * @return mixed */ public function fire() { $txo_repository = app('App\\Repositories\\TXORepository'); $payment_address_repo = app('App\\Repositories\\PaymentAddressRepository'); $bitcoin_payer = app('Tokenly\\BitcoinPayer\\BitcoinPayer'); $payment_address_uuid = $this->input->getArgument('payment-address-uuid'); $should_reconcile = $this->input->getOption('reconcile'); if ($payment_address_uuid) { $payment_address = $payment_address_repo->findByUuid($payment_address_uuid); if (!$payment_address) { throw new Exception("Payment address not found", 1); } $payment_addresses = [$payment_address]; } else { $payment_addresses = $payment_address_repo->findAll(); } foreach ($payment_addresses as $payment_address) { Log::debug("reconciling TXOs for {$payment_address['address']} ({$payment_address['uuid']})"); // get xchain utxos $xchain_utxos_map = []; $db_txos = $txo_repository->findByPaymentAddress($payment_address, null, true); // unspent only foreach ($db_txos as $db_txo) { if ($db_txo['type'] != TXO::CONFIRMED) { continue; } $filtered_utxo = ['txid' => $db_txo['txid'], 'n' => $db_txo['n'], 'script' => $db_txo['script'], 'amount' => $db_txo['amount']]; $xchain_utxos_map[$filtered_utxo['txid'] . ':' . $filtered_utxo['n']] = $filtered_utxo; } // get daemon utxos $daemon_utxos_map = []; $all_utxos = $bitcoin_payer->getAllUTXOs($payment_address['address']); if ($all_utxos) { foreach ($all_utxos as $utxo) { if (!isset($utxo['confirmations']) or $utxo['confirmations'] == 0) { continue; } $filtered_utxo = ['txid' => $utxo['txid'], 'n' => $utxo['vout'], 'script' => $utxo['script'], 'amount' => CurrencyUtil::valueToSatoshis($utxo['amount'])]; $daemon_utxos_map[$utxo['txid'] . ':' . $utxo['vout']] = $filtered_utxo; } } // compare $differences = $this->buildDifferences($xchain_utxos_map, $daemon_utxos_map); if ($differences['any']) { $this->comment("Differences found for {$payment_address['address']} ({$payment_address['uuid']})"); $this->line(json_encode($differences, 192)); $this->line(''); if ($should_reconcile) { $account = AccountHandler::getAccount($payment_address); foreach ($differences['differences'] as $difference) { // delete the xchain txo if it is different in any way if (isset($difference['xchain']) and $difference['xchain']) { $utxo = $difference['xchain']; $this->line('Removing xchain UTXO: ' . json_encode($utxo, 192)); // delete $txo_model = $txo_repository->findByTXIDAndOffset($utxo['txid'], $utxo['n']); if ($txo_model['payment_address_id'] != $payment_address['id']) { throw new Exception("Mismatched payment address id. Expected {$payment_address['id']}. Found {$txo_model['payment_address_id']}", 1); } } if (isset($difference['daemon'])) { $utxo = $difference['daemon']; $this->line('Adding daemon UTXO: ' . json_encode($utxo, 192)); // add the daemon's utxo $txo_repository->create($payment_address, $account, ['txid' => $utxo['txid'], 'n' => $utxo['n'], 'script' => $utxo['script'], 'amount' => $utxo['amount']]); } } } } else { $this->comment("No differences found for {$payment_address['address']} ({$payment_address['uuid']})"); } } $this->info('done'); }
public function buildParsedTransactionData($bitcoin_transaction_data, $ts) { try { $parsed_transaction_data = []; $parsed_transaction_data = ['txid' => $bitcoin_transaction_data['txid'], 'network' => 'bitcoin', 'timestamp' => round($ts / 1000), 'counterPartyTxType' => false, 'sources' => [], 'destinations' => [], 'values' => [], 'asset' => false, 'bitcoinTx' => $bitcoin_transaction_data, 'counterpartyTx' => []]; $xcp_data = $this->parser->parseBitcoinTransaction($bitcoin_transaction_data); if ($xcp_data) { // ensure 1 source and 1 desination if (!isset($xcp_data['sources'][0])) { $xcp_data['sources'] = ['unknown']; Log::error("No source found in transaction: " . ((isset($bitcoin_transaction_data) and isset($bitcoin_transaction_data['txid'])) ? $bitcoin_transaction_data['txid'] : "unknown")); } if (!isset($xcp_data['destinations'][0])) { $xcp_data['destinations'] = ['unknown']; Log::error("No destination found in transaction: " . ((isset($bitcoin_transaction_data) and isset($bitcoin_transaction_data['txid'])) ? $bitcoin_transaction_data['txid'] : "unknown")); } // this is a counterparty transaction $parsed_transaction_data['network'] = 'counterparty'; $parsed_transaction_data['counterPartyTxType'] = $xcp_data['type']; $parsed_transaction_data['sources'] = $xcp_data['sources']; $parsed_transaction_data['destinations'] = $xcp_data['destinations']; if ($xcp_data['type'] === 'send') { $is_divisible = $this->asset_cache->isDivisible($xcp_data['asset']); // if the asset info doesn't exist, assume it is divisible if ($is_divisible === null) { $is_divisible = true; } if ($is_divisible) { $quantity_sat = $xcp_data['quantity']; $quantity_float = CurrencyUtil::satoshisToValue($xcp_data['quantity']); } else { $quantity_sat = CurrencyUtil::valueToSatoshis($xcp_data['quantity']); $quantity_float = intval($xcp_data['quantity']); } $xcp_data['quantity'] = $quantity_float; $xcp_data['quantitySat'] = $quantity_sat; $destination = $xcp_data['destinations'][0]; $parsed_transaction_data['values'] = [$destination => $quantity_float]; $parsed_transaction_data['asset'] = $xcp_data['asset']; // dustSize // dustSizeSat list($sources, $quantity_by_destination) = $this->extractSourcesAndDestinations($bitcoin_transaction_data); $dust_size_float = isset($quantity_by_destination[$destination]) ? $quantity_by_destination[$destination] : 0; $xcp_data['dustSize'] = $dust_size_float; $xcp_data['dustSizeSat'] = CurrencyUtil::valueToSatoshis($dust_size_float); } // Log::debug("\$xcp_data=".json_encode($xcp_data, 192)); $parsed_transaction_data['counterpartyTx'] = $xcp_data; } else { // this is just a bitcoin transaction list($sources, $quantity_by_destination) = $this->extractSourcesAndDestinations($bitcoin_transaction_data); $parsed_transaction_data['network'] = 'bitcoin'; $parsed_transaction_data['sources'] = $sources; $parsed_transaction_data['destinations'] = array_keys($quantity_by_destination); $parsed_transaction_data['values'] = $quantity_by_destination; $parsed_transaction_data['asset'] = 'BTC'; } // add a blockheight if (isset($parsed_transaction_data['bitcoinTx']['blockhash']) and $hash = $parsed_transaction_data['bitcoinTx']['blockhash']) { $block = $this->blockchain_store->findByHash($hash); $parsed_transaction_data['bitcoinTx']['blockheight'] = $block ? $block['height'] : null; } // add a transaction fingerprint $parsed_transaction_data['transactionFingerprint'] = $this->buildFingerprint($parsed_transaction_data['bitcoinTx']); return $parsed_transaction_data; } catch (Exception $e) { Log::warning("Failed to parse transaction: " . ((isset($bitcoin_transaction_data) and isset($bitcoin_transaction_data['txid'])) ? $bitcoin_transaction_data['txid'] : "unknown") . "\n" . $e->getMessage()); // print "ERROR: ".$e->getMessage()."\n"; // echo "\$parsed_transaction_data:\n".json_encode($parsed_transaction_data, 192)."\n"; throw $e; } }
public function testPaymentAddressSenderForBTCMultiSend() { $mock_calls = app('CounterpartySenderMockBuilder')->installMockCounterpartySenderDependencies($this->app, $this); $user = $this->app->make('\\UserHelper')->createSampleUser(); list($payment_address, $input_utxos) = $this->makeAddressAndSampleTXOs($user); $sender = app('App\\Blockchain\\Sender\\PaymentAddressSender'); $destinations = [['1ATEST111XXXXXXXXXXXXXXXXXXXXwLHDB', 0.0001], ['1ATEST222XXXXXXXXXXXXXXXXXXXYzLVeV', 0.0001], ['1ATEST333XXXXXXXXXXXXXXXXXXXatH8WE', 0.1]]; $sender->send($payment_address, $destinations, '0.1002', 'BTC', $float_fee = null, $dust_size = null, $is_sweep = false); // check the first sent call to bitcoind $btcd_send_call = $mock_calls['btcd'][0]; PHPUnit::assertEquals('sendrawtransaction', $btcd_send_call['method']); // sendrawtransaction $send_details = app('TransactionComposerHelper')->parseBTCTransaction($btcd_send_call['args'][0]); PHPUnit::assertEquals('1ATEST111XXXXXXXXXXXXXXXXXXXXwLHDB', $send_details['destination']); // check output amounts PHPUnit::assertEquals(CurrencyUtil::valueToSatoshis(0.0001), $send_details['btc_amount']); $change_amount_satoshis = 50000000 - 10000 - CurrencyUtil::valueToSatoshis(0.123); // 3 change addresses PHPUnit::assertCount(3, $send_details['change']); PHPUnit::assertEquals('1ATEST222XXXXXXXXXXXXXXXXXXXYzLVeV', $send_details['change'][0][0]); PHPUnit::assertEquals(CurrencyUtil::valueToSatoshis(0.0001), $send_details['change'][0][1]); PHPUnit::assertEquals('1ATEST333XXXXXXXXXXXXXXXXXXXatH8WE', $send_details['change'][1][0]); PHPUnit::assertEquals(CurrencyUtil::valueToSatoshis(0.1), $send_details['change'][1][1]); // check that the change utxo exists and is green $txo_repository = app('App\\Repositories\\TXORepository'); $txo = $txo_repository->findByTXIDAndOffset($send_details['txid'], 3); PHPUnit::assertTrue($txo['green']); }
protected function filterTXOsNotMatchingSize($raw_txos, $float_amount) { if ($float_amount === null) { $float_amount = 0; } $amount_satoshis = CurrencyUtil::valueToSatoshis($float_amount); $filtered_txos = []; foreach ($raw_txos as $raw_txo) { if ($raw_txo['amount'] == $amount_satoshis) { continue; } $filtered_txos[] = $raw_txo; } return $filtered_txos; }
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); } } } } } }
public function testChooseTXOsFour() { // receiving a transaction adds TXOs $txo_repository = app('App\\Repositories\\TXORepository'); $txo_chooser = app('App\\Blockchain\\Sender\\TXOChooser'); $float = function ($i) { return CurrencyUtil::satoshisToValue($i); }; list($payment_address, $sample_txos) = $this->makeAddressAndSampleTXOs_4(); // choose exact change from a bunch $chosen_txos = $txo_chooser->chooseUTXOs($payment_address, 0.0005, 0.0001, $float(5430)); // $this->assertFound([2,1,3,4], $sample_txos, $chosen_txos); }
protected function buildNotification($event_type, $parsed_tx, $quantity, $sources, $destinations, $confirmations, $block, $block_seq, $monitored_address) { $confirmation_timestamp = $block ? $block['parsed_block']['time'] : null; if ($event_type === null) { $notification = ['network' => $parsed_tx['network'], 'asset' => $parsed_tx['asset'], 'sources' => $sources, 'destinations' => $destinations, 'notificationId' => null, 'txid' => $parsed_tx['txid'], 'transactionTime' => DateTimeUtil::ISO8601Date($parsed_tx['timestamp']), 'confirmed' => $confirmations > 0 ? true : false, 'confirmations' => $confirmations, 'confirmationTime' => $confirmation_timestamp ? DateTimeUtil::ISO8601Date($confirmation_timestamp) : '', 'blockSeq' => $block_seq, 'bitcoinTx' => $parsed_tx['bitcoinTx'], 'transactionFingerprint' => isset($parsed_tx['transactionFingerprint']) ? $parsed_tx['transactionFingerprint'] : null]; } else { $notification = ['event' => $event_type, 'network' => $parsed_tx['network'], 'asset' => $parsed_tx['asset'], 'quantity' => $quantity, 'quantitySat' => CurrencyUtil::valueToSatoshis($quantity), 'sources' => $sources, 'destinations' => $destinations, 'notificationId' => null, 'txid' => $parsed_tx['txid'], 'transactionTime' => DateTimeUtil::ISO8601Date($parsed_tx['timestamp']), 'confirmed' => $confirmations > 0 ? true : false, 'confirmations' => $confirmations, 'confirmationTime' => $confirmation_timestamp ? DateTimeUtil::ISO8601Date($confirmation_timestamp) : '', 'blockSeq' => $block_seq, 'notifiedAddress' => $monitored_address['address'], 'notifiedAddressId' => $monitored_address['uuid'], 'bitcoinTx' => $parsed_tx['bitcoinTx'], 'transactionFingerprint' => isset($parsed_tx['transactionFingerprint']) ? $parsed_tx['transactionFingerprint'] : null]; } if ($block_seq === null) { unset($notification['blockSeq']); } return $notification; }
public function invalidate($parsed_tx) { DB::transaction(function () use($parsed_tx) { $txid = $parsed_tx['txid']; $existing_txos = $this->txo_repository->findByTXID($txid); if (count($existing_txos) == 0) { return; } // delete each txo foreach ($existing_txos as $existing_txo) { Log::debug("invalidate TXO: {$existing_txo['txid']}:{$existing_txo['n']}/" . CurrencyUtil::valueToSatoshis($existing_txo['amount']) . " " . TXO::typeIntegerToString($existing_txo['type']) . " from " . $existing_txo['payment_address_id']); $this->txo_repository->delete($existing_txo); } }); }
protected function hasSufficientFunds($actual_balances, $quantity, $asset, $float_fee, $dust_size) { // build required balances with fees $balances_required = $this->buildSendBalances($quantity, $asset, $float_fee, $dust_size); // check actual balances $has_sufficient_funds = true; foreach ($balances_required as $asset_required => $quantity_required) { if (!isset($actual_balances[$asset_required]) or CurrencyUtil::valueToSatoshis($actual_balances[$asset_required]) < CurrencyUtil::valueToSatoshis($quantity_required)) { $has_sufficient_funds = false; } } return $has_sufficient_funds; }