Skip to content

Commit

Permalink
added missing files
Browse files Browse the repository at this point in the history
  • Loading branch information
Ekliptor authored and Ekliptor committed Aug 27, 2020
1 parent 7caf245 commit d8b6bbd
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 0 deletions.
197 changes: 197 additions & 0 deletions src/BlockchainApi/BchdProtoGatewayApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<?php
namespace Ekliptor\CashP\BlockchainApi;

use Ekliptor\CashP\CashP;
use Ekliptor\CashP\BlockchainApi\Structs\BchAddress;
use Ekliptor\CashP\BlockchainApi\Structs\SlpToken;
use Ekliptor\CashP\BlockchainApi\Structs\SlpTokenAddress;

class BchdProtoGatewayApi extends AbstractBlockchainApi {

protected function __construct(string $blockchainApiUrl = '') {
parent::__construct($blockchainApiUrl);
if (empty($this->blockchainApiUrl))
$this->blockchainApiUrl = "https://bchd.ny1.simpleledger.io/v1/";
//$this->blockchainApiUrl = "http://localhost:8080/v1/";
}

public function getConfirmationCount(string $transactionID): int {
$txDetails = $this->getTransactionDetails($transactionID);
if (!$txDetails || !isset($txDetails->transaction) || !isset($txDetails->transaction->confirmations))
return -1; // not found
return (int)$txDetails->transaction->confirmations;
}

public function getBlocktime(string $transactionID): int {
$txDetails = $this->getTransactionDetails($transactionID);
if (!$txDetails || !isset($txDetails->transaction) || !isset($txDetails->transaction->timestamp))
return -1; // not found
return (int)$txDetails->transaction->timestamp;
}

public function createNewAddress(string $xPub, int $addressCount, string $hdPathFormat = '0/%d'): ?BchAddress {
// hdPathFormat not needed for BCHD
$url = $this->blockchainApiUrl . 'GetBip44HdAddress';
$data = array(
'xpub' => $xPub,
'change' => true,
'address_index' => $addressCount,
);
$response = $this->httpAgent->post($url, $data);
if ($response === false)
return null;
$jsonRes = json_decode($response);
if (!$jsonRes)
return null;
else if (isset($jsonRes->error) && $jsonRes->error) {
$this->logError("Error creating new address", $jsonRes->error);
return null;
}
return new BchAddress($jsonRes->cash_addr, '', $jsonRes->slp_addr); // TODO remove legacy addresses?
}

public function getTokenInfo(string $tokenID): ?SlpToken {
$tokenIDBase64 = static::ensureBase64Encoding($tokenID, false);
$url = $this->blockchainApiUrl . 'GetTokenMetadata';
$data = array(
'token_ids' => array($tokenIDBase64),
);
$response = $this->httpAgent->post($url, $data);
if ($response === false)
return null;
$jsonRes = json_decode($response);
if (!$jsonRes || empty($jsonRes->token_metadata))
return null;
$token = new SlpToken();
$token->id = $tokenID;
$this->addTokenMetadata($token, $jsonRes->token_metadata);
return $token;
}

public function getAddressBalance(string $address): float {
$bchAddress = $this->getAddressDetails($address);
if ($bchAddress === null || !isset($bchAddress->balance))
return -1.0;
return $bchAddress->balance;
}

public function getAddressTokenBalance(string $address, string $tokenID): float {
$slpAddress = $this->getSlpAddressDetails($address, $tokenID);
if ($slpAddress === null || !isset($slpAddress->balance))
return -1.0;
return $slpAddress->balance;
}

public function getAddressDetails(string $address): ?BchAddress {
$url = $this->blockchainApiUrl . 'GetAddressUnspentOutputs';
$data = array(
'address' => $address,
'include_mempool' => true, // TODO add parameter to decide if we want confirmed balance only (needed in all subclasses)
'include_token_metadata' => false,
);
$response = $this->httpAgent->post($url, $data);
if ($response === false)
return null;
$jsonRes = json_decode($response);
if ($jsonRes === null) // empty object if address is unknown = valid response
return null;
else if (isset($jsonRes->error) && $jsonRes->error) {
$this->logError("Error on receiving BCH address details", $jsonRes->error);
return null;
}
$bchAddress = new BchAddress($address, '', '');
if (!isset($jsonRes->outputs) || empty($jsonRes->outputs)) // grpc proxy returns empty array []
return $bchAddress;

foreach ($jsonRes->outputs as $output) {
$bchAddress->balanceSat += (int)$output->value;
}
$bchAddress->balance = CashP::fromSatoshis($bchAddress->balanceSat);
return $bchAddress;
}

public function getSlpAddressDetails(string $address, string $tokenID): ?SlpTokenAddress {
$url = $this->blockchainApiUrl . 'GetAddressUnspentOutputs';
$data = array(
'address' => $address,
'include_mempool' => true,
'include_token_metadata' => true,
);
$response = $this->httpAgent->post($url, $data);
if ($response === false)
return null;
$jsonRes = json_decode($response);
if ($jsonRes === null) // empty object if address is unknown = valid response
return null;
else if (isset($jsonRes->error) && $jsonRes->error) {
$this->logError("Error on receiving SLP address details", $jsonRes->error);
return null;
}
else if (!isset($jsonRes->token_metadata) || empty($jsonRes->token_metadata)) {
$this->logError("Missing token metadata on SLP address details", $jsonRes);
return null;
}

$token = new SlpToken();
$token->id = $tokenID;
$this->addTokenMetadata($token, $jsonRes->token_metadata);
$slpAddress = SlpTokenAddress::withToken($token, $address);
if (!isset($jsonRes->outputs) || empty($jsonRes->outputs)) // grpc proxy returns empty array []
return $slpAddress;

foreach ($jsonRes->outputs as $output) {
$slpAddress->balanceSat += (int)$output->value;
}
$slpAddress->balance = CashP::fromSatoshis($slpAddress->balanceSat);

return $slpAddress;
}

protected function getTransactionDetails(string $transactionID): ?\stdClass {
$transactionID = static::ensureBase64Encoding($transactionID);
if (isset($this->transactionCache[$transactionID]))
return $this->transactionCache[$transactionID];

$url = $this->blockchainApiUrl . 'GetTransaction';
$data = array(
'hash' => $transactionID,
'include_token_metadata' => true
);
$response = $this->httpAgent->post($url, $data);
if ($response === false)
return null;
$jsonRes = json_decode($response);
if ($jsonRes)
$this->transactionCache[$transactionID] = $jsonRes;
// {"error":"transaction not found","code":5,"message":"transaction not found"}
return $jsonRes;
}

protected function addTokenMetadata(SlpToken $token, array $bchdTokenMetadata): void {
foreach ($bchdTokenMetadata as $meta) {
$id = bin2hex(base64_decode($meta->token_id));
if ($id !== $token->id)
continue;
$type = $meta->token_type;
$typeKey = "type$type";
$typeInfo = $meta->$typeKey;
$token->symbol = base64_decode($typeInfo->token_ticker);
$token->name = base64_decode($typeInfo->token_name);
if (isset($typeInfo->decimals)) // not present with all tokens
$token->decimals = $typeInfo->decimals;
return;
}
$this->logError("Unable to find desired token metadata", $bchdTokenMetadata);
}

protected static function ensureBase64Encoding(string $hash, bool $reverseBytes = true): string {
if (preg_match("/^[0-9a-f]+$/i", $hash) !== 1)
return $hash;
// BCHD wants TX hashes in reverse order as shown on block explorer
if ($reverseBytes === true)
$hash = CashP::reverseBytes($hash);
// the protobuf gateway expects all byte slices in base64 encoding
return CashP::hexToBase64($hash);
}
}
?>
78 changes: 78 additions & 0 deletions tests/BchdBackendTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Ekliptor\CashP\Tests;

use PHPUnit\Framework\TestCase;
use Ekliptor\CashP\CashP;
use Ekliptor\CashP\BlockchainApi\Structs\BchAddress;
use Ekliptor\CashP\BlockchainApi\Http\BasicHttpAgent;
use Ekliptor\CashP\BlockchainApi\Http\CurlHttpAgent;
use Ekliptor\CashP\BlockchainApi\BchdProtoGatewayApi;
use Ekliptor\CashP\CashpOptions;

final class RestBackendTest extends TestCase {
public function testCurrencyRateFetch(): void {
$cashp = $this->getCashpForTesting();
// this uses index-api.bitcoin.com but we keep the test here to ensure it works with BCHD too
$usdRate = $cashp->getRate()->getRate("USD");
$this->assertIsFloat($usdRate, "Returned currency rate is not of type float.");
$this->assertGreaterThan(0.0, $usdRate, "Currency rate can not be negative");
}

public function testConfirmationCount(): void {
$cashp = $this->getCashpForTesting();
$txHash = 'ca87043999ad7c441193ced336577b4ba50fc7a45fbaf6c0bbda825cc42d7fc5';
$tokenBalance = $cashp->getBlockchain()->getConfirmationCount($txHash);
$this->assertGreaterThan(0, $tokenBalance, "Number of confirmations must be greater than 0 for TXID: $txHash");
}

public function testBlocktime(): void {
$cashp = $this->getCashpForTesting();
$txHash = 'ca87043999ad7c441193ced336577b4ba50fc7a45fbaf6c0bbda825cc42d7fc5';
$timestamp = $cashp->getBlockchain()->getBlocktime($txHash);
$this->assertEquals(1593197218, $timestamp, "Block timestamp must be $timestamp for TXID: $txHash");
}

public function testTokenInfo(): void {
$cashp = $this->getCashpForTesting();
$tokenID = '7278363093d3b899e0e1286ff681bf50d7ddc3c2a68565df743d0efc54c0e7fd';
$info = $cashp->getBlockchain()->getTokenInfo($tokenID);
$this->assertIsObject($info, "Token Info must be an object.");
$this->assertEquals("WPT", $info->symbol, "Token Ticker symbol must be 'WPT'");
}

public function testAddressBalance(): void {
$cashp = $this->getCashpForTesting();
$balance = $cashp->getBlockchain()->getAddressBalance('bitcoincash:qz7j7805n9yjdccpz00gq7d70k3h3nef9yj0pwpelz');
$this->assertGreaterThan(0.0, $balance, 'BCH address balance must be greater than 0.');
}

public function testAddressTokenBalance(): void {
$cashp = $this->getCashpForTesting();
$tokenID = '0be40e351ea9249b536ec3d1acd4e082e860ca02ec262777259ffe870d3b5cc3';
$balance = $cashp->getBlockchain()->getAddressTokenBalance('simpleledger:qz7j7805n9yjdccpz00gq7d70k3h3nef9y75245epu', $tokenID);
$this->assertGreaterThan(0.0, $balance, 'SLP address balance must be greater than 0.');
}

public function testGetSlpAddressDetails(): void {
$cashp = $this->getCashpForTesting();
$tokenID = '0be40e351ea9249b536ec3d1acd4e082e860ca02ec262777259ffe870d3b5cc3';
$address = $cashp->getBlockchain()->getSlpAddressDetails('simpleledger:qz7j7805n9yjdccpz00gq7d70k3h3nef9y75245epu', $tokenID);
$this->assertEquals($tokenID, $address->id, "SLP token ID must be: $tokenID");
}

public function testAddressCreation(): void {
$cashp = $this->getCashpForTesting();
$xPub = "xpub6CphSGwqZvKFU9zMfC3qLxxhskBFjNAC9imbSMGXCNVD4DRynJGJCYR63DZe5T4bePEkyRoi9wtZQkmxsNiZfR9D6X3jBxyacHdtRpETDvV";
$address = $cashp->getBlockchain()->createNewAddress($xPub, 3);
$this->assertInstanceOf(BchAddress::class, $address, "BCH address creation failed");
}

protected function getCashpForTesting(): CashP {
$opts = new CashpOptions();
$opts->blockchainApiImplementation = 'BchdProtoGatewayApi';
$opts->httpAgent = new BasicHttpAgent();
return new CashP($opts);
}
}
?>

0 comments on commit d8b6bbd

Please sign in to comment.