Last active
November 24, 2017 19:06
-
-
Save SteenSchutt/64575181c2a395b936e0 to your computer and use it in GitHub Desktop.
PHP class for interaction with bitcoin-abe.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Usage: $abe = new ABE(15, "73", "mysql:host=localhost;dbname=abe", "sqluser", "sqlpass"); | |
// Replace 15 with ABE's internal chain ID and "73" with the address version for your coin. | |
// Base conversion class used for base58 encoding/decoding (https://gist.github.com/DoctorBlue/e50cb9c93e2fa3d66741) | |
require __DIR__ . "/baseconv.php"; | |
class ABE { | |
private $chain; // The chain identifier given by ABE (Check 'chain' table in database) | |
private $addrver; // Address version byte (Hexadecimal) | |
private $db; // Database handle assigned by constructor | |
// Assign properties and connect to database | |
public function __construct($chain, $addrver, $dsn, $dbuser, $dbpass){ | |
$this->chain = $chain; | |
$this->addrver = $addrver; | |
$this->db = new PDO($dsn, $dbuser, $dbpass); | |
} | |
// General function for searching when the term type is unknown | |
// Returns the partial URL for the search result, e.g. "address/oYMpwbDq9QLi2W55tUhzWFwQEKaeHaW55e" | |
// or false if no result was found | |
public function search($term){ | |
// Lightest function, doesn't require any lookups as opposed to the others | |
if($this->isAddress($term)) | |
return "address/$term"; | |
if($this->isBlock($term)) | |
return "block/$term"; | |
if($this->isTransaction($term)) | |
return "transaction/$term"; | |
return false; | |
} | |
// Check whether a term is a base58 address by base58 decoding the term and checking its length | |
public function isAddress($term){ | |
// Omnicoin addresses are always 34 characters long in base58 | |
if(strlen($term) === 34){ | |
// Catch exception thrown if input is not base58 | |
try{ | |
$addrhex = baseconv::base58_hex($term); | |
}catch(Exception $e){ | |
return false; | |
} | |
// Addresses are always 25 bytes (50 hex characters) | |
if(strlen($addrhex) === 50){ | |
return true; | |
} | |
} | |
return false; | |
} | |
// Check whether a term is a block hash or height (performance warning: requires lookup) | |
// NB: Does not check whether the block exists, use getBlock() | |
public function isBlock($term){ | |
return ($this->isBlockHash($term) || $this->isBlockHeight($term)); | |
} | |
// Determine whether the given term is a block hash (light I/O) | |
public function isBlockHash($term) { | |
if(strlen($term) === 64){ | |
// Query for a block with the given hash (Only needed because the hash could also be a transaction) | |
$q = "SELECT block_hash FROM block WHERE block_hash = UNHEX(?) LIMIT 1"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($term)); | |
if($st->fetchColumn() !== false) | |
return true; | |
} | |
} | |
// Determine whether the given term is a block height (no I/O) | |
public function isBlockHeight($term) { | |
if(is_numeric($term)){ | |
return true; | |
} | |
return false; | |
} | |
// Check if a term is a transaction hash (light I/O) | |
public function isTransaction($term){ | |
// All tx hashes are 64 characters | |
if(strlen($term) === 64){ | |
$q = "SELECT tx_hash FROM tx WHERE tx_hash = UNHEX(?)"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($term)); | |
if($st->fetchColumn() !== false) | |
return true; | |
} | |
return false; | |
} | |
// Returns an integer, indicating the height of the highest block (light I/O) | |
public function getNumBlocks(){ | |
$q = "SELECT block.block_height | |
FROM chain | |
JOIN chain_candidate cc ON cc.block_id = chain_last_block_id | |
JOIN block ON block.block_id = chain_last_block_id | |
WHERE chain.chain_id = ? | |
AND cc.in_longest = 1"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($this->chain)); | |
return $st->fetchColumn(); | |
} | |
// Checks if the given term is a height or hash and uses the corresponding method to fetch the block | |
public function getBlock($term) { | |
if($this->isBlockHash($term)) { | |
return $this->getBlockByHash($term); | |
} elseif ($this->isBlockHeight($term)) { | |
return $this->getBlockByHeight($term); | |
} else { | |
return null; | |
} | |
} | |
// Takes a block hash and returns a block (array) containing the following: | |
// height, hash, time, output, difficulty, total amount mined, average age, | |
// chain age, %CoinDD, satoshi-seconds and total satoshi-seconds | |
public function getBlockByHash($hash) { | |
$q = "SELECT | |
b.block_id AS id, | |
HEX(b.block_hash) AS hash, | |
HEX(b.block_hashMerkleRoot) AS merkle, | |
b.block_height AS height, | |
b.block_nTime AS time, | |
b.block_num_tx AS num_tx, | |
b.block_nBits AS bits, | |
b.block_value_out AS output, | |
b.block_total_seconds AS total_secs, | |
b.block_satoshi_seconds AS satoshi_secs, | |
b.block_total_satoshis AS total_satoshis, | |
b.block_ss_destroyed AS satoshi_secs_destroyed, | |
b.block_total_ss AS total_satoshi_secs | |
FROM block b | |
JOIN chain_candidate cc ON cc.block_id = b.block_id | |
WHERE cc.chain_id = ? | |
AND cc.in_longest = 1 | |
AND b.block_hash = UNHEX(?) | |
LIMIT 1"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($this->chain, $hash)); | |
$block = $st->fetch(); | |
$block['time'] = date("Y-m-d H:i:s", $block['time']); // Convert timestamp to readable format | |
$block['output'] = $block['output'] / pow(10, 8); // Convert OMC-satoshi to OMC | |
$block['difficulty'] = round($this->calculateDifficulty($block['bits']),4); // Calculate difficulty from nBits | |
$block['total_coins'] = $block['total_satoshis'] / pow(10, 8); // Convert satoshi to OMC again | |
$block['avg_age'] = round($block['satoshi_secs'] / $block['total_satoshis'] / 86400, 4); // Calculate average | |
$block['chain_age'] = round($block['total_secs'] / 86400, 4); // Convert seconds to days | |
$block['pct_days_destroyed'] = round(($block['total_secs'] == 0 ? 0 : (100 - (100 * $block['satoshi_secs'] / $block['total_satoshi_secs']))),4); // Calculate amount of days destroyed | |
return $block; | |
} | |
// Takes a block height and returns a block (array) containing the following: | |
// height, hash, time, output, difficulty, total amount mined, average age, | |
// chain age, %CoinDD, satoshi-seconds and total satoshi-seconds | |
public function getBlockByHeight($height) { | |
return $this->getBlocksByHeight($height, $height)[0]; | |
} | |
// Returns an array with the following information about blocks in the specified range: | |
// height, hash, time, output, difficulty, total amount mined, average age, | |
// chain age, %CoinDD, satoshi-seconds and total satoshi-seconds | |
public function getBlocksByHeight($fromHeight, $toHeight){ | |
$q = "SELECT | |
b.block_id AS id, | |
HEX(b.block_hash) AS hash, | |
HEX(b.block_hashMerkleRoot) AS merkle, | |
b.block_height AS height, | |
b.block_nTime AS time, | |
b.block_num_tx AS num_tx, | |
b.block_nBits AS bits, | |
b.block_value_out AS output, | |
b.block_total_seconds AS total_secs, | |
b.block_satoshi_seconds AS satoshi_secs, | |
b.block_total_satoshis AS total_satoshis, | |
b.block_ss_destroyed AS satoshi_secs_destroyed, | |
b.block_total_ss AS total_satoshi_secs | |
FROM block b | |
JOIN chain_candidate cc ON (b.block_id = cc.block_id) | |
WHERE cc.chain_id = ? | |
AND cc.block_height BETWEEN ? AND ? | |
AND cc.in_longest = 1 | |
ORDER BY cc.block_height DESC"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($this->chain, $fromHeight, $toHeight)); | |
$blocks = $st->fetchAll(); | |
foreach($blocks as $key => $block){ | |
$blocks[$key]['time'] = date("Y-m-d H:i:s", $block['time']); // Convert timestamp to readable format | |
$blocks[$key]['output'] = $block['output'] / pow(10, 8); // Convert OMC-satoshi to OMC | |
$blocks[$key]['difficulty'] = round($this->calculateDifficulty($block['bits']),4); // Calculate difficulty from nBits | |
$blocks[$key]['total_coins'] = $block['total_satoshis'] / pow(10, 8); // Convert satoshi to OMC again | |
$blocks[$key]['avg_age'] = round($block['satoshi_secs'] / $block['total_satoshis'] / 86400, 4); // Calculate average | |
$blocks[$key]['chain_age'] = round($block['total_secs'] / 86400, 4); // Convert seconds to days | |
$blocks[$key]['pct_days_destroyed'] = round(($block['total_secs'] == 0 ? 0 : (100 - (100 * $block['satoshi_secs'] / $block['total_satoshi_secs']))),4); // Calculate amount of days destroyed | |
} | |
return $blocks; | |
} | |
// Fetches transactions based on the given block id | |
public function getTransactionsByBlock($block) { | |
$q = "SELECT | |
tx.tx_id AS id, | |
HEX(tx.tx_hash) AS hash, | |
tx.tx_size AS size, | |
SUM(txout.txout_value) AS amount, | |
block.block_height AS height, | |
block.block_nTime AS time | |
FROM block | |
JOIN block_tx ON block_tx.block_id = block.block_id | |
JOIN tx ON tx.tx_id = block_tx.tx_id | |
JOIN txout ON txout.tx_id = tx.tx_id | |
JOIN chain_candidate cc ON cc.block_id = block_tx.block_id | |
WHERE cc.chain_id = ? | |
AND cc.in_longest = 1 | |
AND block.block_id = ? | |
GROUP BY tx.tx_id | |
ORDER BY tx.tx_id DESC"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($this->chain, $block)); | |
return $st->fetchAll(); | |
} | |
// Get all transactions sent from or to a given address | |
public function getTransactionsByAddress($address){ | |
$pubkeyhash = $this->addressToPubkeyHash($address); | |
// Fetch all transactions | |
$q = "SELECT | |
tx_hash AS hash, | |
block_height AS height, | |
block_hash AS block, | |
SUM(amount) AS amount, | |
confirmations, | |
time | |
FROM | |
(SELECT | |
HEX(tx.tx_hash) AS tx_hash, | |
block.block_height, | |
HEX(block.block_hash) AS block_hash, | |
SUM(txout.txout_value) AS amount, | |
(SELECT | |
block.block_height | |
FROM | |
chain | |
JOIN chain_candidate cc ON cc.block_id = chain_last_block_id | |
JOIN block ON block.block_id = chain_last_block_id | |
WHERE | |
chain.chain_id = ?) - block.block_height + 1 AS confirmations, | |
block.block_nTime AS time | |
FROM | |
tx | |
JOIN `txout` ON txout.tx_id = tx.tx_id | |
JOIN `pubkey` ON pubkey.pubkey_id = txout.pubkey_id | |
JOIN `block_tx` ON block_tx.tx_id = txout.tx_id | |
JOIN `block` ON block.block_id = block_tx.block_id | |
JOIN `chain_candidate` cc ON cc.block_id = block_tx.block_id | |
WHERE | |
pubkey_hash = UNHEX(?) | |
AND cc.chain_id = ? | |
AND cc.in_longest = 1 | |
GROUP BY tx_hash UNION SELECT | |
HEX(tx_hash) AS tx_hash, | |
block.block_height, | |
HEX(block.block_hash) AS block_hash, | |
SUM(txout_value * - 1) AS amount, | |
(SELECT | |
block.block_height | |
FROM | |
chain | |
JOIN chain_candidate cc ON cc.block_id = chain_last_block_id | |
JOIN block ON block.block_id = chain_last_block_id | |
WHERE | |
chain.chain_id = ?) - block.block_height + 1 AS confirmations, | |
block.block_nTime AS time | |
FROM | |
tx | |
JOIN txin ON txin.tx_id = tx.tx_id | |
JOIN txout ON txin.txout_id = txout.txout_id | |
JOIN pubkey ON pubkey.pubkey_id = txout.pubkey_id | |
JOIN block_tx ON block_tx.tx_id = tx.tx_id | |
JOIN block ON block.block_id = block_tx.block_id | |
JOIN chain_candidate cc ON cc.block_id = block.block_id | |
WHERE | |
pubkey_hash = UNHEX(?) | |
AND cc.chain_id = ? | |
AND cc.in_longest = 1 | |
GROUP BY tx_hash) transactions | |
GROUP BY tx_hash | |
ORDER BY time ASC"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($this->chain, $pubkeyhash, $this->chain, $this->chain, $pubkeyhash, $this->chain)); | |
$txs = $st->fetchAll(); | |
$runbalance = 0; | |
foreach($txs as &$tx) { | |
$runbalance += $tx['amount']; | |
$tx['balance'] = $runbalance; | |
} | |
return $txs; | |
} | |
// Get a transaction by its hash | |
public function getTransaction($hash){ | |
$q = "SELECT | |
HEX(tx.tx_hash) AS hash, | |
tx.tx_size AS size, | |
HEX(block.block_hash) AS block, | |
block.block_height AS height, | |
block.block_nTime AS time | |
FROM | |
tx | |
JOIN block_tx ON block_tx.tx_id = tx.tx_id | |
JOIN block ON block.block_id = block_tx.block_id | |
JOIN chain_candidate ON block.block_id = chain_candidate.block_id | |
WHERE | |
chain_candidate.chain_id = ? | |
AND chain_candidate.in_longest = 1 | |
AND tx_hash = UNHEX(?);"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($this->chain, $hash)); | |
$transaction = $st->fetch(); | |
$transaction['inputs'] = $this->getTransactionInputs($hash); | |
$transaction['outputs'] = $this->getTransactionOutputs($hash); | |
return $transaction; | |
} | |
// Get all inputs for a transaction | |
public function getTransactionInputs($hash){ | |
$q = "SELECT | |
txout_value AS amount, | |
HEX(pubkey_hash) AS pubkey_hash | |
FROM | |
tx | |
JOIN txin ON txin.tx_id = tx.tx_id | |
JOIN txout ON txin.txout_id = txout.txout_id | |
JOIN pubkey ON pubkey.pubkey_id = txout.pubkey_id | |
JOIN block_tx ON block_tx.tx_id = tx.tx_id | |
JOIN block ON block.block_id = block_tx.block_id | |
JOIN chain_candidate cc ON block.block_id = cc.block_id | |
WHERE | |
cc.chain_id = ? | |
AND cc.in_longest = 1 | |
AND tx_hash = UNHEX(?) | |
ORDER BY txin.txin_pos ASC"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($this->chain, $hash)); | |
$inputs = $st->fetchAll(); | |
foreach($inputs as $key => $input){ | |
$inputs[$key]['address'] = $this->pubkeyHashToAddress($input['pubkey_hash']); | |
} | |
return $inputs; | |
} | |
// Get all outputs for a transaction | |
public function getTransactionOutputs($hash){ | |
$q = "SELECT | |
txout_value AS amount, | |
HEX(pubkey_hash) AS pubkey_hash | |
FROM | |
tx | |
JOIN txout ON tx.tx_id = txout.tx_id | |
JOIN pubkey ON pubkey.pubkey_id = txout.pubkey_id | |
JOIN block_tx ON block_tx.tx_id = tx.tx_id | |
JOIN block ON block.block_id = block_tx.block_id | |
JOIN chain_candidate cc ON block.block_id = cc.block_id | |
WHERE | |
cc.chain_id = ? | |
AND cc.in_longest = 1 | |
AND tx_hash = UNHEX(?) | |
ORDER BY txout.txout_pos ASC"; | |
$st = $this->db->prepare($q); | |
$st->execute(array($this->chain, $hash)); | |
$outputs = $st->fetchAll(); | |
foreach($outputs as $key => $output){ | |
$outputs[$key]['address'] = $this->pubkeyHashToAddress($output['pubkey_hash']); | |
} | |
return $outputs; | |
} | |
// Returns an address for the given public key | |
public function pubkeyHashToAddress($pubkey){ | |
$pubkey = $this->addrver . $pubkey; // Prepend version byte | |
$pubkeybs = pack("H*", $pubkey); // Pack to raw bytestring | |
$checksum = hash("sha256", hash("sha256", $pubkeybs, true)); // Calculate checksum | |
$checksum = substr($checksum, 0, 8); // Shorten to 4 bytes | |
$pubkey .= $checksum; // Append checksum to pubkey hash | |
return baseconv::hex_base58($pubkey); // Convert to base 58 and return | |
} | |
// Returns the public key hash extracted from the given address | |
public function addressToPubkeyHash($address){ | |
$hexaddr = baseconv::base58_hex($address); // Convert to hexadecimal | |
$unchecked = substr($hexaddr, 2, 40); // Strip version byte + checksum | |
return $unchecked; | |
} | |
// Calculates difficulty using a block target value | |
public function targetToDifficulty($target){ | |
return ((1 << 224) - 1) * 1000 / ($target + 1) / 1000; | |
} | |
// Calculate a target value using the number of bits required | |
public function calculateTarget($nBits){ | |
$shift = 8 * ((($nBits >> 24) & 0xff) - 3); | |
$bits = $nBits & 0x7fffff; | |
$sign = ($nBits & 0x800000 ? -1 : 1); | |
if($shift >= 0) | |
return $sign * ($bits << $shift); | |
return $sign * ($bits >> ($shift * -1)); | |
} | |
// Calculate the block difficulty from the number of bits required | |
public function calculateDifficulty($nBits){ | |
return $this->targetToDifficulty($this->calculateTarget($nBits)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment