-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Ekliptor
authored and
Ekliptor
committed
Aug 27, 2020
1 parent
7caf245
commit d8b6bbd
Showing
2 changed files
with
275 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
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); | ||
} | ||
} | ||
?> |
This file contains 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
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); | ||
} | ||
} | ||
?> |