diff --git a/src/BlockchainApi/AbstractBlockchainApi.php b/src/BlockchainApi/AbstractBlockchainApi.php index 846f846..8938f06 100644 --- a/src/BlockchainApi/AbstractBlockchainApi.php +++ b/src/BlockchainApi/AbstractBlockchainApi.php @@ -46,6 +46,9 @@ public static function getInstance(string $className, string $blockchainApiUrl = case 'BitcoinComRestApi': self::$instance = new BitcoinComRestApi($blockchainApiUrl); return self::$instance; + case 'BchdProtoGatewayApi': + self::$instance = new BchdProtoGatewayApi($blockchainApiUrl); + return self::$instance; case 'SlpDbApi': self::$instance = new SlpDbApi($blockchainApiUrl); return self::$instance; diff --git a/src/BlockchainApi/Http/AbstractHttpAgent.php b/src/BlockchainApi/Http/AbstractHttpAgent.php index 64cf8f5..3c7fc2a 100644 --- a/src/BlockchainApi/Http/AbstractHttpAgent.php +++ b/src/BlockchainApi/Http/AbstractHttpAgent.php @@ -38,6 +38,15 @@ public function __construct(callable $loggerFn = null, array $options = array()) */ public abstract function get(string $url, array $options = array()); + /** + * Perform a HTTP POST request with Content-Type "application/json". + * @param string $url The full URL string (including possible query params). + * @param array $data The post data as string key-value pairs (not encoded). + * @param array $options additional options (depending on the specific HTTP implementation) valid: timeout|userAgent|maxRedirects + * @return string|bool The response body or false on failure. + */ + public abstract function post(string $url, array $data = array(), array $options = array()); + protected function logError(string $subject, $error, $data = null): void { if (static::$loggerFn !== null) call_user_func(static::$loggerFn, $subject, $error, $data); diff --git a/src/BlockchainApi/Http/BasicHttpAgent.php b/src/BlockchainApi/Http/BasicHttpAgent.php index 3f7c721..23ab140 100644 --- a/src/BlockchainApi/Http/BasicHttpAgent.php +++ b/src/BlockchainApi/Http/BasicHttpAgent.php @@ -21,5 +21,25 @@ public function get(string $url, $options = array()) { $contents = file_get_contents($url, 0, $ctx); return $contents; } + + public function post(string $url, array $data = array(), array $options = array()) { + $ctx = stream_context_create(array('http' => + array( + 'method' => 'POST', + //'header' => 'Content-Type: application/x-www-form-urlencoded', // use \r\n to separate headers + //'content' => http_build_query($data), + 'header' => implode("\r\n", array( + 'accept: application/json', + 'Content-Type: application/json' + )), + 'content' => json_encode($data), + 'timeout' => isset($options['timeout']) ? $options['timeout'] : $this->timeoutSec, + 'user_agent' => isset($options['userAgent']) ? $options['userAgent'] : $this->userAgent, + 'max_redirects' => isset($options['maxRedirects']) ? $options['maxRedirects'] : $this->maxRedirects + )) + ); + $contents = file_get_contents($url, 0, $ctx); + return $contents; + } } ?> \ No newline at end of file diff --git a/src/BlockchainApi/Http/CurlHttpAgent.php b/src/BlockchainApi/Http/CurlHttpAgent.php index 337ef06..c140bc0 100644 --- a/src/BlockchainApi/Http/CurlHttpAgent.php +++ b/src/BlockchainApi/Http/CurlHttpAgent.php @@ -41,5 +41,44 @@ public function get(string $url, $options = array()) { curl_close($ch); return $output; } + + public function post(string $url, array $data = array(), array $options = array()) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_AUTOREFERER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, isset($options['maxRedirects']) ? $options['timeout'] : $this->timeoutSec); + curl_setopt($ch, CURLOPT_USERAGENT, isset($options['userAgent']) ? $options['userAgent'] : $this->userAgent); + $maxRedirects = isset($options['maxRedirects']) ? $options['maxRedirects'] : $this->maxRedirects; + if ($maxRedirects > 0) { + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirects); + } + /* + if ($skip_certificate_check) { + curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, false); + } + */ + //curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + "cache-control: no-cache", + //"content-type: application/x-www-form-urlencoded" + 'Content-Type: application/json', + 'accept: application/json' + )); + $output = curl_exec($ch); + if (curl_errno($ch)) { + $output = false; + $error = "URL: $url Curl error: " . curl_error($ch); + $this->logError("cURL error posting to page", $error); + curl_close($ch); + return false; + } + curl_close($ch); + return $output; + } } ?> \ No newline at end of file diff --git a/src/BlockchainApi/Http/WordpressHttpAgent.php b/src/BlockchainApi/Http/WordpressHttpAgent.php index fb41c58..11d96a3 100644 --- a/src/BlockchainApi/Http/WordpressHttpAgent.php +++ b/src/BlockchainApi/Http/WordpressHttpAgent.php @@ -25,6 +25,24 @@ public function get(string $url, array $options = array()) { return $body; } + public function post(string $url, array $data = array(), array $options = array()) { + $wpOptions = $this->getHttpOptions($options); + $wpOptions['headers']['Content-Type'] = 'application/json'; + $wpOptions['body'] = json_encode($data); + $response = wp_remote_post($url, $wpOptions); + if ($response instanceof \WP_Error) { + $this->logError("Error on HTTP POST $url", $response->get_error_messages()); + return false; + } + $responseCode = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body($response); + if ($responseCode !== 200) { + $this->logError("Invalid HTTP response code $responseCode on GET: $url", $body); + return false; + } + return $body; + } + protected function getHttpOptions(array $options = array()) { return array( 'timeout' => isset($options['timeout']) ? $options['timeout'] : $this->timeoutSec, //seconds diff --git a/src/BlockchainApi/Structs/BchAddress.php b/src/BlockchainApi/Structs/BchAddress.php index 4f37e2e..3c59154 100644 --- a/src/BlockchainApi/Structs/BchAddress.php +++ b/src/BlockchainApi/Structs/BchAddress.php @@ -37,6 +37,8 @@ class BchAddress { // TODO add paging using currentPage and pagesTotal for big addresses public function __construct(string $cashAddress, string $legacyAddress = '', string $slpAddress = '') { + if (substr($cashAddress, 0, 12) !== 'bitcoincash:') + $cashAddress = 'bitcoincash:' . $cashAddress; $this->cashAddress = $cashAddress; $this->legacyAddress = $legacyAddress; $this->slpAddress = $slpAddress; diff --git a/src/BlockchainApi/Structs/SlpTokenAddress.php b/src/BlockchainApi/Structs/SlpTokenAddress.php index 2c239e7..570e9c7 100644 --- a/src/BlockchainApi/Structs/SlpTokenAddress.php +++ b/src/BlockchainApi/Structs/SlpTokenAddress.php @@ -8,6 +8,10 @@ class SlpTokenAddress extends SlpToken { /** @var string */ public $slpAddress = ''; + /** @var float */ + public $balance = 0.0; + /** @var int */ + public $balanceSat = 0; /** * An indexed array with strings of BCH TXIDs @@ -17,9 +21,19 @@ class SlpTokenAddress extends SlpToken { public function __construct(string $slpAddress = '') { parent::__construct(); + if (substr($slpAddress, 0, 13) !== 'simpleledger:') + $slpAddress = 'simpleledger:' . $slpAddress; $this->slpAddress = $slpAddress; } + public static function withToken(SlpToken $token, string $slpAddress = ''): SlpTokenAddress { + $address = new static($slpAddress); + foreach ($token as $prop => $value) { + $address->$prop = $value; + } + return $address; + } + public static function fromAddressJson(array $jsonArr, SlpTokenAddress $instance = null, string $tokenID = ""): SlpTokenAddress { if ($instance === null) $instance = new SlpTokenAddress(); diff --git a/src/CashP.php b/src/CashP.php index f244515..b7bb62c 100644 --- a/src/CashP.php +++ b/src/CashP.php @@ -69,10 +69,14 @@ public function getBlockchain(): AbstractBlockchainApi { return $this->blockchainApi; } - public function toSatoshis(float $bch): float { + public static function toSatoshis(float $bch): float { return floor($bch * 100000000); } + public static function fromSatoshis(float $sats): float { + return $sats / 100000000.0; + } + /** * Generate a QR code for a payment. * @param string $fileLocal A path on your local filesystem to store the QR code file. This should be accessible from the web if you want @@ -145,7 +149,7 @@ public function getBadgerButton(array $btnConf, string $address, float $amountBC 'address' => $address, 'amountBCH' => $amountBCH, 'tokenAmount' => $amountToken, - 'sats' => $this->toSatoshis($amountBCH), + 'sats' => static::toSatoshis($amountBCH), 'tokenID' => $tokenID, 'useTokenPayments' => $useTokenPayments, 'buttonLibSrc' => CashP::BADGER_LIB_URL, @@ -199,5 +203,33 @@ public function isValidSlpAddress(string $slpAddress): bool { return false; return preg_match("/^[a-z0-9]+$/", $addressParts[1]) === 1; } + + /** + * Convert a hex string to base64. + * @param string $hex + * @return string + */ + public static function hexToBase64(string $hex): string { + $return = ''; + $split = str_split($hex, 2); + foreach($split as $pair) { + $return .= chr(hexdec($pair)); + } + return base64_encode($return); + } + + /** + * Reverse bytes in a hex string (to deal with endian-ness). + * @param string $hex + * @return string + */ + public static function reverseBytes(string $hex): string { + $return = ''; + $split = str_split($hex, 2); + for($i = count($split)-1; $i >= 0; $i--) { + $return .= $split[$i]; + } + return $return; + } } ?> \ No newline at end of file diff --git a/src/CashpOptions.php b/src/CashpOptions.php index f342ee8..4db66b9 100644 --- a/src/CashpOptions.php +++ b/src/CashpOptions.php @@ -47,7 +47,7 @@ class CashpOptions { //public $hdPathFormat = "0/%d"; /** - * The REST API backend implementation to use. Allowed values: BitcoinComRestApi|SlpDbApi + * The REST API backend implementation to use. Allowed values: BitcoinComRestApi|BchdProtoGatewayApi|SlpDbApi * @var string */ public $blockchainApiImplementation = "BitcoinComRestApi"; diff --git a/tests/RestBackendTest.php b/tests/RestBackendTest.php index eac0945..5938c35 100644 --- a/tests/RestBackendTest.php +++ b/tests/RestBackendTest.php @@ -5,17 +5,18 @@ use PHPUnit\Framework\TestCase; use Ekliptor\CashP\CashP; use Ekliptor\CashP\BlockchainApi\Structs\BchAddress; +use Ekliptor\CashP\CashpOptions; final class RestBackendTest extends TestCase { public function testCurrencyRateFetch(): void { - $cashp = new CashP(); + $cashp = $this->getCashpForTesting(); $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 testTokenBalance(): void { - $cashp = new CashP(); + $cashp = $this->getCashpForTesting(); $tokenID = "7278363093d3b899e0e1286ff681bf50d7ddc3c2a68565df743d0efc54c0e7fd"; $address = "simpleledger:qrg3pzge6lhy90p4semx2a60w6624nudagqzycecg4"; $tokenBalance = $cashp->getBlockchain()->getAddressTokenBalance($address, $tokenID); @@ -23,10 +24,16 @@ public function testTokenBalance(): void { } public function testAddressCreation(): void { - $cashp = new CashP(); + $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 = 'BitcoinComRestApi'; + return new CashP($opts); + } } ?> \ No newline at end of file diff --git a/tests/SlpDbTest.php b/tests/SlpDbTest.php index 5b90698..a6072c0 100644 --- a/tests/SlpDbTest.php +++ b/tests/SlpDbTest.php @@ -7,7 +7,7 @@ use Ekliptor\CashP\BlockchainApi\Structs\BchAddress; use Ekliptor\CashP\CashpOptions; -final class RestBackendTest extends TestCase { +final class SlpDbTestTest extends TestCase { public function testGetTokenInfo(): void { $cashp = $this->getSlpDbCashP(); $tokenInfo = $cashp->getBlockchain()->getTokenInfo('c4b0d62156b3fa5c8f3436079b5394f7edc1bef5dc1cd2f9d0c4d46f82cca479');