sábado, 19 de abril de 2014

Un grupo de inversores solicitan apoyo a acreedores para convencer a un tribunal que reconsidere la quiebra de la plataforma de intercambio

Pretenden rescatar plataforma de bitcoin Mt. Gox

Publicado por Reuters el Viernes 18-04-2014 
ESTADOS UNIDOS.- Un grupo de inversionistas que quiere comprar Mt. Gox ha puesto en marcha un sitio web para ganar apoyos de los acreedores de la quebrada casa de cambios de bitcoins para evitar la liquidación de sus activos.
"Necesitamos tu ayuda para detener la liquidación, que no sería buena ni para los acreedores de Mt. Gox ni para la reputación del bitcóin entre el público y los reguladores", dijeron los inversores en el sitio web. 
Mt. Gox, que fue la plataforma de intercambio de bitcoines más grande del mundo, probablemente será liquidada después de que un tribunal de Tokio desestimase la oferta de la empresa por resucitar su negocio, dijo el miércoles el administrador designado por el tribunal.
"El tribunal de Tokio reconoce que sería difícil para la empresa realizar una rehabilitación y desestimó su solicitud", dijo.
El grupo de inversores, que ofrece tomar los activos de Mt. Gox y revivirlo, ha recibido apoyo de muchos acreedores y espera convencer al tribunal de que reconsidere su decisión, dijo el Wall Street Journal.
Mt. Gox tiene unos 127,000 acreedores.
La empresa pidió protección por bancarrota en febrero en Japón, diciendo que podría haber perdido casi 500 millones de dólares en valor de la divisa digital debido a piratas informáticos.
Mark Karpeles, fundador de Mt. Gox, dijo que no iría a Estados Unidos para responder por el caso, indicaron abogados de Mt. Gox a un juez federal estadounidense. 

miércoles, 5 de marzo de 2014

Código probablemente utilizado en el sistema de MtGox

<?php

namespace Money;

class Bitcoin {
#const BITCOIN_NODE = '173.224.125.222'; // w001.mo.us temporary
const BITCOIN_NODE = '50.97.137.37';
static private $pending = array();

public static function update() {
// update all nodes
$list = \DB::DAO('Money_Bitcoin_Host')->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] <ArtForz> new is major * 10000 + minor * 100 + revision
$rem = floor($v / 100);
$proto = $v - ($rem*100);
$v = $rem;
} else {
// [22:06:05] <ArtForz> 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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?".">\n");
fwrite($out, '<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">'."\n");
fwrite($out, "<Document>\n<name>Bitcoin nodes in the world</name>\n");
// styles
fwrite($out, "<Style id=\"up\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-blank.png</href></Icon></IconStyle></Style>\n");
fwrite($out, "<Style id=\"down\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-blank.png</href></Icon></IconStyle></Style>\n");
fwrite($out, "<Style id=\"unknown\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/paddle/wht-blank.png</href></Icon></IconStyle></Style>\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, "</Folder>\n");
$folder = $row['Status'];
fwrite($out, "<Folder><name>Bitcoin Nodes in status ".$folder."</name>\n");
}
fwrite($out, "<Placemark><name>".$row['IP']."</name><description><![CDATA[<p>IP: ".$row['IP']."</p><p>Version: ".self::parseVersion($row['Version'])."</p>]]></description><styleUrl>#".$folder."</styleUrl>");
fwrite($out, "<Point><coordinates>".$record['longitude'].",".$record['latitude']."</coordinates></Point></Placemark>\n");
}
fwrite($out, "</Folder>\n</Document>\n</kml>\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());

http://pastebin.com/W8B3CGiN