diff --git a/README.md b/README.md index b6b58c8..0083c14 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,24 @@ returns `bool` - True if the address is valid, false otherwise. --- +##### getReturnAddress(Transaction $tx): string +Gets the return BCH address (belonging to the sender senders address) defined +as the last address in transaction outputs. +* `Transaction $tx` - The transaction from getTransaction() call. + +returns `string` - the address + +--- + +##### getReturnSlpAddress(Transaction $tx): string +Gets the return SLP address (belonging to the sender senders address) defined +as the last address in transaction outputs. +* `Transaction $tx` - The transaction from getTransaction() call. + +returns `string` - the address + +--- + #### CashpOptions class A set of advanced config properties. @@ -189,6 +207,14 @@ returns `SlpTokenAddress` - The token or `null` on failure --- +##### getTransaction(string $transactionID): ?Transaction +Returns a transaction with all inputs and outputs including SLP data. +* `string $transactionID` - + +returns `Transaction` - The transaction or `null` on failure + +--- + ## Testing To run unit tests type the following command in the project root directory (requires PHPUnit, installed automatically with Composer): diff --git a/src/BlockchainApi/AbstractBlockchainApi.php b/src/BlockchainApi/AbstractBlockchainApi.php index 8938f06..e0d2933 100644 --- a/src/BlockchainApi/AbstractBlockchainApi.php +++ b/src/BlockchainApi/AbstractBlockchainApi.php @@ -5,6 +5,7 @@ use Ekliptor\CashP\BlockchainApi\Structs\SlpToken; use Ekliptor\CashP\BlockchainApi\Structs\SlpTokenAddress; use Ekliptor\CashP\BlockchainApi\Http\AbstractHttpAgent; +use Ekliptor\CashP\BlockchainApi\Structs\Transaction; class BlockchainException extends \Exception { public function __construct($message = null, $code = null, $previous = null) { @@ -133,6 +134,13 @@ public abstract function getAddressDetails(string $address): ?BchAddress; */ public abstract function getSlpAddressDetails(string $address, string $tokenID): ?SlpTokenAddress; + /** + * Returns a transaction with all inputs and outputs including SLP data. + * @param string $transactionID + * @return Transaction|NULL + */ + public abstract function getTransaction(string $transactionID): ?Transaction; + /** * Creates a new addresses from the xPub repeatedly by incrementing $addressCount in $hdPathFormat until it finds an address with an empty balance (not used by another wallet). * @param string $xPub The extended public key. Called 'Master Public Key' in Electron Cash. diff --git a/src/BlockchainApi/BchdProtoGatewayApi.php b/src/BlockchainApi/BchdProtoGatewayApi.php index 9708102..fd949be 100644 --- a/src/BlockchainApi/BchdProtoGatewayApi.php +++ b/src/BlockchainApi/BchdProtoGatewayApi.php @@ -5,6 +5,11 @@ use Ekliptor\CashP\BlockchainApi\Structs\BchAddress; use Ekliptor\CashP\BlockchainApi\Structs\SlpToken; use Ekliptor\CashP\BlockchainApi\Structs\SlpTokenAddress; +use Ekliptor\CashP\BlockchainApi\Structs\Transaction; +use Ekliptor\CashP\BlockchainApi\Structs\TransactionInput; +use Ekliptor\CashP\BlockchainApi\Structs\TransactionBaseData; +use Ekliptor\CashP\BlockchainApi\Structs\TransactionOutput; +use Ekliptor\CashP\BlockchainApi\Structs\SlpTransactionData; class BchdProtoGatewayApi extends AbstractBlockchainApi { @@ -157,6 +162,39 @@ public function getSlpAddressDetails(string $address, string $tokenID): ?SlpToke return $slpAddress; } + public function getTransaction(string $transactionID): ?Transaction { + $tx = $this->getTransactionDetails($transactionID); + if (empty($tx) || !isset($tx->transaction) || (isset($tx->error) && !empty($tx->error))) + return null; + + $txID = bin2hex(base64_decode($tx->transaction->hash)); + $transaction = new Transaction($txID); + foreach ($tx->transaction->inputs as $in) { + $input = new TransactionInput(); + if (isset($in->index)) // not present on index == 0 + $input->index = $in->index; + $input->value = intval($in->value); + $input->address = 'bitcoincash:' . $in->address; + if (isset($in->slp_token)) + $this->addSlpTransactionData($input, $in->slp_token); + $transaction->inputs[] = $input; + } + foreach ($tx->transaction->outputs as $out) { + $output = new TransactionOutput(); + if (isset($out->index)) // not present on index == 0 + $output->index = $out->index; + if (isset($out->value)) // missing on OP_RETURN outputs (value 0) + $output->value = intval($out->value); + if (isset($out->address)) // missing on OP_RETURN outputs (value 0) + $output->address = 'bitcoincash:' . $out->address; + if (isset($out->slp_token)) + $this->addSlpTransactionData($output, $out->slp_token); + $transaction->outputs[] = $output; + } + + return $transaction; + } + protected function getTransactionDetails(string $transactionID): ?\stdClass { $transactionID = static::ensureBase64Encoding($transactionID); if (isset($this->transactionCache[$transactionID])) @@ -244,6 +282,19 @@ protected function addTokenMetadata(SlpToken $token, array $bchdTokenMetadata): $this->logError("Unable to find desired token metadata", $bchdTokenMetadata); } + protected function addSlpTransactionData(TransactionBaseData $tx, \stdClass $rawSlpData): void { + if (empty($rawSlpData)) + return; + + $tx->slp = new SlpTransactionData(); + $tx->slp->tokenID = bin2hex(base64_decode($rawSlpData->token_id)); + $tx->slp->amount = intval($rawSlpData->amount); + if (isset($rawSlpData->address)) // not present on inputs + $tx->slp->address = 'simpleledger:' . $rawSlpData->address; + if (isset($rawSlpData->decimals)) // not present on inputs + $tx->slp->decimals = intval($rawSlpData->decimals); + } + protected static function ensureBase64Encoding(string $hash, bool $reverseBytes = true): string { if (preg_match("/^[0-9a-f]+$/i", $hash) !== 1) return $hash; diff --git a/src/BlockchainApi/BitcoinComRestApi.php b/src/BlockchainApi/BitcoinComRestApi.php index 4e89716..e4e1442 100644 --- a/src/BlockchainApi/BitcoinComRestApi.php +++ b/src/BlockchainApi/BitcoinComRestApi.php @@ -4,6 +4,7 @@ use Ekliptor\CashP\BlockchainApi\Structs\BchAddress; use Ekliptor\CashP\BlockchainApi\Structs\SlpToken; use Ekliptor\CashP\BlockchainApi\Structs\SlpTokenAddress; +use Ekliptor\CashP\BlockchainApi\Structs\Transaction; class BitcoinComRestApi extends AbstractBlockchainApi { @@ -103,6 +104,10 @@ public function getSlpAddressDetails(string $address, string $tokenID): ?SlpToke return $slpToken; } + public function getTransaction(string $transactionID): ?Transaction { + throw new \Exception("getTransaction() is not yet implemented on " . get_class($this)); // TODO + } + protected function getTransactionDetails(string $transactionID): ?\stdClass { if (isset($this->transactionCache[$transactionID])) return $this->transactionCache[$transactionID]; diff --git a/src/BlockchainApi/Http/BasicHttpAgent.php b/src/BlockchainApi/Http/BasicHttpAgent.php index f5fe957..02d1945 100644 --- a/src/BlockchainApi/Http/BasicHttpAgent.php +++ b/src/BlockchainApi/Http/BasicHttpAgent.php @@ -15,7 +15,12 @@ public function get(string $url, $options = array()) { $ctx = stream_context_create(array('http' => array('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 + 'max_redirects' => isset($options['maxRedirects']) ? $options['maxRedirects'] : $this->maxRedirects, + 'header' => implode("\r\n", array( + 'accept: application/json', + 'Content-Type: application/json', + 'Cache-Control: no-cache,max-age=0' + )), )) ); $contents = file_get_contents($url, 0, $ctx); diff --git a/src/BlockchainApi/Http/CurlHttpAgent.php b/src/BlockchainApi/Http/CurlHttpAgent.php index b1e1d82..272ad9d 100644 --- a/src/BlockchainApi/Http/CurlHttpAgent.php +++ b/src/BlockchainApi/Http/CurlHttpAgent.php @@ -24,6 +24,12 @@ public function get(string $url, $options = array()) { curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirects); } + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + //"content-type: application/x-www-form-urlencoded" + 'Content-Type: application/json', + 'Accept: application/json', + 'Cache-Control: no-cache,max-age=0' + )); /* if ($skip_certificate_check) { curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, false); diff --git a/src/BlockchainApi/Http/WordpressHttpAgent.php b/src/BlockchainApi/Http/WordpressHttpAgent.php index b49d650..1a51746 100644 --- a/src/BlockchainApi/Http/WordpressHttpAgent.php +++ b/src/BlockchainApi/Http/WordpressHttpAgent.php @@ -27,7 +27,6 @@ public function get(string $url, array $options = array()) { 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) { diff --git a/src/BlockchainApi/SlpDbApi.php b/src/BlockchainApi/SlpDbApi.php index 518b21c..b59c7cd 100644 --- a/src/BlockchainApi/SlpDbApi.php +++ b/src/BlockchainApi/SlpDbApi.php @@ -4,6 +4,7 @@ use Ekliptor\CashP\BlockchainApi\Structs\BchAddress; use Ekliptor\CashP\BlockchainApi\Structs\SlpToken; use Ekliptor\CashP\BlockchainApi\Structs\SlpTokenAddress; +use Ekliptor\CashP\BlockchainApi\Structs\Transaction; class SlpDbApi extends AbstractBlockchainApi { /** @var array */ @@ -100,6 +101,10 @@ public function getSlpAddressDetails(string $address, string $tokenID): ?SlpToke return $slpToken; } + public function getTransaction(string $transactionID): ?Transaction { + throw new \Exception("getTransaction() is not yet implemented on " . get_class($this)); // TODO + } + protected function getTokenTransactionIDs(string $address, string $tokenID): array { $transactions = array(); $response = $this->executeQuery('{ diff --git a/src/CashP.php b/src/CashP.php index 3dd33ef..6ed369d 100644 --- a/src/CashP.php +++ b/src/CashP.php @@ -2,6 +2,7 @@ namespace Ekliptor\CashP; use Ekliptor\CashP\BlockchainApi\Http\BasicHttpAgent; +use Ekliptor\CashP\BlockchainApi\Structs\Transaction; use Ekliptor\CashP\BlockchainApi\AbstractBlockchainApi; @@ -204,6 +205,40 @@ public function isValidSlpAddress(string $slpAddress): bool { return preg_match("/^[a-z0-9]+$/", $addressParts[1]) === 1; } + /** + * Gets the return BCH address (belonging to the sender senders address) defined + * as the last address in transaction outputs. + * @param Transaction $tx + * @return string + */ + public function getReturnAddress(Transaction $tx): string { + $len = count($tx->outputs); + for ($i = $len-1; $i >= 0; $i--) { + $out = $tx->outputs[$i]; + if (!empty($out->address)) + return $out->address; + } + return ''; + } + + /** + * Gets the return SLP address (belonging to the sender senders address) defined + * as the last address in transaction outputs. + * @param Transaction $tx + * @return string + */ + public function getReturnSlpAddress(Transaction $tx): string { + $len = count($tx->outputs); + for ($i = $len-1; $i >= 0; $i--) { + $out = $tx->outputs[$i]; + if (empty($out->slp)) + continue; + if (!empty($out->slp->address)) + return $out->slp->address; + } + return ''; + } + /** * Convert a hex string to base64. * @param string $hex diff --git a/tests/BchdBackendTest.php b/tests/BchdBackendTest.php index ffd4046..a920c7f 100644 --- a/tests/BchdBackendTest.php +++ b/tests/BchdBackendTest.php @@ -69,6 +69,30 @@ public function testAddressCreation(): void { $this->assertInstanceOf(BchAddress::class, $address, "BCH address creation failed"); } + public function testGetTransaction(): void { + $cashp = $this->getCashpForTesting(); + $txHash = 'ca87043999ad7c441193ced336577b4ba50fc7a45fbaf6c0bbda825cc42d7fc5'; + $tx = $cashp->getBlockchain()->getTransaction($txHash); + if (count($tx->outputs) !== 2) + $this->fail("expected 2 outputs in TX $txHash"); + $this->assertEquals(5027, $tx->outputs[1]->value, "TX $txHash output has wrong value"); + + $returnAddress = $cashp->getReturnAddress($tx); + $this->assertEquals('bitcoincash:qpwk4x4pz7xd5mxg7w40v95vhjk2qcuawsmusryd7y', $returnAddress, 'wrong BCH return address'); + } + + public function testGetSlpTransaction(): void { + $cashp = $this->getCashpForTesting(); + $txHash = '1407222af22676f9706847b629d26350eb118c8763875a656abbc3f5df786d18'; + $tx = $cashp->getBlockchain()->getTransaction($txHash); + if (count($tx->outputs) !== 4) + $this->fail("expected 4 outputs in TX $txHash"); + $this->assertEquals(806745, $tx->outputs[3]->value, "TX $txHash output has wrong value"); + + $returnAddress = $cashp->getReturnSlpAddress($tx); + $this->assertEquals('simpleledger:qzapwgc088xj9hf8pcsrzsey8j7svcqysyp9ygxmq8', $returnAddress, 'wrong SLP return address'); + } + protected function getCashpForTesting(): CashP { $opts = new CashpOptions(); $opts->blockchainApiImplementation = 'BchdProtoGatewayApi';