search(null); foreach($list as $bean) { $bean->Last_Update = \DB::i()->now(); $client = \Controller::Driver('Bitcoin', $bean->Money_Bitcoin_Host__); if (!$client->isValid()) continue; $info = $client->getInfo(); if (!$info) { $bean->Status = 'down'; $bean->commit(); continue; } if (($info['generate']) && ($bean->Generate == 'N')) { $client->setGenerate(false); } elseif ((!$info['generate']) && ($bean->Generate != 'N')) { $client->setGenerate(true); } $bean->Version = $info['version']; $bean->Coins = (int)round($info['balance'] * 100000000); $bean->Connections = $info['connections']; $bean->Blocks = $info['blocks']; $bean->Hashes_Per_Sec = $info['hashespersec']; $bean->Status = 'up'; $bean->commit(); if (is_null($bean->Address)) { // get in addr (generate if needed) $list = $client->getAddressesByLabel('_DEFAULT'); if ($list) { $bean->Address = $list[0]; } else { $bean->Address = $client->getNewAddress('_DEFAULT'); } $bean->commit(); } if (($bean->Keep_Empty == 'Y') && ($bean->Coins > 100000000)) { // empty it! $addr = self::getNullAddr(); try { $client->sendToAddress($addr, $bean->Coins / 100000000); } catch(\Exception $e) { // try smaller amount (maybe failed because of fee) try { $c = $bean->Coins / 100000000; $c = round($c/4, 2); if ($c > 0) $client->sendToAddress($addr, $c); } catch(\Exception $e) { // give up } } } if ($bean->Coins > (500*100000000)) { // more than 500 coins on this host, shuffle some~ $client->sendToAddress($client->getNewAddress(), (mt_rand(18,20000)/100)); } } } public static function getRate() { $ticker = \Money\Trade::ticker('BTC','EUR'); $btc = \DB::DAO('Currency')->searchOne(array('Currency__' => 'BTC')); $btc->Ex_Bid = 1/$ticker['vwap']['value']; $btc->Ex_Ask = 1/$ticker['vwap']['value']; $btc->commit(); \DB::DAO('Currency_History')->insert(array('Currency__' => $btc->Currency__, 'Date' => gmdate('Y-m-d'), 'Ex_Bid' => $btc->Ex_Bid, 'Ex_Ask' => $btc->Ex_Ask)); } public static function mergeSmallOutputs() { $transaction = \DB::i()->transaction(); $lock = \DB::i()->lock('Money_Bitcoin_Available_Output'); $list = \DB::DAO('Money_Bitcoin_Available_Output')->search(array('Available' => 'Y', new \DB\Expr('`Value` < 100000000')), null, array(5)); if (count($list) < 3) return false; $list[] = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(['Available' => 'Y', new \DB\Expr('`Value` > 100000000')]); $input = array(); $amount = 0; foreach($list as $bean) { $key = \DB::DAO('Money_Bitcoin_Permanent_Address')->searchOne(array('Money_Bitcoin_Permanent_Address__' => $bean->Money_Bitcoin_Permanent_Address__)); if (!$key) throw new \Exception('Unusable output'); $tmp = array( 'privkey' => \Internal\Crypt::decrypt($key->Private_Key), 'tx' => $bean->Hash, 'N' => $bean->N, 'hash' => $bean->Money_Bitcoin_Permanent_Address__, 'amount' => $bean->Value, 'input_source' => $bean->Money_Bitcoin_Available_Output__, ); $input[] = $tmp; $amount += $bean->Value; $bean->Available = 'N'; $bean->commit(); } $output = \Money\Bitcoin::getNullAddr(); $output = \Util\Bitcoin::decode($output); if (!$output) return false; $tx = \Util\Bitcoin::makeNormalTx($input, $amount, $output, $output); self::publishTransaction($tx); return $transaction->commit(); } public static function splitBigOutputs() { $transaction = \DB::i()->transaction(); $lock = \DB::i()->lock('Money_Bitcoin_Available_Output'); $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(array('Available' => 'Y', new \DB\Expr('`Value` > 1000000000'))); if (!$bean) return; $input = array(); $amount = 0; $key = \DB::DAO('Money_Bitcoin_Permanent_Address')->searchOne(array('Money_Bitcoin_Permanent_Address__' => $bean->Money_Bitcoin_Permanent_Address__)); if (!$key) throw new \Exception('Unusable output'); $tmp = array( 'privkey' => \Internal\Crypt::decrypt($key->Private_Key), 'tx' => $bean->Hash, 'N' => $bean->N, 'hash' => $bean->Money_Bitcoin_Permanent_Address__, 'amount' => $bean->Value, 'input_source' => $bean->Money_Bitcoin_Available_Output__, ); $input[] = $tmp; $amount += $bean->Value; $bean->Available = 'N'; $bean->commit(); $output1 = \Util\Bitcoin::decode(\Money\Bitcoin::getNullAddr()); $output2 = \Util\Bitcoin::decode(\Money\Bitcoin::getNullAddr()); $tx = \Util\Bitcoin::makeNormalTx($input, round(mt_rand($amount*0.4, $amount*0.6)), $output1, $output2); self::publishTransaction($tx); return $transaction->commit(); } public static function getTxInput($amount, $inputs = array()) { // get input that covers at least $amount $tx_list = array(); $total = 0; if ($amount <= 0) throw new \Exception('Invalid TX amount'); // check for forced inputs foreach($inputs as $input) { $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(array('Hash' => $input['hash'], 'N' => $input['n'])); if (!$bean) continue; // not a valid input $total += $bean->Value; $tx_list[$bean->Money_Bitcoin_Available_Output__] = $bean; $bean->Available = 'N'; $bean->commit(); if (count($tx_list) > 5) break; // even only one input is enough to invalidate the old tx, let's grab 5 } while(true) { if ($total == $amount) break; if (($total > $amount) && ($total - $amount > 1000000)) break; // need more inputs $skip_ok = false; if (count($tx_list) >= 3) { // need more inputs, and need those *fast*, take the largest that would fit our remaining balance $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(array('Available' => 'Y', new \DB\Expr('`Money_Bitcoin_Available_Output__` NOT IN ('.\DB::i()->quote(array_keys($tx_list), \DB::QUOTE_LIST).')'), new \DB\Expr('`Value` > '.($amount - $total))), array(new \DB\Expr('RAND()'))); if (!$bean) { // take largest one $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(array('Available' => 'Y', new \DB\Expr('`Money_Bitcoin_Available_Output__` NOT IN ('.\DB::i()->quote(array_keys($tx_list), \DB::QUOTE_LIST).')')), array('Value' => 'DESC')); } if (!$bean) $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(array(new \DB\Expr('`Money_Bitcoin_Available_Output__` NOT IN ('.\DB::i()->quote(array_keys($tx_list), \DB::QUOTE_LIST).')')), array(new \DB\Expr('RAND()'))); } elseif ($tx_list) { $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(array('Available' => 'Y', new \DB\Expr('`Money_Bitcoin_Available_Output__` NOT IN ('.\DB::i()->quote(array_keys($tx_list), \DB::QUOTE_LIST).')')), array(new \DB\Expr('RAND()'))); } else { $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(array('Available' => 'Y'), array(new \DB\Expr('RAND()'))); } if (!$bean) { $skip_ok = true; if ($tx_list) { $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(array(new \DB\Expr('`Money_Bitcoin_Available_Output__` NOT IN ('.\DB::i()->quote(array_keys($tx_list), \DB::QUOTE_LIST).')')), array(new \DB\Expr('RAND()'))); } else { $bean = \DB::DAO('Money_Bitcoin_Available_Output')->searchOne(null, array(new \DB\Expr('RAND()'))); } } if (!$bean) throw new \Exception('No available output for this TX'); // check if really available if (!$skip_ok) { $out = \DB::DAO('Money_Bitcoin_Block_Tx_Out')->searchOne(array('Hash' => $bean->Hash, 'N' => $bean->N)); if ($out) { if ($out->Claimed == 'Y') { $bean->Available = 'N'; $bean->commit(); continue; } } } $total += $bean->Value; $tx_list[$bean->Money_Bitcoin_Available_Output__] = $bean; } $input = array(); foreach($tx_list as $bean) { $key = \DB::DAO('Money_Bitcoin_Permanent_Address')->searchOne(array('Money_Bitcoin_Permanent_Address__' => $bean->Money_Bitcoin_Permanent_Address__)); if (!$key) throw new \Exception('Unusable output'); $tmp = array( 'privkey' => \Internal\Crypt::decrypt($key->Private_Key), 'tx' => $bean->Hash, 'N' => $bean->N, 'hash' => $bean->Money_Bitcoin_Permanent_Address__, 'amount' => $bean->Value, ); $input[] = $tmp; $bean->Available = 'N'; $bean->commit(); } shuffle($input); // randomize inputs order return $input; } public static function getPaymentAddr($payment_id) { $private = \Util\Bitcoin::genPrivKey(); $info = \Util\Bitcoin::decodePrivkey($private); $insert = array( 'Money_Bitcoin_Permanent_Address__' => $info['hash'], 'Money_Bitcoin_Host__' => null, 'Money_Merchant_Transaction_Payment__' => $payment_id, 'Private_Key' => \Internal\Crypt::encrypt($private), 'Created' => \DB::i()->now(), 'Used' => 'Y', 'Callback' => 'Money/Merchant/Transaction::bitcoinEvent' ); if (!\DB::DAO('Money_Bitcoin_Permanent_Address')->insert($insert)) return false; return \Money\Bitcoin\Address::byHash($info['hash']); } public static function getNullAddr($priv = false) { $private = \Util\Bitcoin::genPrivKey(); $info = \Util\Bitcoin::decodePrivkey($private); $address = \Util\Bitcoin::encode($info); $insert = array( 'Money_Bitcoin_Permanent_Address__' => $info['hash'], 'Money_Bitcoin_Host__' => null, 'User_Wallet__' => null, 'Private_Key' => \Internal\Crypt::encrypt($private), 'Created' => \DB::i()->now(), 'Used' => 'Y', ); if (!\DB::DAO('Money_Bitcoin_Permanent_Address')->insert($insert)) return false; if ($priv) return array('priv' => $private, 'info' => $info, 'address' => $address); return $address; } public static function getVerboseAddr($wallet, $description, $ipn = null, $user = null, $callback = null) { if ($wallet && $wallet['Currency__'] != 'BTC') return false; $private = \Util\Bitcoin::genPrivKey(); $info = \Util\Bitcoin::decodePrivkey($private); $address = \Util\Bitcoin::encode($info); $insert = array( 'Money_Bitcoin_Permanent_Address__' => $info['hash'], 'Money_Bitcoin_Host__' => null, 'User_Wallet__' => $wallet ? $wallet->getId() : null, 'Private_Key' => \Internal\Crypt::encrypt($private), 'Created' => \DB::i()->now(), 'Description' => $description, 'Ipn' => $ipn, 'Used' => 'Y', // do not use it for normal purposes 'Callback' => $callback ); if (!is_null($user)) $insert['User_Rest__'] = $user->getRestId(); if (!\DB::DAO('Money_Bitcoin_Permanent_Address')->insert($insert)) return false; return $address; } public static function getPermanentAddr($wallet, $user = null) { if ($wallet['Currency__'] != 'BTC') return false; $unused = \DB::DAO('Money_Bitcoin_Permanent_Address')->searchOne(array('User_Wallet__' => $wallet->getId(), 'Used' => 'N')); if ($unused) { if (strlen($unused->Money_Bitcoin_Permanent_Address__) != 40) return $unused->Money_Bitcoin_Permanent_Address__; return \Util\Bitcoin::encode(array('version' => 0, 'hash' => $unused->Money_Bitcoin_Permanent_Address__)); } $private = \Util\Bitcoin::genPrivKey(); $info = \Util\Bitcoin::decodePrivkey($private); $address = \Util\Bitcoin::encode($info); $insert = array( 'Money_Bitcoin_Permanent_Address__' => $info['hash'], 'Money_Bitcoin_Host__' => null, 'User_Wallet__' => $wallet->getId(), 'Private_Key' => \Internal\Crypt::encrypt($private), 'Created' => \DB::i()->now(), ); if (!is_null($user)) $insert['User_Rest__'] = $user->getRestId(); if (!\DB::DAO('Money_Bitcoin_Permanent_Address')->insert($insert)) return false; return $address; } /** * Create a bitcoin address with some dynamic configuration, like autoselling, mails, etc... * @param \User\Wallet $wallet * @param array $options * @param \User $user * @return bool|string * @throws \TokenException */ public static function getAddrWithOptions(\User\Wallet $wallet, array $options = [], \User $user = null) { if ($wallet['Currency__'] != 'BTC') throw new \TokenException('Invalid currency provided', 'invalid_source_currency'); // filter fields in options // autosell: bool Sell bitcoins when received // email: bool Send email either when receiving bitcoins (no autosell) or once sold // data: string custom data returned in the mail // currency: string The currency used for autosell, default to default wallet $filtered_options = []; $fields = ['autosell' => 'bool', 'email' => 'bool', 'data' => 'string', 'currency' => 'string']; foreach ($fields as $field => $type) { if (isset($options[$field])) { $value = $options[$field]; switch ($type) { case 'bool': $value = (bool)$value; break; default: case 'string': // truncate strings to 128 chars $value = substr((string)$value, 0, 128); break; } $filtered_options[$field] = $value; } } if (isset($filtered_options['autosell']) && $filtered_options['autosell']) { if (!isset($filtered_options['currency'])) { throw new \TokenException('Missing currency for autosell', 'autosell_missing_currency'); } } // check currency if set if (isset($filtered_options['currency'])) { // check if that currency exists $cur = \Currency::get($filtered_options['currency']); if (!$cur || $cur->isVirtual()) { throw new \TokenException('Invalid currency or virtual currency', 'invalid_target_currency'); } } // generate a new bitcoin address $private = \Util\Bitcoin::genPrivKey(); $info = \Util\Bitcoin::decodePrivkey($private); $address = \Util\Bitcoin::encode($info); $insert = array( 'Money_Bitcoin_Permanent_Address__' => $info['hash'], 'Money_Bitcoin_Host__' => null, 'User_Wallet__' => $wallet->getId(), 'Private_Key' => \Internal\Crypt::encrypt($private), 'Created' => \DB::i()->now(), 'Description' => json_encode($filtered_options), 'Used' => 'Y', // do not use it for normal purposes 'Callback' => 'Money/Bitcoin::optionAddrEvent' ); // if the call was done through the API if (!is_null($user)) $insert['User_Rest__'] = $user->getRestId(); if (!\DB::DAO('Money_Bitcoin_Permanent_Address')->insert($insert)) { throw new \TokenException('Couldn\'t create bitcoin address, please contact mtgox', 'unknown_error'); }; return $address; } public static function optionAddrEvent($addr, $hash_n, $block, $amount) { // ignore until we have enough confirmations if (!$block) return; $options = json_decode($addr->Description, true); /** @var $source_wallet \User\Wallet */ $source_wallet = \User\Wallet::byId($addr->User_Wallet__); // manage autosell if (isset($options['autosell']) && $options['autosell']) { $callback = null; if (isset($options['email']) && $options['email']) { $callback = 'Money/Bitcoin::optionAddrSellEmail'; if ($options['data']) { $callback .= '|' . $options['data']; } } \Money\Trade::addOrder($source_wallet->getUser(), 'ask', $amount, $options['currency'], [], null, $callback); } else { // send email with details about the transaction if (isset($options['email']) && $options['email']) { $mail_page = \Registry::getInstance()->OptionAddrBlockEmail ?: 'mail/option_addr_bitcoin_rcvd.mail'; $mail_data = [ '_HASH' => $hash_n, '_BLOCK' => $block, '_AMOUNT' => $amount ]; if (isset($options['data'])) $mail_data['_DATA'] = $options['data']; \Tpl::userMail($mail_page, $source_wallet->getUser(), $mail_data); } } } public static function optionAddrSellEmail($user, $oid, $type, $data = null) { $user = \User::byId($user, false, true); $trade_info = \Money\Trade::getOrderExecutionResult($user, $oid, $type == 'bid'); $mail_page = \Registry::getInstance()->OptionAddrOrderEmail ?: 'mail/option_addr_bitcoin_sold.mail'; $mail_data = [ '_TRADE_INFO' => $trade_info, ]; if ($data) $mail_data['_DATA'] = $data; return \Tpl::userMail($mail_page, $user, $mail_data); } public static function checkOrders() { // check data in Money_Bitcoin_Order to see if any order is completed $db = \DB::i(); $list = $db['Money_Bitcoin_Order']->search(array('Status' => 'pending')); $clients = array(); foreach($list as $bean) { if (!isset($clients[$bean->Money_Bitcoin_Host__])) $clients[$bean->Money_Bitcoin_Host__] = \Controller::Driver('Bitcoin', $bean->Money_Bitcoin_Host__); $client = $clients[$bean->Money_Bitcoin_Host__]; $total = (int)round($client->getReceivedByAddress($bean->Address, 3) * 100000000); // 3 confirmations if ($bean->Coins == $total) { // nothing moved if ($db->dateRead($bean->Expires) < time()) { $bean->Status = 'expired'; $bean->commit(); continue; } } $bean->Coins = $total; $total += $bean->Coins_Extra; if ($bean->Total <= $total) { // payment complete! $bean->Status = 'ok'; $bean->commit(); // mark order paid $order = \Order::byId($bean->Order__); if ($order->isPaid()) continue; // ?! $info = array( 'method' => 'BITCOIN', 'class' => 'Bitcoin', 'stamp' => time(), ); $order->paid($info); continue; } $total_nc = (int)round($client->getReceivedByAddress($bean->Address, 0) * 100000000); $bean->Coins_NC = $total_nc; $bean->commit(); } } public static function getAddressForOrder($order) { $total = $order->getTotal(); if ($total->getCurrency()->Currency__ != 'BTC') return false; $btc = $total['value']; $bean = \DB::DAO('Money_Bitcoin_Order')->searchOne(array('Order__' => $order->getId())); if ($bean) { if ($bean->Status != 'pending') return false; $bean->Total = ((int)round($btc * 100))*1000000; if ($bean->Address != '') { $bean->commit(); return $bean; } elseif ($bean->Coins == $bean->Coins_NC) { $bean->Coins_Extra = $bean->Coins; $bean->Coins = 0; $bean->Coins_NC = 0; // find a (new) random host $host = \DB::DAO('Money_Bitcoin_Host')->searchOne(array('Status' => 'up', 'Allow_Order' => 'Y'), array(new \DB\Expr('RAND()'))); if (!$host) return false; // no available host right now $client = \Controller::Driver('Bitcoin', $host->Money_Bitcoin_Host__); $addr = $client->getNewAddress('ORDER:'.$order->getId()); // update $bean->Address = $addr; $bean->commit(); return $bean; } } // find a random host $host = \DB::DAO('Money_Bitcoin_Host')->searchOne(array('Status' => 'up', 'Allow_Order' => 'Y'), array(new \DB\Expr('RAND()'))); if (!$host) return false; // no available host right now $client = \Controller::Driver('Bitcoin', $host->Money_Bitcoin_Host__); $addr = $client->getNewAddress('ORDER:'.$order->getId()); // new entry $db = \DB::i(); $uuid = \System::uuid(); $insert = array( 'Money_Bitcoin_Order__' => $uuid, 'Order__' => $order->getId(), 'Money_Bitcoin_Host__' => $host->Money_Bitcoin_Host__, 'Address' => $addr, 'Coins' => 0, 'Total' => ((int)round($btc * 100)) * 1000000, 'Created' => $db->now(), 'Expires' => $db->dateWrite(time()+(86400*10)), ); $db['Money_Bitcoin_Order']->insert($insert); $bean = $db['Money_Bitcoin_Order'][$uuid]; if (!$bean) return false; return $bean; } public static function sendAmount($address, $amount, $green = null, $inputs = array(), $fee = 0) { if ($amount instanceof \Internal\Price) $amount = $amount->convert('BTC', null, \Currency::DIRECTION_OUT)->getIntValue(); if ($fee instanceof \Internal\Price) $fee = $fee->convert('BTC', null, \Currency::DIRECTION_OUT)->getIntValue(); $transaction = \DB::i()->transaction(); $lock = \DB::i()->lock('Money_Bitcoin_Available_Output'); $address = \Util\Bitcoin::decode($address); if (!$address) throw new \Exception('Invalid bitcoin address'); $remainder = \Util\Bitcoin::decode(self::getNullAddr()); if (!$remainder) throw new \Exception('Failed to create output TX'); $input = self::getTxInput($amount+$fee, $inputs); if (!is_null($green)) { // green send // default=d47c1c9afc2a18319e7b78762dc8814727473e90 $tmp_total = 0; foreach($input as $tmp) $tmp_total += $tmp['amount']; $key = \DB::DAO('Money_Bitcoin_Permanent_Address')->searchOne(array('Money_Bitcoin_Permanent_Address__' => $green)); if (!$key) throw new \Exception('Invalid green address for transaction'); // intermediate tx $tx = \Util\Bitcoin::makeNormalTx($input, $tmp_total, array('hash' => $green), array('hash' => $green)); $txid = self::publishTransaction($tx); \DB::DAO('Money_Bitcoin_Available_Output')->insert(array('Money_Bitcoin_Available_Output__' => \System::uuid(), 'Money_Bitcoin_Permanent_Address__' => $green, 'Value' => $tmp_total, 'Hash' => $txid, 'N' => 0, 'Available' => 'N')); // final tx $tx = \Util\Bitcoin::makeNormalTx(array(array('amount' => $tmp_total, 'tx' => $txid, 'N' => 0, 'privkey' => \Internal\Crypt::decrypt($key->Private_Key), 'hash' => $green)), $amount, $address, $remainder); $txid = self::publishTransaction($tx); } else { $tx = \Util\Bitcoin::makeNormalTx($input, $amount, $address, $remainder, $fee); $txid = self::publishTransaction($tx); } if (!$transaction->commit()) return false; return $txid; // find a node with enough coins $node = \DB::DAO('Money_Bitcoin_Host')->searchOne(array('Status' => 'up', new \DB\Expr('`Coins` >= '.\DB::i()->quote($amount))), array(new \DB\Expr('RAND()'))); if (!$node) return false; $client = \Controller::Driver('Bitcoin', $node->Money_Bitcoin_Host__); return $client->sendToAddress($address, $amount/100000000); } public function getWalletHost() { throw new \Exception('Method is deprecated'); } public static function parseVersion($v) { if ($v == 0) return '[unknown]'; if ($v > 10000) { // [22:06:18] new is major * 10000 + minor * 100 + revision $rem = floor($v / 100); $proto = $v - ($rem*100); $v = $rem; } else { // [22:06:05] old was major * 100 + minor $proto = 0; } foreach(array('revision','minor','major') as $type) { $rem = floor($v / 100); $$type = $v - ($rem * 100); $v = $rem; } // build string return $major . '.' . $minor . '.' . $revision . ($proto?('[.'.$proto.']'):''); } public static function _Route_getStats($path) { switch($path) { case 'version': $req = 'SELECT `Version`, COUNT(1) AS `Count` FROM `Money_Bitcoin_Node` WHERE `Status` != \'down\' GROUP BY `Version`'; $sqlres = \DB::i()->query($req); $res = array(); while($row = $sqlres->fetch_assoc()) { $res[self::parseVersion($row['Version'])] += $row['Count']; } break; case 'ua': $req = 'SELECT `User_Agent`, COUNT(1) AS `Count` FROM `Money_Bitcoin_Node` WHERE `Status` != \'down\' GROUP BY `User_Agent`'; $sqlres = \DB::i()->query($req); $res = array(); while($row = $sqlres->fetch_assoc()) { $res[$row['User_Agent']] += $row['Count']; } break; case 'nodes': $req = 'SELECT COUNT(1) AS `Count` FROM `Money_Bitcoin_Node` WHERE `Last_Seen` > DATE_SUB(NOW(), INTERVAL 6 HOUR)'; $sqlres = \DB::i()->query($req); $row = $sqlres->fetch_assoc(); header('Content-Type: text/plain'); echo $row['Count']; exit; case 'accepting': $req = 'SELECT `Status`, COUNT(1) AS `Count` FROM `Money_Bitcoin_Node` WHERE `Last_Seen` > DATE_SUB(NOW(), INTERVAL 6 HOUR) GROUP BY `Status`'; $sqlres = \DB::i()->query($req); $res = array(); while($row = $sqlres->fetch_assoc()) { $res[$row['Status']] = $row['Count']; } $res['total_known'] = $res['up'] + $res['down']; $res['total'] = $res['total_known'] + $res['unknown']; $res['rate_accepting'] = $res['up'] / $res['total_known']; break; case 'bootstrap': // select a set of peers appropriate as seed $limit = 50; if (isset($_GET['limit'])) { $limit = (int)$_GET['limit']; if ($limit < 1) $limit = 1; if ($limit > 10000) $limit = 10000; } $req = 'SELECT * FROM `Money_Bitcoin_Node` WHERE `Status` = \'up\' AND `Last_Checked` > DATE_SUB(NOW(), INTERVAL 6 HOUR) AND `Version` >= 31500 AND (`Last_Down` IS NULL OR `Last_Down` < DATE_SUB(NOW(), INTERVAL 2 WEEK)) AND `First_Seen` < DATE_SUB(NOW(), INTERVAL 2 WEEK) ORDER BY RAND() LIMIT '.$limit; $sqlres = \DB::i()->query($req); if ($sqlres->num_rows == 0) { $req = 'SELECT * FROM `Money_Bitcoin_Node` WHERE `Status` = \'up\' AND `Last_Checked` > DATE_SUB(NOW(), INTERVAL 6 HOUR) AND `Version` >= 31500 ORDER BY RAND() LIMIT '.$limit; $sqlres = \DB::i()->query($req); } $res = array(); while($row = $sqlres->fetch_assoc()) { $res[] = array( 'ipv4' => $row['IP'], 'port' => $row['Port'], 'version' => $row['Version'], 'version_str' => self::parseVersion($row['Version']), 'user_agent' => $row['User_Agent'], 'timestamp' => \DB::i()->dateRead($row['Last_Checked']), ); } break; case 'geomap': // select all nodes $req = 'SELECT `IP`, `Status`, `Version` FROM `Money_Bitcoin_Node` WHERE `Last_Seen` > DATE_SUB(NOW(), INTERVAL 3 HOUR)'; $sqlres = \DB::i()->query($req); header('Content-Type: application/json'); echo '['; $first = true; $geoip = \ThirdParty\Geoip::getInstance(); while($row = $sqlres->fetch_assoc()) { $res = array('ipv4' => $row['IP'], 'version' => $row['Version'], 'status' => $row['Status']); $record = $geoip->lookup($row['IP'], false); if (!$record) continue; if (!isset($record['latitude'])) continue; $res['latitude'] = $record['latitude']; $res['longitude'] = $record['longitude']; if ($first) { $first = false; } else { echo ','; } echo json_encode($res); } echo ']'; exit; case 'full': // select all nodes $req = 'SELECT * FROM `Money_Bitcoin_Node`'; $sqlres = \DB::i()->query($req); header('Content-Type: application/json'); echo '['; $first = true; while($row = $sqlres->fetch_assoc()) { if ($first) { $first = false; } else { echo ','; } echo json_encode($row); } echo ']'; exit; case 'bitcoin.kml': header('Content-Type: application/vnd.google-earth.kml+xml'); // check cache $cache = \Cache::getInstance(); $data = $cache->get('bitcoin.kml_full'); if ($data) { echo $data; exit; } // select all nodes $out = fopen('php://temp', 'w'); fwrite($out, "\n"); fwrite($out, ''."\n"); fwrite($out, "\nBitcoin nodes in the world\n"); // styles fwrite($out, "\n"); fwrite($out, "\n"); fwrite($out, "\n"); $req = 'SELECT `IP`, `Status`, `Version` FROM `Money_Bitcoin_Node` WHERE `Last_Seen` > DATE_SUB(NOW(), INTERVAL 3 HOUR) ORDER BY `Status`'; $geoip = \ThirdParty\Geoip::getInstance(); $folder = ''; $sqlres = \DB::i()->query($req); while($row = $sqlres->fetch_assoc()) { // lookup $record = $geoip->lookup($row['IP'], false); if (!$record) continue; if (!isset($record['latitude'])) continue; if ($folder != $row['Status']) { if ($folder) fwrite($out, "\n"); $folder = $row['Status']; fwrite($out, "Bitcoin Nodes in status ".$folder."\n"); } fwrite($out, "".$row['IP']."IP: ".$row['IP']."

Version: ".self::parseVersion($row['Version'])."

]]>
#".$folder.""); fwrite($out, "".$record['longitude'].",".$record['latitude']."
\n"); } fwrite($out, "
\n
\n
\n"); rewind($out); $data = stream_get_contents($out); fclose($out); $cache->set('bitcoin.kml_full', $data, 1800); echo $data; exit; default: header('HTTP/1.0 404 Not Found'); die('Not available'); } header('Content-Type: application/json'); echo json_encode($res); exit; } public static function checkNodes($sched) { // get nodes to check $db = \DB::i(); $list = $db['Money_Bitcoin_Node']->search(array(new \DB\Expr('`Next_Check` < NOW()')), array(new \DB\Expr('`Status` IN (\'up\', \'unknown\') DESC'), 'Last_Checked' => 'ASC'), array(701)); if (count($list) == 701) { $sched->busy(); array_pop($list); } $end_time = (floor(time()/60)*60)+50; $nodes = new Bitcoin\Nodes(); $info = array(); $up = array(); $nodes->on(null, 'ready', function($key) use (&$info, $nodes, $db, &$up) { $node = $info[$key]; $node->Version = $nodes->getVersion($key); $node->User_Agent = $nodes->getUserAgent($key); $node->Status = 'up'; $node->Last_Seen = $db->now(); $node->Last_Checked = $db->now(); $node->Next_Check = $db->dateWrite(time()+(1800)); $node->commit(); $up[$key] = true; $nodes->getAddr($key); // initiate loading of addrs }); $nodes->on(null, 'error', function($key, $error) use (&$info, $db, &$up) { if ($up[$key]) return; // probably getaddr failed $node = $info[$key]; $node->Status = 'down'; $node->Last_Checked = $db->now(); $node->Next_Check = $db->dateWrite(time()+(3600*24)); $node->Last_Down = $db->now(); $node->Last_Error = $error; if ($db->dateRead($node->Last_Seen) < (time() - (3600*24))) { // no news for 24 hours, drop it $node->delete(); return; } $node->commit(); }); $nodes->on(null, 'addr', function($key, $addr_list) use (&$info, $nodes, $db) { $node = $info[$key]; if (count($addr_list) > 1000) { $node->Addresses = 0; $node->commit(); return; } $node->Addresses = count($addr_list); $node->commit(); foreach($addr_list as $addr) { $bean = $db['Money_Bitcoin_Node']->searchOne(array('IP' => $addr['ipv4'], 'Port' => $addr['port'])); if ($bean) { $bean->Last_Seen = $db->now(); $bean->commit(); continue; } $db['Money_Bitcoin_Node']->insert(array( 'IP' => $addr['ipv4'], 'Port' => $addr['port'], 'Next_Check' => $db->now(), 'First_Seen' => $db->now(), 'Last_Seen' => $db->now(), )); } $nodes->close($key); }); foreach($list as $node) { if ($node->Port < 1024) { $node->Status = 'down'; $node->Last_Checked = $db->now(); $node->Next_Check = $db->dateWrite(time()+(3600*24)); $node->Last_Down = $db->now(); if ($db->dateRead($node->Last_Seen) < (time() - (3600*24))) { // no news for 24 hours, drop it $node->delete(); return; } $node->Last_Error = 'invalid_port'; $node->commit(); continue; } $key = 'node_'.$node->Money_Bitcoin_Node__; $info[$key] = $node; if (!$nodes->connect($key, $node->IP, $node->Port)) { $node->Status = 'down'; $node->Last_Checked = $db->now(); $node->Next_Check = $db->dateWrite(time()+(3600*24)); $node->Last_Down = $db->now(); if ($db->dateRead($node->Last_Seen) < (time() - (3600*24))) { // no news for 24 hours, drop it $node->delete(); return; } $node->Last_Error = 'invalid_address'; $node->commit(); } } while($nodes->wait()); } public static function importBlockClaim($hash, $n, $tx) { $trx = \DB::DAO('Money_Bitcoin_Block_Tx_Out')->searchOne(array('Hash' => $hash, 'N' => $n)); if (!$trx) throw new \Exception('Claim from unknown trx: '.$hash.':'.$n); $trx->Claimed = 'Y'; $trx->commit(); \DB::DAO('Money_Bitcoin_Available_Output')->delete(array('Hash' => $hash, 'N' => $n)); return true; } public static function parseScriptPubKey($pubkey) { if (preg_match('/^([0-9a-f]{1,130}) OP_CHECKSIG$/', $pubkey, $matches)) { return array('hash' => \Util\Bitcoin::decodePubkey($matches[1]), 'pubkey' => $matches[1]); } if (preg_match('/^OP_DUP OP_HASH160 ([0-9a-f]{40}) OP_EQUALVERIFY OP_CHECKSIG.*$/', $pubkey, $matches)) { return array('hash' => array('hash' => $matches[1], 'version' => 0)); } \Debug::exception(new \Exception('WEIRD scriptPubKey - dropping it: '.$pubkey)); return array('hash' => ['hash' => '0000000000000000000000000000000000000000', 'version' => 0]); } public static function importBlock($id) { $peer = \Controller::Driver('Bitcoin', 'b54f4d35-dd1c-43aa-9096-88e37a83bda3'); $block = $peer->getBlock($id); $transaction = \DB::i()->transaction(); // insert block $data = array( 'Money_Bitcoin_Block__' => $block['hash'], 'Parent_Money_Bitcoin_Block__' => $block['prev_block'], 'Depth' => $id, 'Version' => $block['version'], 'Mrkl_Root' => $block['mrkl_root'], 'Time' => \DB::i()->dateWrite($block['time']), 'Bits' => $block['bits'], 'Nonce' => $block['nonce'], 'Size' => $block['size'], ); \DB::DAO('Money_Bitcoin_Block')->insert($data); $retry = 0; while($block['tx']) { $tx = array_shift($block['tx']); $tmp = \DB::DAO('Money_Bitcoin_Block_Tx')->search(array('Hash' => $tx['hash'])); if ($tmp) continue; // skip duplicate TXs $tx['block'] = $id; $data = array( 'Hash' => $tx['hash'], 'Block' => $block['hash'], 'Version' => $tx['version'], 'Lock_Time' => $tx['lock_time'], 'size' => $tx['size'], ); \DB::DAO('Money_Bitcoin_Block_Tx')->insert($data); \DB::DAO('Money_Bitcoin_Tx')->delete(array('Money_Bitcoin_Tx__' => $data['Hash'])); \DB::DAO('Money_Bitcoin_Tx_In')->delete(array('Hash' => $data['Hash'])); \DB::DAO('Money_Bitcoin_Tx_Out')->delete(array('Hash' => $data['Hash'])); $watch = null; $taint = null; $taint_c = 0; try { foreach($tx['in'] as $n => $in) { $data = array( 'Hash' => $tx['hash'], 'N' => $n, 'Prev_Out_Hash' => $in['prev_out']['hash'], 'Prev_Out_N' => $in['prev_out']['n'], ); if ($in['coinbase']) { $data['CoinBase'] = $in['coinbase']; } else { $data['scriptSig'] = $in['scriptSig']; self::importBlockClaim($in['prev_out']['hash'], $in['prev_out']['n'], $tx); } // \DB::DAO('Money_Bitcoin_Block_Tx_In')->insert($data); } } catch(\Exception $e) { // retry later if ($retry++ > 10) throw $e; $block['tx'][] = $tx; continue; } if (!is_null($taint)) $taint = (int)floor($taint/$taint_c); foreach($tx['out'] as $n => $out) { $data = array( 'Hash' => $tx['hash'], 'N' => $n, 'Value' => round($out['value']*100000000), ); $addr = self::parseScriptPubKey($out['scriptPubKey']); $data['Addr'] = $addr['hash']['hash']; \DB::DAO('Money_Bitcoin_Block_Tx_Out')->insert($data); if (isset(\DB::DAO('Money_Bitcoin_Permanent_Address')[$data['Addr']])) { $data['Money_Bitcoin_Process_Tx_Out__'] = \System::uuid(); \DB::DAO('Money_Bitcoin_Process_Tx_Out')->insert($data, true); } } } $transaction->commit(); } public static function importBlocks($scheduler) { // determine last imported block $block = \DB::DAO('Money_Bitcoin_Block')->searchOne(null, array('Depth' => 'DESC')); if ($block) { $block_id = $block->Depth + 1; } else { $block_id = 0; } // read blocks from b54f4d35-dd1c-43aa-9096-88e37a83bda3 $peer = \Controller::Driver('Bitcoin', 'b54f4d35-dd1c-43aa-9096-88e37a83bda3'); $info = $peer->getInfo(); if ($info['errors']) { // reschedule for in one hour $scheduler->busy(3600); throw new \Exception('Can\'t import blocks: '.$info['errors']); } $last_block = $peer->getCurrentBlock()-5; // 5 confirmations if ($last_block < $block_id) { // nothing new here // self::runAddrTriggers(); return; } $deadline = time()+50; $c = 0; while($block_id <= $last_block) { try { self::importBlock($block_id); } catch(\Exception $e) { mail('mark@ookoo.org', 'BLOCK IMPORT ERROR', $e->getMessage()."\n\n".$e); $scheduler->busy(600); return; // empty all! $db = \DB::i(); $db->query('TRUNCATE `Money_Bitcoin_Block`'); // $db->query('TRUNCATE `Money_Bitcoin_Block_Addr`'); $db->query('TRUNCATE `Money_Bitcoin_Block_Tx`'); // $db->query('TRUNCATE `Money_Bitcoin_Block_Tx_In`'); $db->query('TRUNCATE `Money_Bitcoin_Block_Tx_Out`'); } $block_id++; if ((time() > $deadline) || ($c++>49)) { $scheduler->busy(0); break; } } // run addr triggers // self::runAddrTriggers(); } public static function insertMisingAvailableOutputs($addr) { // search all unclaimed on this addr $list = \DB::DAO('Money_Bitcoin_Process_Tx_Out')->search(array('Addr' => $addr, 'Claimed' => 'N')); foreach($list as $bean) { $insert = array( 'Money_Bitcoin_Available_Output__' => \System::uuid(), 'Money_Bitcoin_Permanent_Address__' => $bean->Addr, 'Value' => $bean->Value, 'Hash' => $bean->Hash, 'N' => $bean->N, ); \DB::DAO('Money_Bitcoin_Available_Output')->insert($insert, true); } } public static function runAddrTriggers() { // lookup tx out with Trigger = new $list = \DB::DAO('Money_Bitcoin_Process_Tx_Out')->search(array('Trigger' => 'new'), null, array(500)); // limit to 500 // $main_transaction = \DB::i()->transaction(); foreach($list as $bean) { $transaction = \DB::i()->transaction(); $bean->reloadForUpdate(); if ($bean->Trigger != 'new') { // rollback, exit unset($transaction); continue; } $bean->Trigger = 'executed'; $bean->commit(); $tx = $bean->Hash.':'.$bean->N; $addr_str = \Util\Bitcoin::encode(array('version' => 0, 'hash' => $bean->Addr)); $wallet_info = \DB::DAO('Money_Bitcoin_Permanent_Address')->searchOne(array('Money_Bitcoin_Permanent_Address__' => $bean->Addr)); $redirect_value = null; if ($wallet_info) $redirect_value = $wallet_info->Redirect; $base_tx_data = \DB::DAO('Money_Bitcoin_Block_Tx')->searchOne(array('Hash' => $bean->Hash)); $base_block_data = \DB::DAO('Money_Bitcoin_Block')->searchOne(['Money_Bitcoin_Block__' => $base_tx_data->Block]); if (($wallet_info) && (!is_null($wallet_info->Private_Key)) && ($redirect_value == 'none')) { $insert = array( 'Money_Bitcoin_Available_Output__' => \System::uuid(), 'Money_Bitcoin_Permanent_Address__' => $bean->Addr, 'Value' => $bean->Value, 'Hash' => $bean->Hash, 'N' => $bean->N, 'Block' => $base_block_data->Depth, ); \DB::DAO('Money_Bitcoin_Available_Output')->insert($insert, true); } if ($redirect_value == 'fixed') { // redirect funds $target = $wallet_info->Redirect_Value; $pub = \Util\Bitcoin::decode($target); $tx = \Util\Bitcoin::makeNormalTx(array(array('amount' => $bean->Value, 'tx' => $bean->Hash, 'N' => $bean->N, 'privkey' => \Internal\Crypt::decrypt($wallet_info->Private_Key), 'hash' => $bean->Addr)), $bean->Value, $pub, $pub); self::publishTransaction($tx); $transaction->commit(); continue; } if (($wallet_info) && (!is_null($wallet_info->Callback))) { try { $cb = explode('::', str_replace('/', '\\', $wallet_info->Callback)); call_user_func($cb, $wallet_info, $tx, $base_tx_data->Block, \Internal\Price::spawnInt($bean->Value,'BTC')); } catch(\Exception $e) { \Debug::exception($e); unset($transaction); continue; } } if (($wallet_info) && (!is_null($wallet_info->Ipn))) { $base_tx_data = \DB::DAO('Money_Bitcoin_Block_Tx')->searchOne(array('Hash' => $bean->Hash)); $post = array( 'description' => $wallet_info->Description, 'tx' => $tx, 'block' => $base_tx_data->Block, 'status' => 'confirmed', 'amount_int' => $bean->Value, 'item' => 'BTC', 'addr' => \Util\Bitcoin::encode(array('version' => 0, 'hash' => $wallet_info->Money_Bitcoin_Permanent_Address__)), ); \Scheduler::oneshotUrl($wallet_info->Ipn, $post, null, null, null, $wallet_info->User_Rest__); } if (($wallet_info) && (!is_null($wallet_info->User_Wallet__))) { $wallet_info->Used = 'Y'; $wallet_info->commit(); $wallet = \User\Wallet::byId($wallet_info->User_Wallet__); if (($wallet) && ($wallet['Currency__'] == 'BTC')) { // WALLET REDIRECT CODE 1 if ((!is_null($wallet_info->Private_Key)) && ($wallet_info->Redirect == 'wallet') && ($bean->Value > 100000)) { // redirect funds $target = self::getVerboseAddr($wallet, $wallet_info->Description); $pub = \Util\Bitcoin::decode($target); try { $tx = \Util\Bitcoin::makeNormalTx(array(array('amount' => $bean->Value, 'tx' => $bean->Hash, 'N' => $bean->N, 'privkey' => \Internal\Crypt::decrypt($wallet_info->Private_Key), 'hash' => $bean->Addr)), $bean->Value, $pub, $pub); } catch(\Exception $e) { mail('mark@tibanne.com', 'FAILED TO GENERATE REDIRECT TX', 'Error '.$e->getMessage().' on: '.$wallet_info->Money_Bitcoin_Permanent_Address__."\n".print_r($bean->getProperties(), true)); throw $e; } self::publishTransaction($tx); $transaction->commit(); continue; } // search for already add $nfo = \DB::DAO('User_Wallet_History')->searchOne(array('Reference_Type' => 'Money_Bitcoin_Block_Tx_Out', 'Reference' => $tx)); if (!$nfo) { $wallet->deposit(\Internal\Price::spawnInt($bean->Value, 'BTC'), $addr_str.(is_null($wallet_info->Description)?'':"\n".$wallet_info->Description), 'deposit', 'Money_Bitcoin_Block_Tx_Out', $tx); if ($wallet['Balance']['value'] > 10000) $wallet->getUser()->aml('Balance in bitcoin is over 10000', 2); // force AML \Money\Trade::updateUserOrders($wallet->getUser()); } } } $transaction->commit(); } // $main_transaction->commit(); return count($list); } public static function getAddressBalance($addr) { $res = \Internal\Price::spawn(0,'BTC'); $list = \DB::DAO('Money_Bitcoin_Block_Tx_Out')->search(['Addr'=>$addr['hash']]); foreach($list as $bean) $res->add(\Internal\Price::spawnInt($bean->Value, 'BTC')); return $res; } public static function getAddressOutputs($addr) { // get all unclaimed outputs for that addr $list = \DB::DAO('Money_Bitcoin_Block_Tx_Out')->search(array('Addr' => $addr['hash'], 'Claimed' => 'N')); $final = array(); foreach($list as $bean) $final[] = $bean->getProperties(); return $final; } public static function claimPrivateSha256($wallet, $priv, $desc = null) { return self::claimPrivate($wallet, \Util\Bitcoin::hash_sha256($priv), $desc); } public static function claimWalletFile($wallet, $data, $desc = null) { $keys = \Util\Bitcoin::scanWalletFile($data); if (!$keys) return array(); $res = array(); foreach($keys as $key) { $tmp = self::claimPrivate($wallet, $key, $desc); if (!$tmp) continue; $res[] = $tmp; } return $res; } public static function claimPrivate($wallet, $priv, $desc = null) { // get all the funds sent to that private addr and record it for future deposits if (strlen($priv) != 32) throw new \Exception('The private key must be 32 bytes'); // check if privkey is within range $pk_num = gmp_init(bin2hex($priv), 16); if (gmp_cmp($pk_num, '0') <= 0) return false; if (gmp_cmp($pk_num, gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 16)) >= 0) return false; $pub = \Util\Bitcoin::decodePrivkey($priv); $addr = \Util\Bitcoin::encode($pub); $outs = \Money\Bitcoin::getAddressOutputs($pub); $find = \DB::DAO('Money_Bitcoin_Permanent_Address')->searchOne(array('Money_Bitcoin_Permanent_Address__' => $pub['hash'])); if ($find) { if (!is_null($find->Private_Key)) return false; // already got this one $find->Private_Key = \Internal\Crypt::encrypt($priv); $find->Redirect = 'wallet'; $find->Used = 'Y'; $find->commit(); $wallet = \User\Wallet::byId($find->User_Wallet__); } else { $insert = array( 'Money_Bitcoin_Permanent_Address__' => $pub['hash'], 'Money_Bitcoin_Host__' => null, 'Private_Key' => \Internal\Crypt::encrypt($priv), 'Description' => $desc, 'Redirect' => 'nulladdr', 'Used' => 'Y', ); if (!is_null($wallet)) { $insert['User_Wallet__'] = $wallet->getId(); $insert['Redirect'] = 'wallet'; } \DB::DAO('Money_Bitcoin_Permanent_Address')->insert($insert); } $total = 0; if ($outs) { if (is_null($wallet)) { $out = self::getNullAddr(); } else { $out = self::getVerboseAddr($wallet, $desc); } $outpub = \Util\Bitcoin::decode($out); $input = array(); foreach($outs as $t) { $input[] = array('amount' => $t['Value'], 'tx' => $t['Hash'], 'N' => $t['N'], 'privkey' => $priv, 'hash' => $pub['hash']); $total += $t['Value']; } $tx = \Util\Bitcoin::makeNormalTx($input, $total, $outpub, $outpub); self::publishTransaction($tx); } return array('amount' => \Internal\Price::spawnInt($total, 'BTC'), 'address' => $addr); } public static function makeNormalTx($input, $amount, $final_output, $remainder, $fee = 0) { // make a normal tx, merge inputs if preferable $res = array(); while(count($input) > 5) { // merge some inputs $xinput = array(); $output = self::getNullAddr(true); // merge as many inputs as we can in a single tx while(true) { $extra = array_shift($input); if (is_null($extra)) break; $tinput = $xinput; $tinput[] = $extra; $total = 0; foreach($tinput as $t) $total+=$t['amount']; $ttx = \Util\Bitcoin::makeNormalTx($tinput, $total, $output['info'], $output['info']); if (strlen($ttx) >= 1000) break; $xinput[] = $extra; } if (!is_null($extra)) array_unshift($input, $extra); $total = 0; foreach($xinput as $t) $total += $t['amount']; $ttx = \Util\Bitcoin::makeNormalTx($xinput, $total, $output['info'], $output['info']); $res[] = $ttx; $thash = bin2hex(strrev(\Util\Bitcoin::hash_sha256(\Util\Bitcoin::hash_sha256($ttx)))); $input[] = array( 'amount' => $total, 'tx' => $thash, 'N' => 0, 'privkey' => $output['priv'], 'hash' => $output['info']['hash'], ); \DB::DAO('Money_Bitcoin_Available_Output')->insert(array('Money_Bitcoin_Available_Output__' => \System::uuid(), 'Money_Bitcoin_Permanent_Address__' => $output['info']['hash'], 'Value' => $total, 'Hash' => $thash, 'N' => 0, 'Available' => 'N')); } // do the final tx $res[] = \Util\Bitcoin::makeNormalTx($input, $amount, $final_output, $remainder, $fee); return $res; } public static function publishTransaction($txs) { // generate tx id if (!is_array($txs)) $txs = array($txs); foreach($txs as $tx) { $txid = bin2hex(strrev(\Util\Bitcoin::hash_sha256(\Util\Bitcoin::hash_sha256($tx)))); $insert = array( 'Hash' => $txid, 'Blob' => base64_encode($tx), 'Created' => \DB::i()->now(), ); \DB::DAO('Money_Bitcoin_Pending_Tx')->insert($insert); self::$pending[$txid] = $tx; } return $txid; } public static function broadcastPublished() { if (!self::$pending) return; \Controller::MQ('RabbitMQ')->invoke('Money/Bitcoin::broadcastPublished', ['txs' => self::$pending]); self::$pending = []; } public static function _MQ_broadcastPublished($info) { $list = $info['txs']; $node = new \Money\Bitcoin\Node(self::BITCOIN_NODE); foreach($list as $tx) { $node->pushTx($tx); } $node->getAddr(); // force sync } public static function broadcastTransactions() { $list = \DB::DAO('Money_Bitcoin_Pending_Tx')->search(array(new \DB\Expr('`Last_Broadcast` < DATE_SUB(NOW(), INTERVAL 30 MINUTE)')), ['Last_Broadcast' => 'ASC'], array(100)); if (!$list) return; // $ip = gethostbyname('relay.eligius.st'); $ip = gethostbyname('mtgox.relay.eligius.st'); $node = new \Money\Bitcoin\Node(self::BITCOIN_NODE); $peer = \Controller::Driver('Bitcoin', 'b54f4d35-dd1c-43aa-9096-88e37a83bda3'); $el_todo = array(); foreach($list as $bean) { // check if successful $success = \DB::DAO('Money_Bitcoin_Block_Tx')->searchOne(array('Hash' => $bean->Hash)); if ($success) { $bean->delete(); continue; } $bean->Last_Broadcast = \DB::i()->now(); if ((\DB::i()->dateRead($bean->Created) < (time()-7000)) && ($bean->Eligius == 'N')) { try { if (!$el_node) $el_node = new \Money\Bitcoin\Node($ip); $el_node->pushTx(base64_decode($bean->Blob)); $bean->Eligius = 'P'; } catch(\Exception $e) { // too bad } } elseif ($bean->Eligius == 'P') { $bean->Eligius = 'Y'; $el_todo[] = $bean->Hash; } try { $bean->Last_Result = $peer->sendRawTransaction(bin2hex(base64_decode($bean->Blob))); } catch(\Exception $e) { $bean->Last_Result = $e->getMessage(); } $bean->commit(); $node->pushTx(base64_decode($bean->Blob)); } $node->getAddr(); // force sync reply from bitcoin daemon so we know the stuff went through if ($el_node) $el_node->getAddr(); if ($el_todo) { $ssh = new \Network\SSH($ip); if (!$ssh->authKeyUuid('freetxn', '14a70b11-5f36-4890-82ca-5de820882c7f')) { mail('mark@tibanne.com,luke+eligius@dashjr.org', 'SSH connection to freetxn@'.$ip.' failed', 'Used ssh key 14a70b11-5f36-4890-82ca-5de820882c7f, but couldn\'t login to push those txs:'."\n".implode("\n", $el_todo)); return; // failed } foreach($el_todo as $tx) { $channel = $ssh->channel(); $channel->exec($tx); $channel->wait(); } } } /** * Returns the total amount of bitcoins in the world based on that last block generated * * @return int The total amount of bitcoins */ public static function getTotalCount() { // get total count of BTC in the world based on latest block # $last_block = \DB::DAO('Money_Bitcoin_Block')->searchOne(null, ['Depth'=>'DESC']); $current = $last_block->Depth; // this is a chunk of blocks, bitcoins generated per chunk start at 50 and halve every chunks $block_size = 210000; // first compute the total amount of bitcoins for the chunks that are fully done $full_block_count = floor($current / $block_size); $full_block_coeff = (1 - pow(0.5, $full_block_count)) * 100; // those are the bitcoins on the full block chunks $total_bitcoins = $full_block_coeff * $block_size; // then for the last chunk $last_block_coeff = pow(0.5, $full_block_count + 1) * 100; $total_bitcoins += $last_block_coeff * ($current - ($full_block_count * $block_size)); return $total_bitcoins; } public static function _Route_bitcoind($path) { $post = file_get_contents('php://input'); $post = json_decode($post, true); if (!$post) return; $method = $post['method']; $params = $post['params']; $id = $post['id']?:\System::uuid(); try { throw new \Exception('Meh: '.$method); die(json_encode(array('result' => $res, 'id' => $id))); } catch(\Exception $e) { die(json_encode(array('error' => $e->getMessage(), 'id' => $id))); } } public static function _Route_handleTx() { // posted by halfnode with a TX $tx_bin = pack('H*', $_POST['tx']); $tx = \Util\Bitcoin::parseTx($tx_bin); if (!$tx) die('BAD TX'); $hash = $tx['hash']; $dao = \DB::DAO('Money_Bitcoin_Tx'); if (isset($dao[$hash])) die('DUP'); if (\DB::DAO('Money_Bitcoin_Block_Tx')->countByField(array('Hash' => $hash))) die('DUP(blockchain)'); $insert = array( 'Money_Bitcoin_Tx__' => $hash, 'Data' => base64_encode($tx_bin), 'Size' => strlen($tx_bin), ); $dao->insert($insert); foreach($tx['in'] as $i => $txin) { \DB::DAO('Money_Bitcoin_Tx_In')->insert(array( 'Hash' => $hash, 'N' => $i, 'Prev_Out_Hash' => $txin['prev_out']['hash'], 'Prev_Out_N' => $txin['prev_out']['n'], 'scriptSig' => $txin['scriptSig'], 'Addr' => $txin['addr'], )); } foreach($tx['out'] as $i => $txout) { \DB::DAO('Money_Bitcoin_Tx_Out')->insert(array( 'Hash' => $hash, 'N' => $i, 'Value' => $txout['value_int'], 'scriptPubKey' => $txout['scriptPubKey'], 'Addr' => $txout['addr'], )); // check if one of our addrs $info = \DB::DAO('Money_Bitcoin_Permanent_Address')->searchOne(array('Money_Bitcoin_Permanent_Address__' => $txout['addr'])); if (($info) && (!is_null($info->Callback))) { $cb = explode('::', str_replace('/', '\\', $info->Callback)); call_user_func($cb, $info, $hash.':'.$i, null, \Internal\Price::spawnInt($txout['value_int'],'BTC')); } if (($info) && (!is_null($info->Ipn))) { $post = array( 'description' => $info->Description, 'tx' => $hash.':'.$i, 'status' => 'published', 'amount_int' => $txout['value_int'], 'item' => 'BTC', 'addr' => \Util\Bitcoin::encode(array('version' => 0, 'hash' => $info->Money_Bitcoin_Permanent_Address__)), ); \Scheduler::oneshotUrl($info->Ipn, $post, null, null, null, $info->User_Rest__); } // REDIRECT CODE 2 if (($info) && (!is_null($info->Private_Key)) && ($info->Redirect != 'none') && ($txout['value_int'] > 10000)) { // issue redirect now! switch($info->Redirect) { case 'wallet': $wallet = \User\Wallet::byId($info->User_Wallet__); $target = self::getVerboseAddr($wallet, $info->Description); break; case 'fixed': $target = $info->Redirect_Value; break; case 'nulladdr': $target = self::getNullAddr(); break; } $pub = \Util\Bitcoin::decode($target); $tx = \Util\Bitcoin::makeNormalTx(array(array('amount' => $txout['value_int'], 'tx' => $hash, 'N' => $i, 'privkey' => \Internal\Crypt::decrypt($info->Private_Key), 'hash' => $txout['addr'])), $txout['value_int'], $pub, $pub); self::publishTransaction($tx); // self::broadcastPublished(); } } die('OK'); } public static function getTablesStruct() { return array( 'Money_Bitcoin_Host' => array( 'Money_Bitcoin_Host__' => 'UUID', 'Name' => array('type' => 'VARCHAR', 'size' => 16, 'null' => false), 'IP' => array('type' => 'VARCHAR', 'size' => 39, 'null' => false, 'key' => 'UNIQUE:IP'), 'Address' => array('type' => 'VARCHAR', 'size' => 35, 'null' => true), 'Version' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'Coins' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), /* stored in smallest unit of coin */ 'Connections' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'Blocks' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), 'Hashes_Per_Sec' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), 'Status' => array('type' => 'ENUM', 'values' => array('up','down'), 'default' => 'down'), 'Last_Update' => array('type' => 'DATETIME', 'null' => true), 'Keep_Empty' => array('type' => 'ENUM', 'values' => array('Y','N','E'), 'default' => 'N'), /* if set, any money on there will be sent somewhere else. E=exclude */ 'Allow_Order' => array('type' => 'ENUM', 'values' => array('Y','N'), 'default' => 'Y'), /* should we use this node for incoming payments? */ 'Generate' => array('type' => 'ENUM', 'values' => array('Y','N'), 'default' => 'Y'), 'Stamp' => array('type' => 'TIMESTAMP', 'null' => false), ), 'Money_Bitcoin_Tx' => array( 'Money_Bitcoin_Tx__' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'PRIMARY'), 'Data' => array('type' => 'LONGTEXT', 'null' => false), 'Network' => array('type' => 'VARCHAR', 'size' => 32, 'default' => 'bitcoin', 'key' => 'Network'), 'Size' => array('type' => 'INT', 'unsigned' => true, 'size' => 10, 'null' => false), 'Stamp' => array('type' => 'TIMESTAMP', 'null' => false), ), 'Money_Bitcoin_Tx_In' => array( 'Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'UNIQUE:Key'), 'N' => array('type' => 'INT', 'size' => 10, 'unsigned' => true, 'key' => 'UNIQUE:Key'), 'Prev_Out_Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false), 'Prev_Out_N' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'CoinBase' => array('type' => 'TEXT', 'null' => true), 'scriptSig' => array('type' => 'TEXT', 'null' => true), 'Addr' => array('type' => 'CHAR', 'size' => 40, 'null' => true, 'key' => 'Addr'), '_keys' => array( 'Prev_Out' => array('Prev_Out_Hash','Prev_Out_N'), ), ), 'Money_Bitcoin_Tx_Out' => array( 'Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'UNIQUE:Key'), 'N' => array('type' => 'INT', 'size' => 10, 'unsigned' => true, 'key' => 'UNIQUE:Key'), 'Value' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true, 'null' => false), 'scriptPubKey' => array('type' => 'TEXT'), 'Addr' => array('type' => 'CHAR', 'size' => 40, 'null' => true, 'key' => 'Addr'), ), 'Money_Bitcoin_Permanent_Address' => array( 'Money_Bitcoin_Permanent_Address__' => array('type' => 'CHAR', 'size' => 40, 'key' => 'PRIMARY'), 'Money_Bitcoin_Host__' => 'UUID/N', 'User_Wallet__' => 'UUID/N', 'User_Rest__' => 'UUID/N', 'Money_Merchant_Transaction_Payment__' => 'UUID/N', 'Private_Key' => array('type' => 'VARCHAR', 'size' => 255, 'null' => true), 'Redirect' => array('type' => 'ENUM', 'values' => array('wallet','fixed','nulladdr','none'), 'default' => 'none'), // wallet => redirect to new addr on same wallet 'Redirect_Value' => array('type' => 'VARCHAR', 'size' => 35, 'null' => true), 'Description' => array('type' => 'VARCHAR', 'size' => 255, 'null' => true), 'Ipn' => array('type' => 'VARCHAR', 'size' => 255, 'null' => true), 'Callback' => array('type' => 'VARCHAR', 'size' => 255, 'null' => true), 'Used' => array('type' => 'ENUM', 'values' => array('Y','N'), 'default' => 'N'), 'Created' => array('type' => 'DATETIME', 'null' => false), 'Stamp' => array('type' => 'TIMESTAMP', 'null' => false), '_keys' => array( 'Unused_Addr_Key' => array('User_Wallet__','Used'), 'User_Wallet__' => ['User_Wallet__'], ), ), 'Money_Bitcoin_Available_Output' => array( // list available funds 'Money_Bitcoin_Available_Output__' => 'UUID', 'Money_Bitcoin_Permanent_Address__' => array('type' => 'CHAR', 'size' => 40, 'key' => 'Money_Bitcoin_Permanent_Address__'), 'Network' => array('type' => 'VARCHAR', 'size' => 32, 'default' => 'bitcoin', 'key' => 'Network'), 'Value' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true, 'null' => false), 'Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'UNIQUE:Key'), 'N' => array('type' => 'INT', 'size' => 10, 'unsigned' => true, 'key' => 'UNIQUE:Key'), 'Block' => array('type' => 'INT', 'size' => 10, 'unsigned' => true, 'key' => 'Block'), 'Available' => array('type' => 'ENUM', 'values' => array('Y','N'), 'default' => 'Y'), 'Stamp' => array('type' => 'TIMESTAMP', 'null' => false), ), 'Money_Bitcoin_Order' => array( 'Money_Bitcoin_Order__' => 'UUID', 'Order__' => 'UUID', 'Money_Bitcoin_Host__' => 'UUID', 'Address' => array('type' => 'VARCHAR', 'size' => 35, 'null' => true), /* generated only for this order */ 'Coins' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), 'Coins_NC' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), 'Coins_Extra' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), 'Total' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), 'Created' => array('type' => 'DATETIME', 'null' => false), 'Expires' => array('type' => 'DATETIME', 'null' => false), 'Status' => array('type' => 'ENUM', 'values' => array('pending','expired','ok'), 'default' => 'pending'), 'Stamp' => array('type' => 'TIMESTAMP', 'null' => false), '_keys' => array( '@Order__' => array('Order__'), '@Address' => array('Address'), ), ), 'Money_Bitcoin_Wallet' => array( 'Money_Bitcoin_Wallet__' => 'UUID', 'User__' => 'UUID', 'Money_Bitcoin_Host__' => 'UUID', 'Address' => array('type' => 'VARCHAR', 'size' => 35, 'null' => true), 'Coins' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), 'Coins_NC' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true), 'Withdrawn_Coins' => array('type' => 'BIGINT', 'size' => 21, 'unsigned' => false), 'Refresh' => array('type' => 'DATETIME', 'null' => false), 'Stamp' => array('type' => 'TIMESTAMP', 'null' => false), '_keys' => array( '@User__' => array('User__'), '@Address' => array('Address'), ), ), 'Money_Bitcoin_Node' => array( 'Money_Bitcoin_Node__' => NULL, 'IP' => array('type' => 'VARCHAR', 'size' => 15, 'null' => false, 'key' => 'UNIQUE:Unique_Host'), 'Port' => array('type' => 'INT', 'size' => 5, 'unsigned' => true, 'key' => 'UNIQUE:Unique_Host'), 'Version' => array('type' => 'INT', 'unsigned' => true, 'size' => 10), 'User_Agent' => array('type' => 'VARCHAR', 'size' => 256, 'null' => true), 'Status' => array('type' => 'ENUM', 'values' => array('up','down','unknown'), 'default' => 'unknown'), 'Addresses' => array('type' => 'INT', 'unsigned' => true, 'size' => 10, 'default' => 0), 'Last_Checked' => array('type' => 'DATETIME'), 'Next_Check' => array('type' => 'DATETIME'), 'First_Seen' => array('type' => 'DATETIME'), 'Last_Seen' => array('type' => 'DATETIME'), 'Last_Down' => array('type' => 'DATETIME', 'null' => true, 'default' => NULL), 'Last_Error' => array('type' => 'VARCHAR', 'size' => 32, 'null' => true), 'Stamp' => array('type' => 'TIMESTAMP', 'null' => false), '_keys' => [ 'Next_Check' => ['Next_Check'], 'Status' => ['Status'], ], ), 'Money_Bitcoin_Pending_Tx' => array( 'Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'PRIMARY'), 'Network' => array('type' => 'VARCHAR', 'size' => 32, 'default' => 'bitcoin', 'key' => 'Network'), 'Blob' => array('type' => 'LONGTEXT'), 'Eligius' => array('type' => 'ENUM', 'values' => array('Y','P','N'), 'default' => 'N'), 'Created' => array('type' => 'DATETIME'), 'Input_Total' => ['type' => 'BIGINT', 'size' => 20, 'unsigned' => true, 'null' => true], 'Last_Broadcast' => array('type' => 'DATETIME'), 'Last_Result' => ['type' => 'VARCHAR', 'size' => 128, 'null' => true], 'Stamp' => array('type' => 'TIMESTAMP', 'null' => false), ), 'Money_Bitcoin_Block' => array( 'Money_Bitcoin_Block__' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'PRIMARY'), 'Parent_Money_Bitcoin_Block__' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'Parent_Money_Bitcoin_Block__'), 'Depth' => array('type' => 'BIGINT', 'size' => 20, 'null' => false, 'key' => 'Depth'), 'Network' => array('type' => 'VARCHAR', 'size' => 32, 'default' => 'bitcoin', 'key' => 'Network'), 'Version' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'Mrkl_Root' => array('type' => 'CHAR', 'size' => 64, 'null' => false), 'Time' => array('type' => 'DATETIME'), 'Bits' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'Nonce' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'Size' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'Status' => array('type' => 'ENUM', 'values' => array('pending','confirmed','dropped'), 'default' => 'confirmed', 'null' => false), ), 'Money_Bitcoin_Process_Tx_Out' => [ 'Money_Bitcoin_Process_Tx_Out__' => 'UUID', 'Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'UNIQUE:Key'), 'N' => array('type' => 'INT', 'size' => 10, 'unsigned' => true, 'key' => 'UNIQUE:Key'), 'Value' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true, 'null' => false), 'scriptPubKey' => array('type' => 'TEXT'), 'Addr' => array('type' => 'CHAR', 'size' => 40, 'null' => false, 'key' => 'Addr'), 'Trigger' => array('type' => 'ENUM', 'values' => array('new','executed','nil'), 'default' => 'new'), '_keys' => array( 'Trigger' => array('Trigger'), ), ], 'Money_Bitcoin_Block_Tx' => array( 'Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'UNIQUE:Hash'), 'Block' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'Block'), 'Version' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'Lock_Time' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'Size' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), ), /* 'Money_Bitcoin_Block_Tx_In' => array( 'Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'UNIQUE:Key'), 'N' => array('type' => 'INT', 'size' => 10, 'unsigned' => true, 'key' => 'UNIQUE:Key'), 'Prev_Out_Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false), 'Prev_Out_N' => array('type' => 'INT', 'size' => 10, 'unsigned' => true), 'CoinBase' => array('type' => 'TEXT', 'null' => true), 'scriptSig' => array('type' => 'TEXT', 'null' => true), 'Addr' => array('type' => 'CHAR', 'size' => 40, 'null' => true, 'key' => 'Addr'), '_keys' => array( 'Prev_Out' => array('Prev_Out_Hash','Prev_Out_N'), ), ),*/ 'Money_Bitcoin_Block_Tx_Out' => array( 'Hash' => array('type' => 'CHAR', 'size' => 64, 'null' => false, 'key' => 'UNIQUE:Key'), 'N' => array('type' => 'INT', 'size' => 10, 'unsigned' => true, 'key' => 'UNIQUE:Key'), 'Value' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true, 'null' => false), 'scriptPubKey' => array('type' => 'TEXT'), 'Addr' => array('type' => 'CHAR', 'size' => 40, 'null' => false, 'key' => 'Addr'), 'Claimed' => array('type' => 'ENUM', 'values' => array('Y','N'), 'default' => 'N'), 'Trigger' => array('type' => 'ENUM', 'values' => array('new','executed','nil'), 'default' => 'new'), '_keys' => array( 'Trigger' => array('Trigger'), ), ), /* 'Money_Bitcoin_Block_Addr' => array( 'Addr' => array('type' => 'CHAR', 'size' => 40, 'null' => false, 'key' => 'PRIMARY'), 'Network' => array('type' => 'VARCHAR', 'size' => 32, 'default' => 'bitcoin', 'key' => 'Network'), 'Pubkey' => array('type' => 'CHAR', 'size' => 130, 'null' => true), 'Balance' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true, 'null' => false), 'Watch' => array('type' => 'VARCHAR', 'size' => 128, 'null' => true, 'default' => NULL, 'key' => 'Watch'), 'Taint' => array('type' => 'BIGINT', 'size' => 20, 'unsigned' => true, 'null' => true, 'default' => NULL), 'Clean' => array('type' => 'VARCHAR', 'size' => 64, 'null' => true, 'default' => NULL, 'key' => 'Clean'), ), */ 'Money_Bitcoin_Vanity' => array( 'Money_Bitcoin_Vanity__' => array('type' => 'VARCHAR', 'size' => 35, 'null' => false, 'key' => 'PRIMARY'), 'Private_Key' => array('type' => 'VARCHAR', 'size' => 255, 'null' => false), ), ); } } \Scheduler::schedule('MoneyBitcoinUpdate', '10min', 'Money/Bitcoin::update'); \Scheduler::schedule('MoneyBitcoinCheckOrders', '5min', 'Money/Bitcoin::checkOrders'); \Scheduler::schedule('MoneyBitcoinGetRate', array('daily', '5i'), 'Money/Bitcoin::getRate'); \Scheduler::schedule('MoneyBitcoinCheckNodes', '10min', 'Money/Bitcoin::checkNodes'); \Scheduler::schedule('MoneyBitcoinImportBlocks', '1min', 'Money/Bitcoin::importBlocks'); \Scheduler::schedule('MoneyBitcoinAddrTriggers', '1min', 'Money/Bitcoin::runAddrTriggers'); \Scheduler::schedule('MoneyBitcoinBroadcastTxs', '1min', 'Money/Bitcoin::broadcastTransactions'); \Scheduler::schedule('MoneyBitcoinMergeSmallOutputs', '10min', 'Money/Bitcoin::mergeSmallOutputs'); \Scheduler::schedule('MoneyBitcoinSplitBigOutputs', '10min', 'Money/Bitcoin::splitBigOutputs'); \DB::i()->validateStruct(Bitcoin::getTablesStruct());