From 9e97055bed5669e2b8d51a3736373a9ca19b58a5 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Thu, 19 Apr 2018 17:39:14 +0200 Subject: [PATCH 01/18] gitignore coverage --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d5f17ca..6b84ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock dev/ clover* *.phar +./coverage/ From 4bc784330f68b073f645957f3160ec02c7651492 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 10:39:58 +0200 Subject: [PATCH 02/18] mockery and phpunit bump --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8b60641..4139529 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,8 @@ "spomky-labs/php-aes-gcm": "v1.2.0" }, "require-dev": { - "phpunit/phpunit": "4.3.*", + "phpunit/phpunit": "5.*", + "mockery/mockery": "1.0.*", "squizlabs/php_codesniffer": "2.*", "php-coveralls/php-coveralls": "^1.0" } From 967a026dffb1eb97fcce88a3d0982c3a81ba1b9c Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 10:39:49 +0200 Subject: [PATCH 03/18] travis lite --- .travis.yml | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5a1bc44..9907429 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,7 @@ dist: trusty # fix for travis not updating their HHVM images language: php php: - - 5.6 - - 7.0 - 7.1 - - 7.2 - - hhvm # only build master branch (and PRs) branches: @@ -31,7 +27,6 @@ script: fi - sh -c 'if [ "${PHPCS}" = "true" ]; then export RUN_PHPUNIT=false; fi' - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit -c ./phpunit.xml; fi' - - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit -c ./phpunit-recovery.xml; fi'; # run phpcs when enabled - sh -c 'if [ "${PHPCS}" = "true" ]; then ./vendor/bin/phpcs --standard=./phpcs.xml -n ./src/ && echo "PHPCS OK"; fi' @@ -46,9 +41,6 @@ script: fi fi - # check coverage if report was generated (requires xdebug enabled) - - sh -c 'if [ -e clover.xml ]; then php coverage-checker.php clover.xml 70; fi' - after_script: - | if [ "${WITH_XDEBUG}" = "true" ]; then @@ -72,17 +64,4 @@ env: # BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT - secure: "hJDN/XyeCJVi86R08zPN/xpI/7U+SU6tWmfnVp4wF6cyZmdDY6UztSa5FaS/RRh0mF2M66PoW00VxyoZ7dj0avZrZWwILYdBpwiIWCg2Ay7rkBGbE/kiWuMfOAVpf8x3zO/ArunJC7spiWW+amTFxk0sutkz84bLxyQahsPmVgE=" matrix: - - PHPCS=false WITH_XDEBUG=false # default disable phpcs run and disable xdebug - -matrix: - exclude: - # exclude the php7.0 job without xdebug so we can add one with xdebug - - php: 7.0 - env: PHPCS=false WITH_XDEBUG=false - include: - # php7.0 run with xdebug for code coverage (slooooow) - - php: 7.0 - env: PHPCS=false WITH_XDEBUG=true - # php5.6 run with phpcs (won't run tests, only phpcs) - - php: 7.0 - env: PHPCS=true WITH_XDEBUG=false + - PHPCS=false WITH_XDEBUG=true # default disable phpcs run and disable xdebug From 19a39ed799ef1dce7a4fb4b836bb02e6025ed746 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 10:40:46 +0200 Subject: [PATCH 04/18] phpunit --- phpunit-recovery.xml | 2 +- phpunit.xml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/phpunit-recovery.xml b/phpunit-recovery.xml index 558b4d9..c5704a1 100644 --- a/phpunit-recovery.xml +++ b/phpunit-recovery.xml @@ -10,7 +10,7 @@ > - ./tests/WalletRecoveryTest.php + ./tests/IntegrationTests/WalletRecoveryTest.php diff --git a/phpunit.xml b/phpunit.xml index 884bb23..aa78671 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,7 +12,10 @@ ./tests/ ./tests/V3Crypt/ - ./tests/WalletRecoveryTest.php + ./tests/Wallet/ + ./tests/Network/ + ./tests/IntegrationTests/ + ./tests/IntegrationTests/WalletRecoveryTest.php From e5577c17a5eea7bb8e373eeeb610c71b4ff8315e Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 10:52:48 +0200 Subject: [PATCH 05/18] stop on failure --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9907429..7d641cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ script: export RUN_PHPUNIT="true"; fi - sh -c 'if [ "${PHPCS}" = "true" ]; then export RUN_PHPUNIT=false; fi' - - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit -c ./phpunit.xml; fi' + - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit --stop-on-failure -c ./phpunit.xml; fi' # run phpcs when enabled - sh -c 'if [ "${PHPCS}" = "true" ]; then ./vendor/bin/phpcs --standard=./phpcs.xml -n ./src/ && echo "PHPCS OK"; fi' From 62f3fc94bc79e9ad62989e3e00f5de9efac901f5 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 10:39:31 +0200 Subject: [PATCH 06/18] WIP zzzz --- src/BlocktrailSDK.php | 118 +- src/BlocktrailSDKInterface.php | 4 +- src/Wallet.php | 2 +- src/WalletScript.php | 5 +- tests/BitcoinCashAddressTest.php | 116 - tests/BlocktrailSDKTest.php | 479 ++-- .../DataAPIIntegrationTest.php | 232 ++ .../IntegrationTestBase.php} | 13 +- .../WalletRecoveryTest.php | 6 +- tests/{ => IntegrationTests}/WebhookTest.php | 14 +- tests/MiscTest.php | 68 + tests/MockBlocktrailSDK.php | 52 + tests/OutputsNormalizerTest.php | 2 +- tests/SizeEstimationTest.php | 12 +- tests/UtilTest.php | 2 +- tests/Wallet/BitcoinCashAddressTest.php | 127 ++ tests/Wallet/BuildTxTest.php | 682 ++++++ tests/Wallet/CreateWalletTest.php | 383 ++++ tests/Wallet/GetNewAddressPairTest.php | 175 ++ tests/Wallet/InitWalletCheckBackupKeyTest.php | 53 + tests/Wallet/InitWalletTest.php | 175 ++ tests/Wallet/SendTxTest.php | 386 ++++ tests/Wallet/UpgradeKeyIndexWalletTest.php | 52 + tests/{ => Wallet}/WalletPathTest.php | 2 +- tests/Wallet/WalletTestBase.php | 326 +++ tests/Wallet/WalletTxListTest.php | 32 + tests/Wallet/WalletWebhookTest.php | 79 + tests/WalletTest.php | 1918 ----------------- 28 files changed, 3219 insertions(+), 2296 deletions(-) delete mode 100644 tests/BitcoinCashAddressTest.php create mode 100644 tests/IntegrationTests/DataAPIIntegrationTest.php rename tests/{BlocktrailTestCase.php => IntegrationTests/IntegrationTestBase.php} (88%) rename tests/{ => IntegrationTests}/WalletRecoveryTest.php (98%) rename tests/{ => IntegrationTests}/WebhookTest.php (93%) create mode 100644 tests/MiscTest.php create mode 100644 tests/MockBlocktrailSDK.php create mode 100644 tests/Wallet/BitcoinCashAddressTest.php create mode 100644 tests/Wallet/BuildTxTest.php create mode 100644 tests/Wallet/CreateWalletTest.php create mode 100644 tests/Wallet/GetNewAddressPairTest.php create mode 100644 tests/Wallet/InitWalletCheckBackupKeyTest.php create mode 100644 tests/Wallet/InitWalletTest.php create mode 100644 tests/Wallet/SendTxTest.php create mode 100644 tests/Wallet/UpgradeKeyIndexWalletTest.php rename tests/{ => Wallet}/WalletPathTest.php (97%) create mode 100644 tests/Wallet/WalletTestBase.php create mode 100644 tests/Wallet/WalletTxListTest.php create mode 100644 tests/Wallet/WalletWebhookTest.php delete mode 100644 tests/WalletTest.php diff --git a/src/BlocktrailSDK.php b/src/BlocktrailSDK.php index f15f9bb..cfcd444 100644 --- a/src/BlocktrailSDK.php +++ b/src/BlocktrailSDK.php @@ -660,7 +660,7 @@ protected function createNewWalletV1($options) { } else { // create new primary seed /** @var HierarchicalKey $primaryPrivateKey */ - list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']); + list($primaryMnemonic, , $primaryPrivateKey) = $this->newV1PrimarySeed($options['passphrase']); if ($storePrimaryMnemonic !== false) { $storePrimaryMnemonic = true; } @@ -699,7 +699,7 @@ protected function createNewWalletV1($options) { $backupPublicKey = null; if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) { /** @var HierarchicalKey $backupPrivateKey */ - list($backupMnemonic, , ) = $this->newBackupSeed(); + list($backupMnemonic, , ) = $this->newV1BackupSeed(); } else if (isset($options['backup_mnemonic'])) { $backupMnemonic = $options['backup_mnemonic']; } elseif (isset($options['backup_public_key'])) { @@ -764,11 +764,11 @@ protected function createNewWalletV1($options) { ]; } - public static function randomBits($bits) { - return self::randomBytes($bits / 8); + public function randomBits($bits) { + return $this->randomBytes($bits / 8); } - public static function randomBytes($bytes) { + public function randomBytes($bytes) { return (new Random())->bytes($bytes)->getBinary(); } @@ -798,7 +798,7 @@ protected function createNewWalletV2($options) { $backupSeed = null; if (!isset($options['primary_private_key'])) { - $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256); + $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : $this->newV2PrimarySeed(); } if ($storeDataOnServer) { @@ -807,20 +807,17 @@ protected function createNewWalletV2($options) { throw new \InvalidArgumentException("Can't encrypt data without a passphrase"); } - $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase - $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']); + list($secret, $encryptedSecret) = $this->newV2Secret($options['passphrase']); } else { $secret = $options['secret']; } - $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret); - $recoverySecret = bin2hex(self::randomBits(256)); - - $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret); + $encryptedPrimarySeed = $this->newV2EncryptedPrimarySeed($primarySeed, $secret); + list($recoverySecret, $recoveryEncryptedSecret) = $this->newV2RecoverySecret($secret); } if (!isset($options['backup_public_key'])) { - $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256); + $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : $this->newV2BackupSeed(); } if (isset($options['primary_private_key'])) { @@ -928,7 +925,7 @@ protected function createNewWalletV3($options) { } $primarySeed = $options['primary_seed']; } else { - $primarySeed = new Buffer(self::randomBits(256)); + $primarySeed = $this->newV3PrimarySeed(); } } @@ -938,33 +935,27 @@ protected function createNewWalletV3($options) { throw new \InvalidArgumentException("Can't encrypt data without a passphrase"); } - $secret = new Buffer(self::randomBits(256)); - $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS) - ->getBuffer(); + list($secret, $encryptedSecret) = $this->newV3Secret($options['passphrase']); } else { if (!$options['secret'] instanceof Buffer) { - throw new \RuntimeException('Secret must be provided as a Buffer'); + throw new \InvalidArgumentException('Secret must be provided as a Buffer'); } $secret = $options['secret']; } - $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS) - ->getBuffer(); - $recoverySecret = new Buffer(self::randomBits(256)); - - $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS) - ->getBuffer(); + $encryptedPrimarySeed = $this->newV3EncryptedPrimarySeed($primarySeed, $secret); + list($recoverySecret, $recoveryEncryptedSecret) = $this->newV3RecoverySecret($secret); } if (!isset($options['backup_public_key'])) { if (isset($options['backup_seed'])) { if (!$options['backup_seed'] instanceof Buffer) { - throw new \RuntimeException('Backup seed must be an instance of Buffer'); + throw new \InvalidArgumentException('Backup seed must be an instance of Buffer'); } $backupSeed = $options['backup_seed']; } else { - $backupSeed = new Buffer(self::randomBits(256)); + $backupSeed = $this->newV3BackupSeed(); } } @@ -1041,6 +1032,61 @@ protected function createNewWalletV3($options) { ]; } + public function newV2PrimarySeed() { + return $this->randomBits(256); + } + + public function newV2BackupSeed() { + return $this->randomBits(256); + } + + public function newV2Secret($passphrase) { + $secret = bin2hex($this->randomBits(256)); // string because we use it as passphrase + $encryptedSecret = CryptoJSAES::encrypt($secret, $passphrase); + + return [$secret, $encryptedSecret]; + } + + public function newV2EncryptedPrimarySeed($primarySeed, $secret) { + return CryptoJSAES::encrypt(base64_encode($primarySeed), $secret); + } + + public function newV2RecoverySecret($secret) { + $recoverySecret = bin2hex($this->randomBits(256)); + $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret); + + return [$recoverySecret, $recoveryEncryptedSecret]; + } + + public function newV3PrimarySeed() { + return new Buffer($this->randomBits(256)); + } + + public function newV3BackupSeed() { + return new Buffer($this->randomBits(256)); + } + + public function newV3Secret($passphrase) { + $secret = new Buffer($this->randomBits(256)); + $encryptedSecret = Encryption::encrypt($secret, new Buffer($passphrase), KeyDerivation::DEFAULT_ITERATIONS) + ->getBuffer(); + + return [$secret, $encryptedSecret]; + } + + public function newV3EncryptedPrimarySeed(Buffer $primarySeed, Buffer $secret) { + return Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS) + ->getBuffer(); + } + + public function newV3RecoverySecret(Buffer $secret) { + $recoverySecret = new Buffer($this->randomBits(256)); + $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS) + ->getBuffer(); + + return [$recoverySecret, $recoveryEncryptedSecret]; + } + /** * @param array $bip32Key * @throws BlocktrailSDKException @@ -1241,10 +1287,10 @@ public function initWallet($options) { if (array_key_exists('check_backup_key', $options)) { if (!is_string($options['check_backup_key'])) { - throw new \RuntimeException("check_backup_key should be a string (the xpub)"); + throw new \InvalidArgumentException("check_backup_key should be a string (the xpub)"); } if ($options['check_backup_key'] !== $data['backup_public_key'][0]) { - throw new \RuntimeException("Backup key returned from server didn't match our own"); + throw new \InvalidArgumentException("Backup key returned from server didn't match our own"); } } @@ -1381,7 +1427,7 @@ public function deleteWallet($identifier, $checksumAddress, $signature, $force = * * @return array [mnemonic, seed, key] */ - protected function newBackupSeed() { + protected function newV1BackupSeed() { list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed(""); return [$backupMnemonic, $backupSeed, $backupPrivateKey]; @@ -1397,7 +1443,7 @@ protected function newBackupSeed() { * @return array [mnemonic, seed, key] * @TODO: require a strong password? */ - protected function newPrimarySeed($passphrase) { + protected function newV1PrimarySeed($passphrase) { list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase); return [$primaryMnemonic, $primarySeed, $primaryPrivateKey]; @@ -1438,7 +1484,7 @@ protected function generateNewSeed($passphrase = "", $forceEntropy = null) { * @return string * @throws \Exception */ - protected function generateNewMnemonic($forceEntropy = null) { + public function generateNewMnemonic($forceEntropy = null) { if ($forceEntropy === null) { $random = new Random(); $entropy = $random->bytes(512 / 8); @@ -1524,7 +1570,7 @@ public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee $data['base_transaction'] = $rawTransaction['base_transaction']; $data['signed_transaction'] = $rawTransaction['signed_transaction']; } else { - throw new \RuntimeException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']"); + throw new \InvalidArgumentException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']"); } } else { $data['raw_transaction'] = $rawTransaction; @@ -1592,6 +1638,8 @@ public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZe RestClient::AUTH_HTTP_SIG ); + \var_export(self::jsonDecode($response->body(), true)); + return self::jsonDecode($response->body(), true); } @@ -1823,7 +1871,7 @@ public function verifyMessage($message, $address, $signature) { $adapter = Bitcoin::getEcAdapter(); $addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address); if (!$addr instanceof PayToPubKeyHashAddress) { - throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address'); + throw new \InvalidArgumentException('Can only verify a message with a pay-to-pubkey-hash address'); } /** @var CompactSignatureSerializerInterface $csSerializer */ @@ -1984,4 +2032,8 @@ public static function normalizeBIP32Key($key) { throw new \Exception("Bad Input"); } } + + public function shuffle($arr) { + \shuffle($arr); + } } diff --git a/src/BlocktrailSDKInterface.php b/src/BlocktrailSDKInterface.php index 115f8a3..9190441 100644 --- a/src/BlocktrailSDKInterface.php +++ b/src/BlocktrailSDKInterface.php @@ -9,8 +9,6 @@ */ interface BlocktrailSDKInterface { - public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null); - /** * enable CURL debugging output * @@ -640,4 +638,6 @@ public static function toSatoshi($btc); * @return string[] */ public static function sortMultisigKeys(array $pubKeys); + + public function shuffle($arr); } diff --git a/src/Wallet.php b/src/Wallet.php index 6ec1f5c..3856e65 100644 --- a/src/Wallet.php +++ b/src/Wallet.php @@ -785,7 +785,7 @@ public function buildTx(TransactionBuilder $txBuilder) { // outputs should be randomized to make the change harder to detect if ($txBuilder->shouldRandomizeChangeOuput()) { - shuffle($send); + $this->sdk->shuffle($send); } foreach ($send as $out) { diff --git a/src/WalletScript.php b/src/WalletScript.php index 8c4d6c6..00fcc22 100644 --- a/src/WalletScript.php +++ b/src/WalletScript.php @@ -179,10 +179,11 @@ public function isP2WSH() { } /** + * @param bool $allowNull * @return WitnessScript|null */ - public function getWitnessScript() { - if (null === $this->witnessScript) { + public function getWitnessScript($allowNull = false) { + if (!$allowNull && null === $this->witnessScript) { throw new \RuntimeException("WitnessScript not set"); } return $this->witnessScript; diff --git a/tests/BitcoinCashAddressTest.php b/tests/BitcoinCashAddressTest.php deleted file mode 100644 index 0f2e743..0000000 --- a/tests/BitcoinCashAddressTest.php +++ /dev/null @@ -1,116 +0,0 @@ -assertEquals($address, $reader->fromString($address, $bch)->getAddress($bch)); - $this->assertEquals($address, $reader->fromString($short, $bch)->getAddress($bch)); - - $this->setExpectedException(BlocktrailSDKException::class, "Address not recognized"); - $reader->fromString($short, $tbch); - } - - public function testInitializeWithDefaultFormat() { - $isTestnet = true; - $tbcc = new BitcoinCashTestnet(); - $client = $this->setupBlocktrailSDK("BCC", $isTestnet); - $legacyAddressWallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "password" => "password" - ]); - - $legacyAddress = "2N44ThNe8NXHyv4bsX8AoVCXquBRW94Ls7W"; - $this->assertInstanceOf(BitcoinCashAddressReader::class, $legacyAddressWallet->getAddressReader()); - $this->assertInstanceOf(ScriptHashAddress::class, $legacyAddressWallet->getAddressReader()->fromString($legacyAddress, $tbcc)); - - $cashAddress = "bchtest:ppm2qsznhks23z7629mms6s4cwef74vcwvhanqgjxu"; - $newAddressWallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "password" => "password", - "use_cashaddress" => true, - ]); - - $this->assertInstanceOf(BitcoinCashAddressReader::class, $newAddressWallet->getAddressReader()); - $this->assertInstanceOf(CashAddress::class, $newAddressWallet->getAddressReader()->fromString($cashAddress, $tbcc)); - - $convertedLegacy = $client->getLegacyBitcoinCashAddress($cashAddress); - - $this->assertEquals($legacyAddress, $convertedLegacy); - } - - public function testCurrentDefaultIsOldFormat() { - $isTestnet = true; - $tbcc = new BitcoinCashTestnet(); - - $client = $this->setupBlocktrailSDK("BCC", $isTestnet); - $cashAddrWallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "password" => "password", - "use_cashaddress" => false, - ]); - - $newAddress = $cashAddrWallet->getNewAddress(); - - $reader = $cashAddrWallet->getAddressReader(); - $this->assertInstanceOf(ScriptHashAddress::class, $reader->fromString($newAddress, $tbcc)); - } - - public function testCanOptIntoNewAddressFormat() { - $isTestnet = true; - $tbcc = new BitcoinCashTestnet(); - - $client = $this->setupBlocktrailSDK("BCC", $isTestnet); - $cashAddrWallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "password" => "password", - "use_cashaddress" => true, - ]); - - $newAddress = $cashAddrWallet->getNewAddress(); - - $reader = $cashAddrWallet->getAddressReader(); - $this->assertInstanceOf(CashAddress::class, $reader->fromString($newAddress, $tbcc)); - } - - public function testCanCoinSelectNewCashAddresses() - { - $isTestnet = true; - $tbcc = new BitcoinCashTestnet(); - $client = $this->setupBlocktrailSDK("BCC", $isTestnet); - $cashAddrWallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "password" => "password", - "use_cashaddress" => true, - ]); - - $str = "bchtest:ppm2qsznhks23z7629mms6s4cwef74vcwvhanqgjxu"; - $cashaddr = $cashAddrWallet->getAddressReader()->fromString($str, $tbcc); - - $selection = $cashAddrWallet->coinSelection([ - $cashaddr->getAddress($tbcc) => 1234123, - ], false); - - $this->assertArrayHasKey('utxos', $selection); - $this->assertTrue(count($selection['utxos']) > 0); - } -} diff --git a/tests/BlocktrailSDKTest.php b/tests/BlocktrailSDKTest.php index 0dfedae..04df911 100644 --- a/tests/BlocktrailSDKTest.php +++ b/tests/BlocktrailSDKTest.php @@ -2,22 +2,35 @@ namespace Blocktrail\SDK\Tests; +use Blocktrail\SDK\Backend\ConverterInterface; +use Blocktrail\SDK\Connection\Response; use Blocktrail\SDK\Connection\RestClientInterface; use Blocktrail\SDK\BlocktrailSDK; -use Blocktrail\SDK\Connection\Exceptions\InvalidCredentials; +use Mockery\Mock; -class BlocktrailSDKTest extends BlocktrailTestCase +class BlocktrailSDKTest extends \PHPUnit_Framework_TestCase { - protected static $txExceptFields = [ - '.data.confirmations', '.data.estimated_value', '.data.estimated_change', '.data.estimated_change_address', - '.data.time', '.data.block_time', '.data.block_hash', - '.data.inputs.multisig', '.data.inputs.multisig_addresses', - '.data.outputs.multisig', '.data.outputs.multisig_addresses', '.data.outputs.spent_index', - ]; - - public function testRestClient() { - $client = $this->setupBlocktrailSDK(); - $this->assertTrue($client->getRestClient() instanceof RestClientInterface); + + public function tearDown() { + parent::tearDown(); + + \Mockery::close(); + } + + /** + * @param string $network + * @return MockBlocktrailSDK|Mock + */ + protected function mockSDK($network = 'rBTC') { + $apiKey = getenv('BLOCKTRAIL_SDK_APIKEY') ?: 'EXAMPLE_BLOCKTRAIL_SDK_PHP_APIKEY'; + $apiSecret = getenv('BLOCKTRAIL_SDK_APISECRET') ?: 'EXAMPLE_BLOCKTRAIL_SDK_PHP_APISECRET'; + $testnet = substr($network, 0, 1) === 'r' || substr($network, 0, 1) === 't'; + $apiVersion = 'v1'; + $apiEndpoint = null; + + $client = \Mockery::mock(MockBlocktrailSDK::class, [$apiKey, $apiSecret, $network, $testnet, $apiVersion, $apiEndpoint])->makePartial(); + + return $client; } public function testSatoshiConversion() { @@ -92,230 +105,288 @@ public function testSatoshiConversion() { } public function testAddress() { - $client = $this->setupBlocktrailSDK(); + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('addrresponse')); + + $converter->shouldReceive('getUrlForAddress') + ->withArgs(["3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"]) + ->andReturn("address/3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief") + ->once(); + + $dataClient->shouldReceive('get') + ->withArgs(["address/3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"]) + ->andReturn($res) + ->once(); + + $converter->shouldReceive('convertAddress') + ->withArgs(['addrresponse']) + ->andReturn("addrresult") + ->once(); + + $this->assertEquals("addrresult", $client->address("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief")); + } - //address info - $address = $client->address("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"); - $this->assertEqualsExceptKeys(\json_decode(\file_get_contents(__DIR__ . "/data/address.3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief.json"), true), $address, - []); + public function testAddressTransactions() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('addrtxsresponse')); + + $converter->shouldReceive('getUrlForAddressTransactions') + ->withArgs(["3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"]) + ->andReturn("address/3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief/transactions") + ->once(); + + $converter->shouldReceive('paginationParams') + ->withArgs([['page' => 1, 'limit' => 2, 'sort_dir' => 'asc']]) + ->andReturn("pagination") + ->once(); + + $dataClient->shouldReceive('get') + ->withArgs(["address/3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief/transactions", "pagination"]) + ->andReturn($res) + ->once(); + + $converter->shouldReceive('convertAddressTxs') + ->withArgs(['addrtxsresponse']) + ->andReturn("addrtxsresult") + ->once(); + + $this->assertEquals("addrtxsresult", $client->addressTransactions("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief", 1, 2)); + } + + public function testAddressUnspent() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('addrunspentresponse')); + + $converter->shouldReceive('getUrlForAddressUnspent') + ->withArgs(["3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"]) + ->andReturn("address/3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief/unspent") + ->once(); + + $converter->shouldReceive('paginationParams') + ->withArgs([['page' => 1, 'limit' => 2, 'sort_dir' => 'asc']]) + ->andReturn("pagination") + ->once(); + + $dataClient->shouldReceive('get') + ->withArgs(["address/3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief/unspent", "pagination"]) + ->andReturn($res) + ->once(); + + $converter->shouldReceive('convertAddressUnspentOutputs') + ->withArgs(['addrunspentresponse', "3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"]) + ->andReturn("addrunspentresult") + ->once(); + + $this->assertEquals("addrunspentresult", $client->addressUnspentOutputs("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief", 1, 2)); + } - //address transactions - $response = $client->addressTransactions("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief", $page = 1, $limit = 20); - $expectedResponse = \json_decode(\file_get_contents(__DIR__ . "/data/addressTxs.3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief.json"), true); - self::sortByHash($expectedResponse['data']); - self::sortByHash($response['data']); - $this->assertEqualsExceptKeys($expectedResponse, $response, - self::$txExceptFields); + public function testVerifyAddress() { + $client = $this->mockSDK('BTC'); //address verification $response = $client->verifyAddress("16dwJmR4mX5RguGrocMfN9Q9FR2kZcLw2z", "HPMOHRgPSMKdXrU6AqQs/i9S7alOakkHsJiqLGmInt05Cxj6b/WhS7kJxbIQxKmDW08YKzoFnbVZIoTI2qofEzk="); $this->assertTrue(is_array($response), "Default response is not an array"); $this->assertArrayHasKey('result', $response, "'result' key not in response"); - - //address unconfirmed transactions - $response = $client->addressUnspentOutputs("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"); - $this->assertTrue(is_array($response), "Default response is not an array"); - $this->assertArrayHasKey('total', $response, "'total' key not in response"); - $this->assertArrayHasKey('data', $response, "'data' key not in response"); } - public function testBlock() { - $client = $this->setupBlocktrailSDK(); - - //block info - $response = $client->block("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf"); - $this->assertTrue(is_array($response), "Default response is not an array"); - $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); - $this->assertEquals("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf", $response['hash'], "Block hash returned does not match expected value"); -// file_put_contents(__DIR__ . "/data/block.000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf.json", \json_encode($response)); - $this->assertEqualsExceptKeys(\json_decode(file_get_contents(__DIR__ . "/data/block.000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf.json"), true), $response, - ['confirmations', 'value']); - - //block info by height - $response = $client->block(200000); - $this->assertTrue(is_array($response), "Default response is not an array"); - $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); - $this->assertEquals("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf", $response['hash'], "Block hash returned does not match expected value"); -// file_put_contents(__DIR__ . "/data/block.000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf.json", \json_encode($response)); - $this->assertEqualsExceptKeys(\json_decode(file_get_contents(__DIR__ . "/data/block.000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf.json"), true), $response, - ['confirmations', 'value']); - - //all blocks - $response = $client->allBlocks($page = 2, $limit = 23); - $this->assertTrue(is_array($response), "Default response is not an array"); - $this->assertArrayHasKey('total', $response, "'total' key not in response"); - $this->assertArrayHasKey('data', $response, "'data' key not in response"); - $this->assertEquals(23, count($response['data']), "Count of blocks returned is not equal to 23"); - - $this->assertArrayHasKey('hash', $response['data'][0], "'hash' key not in first block of response"); - $this->assertArrayHasKey('hash', $response['data'][1], "'hash' key not in second block of response"); - - //latest block - $response = $client->blockLatest(); - $this->assertTrue(is_array($response), "Default response is not an array for latest block"); - $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); - } + public function testBlockByHash() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('blockresponse')); - public function testTransaction() { - $client = $this->setupBlocktrailSDK(); + $converter->shouldReceive('getUrlForBlock') + ->withArgs(["000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf"]) + ->andReturn("block/000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf") + ->once(); - $response = $client->transaction("95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615"); + $dataClient->shouldReceive('get') + ->withArgs(["block/000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf"]) + ->andReturn($res) + ->once(); - foreach ($response['outputs'] as &$output) { - if ($output['spent_hash'] === null) { - $output['spent_index'] = 0; - } - } + $converter->shouldReceive('convertBlock') + ->withArgs(['blockresponse']) + ->andReturn("blockresult") + ->once(); - $this->assertEqualsExceptKeys(\json_decode(file_get_contents(__DIR__ . "/data/tx.95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615.json"), true), $response, - ['confirmations', 'value', 'first_seen_at', 'last_seen_at', 'estimated_value', 'estimated_change', 'estimated_change_address', - 'high_priority', 'enough_fee', 'contains_dust', 'double_spend_in']); + $this->assertEquals("blockresult", $client->block("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf")); + } - //coinbase TX - $response = $client->transaction("0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"); - $this->assertTrue(is_array($response), "Default response is not an array"); - $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); - $this->assertArrayHasKey('confirmations', $response, "'confirmations' key not in response"); - $this->assertEquals("0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", $response['hash'], "Transaction hash does not match expected value"); + public function testBlockByHeight() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('blockresponse')); - //random TX 1 - $response = $client->transaction("c791b82ed9af681b73eadb7a05b67294c1c3003e52d01e03775bfb79d4ac58d1"); - $this->assertTrue(is_array($response), "Default response is not an array"); - $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); - $this->assertArrayHasKey('confirmations', $response, "'confirmations' key not in response"); - $this->assertEquals("c791b82ed9af681b73eadb7a05b67294c1c3003e52d01e03775bfb79d4ac58d1", $response['hash'], "Transaction hash does not match expected value"); + $converter->shouldReceive('getUrlForBlock') + ->withArgs(["123321"]) + ->andReturn("block/123321") + ->once(); - //coinbase TX - $response = $client->transactions(["0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", "c791b82ed9af681b73eadb7a05b67294c1c3003e52d01e03775bfb79d4ac58d1"]); - $this->assertTrue(is_array($response), "Default response is not an array"); - $this->assertEquals("0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", $response['data'][0]['hash']); - $this->assertEquals("c791b82ed9af681b73eadb7a05b67294c1c3003e52d01e03775bfb79d4ac58d1", $response['data'][1]['hash']); - } + $dataClient->shouldReceive('get') + ->withArgs(["block/123321"]) + ->andReturn($res) + ->once(); - public function testPrice() { - $price = $this->setupBlocktrailSDK()->price(); + $converter->shouldReceive('convertBlock') + ->withArgs(['blockresponse']) + ->andReturn("blockresult") + ->once(); - $this->assertTrue(is_float($price['USD']) || is_int($price['USD']), "is float or int [{$price['USD']}]"); - $this->assertTrue($price['USD'] > 0, "is above 0 [{$price['USD']}]"); + $this->assertEquals("blockresult", $client->block("123321")); } - public function testVerifyMessage() { - $client = $this->setupBlocktrailSDK(); + public function testBlockLatest() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('blockresponse')); + $converter->shouldReceive('getUrlForBlock') + ->withArgs(["latest"]) + ->andReturn("block/latest") + ->once(); - $address = "1F26pNMrywyZJdr22jErtKcjF8R3Ttt55G"; - $message = $address; - $signature = "H85WKpqtNZDrajOnYDgUY+abh0KCAcOsAIOQwx2PftAbLEPRA7mzXA/CjXRxzz0MC225pR/hx02Vf2Ag2x33kU4="; + $dataClient->shouldReceive('get') + ->withArgs(["block/latest"]) + ->andReturn($res) + ->once(); - // test locally - $this->assertTrue($client->verifyMessage($message, $address, $signature)); + $converter->shouldReceive('convertBlock') + ->withArgs(['blockresponse']) + ->andReturn("blockresult") + ->once(); - // test using the API for it - $response = $client->getRestClient()->post("verify_message", null, ['message' => $message, 'address' => $address, 'signature' => $signature]); - $this->assertTrue(json_decode($response->body(), true)['result']); + $this->assertEquals("blockresult", $client->block("latest")); } - private function assertEqualsExceptKeys($expected, $actual, $keys) { - $expected1 = $expected; - $actual1 = $actual; - - foreach (array_keys($actual1) as $key) { - if (!array_key_exists($key, $expected1)) { - unset($actual1[$key]); - } - } + public function testBlockTransactions() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('blocktxsresponse')); + + $converter->shouldReceive('getUrlForBlockTransaction') + ->withArgs(["000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf"]) + ->andReturn("block/000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf/transactions") + ->once(); + + $converter->shouldReceive('paginationParams') + ->withArgs([['page' => 1, 'limit' => 2, 'sort_dir' => 'asc']]) + ->andReturn("pagination") + ->once(); + + $dataClient->shouldReceive('get') + ->withArgs(["block/000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf/transactions", "pagination"]) + ->andReturn($res) + ->once(); + + $converter->shouldReceive('convertBlockTxs') + ->withArgs(['blocktxsresponse']) + ->andReturn("blocktxsresult") + ->once(); + + $this->assertEquals("blocktxsresult", $client->blockTransactions("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf", 1, 2)); + } - foreach ($keys as $key) { - unset($expected1[$key]); - unset($actual1[$key]); - } + public function testAllBlocks() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('allblocksresponse')); + + $converter->shouldReceive('getUrlForAllBlocks') + ->withArgs([]) + ->andReturn("all-blocks") + ->once(); + + $converter->shouldReceive('paginationParams') + ->withArgs([['page' => 1, 'limit' => 2, 'sort_dir' => 'asc']]) + ->andReturn("pagination") + ->once(); + + $dataClient->shouldReceive('get') + ->withArgs(["all-blocks", "pagination"]) + ->andReturn($res) + ->once(); + + $converter->shouldReceive('convertBlocks') + ->withArgs(['allblocksresponse']) + ->andReturn("allblocksresult") + ->once(); + + $this->assertEquals("allblocksresult", $client->allBlocks(1, 2)); + } - if (isset($actual1['data'])) { - $this->assertEquals(count($expected1['data']), count($actual1['data'])); - - foreach ($actual1['data'] as $idx => $row) { - foreach ($row as $key => $value) { - if (!array_key_exists($key, $expected1['data'][0])) { - unset($actual1['data'][$idx][$key]); - } - } - } - - if (isset($actual1['data'][0]['inputs'])) { - foreach ($actual1['data'] as $idx => $row) { - foreach ($row['inputs'] as $inputIdx => $input) { - foreach ($input as $key => $value) { - if (!array_key_exists($key, $expected1['data'][$idx]['inputs'][$inputIdx])) { - unset($actual1['data'][$idx]['inputs'][$inputIdx][$key]); - } - } - } - foreach ($row['outputs'] as $outputIdx => $output) { - foreach ($output as $key => $value) { - if (!array_key_exists($key, $expected1['data'][$idx]['outputs'][$outputIdx])) { - unset($actual1['data'][$idx]['outputs'][$outputIdx][$key]); - } - } - } - } - } - - foreach ($keys as $key) { - if (strpos($key, ".data.inputs.") === 0) { - $key1 = substr($key, strlen(".data.inputs.")); - - foreach ($expected1['data'] as &$expectedRow) { - foreach ($expectedRow['inputs'] as &$expectedInput) { - unset($expectedInput[$key1]); - } - } - foreach ($actual1['data'] as &$actualRow) { - foreach ($actualRow['inputs'] as &$actualInput) { - unset($actualInput[$key1]); - } - } - } else if (strpos($key, ".data.outputs.") === 0) { - $key1 = substr($key, strlen(".data.outputs.")); - foreach ($expected1['data'] as &$expectedRow) { - foreach ($expectedRow['outputs'] as &$expectedOutput) { - unset($expectedOutput[$key1]); - } - } - foreach ($actual1['data'] as &$actualRow) { - foreach ($actualRow['outputs'] as &$actualOutput) { - unset($actualOutput[$key1]); - } - } - } else if (strpos($key, ".data.") === 0) { - $key1 = substr($key, strlen(".data.")); - foreach ($expected1['data'] as &$expectedRow) { - unset($expectedRow[$key1]); - } - foreach ($actual1['data'] as &$actualRow) { - unset($actualRow[$key1]); - } - } - } - } + public function testTransaction() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('txresponse')); + + $converter->shouldReceive('getUrlForTransaction') + ->withArgs(["95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615"]) + ->andReturn("tx/95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615") + ->once(); + + $dataClient->shouldReceive('get') + ->withArgs(["tx/95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615"]) + ->andReturn($res) + ->once(); + + $converter->shouldReceive('convertTx') + ->withArgs(['txresponse', null]) + ->andReturn("txresult") + ->once(); + + $this->assertEquals("txresult", $client->transaction("95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615")); + } - $this->assertEquals($expected1, $actual1); + public function testTransactions() { + $client = $this->mockSDK(); + $dataClient = $client->setDataClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('txsresponse')); + + $converter->shouldReceive('getUrlForTransactions') + ->withArgs([["95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615", "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"]]) + ->andReturn("txs/95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615,0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098") + ->once(); + + $dataClient->shouldReceive('get') + ->withArgs(["txs/95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615,0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"]) + ->andReturn($res) + ->once(); + + $converter->shouldReceive('convertTxs') + ->withArgs(['txsresponse']) + ->andReturn("txsresult") + ->once(); + + $this->assertEquals("txsresult", $client->transactions([ + "95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615", + "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098" + ])); } - public static function sortByHash(&$array) { - \usort($array, function($a, $b) { - return self::compareByHash($a, $b); - }); + public function testPrice() { + $client = $this->mockSDK(); + $blocktrailClient = $client->setBlocktrailClient(\Mockery::mock(RestClientInterface::class)); + $converter = $client->setConverter(\Mockery::mock(ConverterInterface::class)); + $res = new Response(200, \GuzzleHttp\Psr7\stream_for('{"USD": 1}')); - return $array; - } + $blocktrailClient->shouldReceive('get') + ->withArgs(["price"]) + ->andReturn($res) + ->once(); - public static function compareByHash($a, $b) { - if ($a['hash'] == $b['hash']) { - return 0; - } else if ($a['hash'] > $b['hash']) { - return -1; - } else { - return 1; - } + $this->assertEquals(["USD" => 1], $client->price()); } } diff --git a/tests/IntegrationTests/DataAPIIntegrationTest.php b/tests/IntegrationTests/DataAPIIntegrationTest.php new file mode 100644 index 0000000..adb6bb5 --- /dev/null +++ b/tests/IntegrationTests/DataAPIIntegrationTest.php @@ -0,0 +1,232 @@ +setupBlocktrailSDK(); + $this->assertTrue($client->getRestClient() instanceof RestClientInterface); + } + + public function testAddress() { + $client = $this->setupBlocktrailSDK(); + + //address info + $address = $client->address("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"); + $this->assertEqualsExceptKeys(\json_decode(\file_get_contents(__DIR__ . "/../data/address.3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief.json"), true), $address, + []); + + //address transactions + $response = $client->addressTransactions("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief", $page = 1, $limit = 20); + $expectedResponse = \json_decode(\file_get_contents(__DIR__ . "/../data/addressTxs.3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief.json"), true); + self::sortByHash($expectedResponse['data']); + self::sortByHash($response['data']); + $this->assertEqualsExceptKeys($expectedResponse, $response, + self::$txExceptFields); + + //address verification + $response = $client->verifyAddress("16dwJmR4mX5RguGrocMfN9Q9FR2kZcLw2z", "HPMOHRgPSMKdXrU6AqQs/i9S7alOakkHsJiqLGmInt05Cxj6b/WhS7kJxbIQxKmDW08YKzoFnbVZIoTI2qofEzk="); + $this->assertTrue(is_array($response), "Default response is not an array"); + $this->assertArrayHasKey('result', $response, "'result' key not in response"); + + //address unconfirmed transactions + $response = $client->addressUnspentOutputs("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"); + $this->assertTrue(is_array($response), "Default response is not an array"); + $this->assertArrayHasKey('total', $response, "'total' key not in response"); + $this->assertArrayHasKey('data', $response, "'data' key not in response"); + } + + public function testBlock() { + $client = $this->setupBlocktrailSDK(); + + //block info + $response = $client->block("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf"); + $this->assertTrue(is_array($response), "Default response is not an array"); + $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); + $this->assertEquals("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf", $response['hash'], "Block hash returned does not match expected value"); +// file_put_contents(__DIR__ . "/../data/block.000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf.json", \json_encode($response)); + $this->assertEqualsExceptKeys(\json_decode(file_get_contents(__DIR__ . "/../data/block.000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf.json"), true), $response, + ['confirmations', 'value']); + + //block info by height + $response = $client->block(200000); + $this->assertTrue(is_array($response), "Default response is not an array"); + $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); + $this->assertEquals("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf", $response['hash'], "Block hash returned does not match expected value"); +// file_put_contents(__DIR__ . "/../data/block.000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf.json", \json_encode($response)); + $this->assertEqualsExceptKeys(\json_decode(file_get_contents(__DIR__ . "/../data/block.000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf.json"), true), $response, + ['confirmations', 'value']); + + //all blocks + $response = $client->allBlocks($page = 2, $limit = 23); + $this->assertTrue(is_array($response), "Default response is not an array"); + $this->assertArrayHasKey('total', $response, "'total' key not in response"); + $this->assertArrayHasKey('data', $response, "'data' key not in response"); + $this->assertEquals(23, count($response['data']), "Count of blocks returned is not equal to 23"); + + $this->assertArrayHasKey('hash', $response['data'][0], "'hash' key not in first block of response"); + $this->assertArrayHasKey('hash', $response['data'][1], "'hash' key not in second block of response"); + + //latest block + $response = $client->blockLatest(); + $this->assertTrue(is_array($response), "Default response is not an array for latest block"); + $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); + } + + public function testTransaction() { + $client = $this->setupBlocktrailSDK(); + + $response = $client->transaction("95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615"); + + foreach ($response['outputs'] as &$output) { + if ($output['spent_hash'] === null) { + $output['spent_index'] = 0; + } + } + + $this->assertEqualsExceptKeys(\json_decode(file_get_contents(__DIR__ . "/../data/tx.95740451ac22f63c42c0d1b17392a0bf02983176d6de8dd05d6f06944d93e615.json"), true), $response, + ['confirmations', 'value', 'first_seen_at', 'last_seen_at', 'estimated_value', 'estimated_change', 'estimated_change_address', + 'high_priority', 'enough_fee', 'contains_dust', 'double_spend_in']); + + //coinbase TX + $response = $client->transaction("0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"); + $this->assertTrue(is_array($response), "Default response is not an array"); + $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); + $this->assertArrayHasKey('confirmations', $response, "'confirmations' key not in response"); + $this->assertEquals("0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", $response['hash'], "Transaction hash does not match expected value"); + + //random TX 1 + $response = $client->transaction("c791b82ed9af681b73eadb7a05b67294c1c3003e52d01e03775bfb79d4ac58d1"); + $this->assertTrue(is_array($response), "Default response is not an array"); + $this->assertArrayHasKey('hash', $response, "'hash' key not in response"); + $this->assertArrayHasKey('confirmations', $response, "'confirmations' key not in response"); + $this->assertEquals("c791b82ed9af681b73eadb7a05b67294c1c3003e52d01e03775bfb79d4ac58d1", $response['hash'], "Transaction hash does not match expected value"); + + //coinbase TX + $response = $client->transactions(["0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", "c791b82ed9af681b73eadb7a05b67294c1c3003e52d01e03775bfb79d4ac58d1"]); + $this->assertTrue(is_array($response), "Default response is not an array"); + $this->assertEquals("0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", $response['data'][0]['hash']); + $this->assertEquals("c791b82ed9af681b73eadb7a05b67294c1c3003e52d01e03775bfb79d4ac58d1", $response['data'][1]['hash']); + } + + public function testPrice() { + $price = $this->setupBlocktrailSDK()->price(); + + $this->assertTrue(is_float($price['USD']) || is_int($price['USD']), "is float or int [{$price['USD']}]"); + $this->assertTrue($price['USD'] > 0, "is above 0 [{$price['USD']}]"); + } + + private function assertEqualsExceptKeys($expected, $actual, $keys) { + $expected1 = $expected; + $actual1 = $actual; + + foreach (array_keys($actual1) as $key) { + if (!array_key_exists($key, $expected1)) { + unset($actual1[$key]); + } + } + + foreach ($keys as $key) { + unset($expected1[$key]); + unset($actual1[$key]); + } + + if (isset($actual1['data'])) { + $this->assertEquals(count($expected1['data']), count($actual1['data'])); + + foreach ($actual1['data'] as $idx => $row) { + foreach ($row as $key => $value) { + if (!array_key_exists($key, $expected1['data'][0])) { + unset($actual1['data'][$idx][$key]); + } + } + } + + if (isset($actual1['data'][0]['inputs'])) { + foreach ($actual1['data'] as $idx => $row) { + foreach ($row['inputs'] as $inputIdx => $input) { + foreach ($input as $key => $value) { + if (!array_key_exists($key, $expected1['data'][$idx]['inputs'][$inputIdx])) { + unset($actual1['data'][$idx]['inputs'][$inputIdx][$key]); + } + } + } + foreach ($row['outputs'] as $outputIdx => $output) { + foreach ($output as $key => $value) { + if (!array_key_exists($key, $expected1['data'][$idx]['outputs'][$outputIdx])) { + unset($actual1['data'][$idx]['outputs'][$outputIdx][$key]); + } + } + } + } + } + + foreach ($keys as $key) { + if (strpos($key, ".data.inputs.") === 0) { + $key1 = substr($key, strlen(".data.inputs.")); + + foreach ($expected1['data'] as &$expectedRow) { + foreach ($expectedRow['inputs'] as &$expectedInput) { + unset($expectedInput[$key1]); + } + } + foreach ($actual1['data'] as &$actualRow) { + foreach ($actualRow['inputs'] as &$actualInput) { + unset($actualInput[$key1]); + } + } + } else if (strpos($key, ".data.outputs.") === 0) { + $key1 = substr($key, strlen(".data.outputs.")); + foreach ($expected1['data'] as &$expectedRow) { + foreach ($expectedRow['outputs'] as &$expectedOutput) { + unset($expectedOutput[$key1]); + } + } + foreach ($actual1['data'] as &$actualRow) { + foreach ($actualRow['outputs'] as &$actualOutput) { + unset($actualOutput[$key1]); + } + } + } else if (strpos($key, ".data.") === 0) { + $key1 = substr($key, strlen(".data.")); + foreach ($expected1['data'] as &$expectedRow) { + unset($expectedRow[$key1]); + } + foreach ($actual1['data'] as &$actualRow) { + unset($actualRow[$key1]); + } + } + } + } + + $this->assertEquals($expected1, $actual1); + } + + public static function sortByHash(&$array) { + \usort($array, function($a, $b) { + return self::compareByHash($a, $b); + }); + + return $array; + } + + public static function compareByHash($a, $b) { + if ($a['hash'] == $b['hash']) { + return 0; + } else if ($a['hash'] > $b['hash']) { + return -1; + } else { + return 1; + } + } +} diff --git a/tests/BlocktrailTestCase.php b/tests/IntegrationTests/IntegrationTestBase.php similarity index 88% rename from tests/BlocktrailTestCase.php rename to tests/IntegrationTests/IntegrationTestBase.php index 527837d..df87d95 100644 --- a/tests/BlocktrailTestCase.php +++ b/tests/IntegrationTests/IntegrationTestBase.php @@ -1,12 +1,12 @@ cleanUp(); } - protected function onNotSuccessfulTest(\Exception $e) { + protected function onNotSuccessfulTest($e) { //called when a test fails $this->cleanUp(); throw $e; diff --git a/tests/WalletRecoveryTest.php b/tests/IntegrationTests/WalletRecoveryTest.php similarity index 98% rename from tests/WalletRecoveryTest.php rename to tests/IntegrationTests/WalletRecoveryTest.php index 1768531..ef9c629 100644 --- a/tests/WalletRecoveryTest.php +++ b/tests/IntegrationTests/WalletRecoveryTest.php @@ -1,6 +1,6 @@ cleanUp(); throw $e; diff --git a/tests/WebhookTest.php b/tests/IntegrationTests/WebhookTest.php similarity index 93% rename from tests/WebhookTest.php rename to tests/IntegrationTests/WebhookTest.php index 3484174..cdabf1d 100644 --- a/tests/WebhookTest.php +++ b/tests/IntegrationTests/WebhookTest.php @@ -6,9 +6,9 @@ * Time: 3:13 PM */ -namespace Blocktrail\SDK\Tests; +namespace Blocktrail\SDK\Tests\IntegrationTests; -class WebhookTest extends BlocktrailTestCase +class WebhookTest extends IntegrationTestBase { public function testWebhooks() { $client = $this->setupBlocktrailSDK('BTC', false); @@ -66,12 +66,8 @@ public function testWebhooks() { $newIdentifier = bin2hex($bytes); $newUrl = "https://www.blocktrail.com/new-webhook-url"; $response = $client->updateWebhook($webhookID2, $newUrl, $newIdentifier); - $this->assertTrue(is_array($response), "Default response is not an array"); - $this->assertArrayHasKey('url', $response, "'url' key not in response"); - $this->assertArrayHasKey('identifier', $response, "'identifier' key not in response"); - $this->assertEquals($newIdentifier, $response['identifier'], "identifier does not match expected value"); - $this->assertEquals($newUrl, $response['url'], "Webhook url does not match expected value when updating when updating"); - $webhookID2 = $response['identifier']; + $this->assertTrue($response); + $webhookID2 = $newIdentifier; $this->cleanupData['webhooks'][] = $webhookID2; //add webhook event subscription (address-transactions) @@ -166,4 +162,4 @@ public function testWebhooks() { $this->assertEquals($batchData[2]['address'], $response['data'][2]['address'], "Batch created even not as expected"); } -} \ No newline at end of file +} diff --git a/tests/MiscTest.php b/tests/MiscTest.php new file mode 100644 index 0000000..4cfcccd --- /dev/null +++ b/tests/MiscTest.php @@ -0,0 +1,68 @@ +assertEquals("022f6b9339309e89efb41ecabae60e1d40b7809596c68c03b05deb5a694e33cd26", $masterkey->getPublicKey()->getHex()); + $this->assertEquals("tpubDAtJthHcm9MJwmHp4r2UwSTmiDYZWHbQUMqySJ1koGxQpRNSaJdyL2Ab8wwtMm5DsMMk3v68299LQE6KhT8XPQWzxPLK5TbTHKtnrmjV8Gg", $masterkey->derivePath("0")->toExtendedKey()); + $this->assertEquals("tpubDDfqpEKGqEVa5FbdLtwezc6Xgn81teTFFVA69ZfJBHp4UYmUmhqVZMmqXeJBDahvySZrPjpwMy4gKfNfrxuFHmzo1r6srB4MrsDKWbwEw3d", $masterkey->derivePath("0/0")->toExtendedKey()); + + $this->assertEquals( + "tpubDHNy3kAG39ThyiwwsgoKY4iRenXDRtce8qdCFJZXPMCJg5dsCUHayp84raLTpvyiNA9sXPob5rgqkKvkN8S7MMyXbnEhGJMW64Cf4vFAoaF", + HierarchicalKeyFactory::fromEntropy(Buffer::hex("000102030405060708090a0b0c0d0e0f"))->derivePath("M/0'/1/2'/2/1000000000")->toExtendedPublicKey() + ); + } + + public function testNormalizeOutputStruct() { + $expected = [['address' => 'address1', 'value' => 'value1'], ['address' => 'address2', 'value' => 'value2']]; + + $this->assertEquals($expected, Wallet::normalizeOutputsStruct(['address1' => 'value1', 'address2' => 'value2'])); + $this->assertEquals($expected, Wallet::normalizeOutputsStruct([['address1', 'value1'], ['address2', 'value2']])); + $this->assertEquals($expected, Wallet::normalizeOutputsStruct($expected)); + + // duplicate address + $expected = [['address' => 'address1', 'value' => 'value1'], ['address' => 'address1', 'value' => 'value2']]; + + // not possible, keyed by address + // $this->assertEquals($expected, Wallet::normalizeOutputsStruct(['address1' => 'value1', 'address1' => 'value2'])); + $this->assertEquals($expected, Wallet::normalizeOutputsStruct([['address1', 'value1'], ['address1', 'value2']])); + $this->assertEquals($expected, Wallet::normalizeOutputsStruct($expected)); + } + + public function testEstimateFee() { + $this->assertEquals(30000, Wallet::estimateFee(1, 66)); + $this->assertEquals(40000, Wallet::estimateFee(2, 71)); + } + + public function testEstimateSizeOutputs() { + $this->assertEquals(34, Wallet::estimateSizeOutputs(1)); + $this->assertEquals(68, Wallet::estimateSizeOutputs(2)); + $this->assertEquals(102, Wallet::estimateSizeOutputs(3)); + $this->assertEquals(3366, Wallet::estimateSizeOutputs(99)); + } + + public function testEstimateSizeUTXOs() { + $this->assertEquals(297, Wallet::estimateSizeUTXOs(1)); + $this->assertEquals(594, Wallet::estimateSizeUTXOs(2)); + $this->assertEquals(891, Wallet::estimateSizeUTXOs(3)); + $this->assertEquals(29403, Wallet::estimateSizeUTXOs(99)); + } + + public function testEstimateSize() { + $this->assertEquals(347, Wallet::estimateSize(34, 297)); + $this->assertEquals(29453, Wallet::estimateSize(34, 29403)); + $this->assertEquals(3679, Wallet::estimateSize(3366, 297)); + } +} diff --git a/tests/MockBlocktrailSDK.php b/tests/MockBlocktrailSDK.php new file mode 100644 index 0000000..98d2d57 --- /dev/null +++ b/tests/MockBlocktrailSDK.php @@ -0,0 +1,52 @@ +dataClient = null; + $this->blocktrailClient = null; + } + + /** + * @param ConverterInterface|Mock $converter + * @return ConverterInterface|Mock + */ + public function setConverter($converter) { + $this->converter = $converter; + + return $converter; + } + + /** + * @param RestClientInterface|Mock $client + * @return RestClientInterface|Mock + */ + public function setDataClient($client) { + $this->dataClient = $client; + + return $client; + } + + /** + * @param RestClientInterface|Mock $client + * @return RestClientInterface|Mock + */ + public function setBlocktrailClient($client) { + $this->blocktrailClient = $client; + + return $client; + } +} diff --git a/tests/OutputsNormalizerTest.php b/tests/OutputsNormalizerTest.php index 5d5ed4d..b4f20c6 100644 --- a/tests/OutputsNormalizerTest.php +++ b/tests/OutputsNormalizerTest.php @@ -11,7 +11,7 @@ use Blocktrail\SDK\Network\BitcoinCashTestnet; use Blocktrail\SDK\OutputsNormalizer; -class OutputsNormalizerTest extends BlocktrailTestCase +class OutputsNormalizerTest extends \PHPUnit_Framework_TestCase { private function loadAddressReader($network, $testnet) { switch ($network) { diff --git a/tests/SizeEstimationTest.php b/tests/SizeEstimationTest.php index 2688f2f..8c424b6 100644 --- a/tests/SizeEstimationTest.php +++ b/tests/SizeEstimationTest.php @@ -3,6 +3,7 @@ namespace Blocktrail\SDK\Tests; use BitWasp\Bitcoin\Address\SegwitAddress; +use BitWasp\Bitcoin\Bitcoin; use BitWasp\Bitcoin\Network\NetworkFactory; use BitWasp\Bitcoin\Script\ScriptInterface; use BitWasp\Bitcoin\Script\ScriptFactory; @@ -23,8 +24,14 @@ use Blocktrail\SDK\Wallet; use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface; -class SizeEstimationTest extends BlocktrailTestCase +class SizeEstimationTest extends \PHPUnit_Framework_TestCase { + public function setUp() { + parent::setUp(); + + Bitcoin::setNetwork(NetworkFactory::bitcoin()); + } + /** * @return array */ @@ -265,7 +272,8 @@ public function testMultisigUtxoForms(UTXO $utxo, $scriptSigSize, $witSize) { } public function testEquivalentWithOld() { - $c = ['L1Tr4rPUi81XN1Dp48iuva5U9sWxU1eipgiAu8BhnB3xnSfGV5rd', + $c = [ + 'L1Tr4rPUi81XN1Dp48iuva5U9sWxU1eipgiAu8BhnB3xnSfGV5rd', 'KwUZpCvpAkUe1SZj3k3P2acta1V1jY8Dpuj71bEAukEKVrg8NEym', 'Kz2Lm2hzjPWhv3WW9Na5HUKi4qBxoTfv8fNYAU6KV6TZYVGdK5HW', ]; diff --git a/tests/UtilTest.php b/tests/UtilTest.php index 930defe..a7940ff 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -4,7 +4,7 @@ use Blocktrail\SDK\Util; -class UtilTest extends BlocktrailTestCase +class UtilTest extends \PHPUnit_Framework_TestCase { public function parseApiNetworkProvider() { return [ diff --git a/tests/Wallet/BitcoinCashAddressTest.php b/tests/Wallet/BitcoinCashAddressTest.php new file mode 100644 index 0000000..745f0a6 --- /dev/null +++ b/tests/Wallet/BitcoinCashAddressTest.php @@ -0,0 +1,127 @@ +assertEquals($address, $reader->fromString($address, $bch)->getAddress($bch)); + $this->assertEquals($address, $reader->fromString($short, $bch)->getAddress($bch)); + + $this->setExpectedException(BlocktrailSDKException::class, "Address not recognized"); + $reader->fromString($short, $tbch); + } + + public function testInitializeWithDefaultFormat() { + $tbcc = new BitcoinCashRegtest(); + /** @var BlocktrailSDK|Mock $client */ + $client = $this->mockSDK(true); + + /** @var Wallet $legacyAddressWallet */ + list($legacyAddressWallet, $client) = $this->initWallet($client); + + $legacyAddress = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $this->assertInstanceOf(BitcoinCashAddressReader::class, $legacyAddressWallet->getAddressReader()); + $this->assertInstanceOf(ScriptHashAddress::class, $legacyAddressWallet->getAddressReader()->fromString($legacyAddress, $tbcc)); + + $cashAddress = "bchreg:pqaql0az73r2lrtk43l3w3scuazgvarqdyufw37rsn"; + + /** @var Wallet $newAddressWallet */ + list($newAddressWallet, $client) = $this->initWallet($client, self::WALLET_IDENTIFIER, self::WALLET_PASSWORD, false, [ + 'use_cashaddress' => true, + ]); + + $this->assertInstanceOf(BitcoinCashAddressReader::class, $newAddressWallet->getAddressReader()); + $this->assertInstanceOf(CashAddress::class, $newAddressWallet->getAddressReader()->fromString($cashAddress, $tbcc)); + + $convertedLegacy = $client->getLegacyBitcoinCashAddress($cashAddress); + + $this->assertEquals($legacyAddress, $convertedLegacy); + } + + public function testCurrentDefaultIsOldFormat() { + $tbcc = new BitcoinCashRegtest(); + /** @var BlocktrailSDK|Mock $client */ + $client = $this->mockSDK(true); + + /** @var Wallet $cashAddrWallet */ + list($cashAddrWallet, $client) = $this->initWallet($client); + + $this->shouldGetNewDerivation($client, self::WALLET_IDENTIFIER, "m/9999'/1/*", "M/9999'/1/0"); + + $newAddress = $cashAddrWallet->getNewAddress(); + + $reader = $cashAddrWallet->getAddressReader(); + $this->assertInstanceOf(ScriptHashAddress::class, $reader->fromString($newAddress, $tbcc)); + } + + public function testCanOptIntoNewAddressFormat() { + $tbcc = new BitcoinCashRegtest(); + /** @var BlocktrailSDK|Mock $client */ + $client = $this->mockSDK(true); + + /** @var Wallet $cashAddrWallet */ + list($cashAddrWallet, $client) = $this->initWallet($client, self::WALLET_IDENTIFIER, self::WALLET_PASSWORD, false, [ + 'use_cashaddress' => true, + ]); + + $this->shouldGetNewDerivation($client, self::WALLET_IDENTIFIER, "m/9999'/1/*", "M/9999'/1/0"); + + $newAddress = $cashAddrWallet->getNewAddress(); + + $reader = $cashAddrWallet->getAddressReader(); + $this->assertInstanceOf(CashAddress::class, $reader->fromString($newAddress, $tbcc)); + } + + public function testCanCoinSelectNewCashAddresses() { + $tbcc = new BitcoinCashRegtest(); + /** @var BlocktrailSDK|Mock $client */ + $client = $this->mockSDK(true); + + /** @var Wallet $cashAddrWallet */ + list($cashAddrWallet, $client) = $this->initWallet($client, self::WALLET_IDENTIFIER, self::WALLET_PASSWORD, false, [ + 'use_cashaddress' => true, + ]); + + $str = "bchreg:pqaql0az73r2lrtk43l3w3scuazgvarqdyufw37rsn"; + $cashaddr = $cashAddrWallet->getAddressReader()->fromString($str, $tbcc); + + $this->shouldCoinSelect($client, self::WALLET_IDENTIFIER, 1234123, $cashaddr->getScriptPubKey()->getHex(), [ + [ + 'scriptpubkey_hex' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'path' => 'M/9999\'/0/0', + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ], + ]); + + $selection = $cashAddrWallet->coinSelection([ + $cashaddr->getAddress($tbcc) => 1234123, + ]); + + $this->assertArrayHasKey('utxos', $selection); + $this->assertTrue(count($selection['utxos']) > 0); + } +} diff --git a/tests/Wallet/BuildTxTest.php b/tests/Wallet/BuildTxTest.php new file mode 100644 index 0000000..c0720e5 --- /dev/null +++ b/tests/Wallet/BuildTxTest.php @@ -0,0 +1,682 @@ +mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $txid = "cafdeffb255ed7f8175f2bffc745e2dcc0ab0fa9abf9dad70a543c307614d374"; + $vout = 0; + $address = "2MtLjsE6SyBoxXt3Xae2wTU8sPdN8JUkUZc"; + $value = 9900000; + $outValue = 9899999; + $expectfee = $value-$outValue; + $scriptPubKey = "a9140c03259201742cb7476f10f70b2cf75fbfb8ab4087"; + $redeemScript = "0020cc7f3e23ec2a4cbba32d7e8f2e1aaabac38b88623d09f41dc2ee694fd33c6b14"; + $witnessScript = "5221021b3657937c54c616cbb519b447b4e50301c40759282901e04d81b5221cfcce992102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c1217663530210317e37c952644cf08b356671b4bb0308bd2468f548b31a308e8bacb682d55747253ae"; + + $path = "M/9999'/2/0"; + + $utxos = [ + $txid => $value, + ]; + + /** @var Transaction $tx */ + /** @var SignInfo[] $signInfo */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + $txid, + $vout, + $value, + $address, + $scriptPubKey, + $path, + $redeemScript, + $witnessScript + ) + ->addRecipient("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", $outValue) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals($value, $inputTotal); + $this->assertEquals($outValue, $outputTotal); + $this->assertEquals($expectfee, $fee); + + // assert the input(s) + $this->assertEquals(1, count($tx->getInputs())); + $this->assertEquals($txid, $tx->getInput(0)->getOutPoint()->getTxId()->getHex()); + $this->assertEquals(0, $tx->getInput(0)->getOutPoint()->getVout()); + $this->assertEquals($address, AddressFactory::fromOutputScript($signInfo[0]->output->getScript())->getAddress()); + $this->assertEquals($scriptPubKey, $signInfo[0]->output->getScript()->getHex()); + $this->assertEquals($value, $signInfo[0]->output->getValue()); + $this->assertEquals($path, $signInfo[0]->path); + $this->assertEquals( + $redeemScript, + $signInfo[0]->redeemScript->getHex() + ); + $this->assertEquals( + $witnessScript, + $signInfo[0]->witnessScript->getHex() + ); + + // assert the output(s) + $this->assertEquals(1, count($tx->getOutputs())); + $this->assertEquals("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", AddressFactory::fromOutputScript($tx->getOutput(0)->getScript())->getAddress()); + $this->assertEquals($outValue, $tx->getOutput(0)->getValue()); + } + + public function testBuildTx1() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + /* + * test simple (real world TX) scenario + */ + $utxos = [ + '0d8703ab259b03a757e37f3cdba7fc4543e8d47f7cc3556e46c0aeef6f5e832b' => BlocktrailSDK::toSatoshi(0.0001), + 'be837cd8f04911f3ee10d010823a26665980f7bb6c9ed307d798cb968ca00128' => BlocktrailSDK::toSatoshi(0.001), + ]; + + /** @var Transaction $tx */ + /** @var SignInfo[] $signInfo */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->randomizeChangeOutput(false) + ->spendOutput( + "0d8703ab259b03a757e37f3cdba7fc4543e8d47f7cc3556e46c0aeef6f5e832b", + 0, + BlocktrailSDK::toSatoshi(0.0001), + "2N4pMW5nyKG7Ni5N4CiUijEosxM9kS3eVQJ", + "a9147eed61eeecc72e4aaac9d9ff75d8c7171beb03a987", + "M/9999'/0/5", + "52210216a925b43b7f5f0ddcb2d68fa07ab19bfdb3af1eba7190f64b2d18c4a0f11d2a210216d3dbf7f135bed8fb0748798e6253c5ef748959dd317cbddea2cfec514d332121032923eb97175038268cd320ffbb74bbef5a97ad58717026564431b5a131d47a3753ae" + ) + ->spendOutput( + "be837cd8f04911f3ee10d010823a26665980f7bb6c9ed307d798cb968ca00128", + 0, + BlocktrailSDK::toSatoshi(0.001), + "2N3zY6LL4WxdVAbT11Tuf1QwX4ACD6FKFkH", + "a91475e241c516ed913b5b62c46cd95dffea0b4fc0fc87", + "M/9999'/0/12", + "5221020b9e77826a4dc47d681dbe15d5e7bc41746f1fcd142e955a4a56c144e1a3d3d52103628501430353863e2c3986372c251a562709e60238f129e494faf44aedf500dd2103f66d9bea4c46cbde0a3f0efddb2c5dc52ed5b2cd2c59cd11a35560ec9319081253ae" + ) + ->addRecipient("2N7C5Jn1LasbEK9mvHetBYXaDnQACXkarJe", BlocktrailSDK::toSatoshi(0.001)) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0011), $inputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.001), $outputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); + + // assert the input(s) + $this->assertEquals(2, count($tx->getInputs())); + $this->assertEquals("0d8703ab259b03a757e37f3cdba7fc4543e8d47f7cc3556e46c0aeef6f5e832b", $tx->getInput(0)->getOutPoint()->getTxId()->getHex()); + $this->assertEquals(0, $tx->getInput(0)->getOutPoint()->getVout()); + $this->assertEquals(10000, $signInfo[0]->output->getValue()); + $this->assertEquals("M/9999'/0/5", $signInfo[0]->path); + $this->assertEquals( + "52210216a925b43b7f5f0ddcb2d68fa07ab19bfdb3af1eba7190f64b2d18c4a0f11d2a210216d3dbf7f135bed8fb0748798e6253c5ef748959dd317cbddea2cfec514d332121032923eb97175038268cd320ffbb74bbef5a97ad58717026564431b5a131d47a3753ae", + $signInfo[0]->redeemScript->getHex() + ); + + $this->assertEquals("be837cd8f04911f3ee10d010823a26665980f7bb6c9ed307d798cb968ca00128", $tx->getInput(1)->getOutPoint()->getTxId()->getHex()); + $this->assertEquals(0, $tx->getInput(1)->getOutPoint()->getVout()); + $this->assertEquals(100000, $signInfo[1]->output->getValue()); + $this->assertEquals("M/9999'/0/12", $signInfo[1]->path); + $this->assertEquals( + "5221020b9e77826a4dc47d681dbe15d5e7bc41746f1fcd142e955a4a56c144e1a3d3d52103628501430353863e2c3986372c251a562709e60238f129e494faf44aedf500dd2103f66d9bea4c46cbde0a3f0efddb2c5dc52ed5b2cd2c59cd11a35560ec9319081253ae", + $signInfo[1]->redeemScript->getHex() + ); + + // assert the output(s) + $this->assertEquals(1, count($tx->getOutputs())); + $this->assertEquals("2N7C5Jn1LasbEK9mvHetBYXaDnQACXkarJe", AddressFactory::fromOutputScript($tx->getOutput(0)->getScript())->getAddress()); + $this->assertEquals(100000, $tx->getOutput(0)->getValue()); + + + } + + public function testBuildTx2() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + /* + * test trying to spend too much + */ + $utxos = [ + 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.0001) + ]; + $e = null; + try { + /** @var Transaction $tx */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", + 0, + BlocktrailSDK::toSatoshi(0.0001), + "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", + "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", + "M/9999'/0/1537", + "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" + ) + ->addRecipient("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", BlocktrailSDK::toSatoshi(0.0002)) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + } catch (\Exception $e) { + } + $this->assertTrue(!!$e); + $this->assertEquals("Atempting to spend more than sum of UTXOs", $e->getMessage()); + } + + + public function testBuildTx3() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + /* + * test change + */ + $utxos = [ + 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(1) + ]; + /** @var Transaction $tx */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", + 0, + BlocktrailSDK::toSatoshi(1), + "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", + "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", + "M/9999'/0/1537", + "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" + ) + ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NE2uSqCktMXfe512kTPrKPhQck7vMNvaGK", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) + ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") + ->randomizeChangeOutput(false) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals(BlocktrailSDK::toSatoshi(1), $inputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.9999), $outputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); + $this->assertEquals(14, count($tx->getOutputs())); + $this->assertEquals("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", AddressFactory::fromOutputScript($tx->getOutput(13)->getScript())->getAddress()); + $this->assertEquals(99860000, $tx->getOutput(13)->getValue()); + } + + public function testBuildTx4() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + /* + * 1 input (1 * 294b) = 294b + * 19 recipients (19 * 34b) = 646b + * + * size = 8b + 294b + 646b = 948b + * + change output (34b) = 982b + * + * fee = 0.0001 + * + * 1 - (19 * 0.0001) = 0.9981 + * change = 0.9980 + */ + $utxos = [ + 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(1) + ]; + /** @var Transaction $tx */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", + 0, + BlocktrailSDK::toSatoshi(1), + "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", + "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", + "M/9999'/0/1537", + "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" + ) + ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mw34qYu3rCmFkzZeNsDJ9aQri8HjmUZ6wY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MsTtsupHuqy6JvWUscn5HQ54EscLiXaPSF", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtR3Qa9eeYEpBmw3kLNywWGVmUuGjwRGXk", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N6GmkegBNA1D8wbHMLZFwxMPoNRjVnZgvv", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NBCPVQ6xX3KAVPKmGENH1eHhPwJzmN1Bpf", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NAHY321fSVz4wKnE4eWjyLfRmoauCrQpBD", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N2anz2GmZdrKNNeEZD7Xym8djepwnTqPXY", BlocktrailSDK::toSatoshi(0.0001)) + ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") + ->randomizeChangeOutput(false) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals(BlocktrailSDK::toSatoshi(1), $inputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.9999), $outputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); + $this->assertEquals(20, count($tx->getOutputs())); + $this->assertEquals("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", AddressFactory::fromOutputScript($tx->getOutput(19)->getScript())->getAddress()); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.9980), $tx->getOutput(19)->getValue()); + } + + public function testBuildTx5() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + /* + * test change output bumps size over 1kb, fee += 0.0001 + * + * 1 input (1 * 298b) = 298b + * 20 recipients (19 * 33b) = 693b + * + * size = 8b + 298b + 693b = 999b + * + change output (34b) = 1019b + * + * fee = 0.0002 + * input = 1.0000 + * 1.0000 - (20 * 0.0001) = 0.9977 + * change = 0.9977 + */ + $utxos = [ + 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(1) + ]; + /** @var Transaction $tx */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", + 0, + BlocktrailSDK::toSatoshi(1), + "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", + "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", + "M/9999'/0/1537", + "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" + ) + ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mw34qYu3rCmFkzZeNsDJ9aQri8HjmUZ6wY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MsTtsupHuqy6JvWUscn5HQ54EscLiXaPSF", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtR3Qa9eeYEpBmw3kLNywWGVmUuGjwRGXk", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N6GmkegBNA1D8wbHMLZFwxMPoNRjVnZgvv", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NBCPVQ6xX3KAVPKmGENH1eHhPwJzmN1Bpf", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NAHY321fSVz4wKnE4eWjyLfRmoauCrQpBD", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N2anz2GmZdrKNNeEZD7Xym8djepwnTqPXY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mvs5ik3nC9RBho2kPcgi5Q62xxAE2Aryse", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mvs5ik3nC9RBho2kPcgi5Q62xxAE2Aryse", BlocktrailSDK::toSatoshi(0.0001)) + ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") + ->randomizeChangeOutput(false) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals(BlocktrailSDK::toSatoshi(1), $inputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.9998), $outputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0002), $fee); + $this->assertEquals(22, count($tx->getOutputs())); + $change = $tx->getOutput(21); + $this->assertEquals("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", AddressFactory::fromOutputScript($change->getScript())->getAddress()); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.9977), $change->getValue()); + } + + public function testBuildTx6() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + /* + * test change + * + * 1 input (1 * 294b) = 294b + * 20 recipients (19 * 34b) = 680b + * + * size = 8b + 294b + 680b = 982b + * + change output (34b) = 1006b + * + * fee = 0.0001 + * input = 0.0021 + * 0.0021 - (20 * 0.0001) = 0.0001 + */ + $utxos = [ + 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.0021) + ]; + /** @var Transaction $tx */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", + 0, + BlocktrailSDK::toSatoshi(0.0021), + "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", + "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", + "M/9999'/0/1537", + "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" + ) + ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mw34qYu3rCmFkzZeNsDJ9aQri8HjmUZ6wY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MsTtsupHuqy6JvWUscn5HQ54EscLiXaPSF", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtR3Qa9eeYEpBmw3kLNywWGVmUuGjwRGXk", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N6GmkegBNA1D8wbHMLZFwxMPoNRjVnZgvv", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NBCPVQ6xX3KAVPKmGENH1eHhPwJzmN1Bpf", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NAHY321fSVz4wKnE4eWjyLfRmoauCrQpBD", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N2anz2GmZdrKNNeEZD7Xym8djepwnTqPXY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mvs5ik3nC9RBho2kPcgi5Q62xxAE2Aryse", BlocktrailSDK::toSatoshi(0.0001)) + ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") + ->randomizeChangeOutput(false) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0021), $inputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0020), $outputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); + $this->assertEquals(20, count($tx->getOutputs())); + } + + public function testBuildTx7() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + /* + * test change output bumps size over 1kb, fee += 0.0001 + * but change was < 0.0001 so better to just fee it all + * + * 1 input (1 * 294b) = 298b + * 20 recipients (20 * 34b) = 660b + * + * input = 0.00212 + * + * size = 8b + 298b + 660b = 986b + * fee = 0.0001 + * 0.00212 - (20 * 0.0001) = 0.00012 + * + * + change output (0.00009) (34b) = 1006b + * fee = 0.0002 + * + */ + $utxos = [ + 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.00212) + ]; + /** @var Transaction $tx */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", + 0, + BlocktrailSDK::toSatoshi(0.00212), + "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", + "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", + "M/9999'/0/1537", + "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" + ) + ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mw34qYu3rCmFkzZeNsDJ9aQri8HjmUZ6wY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MsTtsupHuqy6JvWUscn5HQ54EscLiXaPSF", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2MtR3Qa9eeYEpBmw3kLNywWGVmUuGjwRGXk", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N6GmkegBNA1D8wbHMLZFwxMPoNRjVnZgvv", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NBCPVQ6xX3KAVPKmGENH1eHhPwJzmN1Bpf", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2NAHY321fSVz4wKnE4eWjyLfRmoauCrQpBD", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2N2anz2GmZdrKNNeEZD7Xym8djepwnTqPXY", BlocktrailSDK::toSatoshi(0.0001)) + ->addRecipient("2Mvs5ik3nC9RBho2kPcgi5Q62xxAE2Aryse", BlocktrailSDK::toSatoshi(0.0001)) + ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") + ->randomizeChangeOutput(false) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals(BlocktrailSDK::toSatoshi(0.00212), $inputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0020), $outputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.00012), $fee); + $this->assertEquals(20, count($tx->getOutputs())); + } + + public function testBuildTx8() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + /* + * custom fee + */ + $utxos = [ + 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.002001) + ]; + /** @var Transaction $tx */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", + 0, + BlocktrailSDK::toSatoshi(0.002001), + "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", + "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", + "M/9999'/0/1537", + "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" + ) + ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.002)) + ->setFee(BlocktrailSDK::toSatoshi(0.000001)) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals(BlocktrailSDK::toSatoshi(0.002001), $inputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.002), $outputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.000001), $fee); + + /* + * multiple outputs same address + */ + $utxos = [ + 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.002) + ]; + /** @var Transaction $tx */ + list($tx, $signInfo) = $wallet->buildTx( + (new TransactionBuilder($wallet->getAddressReader())) + ->spendOutput( + "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", + 0, + BlocktrailSDK::toSatoshi(0.002), + "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", + "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", + "M/9999'/0/1537", + "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" + ) + ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0005)) + ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0005)) + ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") + ->randomizeChangeOutput(false) + ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) + ); + + $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { + return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; + }, $tx->getInputs())); + $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { + return $txout->getValue(); + }, $tx->getOutputs())); + + $fee = $inputTotal - $outputTotal; + + // assert the output(s) + $this->assertEquals(BlocktrailSDK::toSatoshi(0.002), $inputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0019), $outputTotal); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); + + $this->assertEquals("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", AddressFactory::fromOutputScript($tx->getOutput(0)->getScript())->getAddress()); + $this->assertEquals("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", AddressFactory::fromOutputScript($tx->getOutput(1)->getScript())->getAddress()); + } +} diff --git a/tests/Wallet/CreateWalletTest.php b/tests/Wallet/CreateWalletTest.php new file mode 100644 index 0000000..7c93e4e --- /dev/null +++ b/tests/Wallet/CreateWalletTest.php @@ -0,0 +1,383 @@ +getBinary(); + list($key, $iv) = CryptoJSAES::evpkdf($passphrase, $salt); + + $ct = openssl_encrypt($data, 'aes-256-cbc', $key, true, $iv); + + return CryptoJSAES::encode($ct, $salt); + } + + protected function shouldStoreNewWalletV3($client, $identifier) { + $client->shouldReceive('storeNewWalletV3') + ->withArgs([ + $identifier, + ["tpubD8Ke9MXfpW2zvymTPJAxETZE3KnBdsTwjNB3sHRj2iUJMPpGTMbZHyJPuMASFzJC4FbEeCg5r7N7HN36oPwibL9mWTRx9d2J6VPhsH9NEAS", "M/9999'"], + ["tpubD6NzVbkrYhZ4WTekiCroqzjETeQbxvQy8azRH4MT3tKhzZvf8G2QWWFdrTaiTHdYJVNPJJ85nvHjhP9dP7CZAkKsBEJ95DuY7bNZH1Gvw4w", "M"], + "CgAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAHp305+tX2NFSGsQcEiJFpHFXg8L6GJyBt0Zzr7M+KsD5P1zH0PgehzhxdIJZzmHlA==", + "CgAAAAAAAAAAAAC4iAAAAAAAAAAAAAAAAAAAAAAAADOe7reb5jZH+CmNxEtRtYQzMp/cZPAigKzZWo9yYxOqD4smZbROEj5zn8aeek5ZfA==", + "0000000000000000000000000000000000007265636f76657279736563726574", + "mgGXvgCUxJMt1mf9vdzdyPaJjmaKVHAtkZ", + 9999, + false + ]) + ->andReturn([ + "blocktrail_public_keys" => [ + 9999 => ["tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", "M/9999'"], + ], + "segwit" => false, + "key_index" => 9999, + "upgrade_key_index" => null + ]) + ->once(); + } + + public function testCreateWalletV3() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + $password = self::WALLET_PASSWORD; + $primarySeed = new Buffer('primaryseed', 32); + $backupSeed = new Buffer('backupseed', 32); + $secret = new Buffer('secret', 32); + $recoverySecret = new Buffer('recoverysecret', 32); + + $encryptedSecret = self::mockV3Encrypt($secret, new Buffer($password), KeyDerivation::DEFAULT_ITERATIONS)->getBuffer(); + $encryptedPrimarySeed = self::mockV3Encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)->getBuffer(); + $recoveryEncryptedSecret = self::mockV3Encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)->getBuffer(); + + $client->shouldReceive('newV3PrimarySeed')->andReturn($primarySeed)->once(); + $client->shouldReceive('newV3BackupSeed')->andReturn($backupSeed)->once(); + $client->shouldReceive('newV3Secret')->withArgs([$password])->andReturn([$secret, $encryptedSecret])->once(); + $client->shouldReceive('newV3EncryptedPrimarySeed')->andReturn($encryptedPrimarySeed)->once(); + $client->shouldReceive('newV3RecoverySecret')->andReturn([$recoverySecret, $recoveryEncryptedSecret])->once(); + + $this->shouldStoreNewWalletV3($client, $identifier); + + $res = $client->createNewWallet([ + 'identifier' => $identifier, + 'password' => $password, + 'key_index' => 9999, + ]); + /** @var Wallet $wallet */ + $wallet = $res[0]; + + $this->assertEquals($identifier, $wallet->getIdentifier()); + $this->assertEquals("M/9999'", $wallet->getBlocktrailPublicKeys()[9999][1]); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKeys()[9999][0]); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("m/9999'")->key()->toExtendedKey()); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("M/9999'")->key()->toExtendedKey()); + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/0", + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'scriptpubkey' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ]) + ->once(); + + // get a new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/0", $path); + $this->assertEquals("2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa", $address); + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/1", + 'address' => '2N38FErCcqDq1sqKAoQPkFtBKEosjTe4uaz', + 'scriptpubkey' => 'a9146c5f63f72c26725c100bc6ab0156e0dd36ffc8a087', + 'redeem_script' => '522102382362e09b4f9e15ba16397fc3b4c4caaf82da29819c5decf1f4d650bf088adf2102453a9ebff02859cb285987a2b14da82a05f87250bca5ba994e05dec4a6c7a6df21033dcb8536d7e0c6a3a556f07accd55262dd4f452d4ca702e100557be6c26b096453ae', + 'witness_script' => null, + ]) + ->once(); + + // get another new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/1", $path); + $this->assertEquals("2N38FErCcqDq1sqKAoQPkFtBKEosjTe4uaz", $address); + + // get the 2nd address again + $this->assertEquals("2N38FErCcqDq1sqKAoQPkFtBKEosjTe4uaz", $wallet->getAddressByPath("M/9999'/0/1")); + + // get some more addresses + $this->assertEquals("2NDeL5p8sX89QE2FAxTvuiZdNbk6Jv2vRVs", $wallet->getAddressByPath("M/9999'/0/6")); + $this->assertEquals("2NBP1aarake1UfiTU6aPrtdyooSQY7Dgm4T", $wallet->getAddressByPath("M/9999'/0/44")); + } + + public function testCreateWalletV3CustomPrimary() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + $password = self::WALLET_PASSWORD; + $primarySeed = new Buffer('primaryseed', 32); + $backupSeed = new Buffer('backupseed', 32); + $secret = new Buffer('secret', 32); + $recoverySecret = new Buffer('recoverysecret', 32); + + $encryptedSecret = self::mockV3Encrypt($secret, new Buffer($password), KeyDerivation::DEFAULT_ITERATIONS)->getBuffer(); + $encryptedPrimarySeed = self::mockV3Encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)->getBuffer(); + $recoveryEncryptedSecret = self::mockV3Encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)->getBuffer(); + + $client->shouldReceive('newV3BackupSeed')->andReturn($backupSeed)->once(); + + $client->shouldReceive('storeNewWalletV3') + ->withArgs([ + $identifier, + ["tpubD8Ke9MXfpW2zvymTPJAxETZE3KnBdsTwjNB3sHRj2iUJMPpGTMbZHyJPuMASFzJC4FbEeCg5r7N7HN36oPwibL9mWTRx9d2J6VPhsH9NEAS", "M/9999'"], + ["tpubD6NzVbkrYhZ4WTekiCroqzjETeQbxvQy8azRH4MT3tKhzZvf8G2QWWFdrTaiTHdYJVNPJJ85nvHjhP9dP7CZAkKsBEJ95DuY7bNZH1Gvw4w", "M"], + false, + false, + false, + "mgGXvgCUxJMt1mf9vdzdyPaJjmaKVHAtkZ", + 9999, + false + ]) + ->andReturn([ + "blocktrail_public_keys" => [ + 9999 => ["tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", "M/9999'"], + ], + "segwit" => false, + "key_index" => 9999, + "upgrade_key_index" => null + ]) + ->once(); + + $res = $client->createNewWallet([ + 'identifier' => $identifier, + 'password' => $password, + 'store_data_on_server' => false, + 'primary_seed' => $primarySeed, + 'key_index' => 9999, + ]); + /** @var Wallet $wallet */ + $wallet = $res[0]; + + $this->assertTrue(!!$wallet); + } + + public function testCreateWalletV2() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + $password = self::WALLET_PASSWORD; + $primarySeed = (new Buffer('primaryseed', 32))->getBinary(); + $backupSeed = (new Buffer('backupseed', 32))->getBinary(); + $secret = (new Buffer('secret', 32))->getBinary(); + $recoverySecret = 'recoverysecretrecoverysecretreco'; + + $encryptedSecret = self::mockV2Encrypt($secret, $password); + $encryptedPrimarySeed = self::mockV2Encrypt($primarySeed, $secret); + $recoveryEncryptedSecret = self::mockV2Encrypt($secret, $recoverySecret); + + $client->shouldReceive('newV2PrimarySeed')->andReturn($primarySeed)->once(); + $client->shouldReceive('newV2BackupSeed')->andReturn($backupSeed)->once(); + $client->shouldReceive('newV2Secret')->withArgs([$password])->andReturn([$secret, $encryptedSecret])->once(); + $client->shouldReceive('newV2EncryptedPrimarySeed')->andReturn($encryptedPrimarySeed)->once(); + $client->shouldReceive('newV2RecoverySecret')->andReturn([$recoverySecret, $recoveryEncryptedSecret])->once(); + + $client->shouldReceive('storeNewWalletV2') + ->withArgs([ + $identifier, + ["tpubD8Ke9MXfpW2zvymTPJAxETZE3KnBdsTwjNB3sHRj2iUJMPpGTMbZHyJPuMASFzJC4FbEeCg5r7N7HN36oPwibL9mWTRx9d2J6VPhsH9NEAS", "M/9999'"], + ["tpubD6NzVbkrYhZ4WTekiCroqzjETeQbxvQy8azRH4MT3tKhzZvf8G2QWWFdrTaiTHdYJVNPJJ85nvHjhP9dP7CZAkKsBEJ95DuY7bNZH1Gvw4w", "M"], + "U2FsdGVkX18AAAAAAAAAAIgSvbD8v2Jln/Psv74dZRFwMaQtESkRg3Qwu+vgQjN2h+Dh+LfP2Y0qIf04o2IUIA==", + "U2FsdGVkX18AAAAAAAAAAMBAD4ly9jVWNEGJZ39HZ33es71t3TUtIVIqwDqQ6XdDcmRHmm6GBqZYZAoNgfHbyw==", + "recoverysecretrecoverysecretreco", + "mgGXvgCUxJMt1mf9vdzdyPaJjmaKVHAtkZ", + 9999, + false + ]) + ->andReturn([ + "blocktrail_public_keys" => [ + 9999 => ["tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", "M/9999'"], + ], + "segwit" => false, + "key_index" => 9999, + "upgrade_key_index" => null + ]) + ->once(); + + $res = $client->createNewWallet([ + 'identifier' => $identifier, + 'password' => $password, + 'key_index' => 9999, + 'wallet_version' => Wallet::WALLET_VERSION_V2, + ]); + /** @var Wallet $wallet */ + $wallet = $res[0]; + + $this->assertEquals($identifier, $wallet->getIdentifier()); + $this->assertEquals("M/9999'", $wallet->getBlocktrailPublicKeys()[9999][1]); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKeys()[9999][0]); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("m/9999'")->key()->toExtendedKey()); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("M/9999'")->key()->toExtendedKey()); + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/0", + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'scriptpubkey' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ]) + ->once(); + + // get a new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/0", $path); + $this->assertEquals("2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa", $address); + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/1", + 'address' => '2N38FErCcqDq1sqKAoQPkFtBKEosjTe4uaz', + 'scriptpubkey' => 'a9146c5f63f72c26725c100bc6ab0156e0dd36ffc8a087', + 'redeem_script' => '522102382362e09b4f9e15ba16397fc3b4c4caaf82da29819c5decf1f4d650bf088adf2102453a9ebff02859cb285987a2b14da82a05f87250bca5ba994e05dec4a6c7a6df21033dcb8536d7e0c6a3a556f07accd55262dd4f452d4ca702e100557be6c26b096453ae', + 'witness_script' => null, + ]) + ->once(); + + // get another new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/1", $path); + $this->assertEquals("2N38FErCcqDq1sqKAoQPkFtBKEosjTe4uaz", $address); + + // get the 2nd address again + $this->assertEquals("2N38FErCcqDq1sqKAoQPkFtBKEosjTe4uaz", $wallet->getAddressByPath("M/9999'/0/1")); + + // get some more addresses + $this->assertEquals("2NDeL5p8sX89QE2FAxTvuiZdNbk6Jv2vRVs", $wallet->getAddressByPath("M/9999'/0/6")); + $this->assertEquals("2NBP1aarake1UfiTU6aPrtdyooSQY7Dgm4T", $wallet->getAddressByPath("M/9999'/0/44")); + } + + public function testCreateWalletV1() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + $password = self::WALLET_PASSWORD; + $primarySeed = new Buffer('primaryseed', 64); + $backupSeed = new Buffer('backupseed', 64); + + $primaryMnemonic = MnemonicFactory::bip39()->entropyToMnemonic($primarySeed); + $backupMnemonic = MnemonicFactory::bip39()->entropyToMnemonic($backupSeed); + + // primary + $client->shouldReceive('generateNewMnemonic')->andReturn($primaryMnemonic)->once(); + // backup + $client->shouldReceive('generateNewMnemonic')->andReturn($backupMnemonic)->once(); + + $client->shouldReceive('storeNewWalletV1') + ->withArgs([ + $identifier, + ["tpubDA5AEZ2jW8afwPeKGwNGb52ydoNVtYRTiPsxtyw7i6eJ2qBwzhXrR9mRV1AkBPkYzPMApdiiGPtJ6CetcYzZkWAHVWxk4hsZU9vc1XWS8aa", "M/9999'"], + ["tpubD6NzVbkrYhZ4Wg9K1EjgX5F9rCi3nMjpMsyMZGyketABh8Y2Cfn92dfHAWEffZB3VJAZS2rd5iYTGfyiW32dEzWCnxubrDbbLHig3JGcvEZ", "M"], + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon achieve atom harvest help frame very curve razor muscle spawn", + "n3yffrKj4xReaWjt5pdLoe7ZoBwe1UGTm8", + 9999, + false + ]) + ->andReturn([ + "blocktrail_public_keys" => [ + 9999 => ["tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", "M/9999'"], + ], + "segwit" => false, + "key_index" => 9999, + "upgrade_key_index" => null + ]) + ->once(); + + $res = $client->createNewWallet([ + 'identifier' => $identifier, + 'password' => $password, + 'key_index' => 9999, + 'wallet_version' => Wallet::WALLET_VERSION_V1, + ]); + /** @var Wallet $wallet */ + $wallet = $res[0]; + + $this->assertEquals($identifier, $wallet->getIdentifier()); + $this->assertEquals("M/9999'", $wallet->getBlocktrailPublicKeys()[9999][1]); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKeys()[9999][0]); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("m/9999'")->key()->toExtendedKey()); + $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("M/9999'")->key()->toExtendedKey()); + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/0", + 'address' => '2NEcM4eKZVhdjjHzpf19wBYThdQxD4ehevG', + 'scriptpubkey' => 'a914ea594860336ee7ec1e4b67e0ed59452dec088ba887', + 'redeem_script' => '52210200af504e1da5fd53537bffc245ac5948cca756ee530c9fba4a797b5de9e4e48a210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e372103dd7a2df2216b3729eb7d19c654b9fd4816a34768d51e299762e0781e8f8f16c253ae', + 'witness_script' => null, + ]) + ->once(); + + // get a new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/0", $path); + $this->assertEquals("2NEcM4eKZVhdjjHzpf19wBYThdQxD4ehevG", $address); + + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/1", + 'address' => '2N3RS2ndcGy69zEfFLQ4MW7HMwUXjx1diEg', + 'scriptpubkey' => 'a9146f9f7878330a1b3416df6122b08ebf3be5624d0487', + 'redeem_script' => '5221030d057824924d0400bd9f3525a3913c74719a6f6b2f7dec8e898df378d5b2aeee21033dcb8536d7e0c6a3a556f07accd55262dd4f452d4ca702e100557be6c26b096421035a058ca98ac2640062b1a1313a74fd754f48bc29079e6830f03d45b225f54ebb53ae', + 'witness_script' => null, + ]) + ->once(); + + // get another new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/1", $path); + $this->assertEquals("2N3RS2ndcGy69zEfFLQ4MW7HMwUXjx1diEg", $address); + + // get the 2nd address again + $this->assertEquals("2N3RS2ndcGy69zEfFLQ4MW7HMwUXjx1diEg", $wallet->getAddressByPath("M/9999'/0/1")); + + // get some more addresses + $this->assertEquals("2NBz1n7vHdrtCJKtKUeQipRd8rBxsoKv39M", $wallet->getAddressByPath("M/9999'/0/6")); + $this->assertEquals("2MzEv48F5qkhpaEFwDVcNUa595NJJZjqb6C", $wallet->getAddressByPath("M/9999'/0/44")); + } +} diff --git a/tests/Wallet/GetNewAddressPairTest.php b/tests/Wallet/GetNewAddressPairTest.php new file mode 100644 index 0000000..ba512ec --- /dev/null +++ b/tests/Wallet/GetNewAddressPairTest.php @@ -0,0 +1,175 @@ +mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, "mypassword", false); + + $this->assertFalse($wallet->isSegwit()); + + $this->shouldGetNewDerivation($client, $identifier, "m/9999'/0/*", "M/9999'/0/0"); + $this->checkWalletScriptAgainstAddressPair($wallet, Wallet::CHAIN_BTC_DEFAULT); + + $path = "M/9999'/0/0"; + $script = $wallet->getWalletScriptByPath($path); + + $this->assertTrue($script->isP2SH()); + $this->assertFalse($script->isP2WSH()); + + $this->isBase58Address($script); + $this->checkP2sh($script->getScriptPubKey(), $script->getRedeemScript()); + + $this->assertEquals("2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa", $script->getAddress()->getAddress()); + $this->assertEquals("a9143a0fbfa2f446af8d76ac7f174618e7448674606987", $script->getScriptPubKey()->getHex()); + $this->assertEquals("5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae", $script->getRedeemScript()->getHex()); + $this->assertEquals(null, $script->getWitnessScript(true)); + } + + /** + * @expectedException \Blocktrail\SDK\Exceptions\BlocktrailSDKException + * @expectedExceptionMessage Unsupported chain in path + */ + public function testWalletGetNewAddressPairNonSegwitDisallowSegwit() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, "mypassword", false); + + $this->assertFalse($wallet->isSegwit()); + $wallet->getRedeemScriptByPath("M/9999'/2/0"); + } + + public function testWalletGetNewAddressPairBcash() { + $client = $this->mockSDK(true); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, "mypassword", false, [ + 'use_cashaddress' => true + ]); + + $this->assertFalse($wallet->isSegwit()); + + $this->shouldGetNewDerivation($client, $identifier, "m/9999'/0/*", "M/9999'/0/0"); + $this->checkWalletScriptAgainstAddressPair($wallet, Wallet::CHAIN_BTC_DEFAULT); + + $path = "M/9999'/0/0"; + $script = $wallet->getWalletScriptByPath($path); + + $this->assertTrue($script->isP2SH()); + $this->assertFalse($script->isP2WSH()); + + $this->isCashAddress($script); + $this->checkP2sh($script->getScriptPubKey(), $script->getRedeemScript()); + + $this->assertEquals("bchreg:pqaql0az73r2lrtk43l3w3scuazgvarqdyufw37rsn", $script->getAddress()->getAddress()); + $this->assertEquals("a9143a0fbfa2f446af8d76ac7f174618e7448674606987", $script->getScriptPubKey()->getHex()); + $this->assertEquals("5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae", $script->getRedeemScript()->getHex()); + $this->assertEquals(null, $script->getWitnessScript(true)); + } + + public function testWalletGetNewAddressPairSegwit() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, "mypassword", true); + + $this->assertTrue($wallet->isSegwit()); + + $this->shouldGetNewDerivation($client, $identifier, "m/9999'/0/*", "M/9999'/0/0"); + $this->checkWalletScriptAgainstAddressPair($wallet, Wallet::CHAIN_BTC_DEFAULT); + + $this->shouldGetNewDerivation($client, $identifier, "m/9999'/2/*", "M/9999'/2/0"); + $this->checkWalletScriptAgainstAddressPair($wallet, Wallet::CHAIN_BTC_SEGWIT); + + $nestedP2wshPath = "M/9999'/2/0"; + $script = $wallet->getWalletScriptByPath($nestedP2wshPath); + + $this->assertTrue($script->isP2SH()); + $this->assertTrue($script->isP2WSH()); + + $this->isBase58Address($script); + $this->checkP2sh($script->getScriptPubKey(), $script->getRedeemScript()); + $this->checkP2wsh($script->getRedeemScript(), $script->getWitnessScript()); + + $this->assertEquals("2MtPQLhhzFNtaY59QQ9wt28a5qyQ2t8rnvd", $script->getAddress()->getAddress()); + $this->assertEquals("a9140c84184d6d00096482c1359ce0194376ad248d2287", $script->getScriptPubKey()->getHex()); + $this->assertEquals("00204679ecfef34eb556071e349442e74837d059cb42ef22c4946a2efbb0c9041e68", $script->getRedeemScript()->getHex()); + $this->assertEquals("522102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c121766353021028e2ec167fbdd100fcb95e22ce4b49d4733b1f291eef6b5f5748852c96aa9522721038ba8bf95fc3449c3b745232eeca5493fd3277b801e659482cc868ca6e55ce23b53ae", $script->getWitnessScript()->getHex()); + } + + private function checkWalletScriptAgainstAddressPair(WalletInterface $wallet, $chainIdx) + { + list ($path, $address) = $wallet->getNewAddressPair($chainIdx); + $this->assertTrue(strpos("M/9999'/{$chainIdx}/", $path) !== -1); + + $defaultScript = $wallet->getWalletScriptByPath($path); + $this->assertEquals($defaultScript->getAddress()->getAddress(), $address); + + $classifier = new OutputClassifier(); + + switch($chainIdx) { + case Wallet::CHAIN_BTC_SEGWIT: + $this->assertTrue($defaultScript->isP2SH()); + $this->assertTrue($defaultScript->isP2WSH()); + $this->assertTrue($classifier->isMultisig($defaultScript->getWitnessScript())); + $this->assertTrue($classifier->isWitness($defaultScript->getRedeemScript())); + break; + case Wallet::CHAIN_BTC_DEFAULT: + $this->assertTrue($defaultScript->isP2SH()); + $this->assertTrue($classifier->isMultisig($defaultScript->getRedeemScript())); + break; + } + } + + /** + * @expectedException \Blocktrail\SDK\Exceptions\BlocktrailSDKException + * @expectedExceptionMessage Unsupported chain in path + */ + public function testWalletRejectsUnknownPaths() + { + $client = $this->mockSDK(true); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client); + + $wallet->getWalletScriptByPath("M/9999'/123123123/0"); + } + + + /** + * @expectedException \Blocktrail\SDK\Exceptions\BlocktrailSDKException + * @expectedExceptionMessage Chain index is invalid - should be an integer + */ + public function testCheckRejectsInvalidChainINdex() + { + $client = $this->mockSDK(true); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client); + + $wallet->getNewAddress(''); + } +} diff --git a/tests/Wallet/InitWalletCheckBackupKeyTest.php b/tests/Wallet/InitWalletCheckBackupKeyTest.php new file mode 100644 index 0000000..87fca6a --- /dev/null +++ b/tests/Wallet/InitWalletCheckBackupKeyTest.php @@ -0,0 +1,53 @@ +mockSDK(); + + $identifier = "mywallet"; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, "mypassword", true, [ + 'check_backup_key' => [], + ]); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testChecksBackupKeyShouldBeXpub2() { + $client = $this->mockSDK(); + + $identifier = "mywallet"; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, "mypassword", true, [ + 'check_backup_key' => 'for demonstration purposes only' + ]); + } + + public function testChecksBackupKey() { + $client = $this->mockSDK(); + + $identifier = "mywallet"; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, "mypassword", true, [ + 'check_backup_key' => 'tpubD6NzVbkrYhZ4WTekiCroqzjETeQbxvQy8azRH4MT3tKhzZvf8G2QWWFdrTaiTHdYJVNPJJ85nvHjhP9dP7CZAkKsBEJ95DuY7bNZH1Gvw4w', + ]); + } +} diff --git a/tests/Wallet/InitWalletTest.php b/tests/Wallet/InitWalletTest.php new file mode 100644 index 0000000..ebda74c --- /dev/null +++ b/tests/Wallet/InitWalletTest.php @@ -0,0 +1,175 @@ +mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + $password = self::WALLET_PASSWORD; + + $client->shouldReceive('getWallet') + ->withArgs([$identifier]) + ->andReturn([ + 'primary_mnemonic' => NULL, + 'encrypted_secret' => 'CgAAAAAAAAAAAAC4iAAAAAAAAAAAAAAAAAAAAAAAADOe7reb5jZH+CmNxEtRtYQzMp/cZPAigKzZWo9yYxOqD4smZbROEj5zn8aeek5ZfA==', + 'encrypted_primary_seed' => 'CgAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAHp305+tX2NFSGsQcEiJFpHFXg8L6GJyBt0Zzr7M+KsD5P1zH0PgehzhxdIJZzmHlA==', + 'wallet_version' => 'v3', + 'key_index' => 9999, + 'checksum' => 'mgGXvgCUxJMt1mf9vdzdyPaJjmaKVHAtkZ', + 'segwit' => false, + 'backup_public_key' => ['tpubD6NzVbkrYhZ4WTekiCroqzjETeQbxvQy8azRH4MT3tKhzZvf8G2QWWFdrTaiTHdYJVNPJJ85nvHjhP9dP7CZAkKsBEJ95DuY7bNZH1Gvw4w','M'], + 'blocktrail_public_keys' => [ + 9999 => ['tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ', 'M/9999\''], + ], + 'primary_public_keys' => [ + 9999 => ['tpubD8Ke9MXfpW2zvymTPJAxETZE3KnBdsTwjNB3sHRj2iUJMPpGTMbZHyJPuMASFzJC4FbEeCg5r7N7HN36oPwibL9mWTRx9d2J6VPhsH9NEAS', 'M/9999\''], + ], + 'upgrade_key_index' => NULL, + ]) + ->once(); + + /** @var Wallet $wallet */ + $wallet = $client->initWallet([ + 'identifier' => $identifier, + 'password' => $password, + ]); + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/0", + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'scriptpubkey' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ]) + ->once(); + + // get a new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/0", $path); + $this->assertEquals("2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa", $address); + } + + /** + * this tests BC input to initWallet + */ + public function testInitWalletOldSyntax() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + $password = self::WALLET_PASSWORD; + + $this->shouldGetWalletV3($client, $identifier); + + /** @var Wallet $wallet */ + $wallet = $client->initWallet($identifier, $password); + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/0", + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'scriptpubkey' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ]) + ->once(); + + // get a new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/0", $path); + $this->assertEquals("2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa", $address); + } + + /** + * this test is the basis for WalletBaseTest::initWalletV1 + */ + public function testInitWalletV1() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + $password = self::WALLET_PASSWORD; + + $client->shouldReceive('getWallet') + ->withArgs([$identifier]) + ->andReturn([ + 'primary_mnemonic' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon achieve atom harvest help frame very curve razor muscle spawn', + 'wallet_version' => 'v1', + 'key_index' => 9999, + 'checksum' => 'n3yffrKj4xReaWjt5pdLoe7ZoBwe1UGTm8', + 'segwit' => false, + 'backup_public_key' => ['tpubD6NzVbkrYhZ4Wg9K1EjgX5F9rCi3nMjpMsyMZGyketABh8Y2Cfn92dfHAWEffZB3VJAZS2rd5iYTGfyiW32dEzWCnxubrDbbLHig3JGcvEZ','M'], + 'blocktrail_public_keys' => [ + 9999 => ['tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ', 'M/9999\''], + ], + 'primary_public_keys' => [ + 9999 => ['tpubDA5AEZ2jW8afwPeKGwNGb52ydoNVtYRTiPsxtyw7i6eJ2qBwzhXrR9mRV1AkBPkYzPMApdiiGPtJ6CetcYzZkWAHVWxk4hsZU9vc1XWS8aa', 'M/9999\''], + ], + 'upgrade_key_index' => NULL, + ]) + ->once(); + + /** @var Wallet $wallet */ + $wallet = $client->initWallet([ + 'identifier' => $identifier, + 'password' => $password, + ]); + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, "m/9999'/0/*"]) + ->andReturn([ + 'path' => "M/9999'/0/0", + 'address' => '2NEcM4eKZVhdjjHzpf19wBYThdQxD4ehevG', + 'scriptpubkey' => 'a914ea594860336ee7ec1e4b67e0ed59452dec088ba887', + 'redeem_script' => '52210200af504e1da5fd53537bffc245ac5948cca756ee530c9fba4a797b5de9e4e48a210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e372103dd7a2df2216b3729eb7d19c654b9fd4816a34768d51e299762e0781e8f8f16c253ae', + 'witness_script' => null, + ]) + ->once(); + + // get a new pair + list($path, $address) = $wallet->getNewAddressPair(); + $this->assertEquals("M/9999'/0/0", $path); + $this->assertEquals("2NEcM4eKZVhdjjHzpf19wBYThdQxD4ehevG", $address); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to decrypt or to verify the tag. + */ + public function testBadPassword() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, "badpassword"); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Checksum [msDQeMojn8ooMv9FniuCkqjdyvXRUY86eR] does not match [n3yffrKj4xReaWjt5pdLoe7ZoBwe1UGTm8], most likely due to incorrect password + */ + public function testBadPasswordV1() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWalletV1($client, $identifier, "badpassword"); + } +} diff --git a/tests/Wallet/SendTxTest.php b/tests/Wallet/SendTxTest.php new file mode 100644 index 0000000..69b5d70 --- /dev/null +++ b/tests/Wallet/SendTxTest.php @@ -0,0 +1,386 @@ +mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $addr = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $spk = "a9143a0fbfa2f446af8d76ac7f174618e7448674606987"; + $value = BlocktrailSDK::toSatoshi(1.0); + $pay = [$addr => $value]; + + $this->shouldCoinSelect($client, $identifier, $value, $spk, [ + [ + 'scriptpubkey_hex' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'path' => 'M/9999\'/0/0', + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ], + ]); + + $partiallySignedTx = "0100000001ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e00000000b500483045022100a408d1845fe3b2980030cacfbcfb9cf0580d7e93ec3f048bf19c002c1066f74f022060b6001d631fc43c3e9a1764ff7c985226f10647d8aadb4bdce9cfc98d792f3b014c695221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153aeffffffff0200e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e74486746069876edaa4350000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698700000000"; + $baseTx = $partiallySignedTx; + $coSignedTx = "ok"; + + $client->shouldReceive('sendTransaction') + ->withArgs([ + $identifier, + ['signed_transaction' => $partiallySignedTx, 'base_transaction' => $baseTx], + ["M/9999'/0/0"], + true + ]) + ->andReturn($coSignedTx) + ->once(); + + $this->assertEquals($coSignedTx, $wallet->pay($pay, $addr, false, false, Wallet::FEE_STRATEGY_OPTIMAL)); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Wallet needs to be unlocked to pay + */ + public function testSendTxUnlockRequired() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, null, false, [ + 'readonly' => true, + ]); + + $addr = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $spk = "a9143a0fbfa2f446af8d76ac7f174618e7448674606987"; + $value = BlocktrailSDK::toSatoshi(1.0); + $pay = [$addr => $value]; + + $wallet->pay($pay, $addr, false, false, Wallet::FEE_STRATEGY_OPTIMAL); + } + + public function testSendTxLateUnlock() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, null, false, [ + 'readonly' => true, + ]); + + $wallet->unlock([ + 'password' => self::WALLET_PASSWORD, + ]); + + $addr = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $spk = "a9143a0fbfa2f446af8d76ac7f174618e7448674606987"; + $value = BlocktrailSDK::toSatoshi(1.0); + $pay = [$addr => $value]; + + $this->shouldCoinSelect($client, $identifier, $value, $spk, [ + [ + 'scriptpubkey_hex' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'path' => 'M/9999\'/0/0', + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ], + ]); + + $partiallySignedTx = "0100000001ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e00000000b500483045022100a408d1845fe3b2980030cacfbcfb9cf0580d7e93ec3f048bf19c002c1066f74f022060b6001d631fc43c3e9a1764ff7c985226f10647d8aadb4bdce9cfc98d792f3b014c695221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153aeffffffff0200e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e74486746069876edaa4350000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698700000000"; + $baseTx = $partiallySignedTx; + $coSignedTx = "ok"; + + $client->shouldReceive('sendTransaction') + ->withArgs([ + $identifier, + ['signed_transaction' => $partiallySignedTx, 'base_transaction' => $baseTx], + ["M/9999'/0/0"], + true + ]) + ->andReturn($coSignedTx) + ->once(); + + $this->assertEquals($coSignedTx, $wallet->pay($pay, $addr, false, false, Wallet::FEE_STRATEGY_OPTIMAL)); + } + + public function testSendLowPriorityFeeTxRandomizeChange() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $addr = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $spk = "a9143a0fbfa2f446af8d76ac7f174618e7448674606987"; + $value = BlocktrailSDK::toSatoshi(1.0); + $pay = [$addr => $value]; + + $this->shouldCoinSelect($client, $identifier, $value, $spk, [ + [ + 'scriptpubkey_hex' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'path' => 'M/9999\'/0/0', + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ], + ], true, true, Wallet::FEE_STRATEGY_LOW_PRIORITY); + + $partiallySignedTx = "0100000001ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e00000000b40047304402201b13751d27f736b27c771bd9b043ef8f976ef797ddcff38074c667bee5c8879f0220661f6fe754949d864228a9a495589a7e1ade9adccdbfc323d8f01d342f210b04014c695221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153aeffffffff0200e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e7448674606987b7e1a4350000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698700000000"; + $baseTx = $partiallySignedTx; + $coSignedTx = "ok"; + + // randomize change will shuffle + $client->shouldReceive('shuffle') + ->andReturns() + ->once(); + + $client->shouldReceive('sendTransaction') + ->withArgs([ + $identifier, + ['signed_transaction' => $partiallySignedTx, 'base_transaction' => $baseTx], + ["M/9999'/0/0"], + true + ]) + ->andReturn($coSignedTx) + ->once(); + + $this->assertEquals($coSignedTx, $wallet->pay($pay, $addr, true, true, Wallet::FEE_STRATEGY_LOW_PRIORITY)); + } + + public function testSendForceFeeTx() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $addr = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $spk = "a9143a0fbfa2f446af8d76ac7f174618e7448674606987"; + $value = BlocktrailSDK::toSatoshi(1.0); + $fee = BlocktrailSDK::toSatoshi(0.5); + $pay = [$addr => $value]; + + $this->shouldCoinSelect($client, $identifier, $value, $spk, [ + [ + 'scriptpubkey_hex' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'path' => 'M/9999\'/0/0', + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ], + ], true, true, Wallet::FEE_STRATEGY_FORCE_FEE, $fee); + + $partiallySignedTx = "0100000001ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e00000000b500483045022100e7201b73be42d0dfa2e9ad6190b6bd787ea4c8b1ed0282192f5b0c6c633c5d910220505a02281d4ac4c7f95a0cfc20e9b82ae9cff26b8bdb055cab689aba2adb34c2014c695221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153aeffffffff0200e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698780f8a9320000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698700000000"; + $baseTx = $partiallySignedTx; + $coSignedTx = "ok"; + + // randomize change will shuffle + $client->shouldReceive('shuffle') + ->andReturns() + ->once(); + + $client->shouldReceive('sendTransaction') + ->withArgs([ + $identifier, + ['signed_transaction' => $partiallySignedTx, 'base_transaction' => $baseTx], + ["M/9999'/0/0"], + true + ]) + ->andReturn($coSignedTx) + ->once(); + + $this->assertEquals($coSignedTx, $wallet->pay($pay, $addr, true, true, Wallet::FEE_STRATEGY_FORCE_FEE, $fee)); + } + + public function testSendToBech32() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $pubKeyHash = Buffer::hex('5d6f02f47dc6c57093df246e3742cfe1e22ab410'); + $wp = WitnessProgram::v0($pubKeyHash); + $addr = new SegwitAddress($wp); + + $value = BlocktrailSDK::toSatoshi(1.0); + + $builder = (new TransactionBuilder($wallet->getAddressReader())) + ->addRecipient($addr->getAddress(), $value) + ->setFeeStrategy(Wallet::FEE_STRATEGY_OPTIMAL) + ->setChangeAddress("2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"); + + $this->shouldCoinSelect($client, $identifier, $value, $wp->getScript()->getHex(), [ + [ + 'scriptpubkey_hex' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'path' => 'M/9999\'/0/0', + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ], + ]); + + $builder = $wallet->coinSelectionForTxBuilder($builder); + $this->assertTrue(count($builder->getUtxos()) > 0); + $this->assertTrue(count($builder->getOutputs()) <= 2); + + /** @var TransactionInterface $tx */ + /** @var SignInfo $signInfo */ + list ($tx, $signInfo) = $wallet->buildTx($builder); + $this->assertEquals("0100000001ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e0000000000ffffffff0200e1f505000000001600145d6f02f47dc6c57093df246e3742cfe1e22ab41078daa4350000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698700000000", + $tx->getHex()); + } + + public function testOpreturn() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $addr = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $spk = "a9143a0fbfa2f446af8d76ac7f174618e7448674606987"; + $value = BlocktrailSDK::toSatoshi(1.0); + + $moon = "MOOOOOOOOOOOOON!"; + $builder = new TransactionBuilder($wallet->getAddressReader()); + $builder->randomizeChangeOutput(false); + $builder->addRecipient($addr, $value); + $builder->addOpReturn($moon); + $builder->setChangeAddress("2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"); + + $this->shouldCoinSelect($client, $identifier, [ + ['value' => $value, 'scriptPubKey' => $spk], + ['value' => 0, 'scriptPubKey' => "6a104d4f4f4f4f4f4f4f4f4f4f4f4f4f4e21"], + ], null, [ + [ + 'scriptpubkey_hex' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'path' => 'M/9999\'/0/0', + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ], + ]); + + $builder = $wallet->coinSelectionForTxBuilder($builder); + $this->assertTrue(count($builder->getUtxos()) > 0); + $this->assertTrue(count($builder->getOutputs()) <= 2); + + /** @var TransactionInterface $tx */ + /** @var SignInfo $signInfo */ + list ($tx, $signInfo) = $wallet->buildTx($builder); + $this->assertEquals("0100000001ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e0000000000ffffffff0300e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e74486746069870000000000000000126a104d4f4f4f4f4f4f4f4f4f4f4f4f4f4e2160d9a4350000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698700000000", + $tx->getHex()); + } + + public function testSendSegwitTx() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $addr = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $spk = "a9143a0fbfa2f446af8d76ac7f174618e7448674606987"; + $value = BlocktrailSDK::toSatoshi(1.0); + $pay = [$addr => $value]; + + $this->shouldCoinSelect($client, $identifier, $value, $spk, [ + [ + 'scriptpubkey_hex' => 'a9140c84184d6d00096482c1359ce0194376ad248d2287', + 'path' => "M/9999'/2/0", + 'address' => '2MtPQLhhzFNtaY59QQ9wt28a5qyQ2t8rnvd', + 'redeem_script' => '00204679ecfef34eb556071e349442e74837d059cb42ef22c4946a2efbb0c9041e68', + 'witness_script' => '522102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c121766353021028e2ec167fbdd100fcb95e22ce4b49d4733b1f291eef6b5f5748852c96aa9522721038ba8bf95fc3449c3b745232eeca5493fd3277b801e659482cc868ca6e55ce23b53ae', + ], + ]); + + $partiallySignedTx = "01000000000101ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e00000000232200204679ecfef34eb556071e349442e74837d059cb42ef22c4946a2efbb0c9041e68ffffffff0200e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698790e0a4350000000017a9143a0fbfa2f446af8d76ac7f174618e7448674606987030047304402206e4ce61afeb5cfa06a1d7dcc7840eba65d3d4f47337ba29c74e73ff9adff8b6402207ed98b1e09cda1517b885ae01f0e2edc2d5dff37ca204a0f170c45622fe919140169522102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c121766353021028e2ec167fbdd100fcb95e22ce4b49d4733b1f291eef6b5f5748852c96aa9522721038ba8bf95fc3449c3b745232eeca5493fd3277b801e659482cc868ca6e55ce23b53ae00000000"; + $baseTx = "0100000001ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e00000000232200204679ecfef34eb556071e349442e74837d059cb42ef22c4946a2efbb0c9041e68ffffffff0200e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698790e0a4350000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698700000000"; + $coSignedTx = "ok"; + + $client->shouldReceive('sendTransaction') + ->withArgs([ + $identifier, + ['signed_transaction' => $partiallySignedTx, 'base_transaction' => $baseTx], + ["M/9999'/2/0"], + true + ]) + ->andReturn($coSignedTx) + ->once(); + + $this->assertEquals($coSignedTx, $wallet->pay($pay, $addr, false, false, Wallet::FEE_STRATEGY_OPTIMAL)); + } + + public function testSpendMixedUtxoTypes() + { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $addr = "2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa"; + $spk = "a9143a0fbfa2f446af8d76ac7f174618e7448674606987"; + $value = BlocktrailSDK::toSatoshi(1.0); + $pay = [$addr => $value]; + + $this->shouldCoinSelect($client, $identifier, $value, $spk, [ + [ + 'scriptpubkey_hex' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'path' => 'M/9999\'/0/0', + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ], [ + 'scriptpubkey_hex' => 'a9140c84184d6d00096482c1359ce0194376ad248d2287', + 'path' => "M/9999'/2/0", + 'address' => '2MtPQLhhzFNtaY59QQ9wt28a5qyQ2t8rnvd', + 'redeem_script' => '00204679ecfef34eb556071e349442e74837d059cb42ef22c4946a2efbb0c9041e68', + 'witness_script' => '522102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c121766353021028e2ec167fbdd100fcb95e22ce4b49d4733b1f291eef6b5f5748852c96aa9522721038ba8bf95fc3449c3b745232eeca5493fd3277b801e659482cc868ca6e55ce23b53ae', + ], + ]); + + $partiallySignedTx = "01000000000102ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e00000000b40047304402204e0026b03617602050165d4f9420b5b057208338484ade85a129aa0643ce66af022026b6b0f84f03b62ba46223a38dfd771ea308069aeb1c61823010d098fe44f9bd014c695221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153aeffffffffec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e01000000232200204679ecfef34eb556071e349442e74837d059cb42ef22c4946a2efbb0c9041e68ffffffff0200e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e7448674606987f69e3f710000000017a9143a0fbfa2f446af8d76ac7f174618e7448674606987000300473044022038a11f1d423ccf708e474f4c8cbbd57982e7c24c966f1a5fb56b6696dc4f3b4c022019937754e55a622ac083ffd0f3d30f818d1481be3cebb1e94bbac859feb59bad0169522102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c121766353021028e2ec167fbdd100fcb95e22ce4b49d4733b1f291eef6b5f5748852c96aa9522721038ba8bf95fc3449c3b745232eeca5493fd3277b801e659482cc868ca6e55ce23b53ae00000000"; + $baseTx = "0100000002ec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e00000000b40047304402204e0026b03617602050165d4f9420b5b057208338484ade85a129aa0643ce66af022026b6b0f84f03b62ba46223a38dfd771ea308069aeb1c61823010d098fe44f9bd014c695221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153aeffffffffec7758e4eeacd0993baa03fec37d3b33b401cac7fd2680f79cb230d467e57e6e01000000232200204679ecfef34eb556071e349442e74837d059cb42ef22c4946a2efbb0c9041e68ffffffff0200e1f5050000000017a9143a0fbfa2f446af8d76ac7f174618e7448674606987f69e3f710000000017a9143a0fbfa2f446af8d76ac7f174618e744867460698700000000"; + $coSignedTx = "ok"; + + $client->shouldReceive('sendTransaction') + ->withArgs([ + $identifier, + ['signed_transaction' => $partiallySignedTx, 'base_transaction' => $baseTx], + ["M/9999'/0/0", "M/9999'/2/0"], + true + ]) + ->andReturn($coSignedTx) + ->once(); + + $this->assertEquals($coSignedTx, $wallet->pay($pay, $addr, false, false, Wallet::FEE_STRATEGY_OPTIMAL)); + + } +} diff --git a/tests/Wallet/UpgradeKeyIndexWalletTest.php b/tests/Wallet/UpgradeKeyIndexWalletTest.php new file mode 100644 index 0000000..23cc079 --- /dev/null +++ b/tests/Wallet/UpgradeKeyIndexWalletTest.php @@ -0,0 +1,52 @@ +mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $client->shouldReceive('upgradeKeyIndex') + ->withArgs([$identifier, 10000, ["tpubD8Ke9MXfpW2zy79D86PwryY3rt6FdHmFg1V3T666ieNSaLrwXXAG8VwrTqR8oX553FoQMH9WdY3Q4qCMP9Uc23GXrJV9tRLySTAt4WVEox5", "M/10000'"]]) + ->andReturn([ + 'blocktrail_public_keys' => [ + 9999 => ['tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ', "M/9999'"], + 10000 => ['tpubD9m9hziKhYQExWgzMUNXdYMNUtourv96sjTUS9jJKdo3EDJAnCBJooMPm6vGSmkNTNAmVt988dzNfNY12YYzk9E6PkA7JbxYeZBFy4XAaCp', "M/10000'"], + ], + ]) + ->once(); + + $wallet->upgradeKeyIndex(10000); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Wallet needs to be unlocked to upgrade key index + */ + public function testUpgradeKeyIndexRequiresUnlock() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier, null, false, [ + 'readonly' => true, + ]); + + $wallet->upgradeKeyIndex(10000); + } +} diff --git a/tests/WalletPathTest.php b/tests/Wallet/WalletPathTest.php similarity index 97% rename from tests/WalletPathTest.php rename to tests/Wallet/WalletPathTest.php index 7570dcb..f24afd9 100644 --- a/tests/WalletPathTest.php +++ b/tests/Wallet/WalletPathTest.php @@ -1,6 +1,6 @@ makePartial(); + + $client->shouldReceive('feePerKB') + ->andReturn([ + Wallet::FEE_STRATEGY_HIGH_PRIORITY => 40000, + Wallet::FEE_STRATEGY_LOW_PRIORITY => 10000, + Wallet::FEE_STRATEGY_OPTIMAL => 20000, + ]); + + // this makes debugging a lot easier + if (\getenv('BLOCKTRAIL_VAR_DUMP_MOCK')) { + foreach (['getWallet', + 'sendTransaction', + 'coinSelection', + 'storeNewWalletV1', + 'storeNewWalletV2', + 'storeNewWalletV3', + '_getNewDerivation', + 'upgradeKeyIndex', + 'walletTransactions', + 'setupWalletWebhook', + 'deleteWalletWebhook', + 'feePerKB' + ] as $method) { + + $client->shouldReceive($method) + ->withArgs(function () use($method) { + var_dump($method, \func_get_args()); + return false; + }); + } + } + + return $client; + } + + /** + * @param BlocktrailSDK|Mock $client + * @param string $identifier + * @param string $password + * @param bool $segwit + * @param array $options + * @return array + * @throws \Exception + */ + protected function initWallet($client, $identifier = self::WALLET_IDENTIFIER, $password = self::WALLET_PASSWORD, $segwit = false, $options = []) { + $this->shouldGetWalletV3($client, $identifier, $segwit); + + $options = array_merge([ + 'identifier' => $identifier, + 'password' => $password, + ], (array)$options); + + if ($password === null) { + unset($options['password']); + } + + /** @var Wallet $wallet */ + $wallet = $client->initWallet($options); + + return [$wallet, $client]; + } + + /** + * @param BlocktrailSDK|Mock $client + * @param $identifier + * @param bool $segwit + */ + protected function shouldGetWalletV3($client, $identifier, $segwit = false) { + $client->shouldReceive('getWallet') + ->withArgs([$identifier]) + ->andReturn([ + 'primary_mnemonic' => NULL, + 'encrypted_secret' => 'CgAAAAAAAAAAAAC4iAAAAAAAAAAAAAAAAAAAAAAAADOe7reb5jZH+CmNxEtRtYQzMp/cZPAigKzZWo9yYxOqD4smZbROEj5zn8aeek5ZfA==', + 'encrypted_primary_seed' => 'CgAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAHp305+tX2NFSGsQcEiJFpHFXg8L6GJyBt0Zzr7M+KsD5P1zH0PgehzhxdIJZzmHlA==', + 'wallet_version' => 'v3', + 'key_index' => 9999, + 'checksum' => 'mgGXvgCUxJMt1mf9vdzdyPaJjmaKVHAtkZ', + 'segwit' => $segwit, + 'backup_public_key' => ['tpubD6NzVbkrYhZ4WTekiCroqzjETeQbxvQy8azRH4MT3tKhzZvf8G2QWWFdrTaiTHdYJVNPJJ85nvHjhP9dP7CZAkKsBEJ95DuY7bNZH1Gvw4w','M'], + 'blocktrail_public_keys' => [ + 9999 => ['tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ', 'M/9999\''], + ], + 'primary_public_keys' => [ + 9999 => ['tpubD8Ke9MXfpW2zvymTPJAxETZE3KnBdsTwjNB3sHRj2iUJMPpGTMbZHyJPuMASFzJC4FbEeCg5r7N7HN36oPwibL9mWTRx9d2J6VPhsH9NEAS', 'M/9999\''], + ], + 'upgrade_key_index' => NULL, + ]) + ->once(); + } + + /** + * @param BlocktrailSDK|Mock $client + * @param string $identifier + * @param string $password + * @param bool $segwit + * @param array $options + * @return array + * @throws \Exception + */ + protected function initWalletV1($client, $identifier = self::WALLET_IDENTIFIER, $password = self::WALLET_PASSWORD, $segwit = false, $options = []) { + $client->shouldReceive('getWallet') + ->withArgs([$identifier]) + ->andReturn([ + 'primary_mnemonic' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon achieve atom harvest help frame very curve razor muscle spawn', + 'wallet_version' => 'v1', + 'key_index' => 9999, + 'checksum' => 'n3yffrKj4xReaWjt5pdLoe7ZoBwe1UGTm8', + 'segwit' => false, + 'backup_public_key' => ['tpubD6NzVbkrYhZ4Wg9K1EjgX5F9rCi3nMjpMsyMZGyketABh8Y2Cfn92dfHAWEffZB3VJAZS2rd5iYTGfyiW32dEzWCnxubrDbbLHig3JGcvEZ','M'], + 'blocktrail_public_keys' => [ + 9999 => ['tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ', 'M/9999\''], + ], + 'primary_public_keys' => [ + 9999 => ['tpubDA5AEZ2jW8afwPeKGwNGb52ydoNVtYRTiPsxtyw7i6eJ2qBwzhXrR9mRV1AkBPkYzPMApdiiGPtJ6CetcYzZkWAHVWxk4hsZU9vc1XWS8aa', 'M/9999\''], + ], + 'upgrade_key_index' => NULL, + ]) + ->once(); + + $options = array_merge([ + 'identifier' => $identifier, + 'password' => $password, + ], (array)$options); + + if ($password === null) { + unset($options['password']); + } + + /** @var Wallet $wallet */ + $wallet = $client->initWallet($options); + + return [$wallet, $client]; + } + + /** + * @param Mock|BlocktrailSDK $client + * @param string $identifier + * @param string $pathInput + * @param string $pathResult + * @throws \Exception + */ + protected function shouldGetNewDerivation($client, $identifier, $pathInput, $pathResult) { + switch (\strtoupper($pathResult)) { + case "M/9999'/0/0": + $res = [ + 'path' => "M/9999'/0/0", + 'address' => '2MxYE3e7R2e1NBLDicBMXMy9FRUygeTyEGa', + 'scriptpubkey' => 'a9143a0fbfa2f446af8d76ac7f174618e7448674606987', + 'redeem_script' => '5221031cc2f421316d93aab290c82cd01ca53a19a82c6bd4285a68b6f12659626f834d210334eea53fc0b0a6261e7e31a71437924779c5460435ebf79428f204b3ef791e37210342a88cc5467c9846b86fc37ac97823aebd3d754470cf7786fe6605674366120153ae', + 'witness_script' => null, + ]; + break; + case "M/9999'/0/1": + $res = [ + 'path' => "M/9999'/0/1", + 'address' => '2N38FErCcqDq1sqKAoQPkFtBKEosjTe4uaz', + 'scriptpubkey' => 'a9146c5f63f72c26725c100bc6ab0156e0dd36ffc8a087', + 'redeem_script' => '522102382362e09b4f9e15ba16397fc3b4c4caaf82da29819c5decf1f4d650bf088adf2102453a9ebff02859cb285987a2b14da82a05f87250bca5ba994e05dec4a6c7a6df21033dcb8536d7e0c6a3a556f07accd55262dd4f452d4ca702e100557be6c26b096453ae', + 'witness_script' => null, + ]; + break; + case "M/9999'/1/0": + $res = [ + 'path' => "M/9999'/1/0", + 'address' => '2MtkfTCz3xYTzfU4LS4PZGtTaDgzRf1nZnT', + 'scriptpubkey' => 'a914108971f33a5fd5a33df0df690592c25f45e5a97e87', + 'redeem_script' => '522103df848467aa6f6ab3982dd20556d0c27dc187e7fb7a7d5c10c66e6d05389f7a572103e33daa795617e9075140e0db47a5a77371c0e9c8fe6571af24272c7f0a2130be2103e98c09c808f8934442897eebb396a3718d889186fcd5b7f075d5238acdad2b5853ae', + 'witness_script' => null, + ]; + break; + case "M/9999'/2/0": + $res = [ + 'path' => "M/9999'/2/0", + 'address' => '2MtPQLhhzFNtaY59QQ9wt28a5qyQ2t8rnvd', + 'scriptpubkey' => 'a9140c84184d6d00096482c1359ce0194376ad248d2287', + 'redeem_script' => '00204679ecfef34eb556071e349442e74837d059cb42ef22c4946a2efbb0c9041e68', + 'witness_script' => '522102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c121766353021028e2ec167fbdd100fcb95e22ce4b49d4733b1f291eef6b5f5748852c96aa9522721038ba8bf95fc3449c3b745232eeca5493fd3277b801e659482cc868ca6e55ce23b53ae', + ]; + break; + default: + throw new \Exception("Unsupported path {$pathResult}"); + } + + $client->shouldReceive('_getNewDerivation') + ->withArgs([$identifier, $pathInput]) + ->andReturn($res) + ->once(); + } + + protected function checkP2sh(ScriptInterface $spk, ScriptInterface $rs) { + $spkData = (new OutputClassifier())->decode($spk); + $this->assertEquals(ScriptType::P2SH, $spkData->getType()); + + $scriptHash = $rs->getScriptHash(); + $this->assertTrue($spkData->getSolution()->equals($scriptHash)); + } + + protected function checkP2wsh(ScriptInterface $wp, ScriptInterface $ws) { + $spkData = (new OutputClassifier())->decode($wp); + $this->assertEquals(ScriptType::P2WSH, $spkData->getType()); + + $scriptHash = $ws->getWitnessScriptHash(); + $this->assertTrue($spkData->getSolution()->equals($scriptHash)); + } + + protected function isBase58Address(WalletScript $script) { + + try { + $addr = $script->getAddress(); + $ok = $addr instanceof PayToPubKeyHashAddress || $addr instanceof ScriptHashAddress; + } catch (\Exception $e) { + $ok = false; + } + + $this->assertTrue($ok, "address should be a base58 type"); + + } + + protected function isCashAddress(WalletScript $script) { + + try { + $addr = $script->getAddress(); + $ok = $addr instanceof CashAddress; + } catch (\Exception $e) { + $ok = false; + } + + $this->assertTrue($ok, "address should be a cashaddr type"); + + } + + /** + * @param BlocktrailSDK|Mock $client + * @param $identifier + * @param int|array $value + * @param string|null $spk + * @param $utxos + * @param bool $lockUTXOs + * @param bool $allowZeroConf + * @param string $feeStrategy + * @param null $forceFee + */ + protected function shouldCoinSelect( + $client, $identifier, + $value, $spk, + $utxos, + $lockUTXOs = true, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null + ) { + if ($spk === null && is_array($value)) { + $pay = $value; + } else { + $pay = [['value' => $value, 'scriptPubKey' => $spk]]; + } + + $retUtxos = []; + foreach ($utxos as $i => $utxo) { + $retUtxos[$i] = array_merge([ + 'hash' => '6e7ee567d430b29cf78026fdc7ca01b4333b7dc3fe03aa3b99d0aceee45877ec', + 'idx' => $i, + 'value' => 1000000000, + 'confirmations' => 10, + 'green' => false, + ], $utxo); + } + + $client->shouldReceive('coinSelection') + ->withArgs([$identifier, $pay, $lockUTXOs, $allowZeroConf, $feeStrategy, $forceFee]) + ->andReturn([ + 'utxos' => $retUtxos, + 'size' => 366, + 'fee' => 3660, + 'fees' => [ + 'low_priority' => 5000, + 'optimal' => 10000, + 'high_priority' => 12000, + ], + 'change' => 1000000000 - array_sum(\array_column($pay, 'value')) - 3660, + 'lock_time' => 7.2, + ]) + ->once(); + } +} diff --git a/tests/Wallet/WalletTxListTest.php b/tests/Wallet/WalletTxListTest.php new file mode 100644 index 0000000..aff5eec --- /dev/null +++ b/tests/Wallet/WalletTxListTest.php @@ -0,0 +1,32 @@ +mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $client->shouldReceive('walletTransactions') + ->withArgs([$identifier, 5, 36, 'desc']) + ->andReturn([]) + ->once(); + + $wallet->transactions(5, 36, 'desc'); + } +} diff --git a/tests/Wallet/WalletWebhookTest.php b/tests/Wallet/WalletWebhookTest.php new file mode 100644 index 0000000..2ed044b --- /dev/null +++ b/tests/Wallet/WalletWebhookTest.php @@ -0,0 +1,79 @@ +mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $client->shouldReceive('setupWalletWebhook') + ->withArgs([$identifier, 'WALLET-mywallet', 'http://example.com']) + ->andReturn([]) + ->once(); + + $wallet->setupWebhook('http://example.com'); + } + + public function testSetupWalletWebhookCustom() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $client->shouldReceive('setupWalletWebhook') + ->withArgs([$identifier, 'gg', 'http://example.com']) + ->andReturn([]) + ->once(); + + $wallet->setupWebhook('http://example.com', 'gg'); + } + public function testDeleteWalletWebhookDefault() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $client->shouldReceive('deleteWalletWebhook') + ->withArgs([$identifier, 'WALLET-mywallet']) + ->andReturn([]) + ->once(); + + $wallet->deleteWebhook(); + } + + public function testDeleteWalletWebhookCustom() { + $client = $this->mockSDK(); + + $identifier = self::WALLET_IDENTIFIER; + /** @var Wallet $wallet */ + /** @var BlocktrailSDK|Mock $client */ + list($wallet, $client) = $this->initWallet($client, $identifier); + + $client->shouldReceive('deleteWalletWebhook') + ->withArgs([$identifier, 'gg']) + ->andReturn([]) + ->once(); + + $wallet->deleteWebhook('gg'); + } +} diff --git a/tests/WalletTest.php b/tests/WalletTest.php deleted file mode 100644 index 30ec7d9..0000000 --- a/tests/WalletTest.php +++ /dev/null @@ -1,1918 +0,0 @@ -setupBlocktrailSDK(); - - // wallet used for testing sending a transaction - // - we reuse this wallet - // $this->createTransactionTestWallet($client); - - // wallet used for - // * testing keyIndex upgrade - // * bad password - // - we recreate this wallet everytime because we need to be able to upgrade it again - // $this->createKeyIndexUpgradeTestWallet($client, "unittest-discovery"); - } - - protected function createKeyIndexUpgradeTestWallet(BlocktrailSDKInterface $client, $identifier, $passphrase = "password") { - $primaryMnemonic = "give pause forget seed dance crawl situate hole kingdom"; - $backupMnemonic = "give pause forget seed dance crawl situate hole course"; - - return $this->_createTestWallet($client, $identifier, $passphrase, $primaryMnemonic, $backupMnemonic); - } - - protected function createTransactionTestWallet(BlocktrailSDKInterface $client, $identifier = "unittest-transaction") { - $primaryMnemonic = "give pause forget seed dance crawl situate hole keen"; - $backupMnemonic = "give pause forget seed dance crawl situate hole give"; - $passphrase = "password"; - - return $this->_createTestWallet($client, $identifier, $passphrase, $primaryMnemonic, $backupMnemonic); - } - - protected function _createTestWallet(BlocktrailSDKInterface $client, $identifier, $passphrase, $primaryMnemonic, $backupMnemonic, $readOnly = false) { - $walletPath = WalletPath::create(9999); - - $secret = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; - $encryptedSecret = CryptoJSAES::encrypt($secret, $passphrase); - - // still using BIP39 to get seedhex to keep all fixtures the same - $seed = (new Bip39SeedGenerator())->getSeed($primaryMnemonic, $passphrase); - $primaryPrivateKey = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($seed), "m"); - - $primaryPublicKey = $primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath()); - $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($seed->getBinary()), $secret); - - // still using BIP39 to get seedhex to keep all fixtures the same - $backupPrivateKey = BIP32Key::create(HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, "")), "m"); - $backupPublicKey = $backupPrivateKey->buildKey("M"); - - $testnet = true; - - $checksum = $primaryPrivateKey->publicKey()->getAddress()->getAddress(); - - $result = $client->storeNewWalletV2( - $identifier, - $primaryPublicKey->tuple(), - $backupPublicKey->tuple(), - $encryptedPrimarySeed, - $encryptedSecret, - false, - $checksum, - 9999 - ); - - $blocktrailPublicKeys = $result['blocktrail_public_keys']; - $keyIndex = $result['key_index']; - - // todo: this hardcodes bitcoin, and testnet, despite the SDK client being initialized outside this function - $wallet = new WalletV2( - $client, - $identifier, - $encryptedPrimarySeed, - $encryptedSecret, - [$keyIndex => $primaryPublicKey], - $backupPublicKey, - $blocktrailPublicKeys, - $keyIndex, - 'bitcoin', - $testnet, - false, - new BitcoinAddressReader(), - $checksum - ); - - if (!$readOnly) { - $wallet->unlock(['password' => $passphrase]); - } - - return $wallet; - } - - public function testBIP32() { - $masterkey = HierarchicalKeyFactory::fromExtended("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ"); - - $this->assertEquals("022f6b9339309e89efb41ecabae60e1d40b7809596c68c03b05deb5a694e33cd26", $masterkey->getPublicKey()->getHex()); - $this->assertEquals("tpubDAtJthHcm9MJwmHp4r2UwSTmiDYZWHbQUMqySJ1koGxQpRNSaJdyL2Ab8wwtMm5DsMMk3v68299LQE6KhT8XPQWzxPLK5TbTHKtnrmjV8Gg", $masterkey->derivePath("0")->toExtendedKey()); - $this->assertEquals("tpubDDfqpEKGqEVa5FbdLtwezc6Xgn81teTFFVA69ZfJBHp4UYmUmhqVZMmqXeJBDahvySZrPjpwMy4gKfNfrxuFHmzo1r6srB4MrsDKWbwEw3d", $masterkey->derivePath("0/0")->toExtendedKey()); - - $this->assertEquals( - "tpubDHNy3kAG39ThyiwwsgoKY4iRenXDRtce8qdCFJZXPMCJg5dsCUHayp84raLTpvyiNA9sXPob5rgqkKvkN8S7MMyXbnEhGJMW64Cf4vFAoaF", - HierarchicalKeyFactory::fromEntropy(Buffer::hex("000102030405060708090a0b0c0d0e0f"))->derivePath("M/0'/1/2'/2/1000000000")->toExtendedPublicKey() - ); - } - - public function testCreateWallet() { - $client = $this->setupBlocktrailSDK(); - - $identifier = $this->getRandomTestIdentifier(); - $wallet = $this->createTransactionTestWallet($client, $identifier); - $this->cleanupData['wallets'][] = $wallet; // store for cleanup - - $wallets = $client->allWallets(); - $this->assertTrue(count($wallets) > 0); - - $this->assertEquals($identifier, $wallet->getIdentifier()); - $this->assertEquals("M/9999'", $wallet->getBlocktrailPublicKeys()[9999][1]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKeys()[9999][0]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("m/9999'")->key()->toExtendedKey()); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("M/9999'")->key()->toExtendedKey()); - - // get a new pair - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertEquals("M/9999'/0/0", $path); - $this->assertEquals("2MzyKviSL6pnWxkbHV7ecFRE3hWKfzmT8WS", $address); - - // get another new pair - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertEquals("M/9999'/0/1", $path); - $this->assertEquals("2N65RcfKHiKQcPGZAA2QVeqitJvAQ8HroHD", $address); - - // get another new path, directly from the SDK - $this->assertEquals("M/9999'/0/2", $client->getNewDerivation($wallet->getIdentifier(), "M/9999'/0")); - - // get the 2nd address again - $this->assertEquals("2N65RcfKHiKQcPGZAA2QVeqitJvAQ8HroHD", $wallet->getAddressByPath("M/9999'/0/1")); - - // get some more addresses - $this->assertEquals("2MynrezSyqCq1x5dMPtRDupTPA4sfVrNBKq", $wallet->getAddressByPath("M/9999'/0/6")); - $this->assertEquals("2N5eqrZE7LcfRyCWqpeh1T1YpMdgrq8HWzh", $wallet->getAddressByPath("M/9999'/0/44")); - - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertTrue(strpos($path, "M/9999'/0/") === 0); - $this->assertEquals($address, AddressFactory::fromString($address)->getAddress()); - - } - - private function checkP2sh(ScriptInterface $spk, ScriptInterface $rs) { - $spkData = (new OutputClassifier())->decode($spk); - $this->assertEquals(ScriptType::P2SH, $spkData->getType()); - - $scriptHash = $rs->getScriptHash(); - $this->assertTrue($spkData->getSolution()->equals($scriptHash)); - } - - private function checkP2wsh(ScriptInterface $wp, ScriptInterface $ws) { - $spkData = (new OutputClassifier())->decode($wp); - $this->assertEquals(ScriptType::P2WSH, $spkData->getType()); - - $scriptHash = $ws->getWitnessScriptHash(); - $this->assertTrue($spkData->getSolution()->equals($scriptHash)); - } - - private function isBase58Address(WalletScript $script) { - - try { - $addr = $script->getAddress(); - $ok = $addr instanceof PayToPubKeyHashAddress || $addr instanceof ScriptHashAddress; - } catch (\Exception $e) { - $ok = false; - } - - $this->assertTrue($ok, "address should be a base58 type"); - - } - - public function testNormalizeOutputStruct() { - $expected = [['address' => 'address1', 'value' => 'value1'], ['address' => 'address2', 'value' => 'value2']]; - - $this->assertEquals($expected, Wallet::normalizeOutputsStruct(['address1' => 'value1', 'address2' => 'value2'])); - $this->assertEquals($expected, Wallet::normalizeOutputsStruct([['address1', 'value1'], ['address2', 'value2']])); - $this->assertEquals($expected, Wallet::normalizeOutputsStruct($expected)); - - // duplicate address - $expected = [['address' => 'address1', 'value' => 'value1'], ['address' => 'address1', 'value' => 'value2']]; - - // not possible, keyed by address - // $this->assertEquals($expected, Wallet::normalizeOutputsStruct(['address1' => 'value1', 'address1' => 'value2'])); - $this->assertEquals($expected, Wallet::normalizeOutputsStruct([['address1', 'value1'], ['address1', 'value2']])); - $this->assertEquals($expected, Wallet::normalizeOutputsStruct($expected)); - } - - public function testChecksBackupKey() { - $identifier = "unittest-transaction"; - $password = 'password'; - - $client = $this->setupBlocktrailSDK(); - - $backupMnemonic = "give pause forget seed dance crawl situate hole give"; - $backupPrivateKey = BIP32Key::create(HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, "")), "m"); - $backupKey = $backupPrivateKey->buildKey("M")->key()->toExtendedPublicKey(); - - try { - $client->initWallet([ - 'identifier' => $identifier, - 'password' => $password, - 'check_backup_key' => [] - ]); - $this->fail("this should have caused an exception"); - } catch (\Exception $e) { - $this->assertEquals("check_backup_key should be a string (the xpub)", $e->getMessage()); - } - - try { - $client->initWallet([ - 'identifier' => $identifier, - 'password' => $password, - 'check_backup_key' => 'for demonstration purposes only' - ]); - $this->fail("test should have caused an exception"); - } catch (\Exception $e) { - $this->assertEquals("Backup key returned from server didn't match our own", $e->getMessage()); - } - - try { - $init = $client->initWallet([ - 'identifier' => $identifier, - 'password' => $password, - ]); - $this->assertEquals($backupKey, $init->getBackupKey()[0]); - } catch (\Exception $e) { - $this->fail("this should not fail"); - } - - try { - $init = $client->initWallet([ - 'identifier' => $identifier, - 'password' => $password, - 'check_backup_key' => $backupKey, - ]); - $this->assertEquals($backupKey, $init->getBackupKey()[0]); - } catch (\Exception $e) { - $this->fail("this should not fail"); - } - } - - /** - * this test requires / asumes that the test wallet it uses contains a balance - * - * we keep the wallet topped off with some coins, - * but if some funny guy ever empties it or if you use your own API key to run the test then it needs to be topped off again - * - * @throws \Exception - */ - public function testSegwitWalletTransaction() - { - $client = $this->setupBlocktrailSDK(); - - // This test was edited so it works out - // even if segwit isn't enabled yet - - /** @var Wallet $segwitwallet */ - $segwitwallet = $client->initWallet([ - "identifier" => "unittest-transaction-sw", - "passphrase" => "password" - ]); - - $this->assertTrue($segwitwallet->isSegwit()); - - list ($path, $address) = $segwitwallet->getNewAddressPair(); - $addrObj = AddressFactory::fromString($address); - - // Send back to unittest-transaction-sw - - $rand = random_int(1, 3); - - $builder = (new TransactionBuilder($segwitwallet->getAddressReader())) - ->addRecipient($address, BlocktrailSDK::toSatoshi(0.015) * $rand) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE); - - $builder = $segwitwallet->coinSelectionForTxBuilder($builder); - - $this->assertTrue(count($builder->getUtxos()) > 0); - $this->assertEquals(1, count($builder->getOutputs())); - - $txHash = $segwitwallet->sendTx($builder); - - $tx = $this->getTx($client, $txHash); - - $this->assertTrue(count($tx['outputs']) === 1 || count($tx['outputs']) === 2); - - $checkChange = count($tx['outputs']) > 1; - - $changeIdx = null; - $utxoIdx = null; - foreach ($tx['outputs'] as $i => $output) { - if ($output['address'] === $address) { - $utxoIdx = $i; - } - if ($checkChange && $output['address'] !== $address) { - $changeIdx = $i; - } - } - - if ($checkChange) { - $pathForChangeAddr = $segwitwallet->getPathForAddress($tx['outputs'][$changeIdx]['address']); - $this->assertTrue(strpos("M/9999'/" . Wallet::CHAIN_BTC_SEGWIT . "/", $pathForChangeAddr) !== -1); - } - - $addresses = implode(" ", array_column($tx['outputs'], 'address')); - $this->assertNotNull($utxoIdx, "should have found utxo paying {$address} in $addresses"); - - $utxo = $tx['outputs'][$utxoIdx]; - - $spendSegwit = (new TransactionBuilder($segwitwallet->getAddressReader())) - ->addRecipient($segwitwallet->getNewAddress(), BlocktrailSDK::toSatoshi(0.010) * $rand) - ->spendOutput($tx['hash'], $utxoIdx, $utxo['value'], $utxo['address'], $utxo['script_hex'], $path) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE); - - $txid = $segwitwallet->sendTx($spendSegwit); - - $this->assertTrue(strlen($txid) === 64); - - $tx = $this->getTx($client, $txid); - $this->assertEquals(1, count($tx['inputs'])); - - } - - /** - * this test requires / asumes that the test wallet it uses contains a balance - * - * we keep the wallet topped off with some coins, - * but if some funny guy ever empties it or if you use your own API key to run the test then it needs to be topped off again - * - * @throws \Exception - */ - public function testCheckRejectsInvalidChainINdex() - { - $client = $this->setupBlocktrailSDK(); - - $unittestWallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "passphrase" => "password" - ]); - - $exception = BlocktrailSDKException::class; - $msg = "Chain index is invalid - should be an integer"; - $this->setExpectedException($exception, $msg); - - $unittestWallet->getNewAddress(''); - } - - /** - * this test requires / asumes that the test wallet it uses contains a balance - * - * we keep the wallet topped off with some coins, - * but if some funny guy ever empties it or if you use your own API key to run the test then it needs to be topped off again - * - * @throws \Exception - */ - public function testSendToBech32() - { - $client = $this->setupBlocktrailSDK(); - - $unittestWallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "passphrase" => "password" - ]); - - $pubKeyHash = Buffer::hex('5d6f02f47dc6c57093df246e3742cfe1e22ab410'); - $wp = WitnessProgram::v0($pubKeyHash); - $addr = new SegwitAddress($wp); - - $random = random_int(1, 4); - $receiveAmount = BlocktrailSDK::toSatoshi(0.0001) * $random; - - $builder = (new TransactionBuilder($unittestWallet->getAddressReader())) - ->addRecipient($addr->getAddress(), $receiveAmount) - ->setFeeStrategy(Wallet::FEE_STRATEGY_OPTIMAL); - - $builder = $unittestWallet->coinSelectionForTxBuilder($builder, false, true); - $this->assertTrue(count($builder->getUtxos()) > 0); - $this->assertTrue(count($builder->getOutputs()) <= 2); - - /** - * @var TransactionInterface $tx - * @var SignInfo $signInfo - */ - list ($tx, $signInfo) = $unittestWallet->buildTx($builder); - - $found = false; - foreach ($tx->getOutputs() as $output) { - if ($output->getScript()->equals($wp->getScript())) { - $found = true; - $this->assertEquals($receiveAmount, $output->getValue()); - } - } - - $this->assertTrue($found, 'should find the address with our output script'); - - } - - /** - * this test requires / asumes that the test wallet it uses contains a balance - * - * we keep the wallet topped off with some coins, - * but if some funny guy ever empties it or if you use your own API key to run the test then it needs to be topped off again - * - * @throws \Exception - */ - public function testSpendMixedUtxoTypes() - { - $client = $this->setupBlocktrailSDK(); - - /** @var Wallet $unittestWallet */ - /** @var Wallet $segwitwallet */ - $unittestWallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "passphrase" => "password" - ]); - - $segwitwallet = $client->initWallet([ - "identifier" => "unittest-transaction-sw", - "passphrase" => "password" - ]); - - $this->assertTrue($segwitwallet->isSegwit()); - - list (, $unittestAddress) = $unittestWallet->getNewAddressPair(); - - $testingChain = Wallet::CHAIN_BTC_SEGWIT; - $defaultChain = Wallet::CHAIN_BTC_DEFAULT; - - $task = [$testingChain, $defaultChain]; - - $quanta = random_int(1, 5); - $value = BlocktrailSDK::toSatoshi(0.0005) * $quanta; - - $fundTxHash = []; - $fundTx = []; - $fundTxOutIdx = []; - - $builder = (new TransactionBuilder($unittestWallet->getAddressReader())) - ->addRecipient($unittestAddress, BlocktrailSDK::toSatoshi(0.0003)) - ->setFeeStrategy(Wallet::FEE_STRATEGY_OPTIMAL); - - foreach ($task as $i => $chainIndex) { - list ($path, $address) = $segwitwallet->getNewAddressPair($chainIndex); - $this->assertTrue(strpos($path, "M/9999'/{$chainIndex}/") !== -1); - - // Fund segwit address 1 - $fundTxHash[$i] = $unittestWallet->pay([$address => $value,], null, true, true, Wallet::FEE_STRATEGY_OPTIMAL); - - $this->assertTrue(!!$fundTxHash[$i]); - $fundTx[$i] = $this->getTx($client, $fundTxHash[$i]); - $this->assertEquals($fundTxHash[$i], $fundTx[$i]['hash']); - - $outIdx = -1; - foreach ($fundTx[$i]['outputs'] as $j => $output) { - if ($output['address'] == $address) { - $outIdx = $j; - } - } - $this->assertNotEquals(-1, $outIdx, "should find the output we created"); - $builder->spendOutput($fundTxHash[$i], $outIdx, $value, $address, null, $path, null, null); - $fundTxOutIdx[$i] = $outIdx; - } - - // Send back to unittest-wallet - - $spendAt = []; - foreach ($fundTx as $i => $tx) { - $hash = $tx['hash']; - $vout = $fundTxOutIdx[$i]; - foreach ($builder->getUtxos() as $k => $utxo) { - if ($utxo->hash == $hash && $utxo->index == $vout) { - $spendAt[$i] = $k; - } - } - $this->assertTrue(array_key_exists($i, $spendAt), "should have found utxo in transaction"); - } - - list ($tx, ) = $segwitwallet->buildTx($builder); - $this->assertInstanceOf(TransactionInterface::class, $tx); - } - - private function checkWalletScriptAgainstAddressPair(WalletInterface $wallet, $chainIdx) - { - list ($path, $address) = $wallet->getNewAddressPair($chainIdx); - $this->assertTrue(strpos("M/9999'/{$chainIdx}/", $path) !== -1); - - $defaultScript = $wallet->getWalletScriptByPath($path); - $this->assertEquals($defaultScript->getAddress()->getAddress(), $address); - - $classifier = new OutputClassifier(); - - switch($chainIdx) { - case Wallet::CHAIN_BTC_SEGWIT: - $this->assertTrue($defaultScript->isP2SH()); - $this->assertTrue($defaultScript->isP2WSH()); - $this->assertTrue($classifier->isMultisig($defaultScript->getWitnessScript())); - $this->assertTrue($classifier->isWitness($defaultScript->getRedeemScript())); - break; - case Wallet::CHAIN_BTC_DEFAULT: - $this->assertTrue($defaultScript->isP2SH()); - $this->assertTrue($classifier->isMultisig($defaultScript->getRedeemScript())); - break; - } - } - - public function testWalletRejectsUnknownPaths() - { - $client = $this->setupBlocktrailSDK(); - - /** @var Wallet $wallet */ - $wallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "passphrase" => "password" - ]); - - $this->setExpectedException(BlocktrailSDKException::class, "Unsupported chain in path"); - - $wallet->getWalletScriptByPath("M/9999'/123123123/0"); - - } - - public function testWalletGetNewAddressPair() { - $client = $this->setupBlocktrailSDK(); - - // testnet/mainnet only - - /** @var Wallet $wallet */ - $wallet = $client->initWallet([ - "identifier" => "unittest-transaction-sw", - "passphrase" => "password" - ]); - - $this->assertTrue($wallet->isSegwit()); - - $this->checkWalletScriptAgainstAddressPair($wallet, Wallet::CHAIN_BTC_DEFAULT); - - $this->checkWalletScriptAgainstAddressPair($wallet, Wallet::CHAIN_BTC_SEGWIT); - - $nestedP2wshPath = "M/9999'/2/0"; - $script = $wallet->getWalletScriptByPath($nestedP2wshPath); - - $this->assertTrue($script->isP2SH()); - $this->assertTrue($script->isP2WSH()); - - $this->isBase58Address($script); - $this->checkP2sh($script->getScriptPubKey(), $script->getRedeemScript()); - $this->checkP2wsh($script->getRedeemScript(), $script->getWitnessScript()); - - $this->assertEquals("2N3j4Vx3D9LPumjtRbRe2RJpwVocvCCkHKh", $script->getAddress()->getAddress()); - $this->assertEquals("a91472f4fbf13b171d3acfe3316264835cc4767549a187", $script->getScriptPubKey()->getHex()); - $this->assertEquals("0020bbb712fe7c81544b588b6f6d8d915b4e6f485ba2b43a70761e1dd9c68e391094", $script->getRedeemScript()->getHex()); - $this->assertEquals("5221020c9855979a83bedd4f45f47938f1008038b703506dd097bf81b76c4c8127482e2102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c12176635302102ff3475471c1f6caa27def90b97ceee72e2e9c569ebe59232fc19ef3db9e7ecbc53ae", $script->getWitnessScript()->getHex()); - } - - /** - * this test requires / asumes that the test wallet it uses contains a balance - * - * we keep the wallet topped off with some coins, - * but if some funny guy ever empties it or if you use your own API key to run the test then it needs to be topped off again - * - * @throws \Exception - */ - public function testWalletTransaction() { - $client = $this->setupBlocktrailSDK(); - - /** @var Wallet $wallet */ - $wallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "passphrase" => "password" - ]); - - $this->assertEquals("unittest-transaction", $wallet->getIdentifier()); - $this->assertEquals("M/9999'", $wallet->getBlocktrailPublicKeys()[9999][1]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKeys()[9999][0]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("m/9999'")->key()->toExtendedKey()); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("M/9999'")->key()->toExtendedKey()); - - list($confirmed, $unconfirmed) = $wallet->getBalance(); - $this->assertGreaterThan(0, $confirmed + $unconfirmed, "positive unconfirmed balance"); - $this->assertGreaterThan(0, $confirmed, "positive confirmed balance"); - - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertTrue(strpos($path, "M/9999'/0/") === 0); - $this->assertTrue(AddressFactory::fromString($address)->getAddress() == $address); - - $value = BlocktrailSDK::toSatoshi(0.0002); - $txHash = $wallet->pay([$address => $value,], null, false, true, Wallet::FEE_STRATEGY_BASE_FEE); - - $this->assertTrue(!!$txHash); - - $tx = $this->getTx($client, $txHash); - - $this->assertEquals($txHash, $tx['hash']); - $this->assertTrue(count($tx['outputs']) <= 2, "txId={$txHash}"); - $this->assertTrue(in_array($value, array_column($tx['outputs'], 'value')), "txId={$txHash}"); - - $baseFee = ceil($tx['size'] / 1000) * BlocktrailSDK::toSatoshi(0.0001); - - // dust was added to fee - if (count($tx['outputs']) === 1) { - $this->assertTrue(abs($baseFee - $tx['total_fee']) < Blocktrail::DUST, "txId={$txHash}"); - } else { - $this->assertEquals($baseFee, $tx['total_fee'], "txId={$txHash}"); - } - - /* - * do another TX but with a LOW_PRIORITY_FEE - */ - $value = BlocktrailSDK::toSatoshi(0.0001); - $txHash = $wallet->pay([$address => $value,], null, false, true, Wallet::FEE_STRATEGY_LOW_PRIORITY); - - $this->assertTrue(!!$txHash); - - $tx = $this->getTx($client, $txHash); - - $this->assertTrue(!!$tx, "check for tx[{$txHash}] [" . gmdate('Y-m-d H:i:s') . "]"); - $this->assertEquals($txHash, $tx['hash']); - $this->assertLessThanOrEqual(2, count($tx['outputs'])); - $this->assertTrue(in_array($value, array_column($tx['outputs'], 'value'))); - - /* - * do another TX but with a custom - high - fee - */ - $value = BlocktrailSDK::toSatoshi(0.0001); - $forceFee = BlocktrailSDK::toSatoshi(0.0010); - $txHash = $wallet->pay([$address => $value,], null, false, true, Wallet::FEE_STRATEGY_FORCE_FEE, $forceFee); - - $this->assertTrue(!!$txHash); - - $tx = $this->getTx($client, $txHash); - - - $this->assertEquals($txHash, $tx['hash']); - $this->assertTrue(count($tx['outputs']) <= 2, "txId={$txHash}"); - $this->assertTrue(in_array($value, array_column($tx['outputs'], 'value')), "txId={$txHash}"); - // dust was added to fee - if (count($tx['outputs']) === 1) { - $this->assertTrue(abs($forceFee - $tx['total_fee']) < Blocktrail::DUST, "txId={$txHash}"); - } else { - $this->assertEquals($forceFee, $tx['total_fee'], "txId={$txHash}"); - } - - /* - * do another TX with OP_RETURN using TxBuilder - */ - $value = BlocktrailSDK::toSatoshi(0.0002); - $moon = "MOOOOOOOOOOOOON!"; - $txBuilder = new TransactionBuilder($wallet->getAddressReader()); - $txBuilder->randomizeChangeOutput(false); - $txBuilder->addRecipient($address, $value); - $txBuilder->addOpReturn($moon); - - $txBuilder = $wallet->coinSelectionForTxBuilder($txBuilder); - - $txHash = $wallet->sendTx($txBuilder); - - $this->assertTrue(!!$txHash); - - $tx = $this->getTx($client, $txHash); - - $this->assertTrue(!!$tx, "check for tx[{$txHash}] [" . gmdate('Y-m-d H:i:s') . "]"); - $this->assertEquals($txHash, $tx['hash']); - $this->assertTrue(count($tx['outputs']) <= 3); - $this->assertEquals($value, $tx['outputs'][0]['value']); - $this->assertEquals(0, $tx['outputs'][1]['value']); - $this->assertEquals("6a" /* OP_RETURN */ . "10" /* OP_PUSH16 */ . bin2hex($moon), $tx['outputs'][1]['script_hex']); - } - - /** - * this test requires / asumes that the test wallet it uses contains a balance - * - * we keep the wallet topped off with some coins, - * but if some funny guy ever empties it or if you use your own API key to run the test then it needs to be topped off again - * - * @throws \Exception - */ - public function testWalletTransactionWithoutMnemonics() { - $client = $this->setupBlocktrailSDK(); - - $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed("give pause forget seed dance crawl situate hole keen", "password")); - - $wallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "primary_private_key" => $primaryPrivateKey, - "primary_mnemonic" => false, // explicitly set false because we're reusing unittest-transaction which has a mnemonic stored - ]); - - $this->assertEquals("unittest-transaction", $wallet->getIdentifier()); - $this->assertEquals("M/9999'", $wallet->getBlocktrailPublicKeys()[9999][1]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKeys()[9999][0]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("m/9999'")->key()->toExtendedKey()); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("M/9999'")->key()->toExtendedKey()); - - list($confirmed, $unconfirmed) = $wallet->getBalance(); - $this->assertGreaterThan(0, $confirmed + $unconfirmed, "positive unconfirmed balance"); - $this->assertGreaterThan(0, $confirmed, "positive confirmed balance"); - - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertTrue(strpos($path, "M/9999'/0/") === 0); - - $this->assertTrue(AddressFactory::fromString($address)->getAddress() == $address); - - $value = BlocktrailSDK::toSatoshi(0.0002); - $txHash = $wallet->pay([ - $address => $value, - ]); - - $this->assertTrue(!!$txHash); - - $tx = $this->getTx($client, $txHash); - - $this->assertEquals($txHash, $tx['hash']); - $this->assertTrue(count($tx['outputs']) <= 2); - $this->assertTrue(in_array($value, array_column($tx['outputs'], 'value'))); - } - - public function testKeyIndexUpgrade() { - $client = $this->setupBlocktrailSDK(); - - $identifier = $this->getRandomTestIdentifier(); - $wallet = $this->createKeyIndexUpgradeTestWallet($client, $identifier); - $this->cleanupData['wallets'][] = $wallet; // store for cleanup - - $this->assertEquals($identifier, $wallet->getIdentifier()); - $this->assertEquals("M/9999'", $wallet->getBlocktrailPublicKeys()[9999][1]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKeys()[9999][0]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("m/9999'")->key()->toExtendedKey()); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("M/9999'")->key()->toExtendedKey()); - - // get a new pair - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertEquals("M/9999'/0/0", $path); - $this->assertEquals("2Mtfn5S9tVWnnHsBQixCLTsCAPFHvfhu6bM", $address); - - // get another new pair - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertEquals("M/9999'/0/1", $path); - $this->assertEquals("2NG49GDkm5qCYvDFi4cxAnkSho8qLbEz6C4", $address); - - $wallet->upgradeKeyIndex(10000); - - $this->assertEquals("tpubD9m9hziKhYQExWgzMUNXdYMNUtourv96sjTUS9jJKdo3EDJAnCBJooMPm6vGSmkNTNAmVt988dzNfNY12YYzk9E6PkA7JbxYeZBFy4XAaCp", $wallet->getBlocktrailPublicKey("m/10000")->key()->toExtendedKey()); - - $this->assertEquals("2N9ZLKXgs12JQKXvLkngn7u9tsYaQ5kXJmk", $wallet->getAddressByPath("M/10000'/0/0")); - - // get a new pair - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertEquals("M/10000'/0/0", $path); - $this->assertEquals("2N9ZLKXgs12JQKXvLkngn7u9tsYaQ5kXJmk", $address); - } - - public function testListWalletTxsAddrs() { - $client = $this->setupBlocktrailSDK(); - - $wallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "passphrase" => "password" - ]); - $transactions = $wallet->transactions(1, 23); - - $this->assertEquals(23, count($transactions['data'])); - $this->assertEquals('2cb21783635a5f22e9934b8c3262146b42d251dfb14ee961d120936a6c40fe89', $transactions['data'][0]['hash']); - - $addresses = $wallet->addresses(1, 23); - - $this->assertEquals(23, count($addresses['data'])); - $this->assertEquals('2MzyKviSL6pnWxkbHV7ecFRE3hWKfzmT8WS', $addresses['data'][0]['address']); - - $utxos = $wallet->utxos(0, 23); - - $this->assertEquals(23, count($utxos['data'])); - } - - /** - * same wallet as testWallet but with a different password should result in a completely different wallet! - */ - public function testBadPasswordWallet() { - $client = $this->setupBlocktrailSDK(); - - $identifier = $this->getRandomTestIdentifier(); - $wallet = $this->createKeyIndexUpgradeTestWallet($client, $identifier, "badpassword"); - $this->cleanupData['wallets'][] = $wallet; // store for cleanup - - $this->assertEquals($identifier, $wallet->getIdentifier()); - $this->assertEquals("M/9999'", $wallet->getBlocktrailPublicKeys()[9999][1]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKeys()[9999][0]); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("m/9999'")->key()->toExtendedKey()); - $this->assertEquals("tpubD9q6vq9zdP3gbhpjs7n2TRvT7h4PeBhxg1Kv9jEc1XAss7429VenxvQTsJaZhzTk54gnsHRpgeeNMbm1QTag4Wf1QpQ3gy221GDuUCxgfeZ", $wallet->getBlocktrailPublicKey("M/9999'")->key()->toExtendedKey()); - - // get a new pair - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertEquals("M/9999'/0/0", $path); - $this->assertEquals("2N9SGrV4NKRjdACYvHLPpy2oiPrxTPd44rg", $address); - - // get another new pair - list($path, $address) = $wallet->getNewAddressPair(); - $this->assertEquals("M/9999'/0/1", $path); - $this->assertEquals("2NDq3DRy9E3YgHDA3haPJj3FtUS6V93avkf", $address); - } - - public function getVersionedWalletVectors() - { - return [ - [Wallet::WALLET_VERSION_V2, WalletV2::class], - [Wallet::WALLET_VERSION_V3, WalletV3::class], - [null, self::DEFAULT_WALLET_INSTANCE] - ]; - } - - /** - * Tests creation of both V2 and V3, and the 'default' wallets. - * - * @dataProvider getVersionedWalletVectors - * @param $walletVersion - * @param $expectedWalletClass - */ - public function testVersionedWallet($walletVersion, $expectedWalletClass) - { - $client = $this->setupBlocktrailSDK(); - - $identifier = $this->getRandomTestIdentifier(); - - /** - * @var $wallet \Blocktrail\SDK\Wallet - */ - $e = null; - $wallet = null; - try { - // Specify a wallet version if provided - $wallet = $client->initWallet([ - "identifier" => $identifier, - "passphrase" => "password" - ]); - } catch (ObjectNotFound $e) { - $new = [ - "identifier" => $identifier, - "passphrase" => "password", - "key_index" => 9999 - ]; - // Specify the wallet version if provided in the test - if ($walletVersion) { - $new['wallet_version'] = $walletVersion; - } - - list($wallet, $backupInfo) = $client->createNewWallet($new); - } - $this->assertTrue(!!$e, "New wallet with ID [{$identifier}] already exists..."); - $this->assertEquals($expectedWalletClass, get_class($wallet)); - - $wallet = $client->initWallet([ - "identifier" => $identifier, - "passphrase" => "password" - ]); - $this->cleanupData['wallets'][] = $wallet; // store for cleanup - - $this->assertEquals($expectedWalletClass, get_class($wallet)); - - $this->_testNewBlankWallet($wallet); - - /* - * test password change - */ - $wallet->unlock(['passphrase' => "password"]); - $wallet->passwordChange("password2"); - $wallet = $client->initWallet([ - "identifier" => $identifier, - "passphrase" => "password2" - ]); - - $this->assertEquals($expectedWalletClass, get_class($wallet)); - $this->assertTrue(!$wallet->isLocked()); - } - - public function testNewBlankWalletV1() { - $client = $this->setupBlocktrailSDK(); - - $identifier = $this->getRandomTestIdentifier(); - - /** - * @var $wallet \Blocktrail\SDK\Wallet - */ - $e = null; - try { - $wallet = $client->initWallet([ - "identifier" => $identifier, - "passphrase" => "password" - ]); - } catch (ObjectNotFound $e) { - list($wallet, $backupInfo) = $client->createNewWallet([ - "identifier" => $identifier, - "passphrase" => "password", - "key_index" => 9999, - "wallet_version" => Wallet::WALLET_VERSION_V1 - ]); - } - $this->assertTrue(!!$e, "New wallet with ID [{$identifier}] already exists..."); - $this->assertTrue($wallet instanceof WalletV1); - - $wallet = $client->initWallet([ - "identifier" => $identifier, - "passphrase" => "password" - ]); - $this->cleanupData['wallets'][] = $wallet; // store for cleanup - - $this->assertTrue($wallet instanceof WalletV1); - - $this->_testNewBlankWallet($wallet); - } - - public function testNewBlankWithoutMnemonicsWalletV2() { - $client = $this->setupBlocktrailSDK(); - - $identifier = $this->getRandomTestIdentifier(); - $primaryPrivateKey = BIP32Key::create(HierarchicalKeyFactory::generateMasterKey(), 'm'); - $backupPublicKey = BIP32Key::create(HierarchicalKeyFactory::generateMasterKey()->toPublic(), 'M'); - - /** - * @var $wallet \Blocktrail\SDK\Wallet - */ - $e = null; - try { - $wallet = $client->initWallet([ - "identifier" => $identifier - ]); - } catch (ObjectNotFound $e) { - list($wallet, $backupInfo) = $client->createNewWallet([ - "wallet_version" => Wallet::WALLET_VERSION_V2, - "identifier" => $identifier, - "primary_private_key" => $primaryPrivateKey, - "backup_public_key" => $backupPublicKey, - "key_index" => 9999 - ]); - } - $this->assertTrue(!!$e, "New wallet with ID [{$identifier}] already exists..."); - $this->assertTrue($wallet instanceof WalletV2); - - $wallet = $client->initWallet([ - "identifier" => $identifier, - "primary_private_key" => $primaryPrivateKey - ]); - $this->cleanupData['wallets'][] = $wallet; // store for cleanup - - $this->assertTrue($wallet instanceof WalletV2); - $this->assertEquals(0, $wallet->getBalance()[0]); - - $e = null; - try { - $wallet->pay([ - "2N6Fg6T74Fcv1JQ8FkPJMs8mYmbm9kitTxy" => BlocktrailSDK::toSatoshi(0.001) - ]); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e, "Wallet without balance is able to pay..."); - } - - public function testNewBlankWalletOldSyntax() { - $client = $this->setupBlocktrailSDK(); - $identifier = $this->getRandomTestIdentifier(); - $default = self::DEFAULT_WALLET_INSTANCE; - - /** - * @var $wallet \Blocktrail\SDK\Wallet - */ - $e = null; - try { - $wallet = $client->initWallet($identifier, "password"); - } catch (ObjectNotFound $e) { - list($wallet, $backupInfo) = $client->createNewWallet($identifier, "password", 9999); - } - $this->assertTrue(!!$e, "New wallet with ID [{$identifier}] already exists..."); - $this->assertTrue($wallet instanceof $default); - - $wallet = $client->initWallet([ - "identifier" => $identifier, - "passphrase" => "password" - ]); - $this->cleanupData['wallets'][] = $wallet; // store for cleanup - - $this->assertTrue($wallet instanceof $default); - - $this->assertEquals(0, $wallet->getBalance()[0]); - - $e = null; - try { - $wallet->pay([ - "2N6Fg6T74Fcv1JQ8FkPJMs8mYmbm9kitTxy" => BlocktrailSDK::toSatoshi(0.001) - ]); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e, "Wallet without balance is able to pay..."); - - /* - * init same wallet by with bad password - */ - $e = null; - try { - $wallet = $client->initWallet($identifier, "password2"); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e, "Wallet with bad pass initialized"); - } - - /** - * helper to test blank wallet - * - * @param Wallet $wallet - * @throws \Exception - */ - protected function _testNewBlankWallet(Wallet $wallet) { - $client = $this->setupBlocktrailSDK(); - - $this->assertFalse($wallet->isLocked()); - $wallet->lock(); - $this->assertTrue($wallet->isLocked()); - - $address = $wallet->getNewAddress(); - $this->assertTrue(!!$address, "should generate an address, was `".$address."`"); - - $this->assertEquals(0, $wallet->getBalance()[0]); - - $e = null; - try { - $wallet->pay([ - "2N6Fg6T74Fcv1JQ8FkPJMs8mYmbm9kitTxy" => BlocktrailSDK::toSatoshi(0.001) - ]); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e && strpos($e->getMessage(), "lock") !== false, "Locked wallet is able to pay..."); - - $e = null; - try { - $wallet->upgradeKeyIndex(10000); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e && strpos($e->getMessage(), "lock") !== false, "Locked wallet is able to upgrade key index..."); - - // repeat above but starting with readonly = true - $wallet = $client->initWallet([ - "identifier" => $wallet->getIdentifier(), - "readonly" => true - ]); - - $this->assertTrue($wallet->isLocked()); - - $this->assertTrue(!!$wallet->getNewAddress()); - - $this->assertEquals(0, $wallet->getBalance()[0]); - - $e = null; - try { - $wallet->pay([ - "2N6Fg6T74Fcv1JQ8FkPJMs8mYmbm9kitTxy" => BlocktrailSDK::toSatoshi(0.001) - ]); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e && strpos($e->getMessage(), "lock") !== false, "Locked wallet is able to pay..."); - - $e = null; - try { - $wallet->upgradeKeyIndex(10000); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e && strpos($e->getMessage(), "lock") !== false, "Locked wallet is able to upgrade key index..."); - - $wallet->unlock(['passphrase' => "password"]); - $this->assertFalse($wallet->isLocked()); - - $e = null; - try { - $wallet->pay([ - "2N6Fg6T74Fcv1JQ8FkPJMs8mYmbm9kitTxy" => BlocktrailSDK::toSatoshi(0.001) - ]); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e, "Wallet without balance is able to pay..."); - - $wallet->upgradeKeyIndex(10000); - - /* - * init same wallet by with bad password - */ - $e = null; - try { - $wallet = $client->initWallet([ - "identifier" => $wallet->getIdentifier(), - "passphrase" => "password2", - ]); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e, "Wallet with bad pass initialized"); - } - - public function testWebhookForWallet() { - $client = $this->setupBlocktrailSDK(); - - $identifier = $this->getRandomTestIdentifier(); - - /** - * @var $wallet \Blocktrail\SDK\Wallet - */ - $e = null; - try { - $wallet = $client->initWallet([ - "identifier" => $identifier, - "passphrase" => "password" - ]); - } catch (ObjectNotFound $e) { - list($wallet, $backupInfo) = $client->createNewWallet([ - "identifier" => $identifier, - "passphrase" => "password", - "key_index" => 9999 - ]); - } - $this->assertTrue(!!$e, "New wallet with ID [{$identifier}] already exists..."); - - $wallet = $client->initWallet([ - "identifier" => $identifier, - "passphrase" => "password" - ]); - $this->cleanupData['wallets'][] = $wallet; // store for cleanup - - $wallets = $client->allWallets(); - $this->assertTrue(count($wallets) > 0); - - $this->assertEquals(0, $wallet->getBalance()[0]); - - // create webhook with default identifier - $webhook = $wallet->setupWebhook("https://www.blocktrail.com/webhook-test"); - $this->assertEquals("https://www.blocktrail.com/webhook-test", $webhook["url"]); - $this->assertEquals("WALLET-{$wallet->getIdentifier()}", $webhook["identifier"]); - - // delete webhook - $this->assertTrue($wallet->deleteWebhook()); - - // create webhook with custom identifier - $webhookIdentifier = $this->getRandomTestIdentifier(); - $webhook = $wallet->setupWebhook("https://www.blocktrail.com/webhook-test", $webhookIdentifier); - $this->assertEquals("https://www.blocktrail.com/webhook-test", $webhook["url"]); - $this->assertEquals($webhookIdentifier, $webhook["identifier"]); - - // get events - $events = $client->getWebhookEvents($webhookIdentifier); - - // events should still be 0 at this point - // @TODO: $this->assertEquals(0, count($events['data'])); - - // create address - $addr1 = $wallet->getNewAddress(); - - $this->assertTrue($wallet->deleteWebhook($webhookIdentifier)); - - // create webhook with custom identifier - $webhookIdentifier = $this->getRandomTestIdentifier(); - $webhook = $wallet->setupWebhook("https://www.blocktrail.com/webhook-test", $webhookIdentifier); - $this->assertEquals("https://www.blocktrail.com/webhook-test", $webhook["url"]); - $this->assertEquals($webhookIdentifier, $webhook["identifier"]); - - // get events - $events = $client->getWebhookEvents($webhookIdentifier); - - // event for first address should already be there - // @TODO: $this->assertEquals(1, count($events['data'])); - $this->assertTrue(count(array_diff([$addr1], array_column($events['data'], 'address'))) == 0); - - // create address - $addr2 = $wallet->getNewAddress(); - - // get events - $events = $client->getWebhookEvents($webhookIdentifier); - - // event for 2nd address should be there too - // @TODO: $this->assertEquals(2, count($events['data'])); - // using inarray because order isn't deterministic - $this->assertTrue(count(array_diff([$addr1, $addr2], array_column($events['data'], 'address'))) == 0); - - // delete wallet (should delete webhook too) - $wallet->deleteWallet(); - - // check if webhook is deleted - $e = null; - try { - $this->assertFalse($wallet->deleteWebhook($webhookIdentifier)); - } catch (ObjectNotFound $e) { - } - $this->assertTrue(!!$e, "should throw exception"); - } - - public function testEstimateFee() { - $this->assertEquals(30000, Wallet::estimateFee(1, 66)); - $this->assertEquals(40000, Wallet::estimateFee(2, 71)); - } - - public function testEstimateSizeOutputs() { - $this->assertEquals(34, Wallet::estimateSizeOutputs(1)); - $this->assertEquals(68, Wallet::estimateSizeOutputs(2)); - $this->assertEquals(102, Wallet::estimateSizeOutputs(3)); - $this->assertEquals(3366, Wallet::estimateSizeOutputs(99)); - } - - public function testEstimateSizeUTXOs() { - $this->assertEquals(297, Wallet::estimateSizeUTXOs(1)); - $this->assertEquals(594, Wallet::estimateSizeUTXOs(2)); - $this->assertEquals(891, Wallet::estimateSizeUTXOs(3)); - $this->assertEquals(29403, Wallet::estimateSizeUTXOs(99)); - } - - public function testEstimateSize() { - $this->assertEquals(347, Wallet::estimateSize(34, 297)); - $this->assertEquals(29453, Wallet::estimateSize(34, 29403)); - $this->assertEquals(3679, Wallet::estimateSize(3366, 297)); - } - - public function testSegwitBuildTx() { - $client = $this->setupBlocktrailSDK(); - - $wallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "passphrase" => "password" - ]); - - $txid = "cafdeffb255ed7f8175f2bffc745e2dcc0ab0fa9abf9dad70a543c307614d374"; - $vout = 0; - $address = "2MtLjsE6SyBoxXt3Xae2wTU8sPdN8JUkUZc"; - $value = 9900000; - $outValue = 9899999; - $expectfee = $value-$outValue; - $scriptPubKey = "a9140c03259201742cb7476f10f70b2cf75fbfb8ab4087"; - $redeemScript = "0020cc7f3e23ec2a4cbba32d7e8f2e1aaabac38b88623d09f41dc2ee694fd33c6b14"; - $witnessScript = "5221021b3657937c54c616cbb519b447b4e50301c40759282901e04d81b5221cfcce992102381a4cf140c24080523b5e63082496b514e99657d3506444b7f77c1217663530210317e37c952644cf08b356671b4bb0308bd2468f548b31a308e8bacb682d55747253ae"; - - $path = "M/9999'/2/0"; - - $utxos = [ - $txid => $value, - ]; - - /** @var Transaction $tx */ - /** @var SignInfo[] $signInfo */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - $txid, - $vout, - $value, - $address, - $scriptPubKey, - $path, - $redeemScript, - $witnessScript - ) - ->addRecipient("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", $outValue) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals($value, $inputTotal); - $this->assertEquals($outValue, $outputTotal); - $this->assertEquals($expectfee, $fee); - - // assert the input(s) - $this->assertEquals(1, count($tx->getInputs())); - $this->assertEquals($txid, $tx->getInput(0)->getOutPoint()->getTxId()->getHex()); - $this->assertEquals(0, $tx->getInput(0)->getOutPoint()->getVout()); - $this->assertEquals($address, AddressFactory::fromOutputScript($signInfo[0]->output->getScript())->getAddress()); - $this->assertEquals($scriptPubKey, $signInfo[0]->output->getScript()->getHex()); - $this->assertEquals($value, $signInfo[0]->output->getValue()); - $this->assertEquals($path, $signInfo[0]->path); - $this->assertEquals( - $redeemScript, - $signInfo[0]->redeemScript->getHex() - ); - $this->assertEquals( - $witnessScript, - $signInfo[0]->witnessScript->getHex() - ); - - // assert the output(s) - $this->assertEquals(1, count($tx->getOutputs())); - $this->assertEquals("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", AddressFactory::fromOutputScript($tx->getOutput(0)->getScript())->getAddress()); - $this->assertEquals($outValue, $tx->getOutput(0)->getValue()); - } - - public function testBuildTx() { - $client = $this->setupBlocktrailSDK(); - - $wallet = $client->initWallet([ - "identifier" => "unittest-transaction", - "passphrase" => "password" - ]); - - /* - * test simple (real world TX) scenario - */ - $utxos = [ - '0d8703ab259b03a757e37f3cdba7fc4543e8d47f7cc3556e46c0aeef6f5e832b' => BlocktrailSDK::toSatoshi(0.0001), - 'be837cd8f04911f3ee10d010823a26665980f7bb6c9ed307d798cb968ca00128' => BlocktrailSDK::toSatoshi(0.001), - ]; - /** @var Transaction $tx */ - /** @var SignInfo[] $signInfo */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "0d8703ab259b03a757e37f3cdba7fc4543e8d47f7cc3556e46c0aeef6f5e832b", - 0, - BlocktrailSDK::toSatoshi(0.0001), - "2N9os1eAZXrWwKWgo7ppDRsY778PyxbScYH", - "a914b5ae3a9950fa66efa4aab2c21ce4a4275e7c95b487", - "M/9999'/0/5", - "5221032923eb97175038268cd320ffbb74bbef5a97ad58717026564431b5a131d47a3721036965ca88b87b25e1fb48df54ef6401eaa48383216f6725f6ec73009f84bfd79a2103cdea44d3fb80b36794fb393360279a54392785fbf05ff7f6af93d4d68448f53753ae" - ) - ->spendOutput( - "be837cd8f04911f3ee10d010823a26665980f7bb6c9ed307d798cb968ca00128", - 0, - BlocktrailSDK::toSatoshi(0.001), - "2NBV4sxQMYNyBbUeZkmPTZYtpdmKcuZ4Cyw", - "a914c8107bd24bae2c521a5a9f56c9b72e047eafa1f587", - "M/9999'/0/12", - "5221031a51c189641ff16a0afd9658b4864357e5ec4913ee5822103319adbd16d8fc262103628501430353863e2c3986372c251a562709e60238f129e494faf44aedf500dd2103ec9869b201d54cd1df80f49eafe7ff1a5a8a80b4b1e99b7a7fd2423e717e8d2753ae" - ) - ->addRecipient("2N7C5Jn1LasbEK9mvHetBYXaDnQACXkarJe", BlocktrailSDK::toSatoshi(0.001)) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0011), $inputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.001), $outputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); - - // assert the input(s) - $this->assertEquals(2, count($tx->getInputs())); - $this->assertEquals("0d8703ab259b03a757e37f3cdba7fc4543e8d47f7cc3556e46c0aeef6f5e832b", $tx->getInput(0)->getOutPoint()->getTxId()->getHex()); - $this->assertEquals(0, $tx->getInput(0)->getOutPoint()->getVout()); - $this->assertEquals("2N9os1eAZXrWwKWgo7ppDRsY778PyxbScYH", AddressFactory::fromOutputScript($signInfo[0]->output->getScript())->getAddress()); - $this->assertEquals("a914b5ae3a9950fa66efa4aab2c21ce4a4275e7c95b487", $signInfo[0]->output->getScript()->getHex()); - $this->assertEquals(10000, $signInfo[0]->output->getValue()); - $this->assertEquals("M/9999'/0/5", $signInfo[0]->path); - $this->assertEquals( - "5221032923eb97175038268cd320ffbb74bbef5a97ad58717026564431b5a131d47a3721036965ca88b87b25e1fb48df54ef6401eaa48383216f6725f6ec73009f84bfd79a2103cdea44d3fb80b36794fb393360279a54392785fbf05ff7f6af93d4d68448f53753ae", - $signInfo[0]->redeemScript->getHex() - ); - - $this->assertEquals("be837cd8f04911f3ee10d010823a26665980f7bb6c9ed307d798cb968ca00128", $tx->getInput(1)->getOutPoint()->getTxId()->getHex()); - $this->assertEquals(0, $tx->getInput(1)->getOutPoint()->getVout()); - $this->assertEquals("2NBV4sxQMYNyBbUeZkmPTZYtpdmKcuZ4Cyw", AddressFactory::fromOutputScript($signInfo[1]->output->getScript())->getAddress()); - $this->assertEquals("a914c8107bd24bae2c521a5a9f56c9b72e047eafa1f587", $signInfo[1]->output->getScript()->getHex()); - $this->assertEquals(100000, $signInfo[1]->output->getValue()); - $this->assertEquals("M/9999'/0/12", $signInfo[1]->path); - $this->assertEquals( - "5221031a51c189641ff16a0afd9658b4864357e5ec4913ee5822103319adbd16d8fc262103628501430353863e2c3986372c251a562709e60238f129e494faf44aedf500dd2103ec9869b201d54cd1df80f49eafe7ff1a5a8a80b4b1e99b7a7fd2423e717e8d2753ae", - $signInfo[1]->redeemScript->getHex() - ); - - // assert the output(s) - $this->assertEquals(1, count($tx->getOutputs())); - $this->assertEquals("2N7C5Jn1LasbEK9mvHetBYXaDnQACXkarJe", AddressFactory::fromOutputScript($tx->getOutput(0)->getScript())->getAddress()); - $this->assertEquals(100000, $tx->getOutput(0)->getValue()); - - /* - * test trying to spend too much - */ - $utxos = [ - 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.0001) - ]; - $e = null; - try { - /** @var Transaction $tx */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", - 0, - BlocktrailSDK::toSatoshi(0.0001), - "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", - "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", - "M/9999'/0/1537", - "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" - ) - ->addRecipient("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", BlocktrailSDK::toSatoshi(0.0002)) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - } catch (\Exception $e) { - } - $this->assertTrue(!!$e); - $this->assertEquals("Atempting to spend more than sum of UTXOs", $e->getMessage()); - - - /* - * test change - */ - $utxos = [ - 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(1) - ]; - /** @var Transaction $tx */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", - 0, - BlocktrailSDK::toSatoshi(1), - "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", - "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", - "M/9999'/0/1537", - "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" - ) - ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NE2uSqCktMXfe512kTPrKPhQck7vMNvaGK", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) - ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") - ->randomizeChangeOutput(false) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals(BlocktrailSDK::toSatoshi(1), $inputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.9999), $outputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); - $this->assertEquals(14, count($tx->getOutputs())); - $this->assertEquals("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", AddressFactory::fromOutputScript($tx->getOutput(13)->getScript())->getAddress()); - $this->assertEquals(99860000, $tx->getOutput(13)->getValue()); - - /* - * 1 input (1 * 294b) = 294b - * 19 recipients (19 * 34b) = 646b - * - * size = 8b + 294b + 646b = 948b - * + change output (34b) = 982b - * - * fee = 0.0001 - * - * 1 - (19 * 0.0001) = 0.9981 - * change = 0.9980 - */ - $utxos = [ - 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(1) - ]; - /** @var Transaction $tx */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", - 0, - BlocktrailSDK::toSatoshi(1), - "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", - "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", - "M/9999'/0/1537", - "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" - ) - ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mw34qYu3rCmFkzZeNsDJ9aQri8HjmUZ6wY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MsTtsupHuqy6JvWUscn5HQ54EscLiXaPSF", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtR3Qa9eeYEpBmw3kLNywWGVmUuGjwRGXk", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N6GmkegBNA1D8wbHMLZFwxMPoNRjVnZgvv", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NBCPVQ6xX3KAVPKmGENH1eHhPwJzmN1Bpf", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NAHY321fSVz4wKnE4eWjyLfRmoauCrQpBD", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N2anz2GmZdrKNNeEZD7Xym8djepwnTqPXY", BlocktrailSDK::toSatoshi(0.0001)) - ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") - ->randomizeChangeOutput(false) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals(BlocktrailSDK::toSatoshi(1), $inputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.9999), $outputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); - $this->assertEquals(20, count($tx->getOutputs())); - $this->assertEquals("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", AddressFactory::fromOutputScript($tx->getOutput(19)->getScript())->getAddress()); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.9980), $tx->getOutput(19)->getValue()); - - /* - * test change output bumps size over 1kb, fee += 0.0001 - * - * 1 input (1 * 298b) = 298b - * 20 recipients (19 * 33b) = 693b - * - * size = 8b + 298b + 693b = 999b - * + change output (34b) = 1019b - * - * fee = 0.0002 - * input = 1.0000 - * 1.0000 - (20 * 0.0001) = 0.9977 - * change = 0.9977 - */ - $utxos = [ - 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(1) - ]; - /** @var Transaction $tx */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", - 0, - BlocktrailSDK::toSatoshi(1), - "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", - "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", - "M/9999'/0/1537", - "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" - ) - ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mw34qYu3rCmFkzZeNsDJ9aQri8HjmUZ6wY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MsTtsupHuqy6JvWUscn5HQ54EscLiXaPSF", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtR3Qa9eeYEpBmw3kLNywWGVmUuGjwRGXk", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N6GmkegBNA1D8wbHMLZFwxMPoNRjVnZgvv", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NBCPVQ6xX3KAVPKmGENH1eHhPwJzmN1Bpf", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NAHY321fSVz4wKnE4eWjyLfRmoauCrQpBD", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N2anz2GmZdrKNNeEZD7Xym8djepwnTqPXY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mvs5ik3nC9RBho2kPcgi5Q62xxAE2Aryse", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mvs5ik3nC9RBho2kPcgi5Q62xxAE2Aryse", BlocktrailSDK::toSatoshi(0.0001)) - ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") - ->randomizeChangeOutput(false) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals(BlocktrailSDK::toSatoshi(1), $inputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.9998), $outputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0002), $fee); - $this->assertEquals(22, count($tx->getOutputs())); - $change = $tx->getOutput(21); - $this->assertEquals("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", AddressFactory::fromOutputScript($change->getScript())->getAddress()); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.9977), $change->getValue()); - - /* - * test change - * - * 1 input (1 * 294b) = 294b - * 20 recipients (19 * 34b) = 680b - * - * size = 8b + 294b + 680b = 982b - * + change output (34b) = 1006b - * - * fee = 0.0001 - * input = 0.0021 - * 0.0021 - (20 * 0.0001) = 0.0001 - */ - $utxos = [ - 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.0021) - ]; - /** @var Transaction $tx */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", - 0, - BlocktrailSDK::toSatoshi(0.0021), - "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", - "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", - "M/9999'/0/1537", - "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" - ) - ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mw34qYu3rCmFkzZeNsDJ9aQri8HjmUZ6wY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MsTtsupHuqy6JvWUscn5HQ54EscLiXaPSF", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtR3Qa9eeYEpBmw3kLNywWGVmUuGjwRGXk", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N6GmkegBNA1D8wbHMLZFwxMPoNRjVnZgvv", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NBCPVQ6xX3KAVPKmGENH1eHhPwJzmN1Bpf", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NAHY321fSVz4wKnE4eWjyLfRmoauCrQpBD", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N2anz2GmZdrKNNeEZD7Xym8djepwnTqPXY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mvs5ik3nC9RBho2kPcgi5Q62xxAE2Aryse", BlocktrailSDK::toSatoshi(0.0001)) - ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") - ->randomizeChangeOutput(false) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0021), $inputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0020), $outputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); - $this->assertEquals(20, count($tx->getOutputs())); - - /* - * test change output bumps size over 1kb, fee += 0.0001 - * but change was < 0.0001 so better to just fee it all - * - * 1 input (1 * 294b) = 298b - * 20 recipients (20 * 34b) = 660b - * - * input = 0.00212 - * - * size = 8b + 298b + 660b = 986b - * fee = 0.0001 - * 0.00212 - (20 * 0.0001) = 0.00012 - * - * + change output (0.00009) (34b) = 1006b - * fee = 0.0002 - * - */ - $utxos = [ - 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.00212) - ]; - /** @var Transaction $tx */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", - 0, - BlocktrailSDK::toSatoshi(0.00212), - "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", - "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", - "M/9999'/0/1537", - "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" - ) - ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NFK27bVrNfDHSrcykALm29DTi85TLuNm1A", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3y477rv4TwAwW1t8rDxGQzrWcSqkzheNr", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N3SEVZ8cpT8zm6yiQphgxCL3wLfQ75f7wK", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MyCfumM2MwnCfMAqLFQeRzaovetvpV63Pt", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7ZNbgb6kEPuok2L8KwAEGnHq7Y8k6XR8B", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtamUPcc8U12sUQ2zhgoXg34c31XRd9h2E", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N1jJJxEHnQfdKMh2wor7HQ9aHp6KfpeSgw", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mx5ZJEdJus7TekzM8Jr9H2xaKH1iy4775y", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MzzvxQNZtE4NP5U2HGgLhFzsRQJaRQouKY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N8inYmbUT9wM1ewvtEy6RW4zBQstgPkfCQ", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N7TZBUcr7dTJaDFPN6aWtWmRsh5MErv4nu", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mw34qYu3rCmFkzZeNsDJ9aQri8HjmUZ6wY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MsTtsupHuqy6JvWUscn5HQ54EscLiXaPSF", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2MtR3Qa9eeYEpBmw3kLNywWGVmUuGjwRGXk", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N6GmkegBNA1D8wbHMLZFwxMPoNRjVnZgvv", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NBCPVQ6xX3KAVPKmGENH1eHhPwJzmN1Bpf", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2NAHY321fSVz4wKnE4eWjyLfRmoauCrQpBD", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2N2anz2GmZdrKNNeEZD7Xym8djepwnTqPXY", BlocktrailSDK::toSatoshi(0.0001)) - ->addRecipient("2Mvs5ik3nC9RBho2kPcgi5Q62xxAE2Aryse", BlocktrailSDK::toSatoshi(0.0001)) - ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") - ->randomizeChangeOutput(false) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals(BlocktrailSDK::toSatoshi(0.00212), $inputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0020), $outputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.00012), $fee); - $this->assertEquals(20, count($tx->getOutputs())); - - /* - * custom fee - */ - $utxos = [ - 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.002001) - ]; - /** @var Transaction $tx */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", - 0, - BlocktrailSDK::toSatoshi(0.002001), - "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", - "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", - "M/9999'/0/1537", - "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" - ) - ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.002)) - ->setFee(BlocktrailSDK::toSatoshi(0.000001)) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals(BlocktrailSDK::toSatoshi(0.002001), $inputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.002), $outputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.000001), $fee); - - /* - * multiple outputs same address - */ - $utxos = [ - 'ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396' => BlocktrailSDK::toSatoshi(0.002) - ]; - /** @var Transaction $tx */ - list($tx, $signInfo) = $wallet->buildTx( - (new TransactionBuilder($wallet->getAddressReader())) - ->spendOutput( - "ed6458f2567c3a6847e96ca5244c8eb097efaf19fd8da2d25ec33d54a49b4396", - 0, - BlocktrailSDK::toSatoshi(0.002), - "2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT", - "a9148e3c73aaf758dc4f4186cd49c3d523954992a46a87", - "M/9999'/0/1537", - "5221025a341fad401c73eaa1ee40ba850cc7368c41f7a29b3c6e1bbb537be51b398c4d210331801794a117dac34b72d61262aa0fcec7990d72a82ddde674cf583b4c6a5cdf21033247488e521170da034e4d8d0251530df0e0d807419792492af3e54f6226441053ae" - ) - ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0005)) - ->addRecipient("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", BlocktrailSDK::toSatoshi(0.0005)) - ->setChangeAddress("2N6DJMnoS3xaxpCSDRMULgneCghA1dKJBmT") - ->randomizeChangeOutput(false) - ->setFeeStrategy(Wallet::FEE_STRATEGY_BASE_FEE) - ); - - $inputTotal = array_sum(array_map(function (TransactionInput $txin) use ($utxos) { - return $utxos[$txin->getOutPoint()->getTxId()->getHex()]; - }, $tx->getInputs())); - $outputTotal = array_sum(array_map(function (TransactionOutput $txout) { - return $txout->getValue(); - }, $tx->getOutputs())); - - $fee = $inputTotal - $outputTotal; - - // assert the output(s) - $this->assertEquals(BlocktrailSDK::toSatoshi(0.002), $inputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0019), $outputTotal); - $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $fee); - - $this->assertEquals("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", AddressFactory::fromOutputScript($tx->getOutput(0)->getScript())->getAddress()); - $this->assertEquals("2NAUFsSps9S2mEnhaWZoaufwyuCaVPUv8op", AddressFactory::fromOutputScript($tx->getOutput(1)->getScript())->getAddress()); - } - - protected function getTx(BlocktrailSDK $client, $txId, $retries = 3) { - sleep(1); - - for ($i = 0; $i < $retries; $i++) { - try { - $tx = $client->transaction($txId); - return $tx; - } catch (ObjectNotFound $e) { - sleep(1); // sleep to wait for the TX to be processed - } - } - - $this->fail("404 for tx[{$txId}] [" . gmdate('Y-m-d H:i:s') . "]"); - } -} From 60f97fc8a1f4cde734ab6dabeaf28f517b9f0648 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 11:01:39 +0200 Subject: [PATCH 07/18] WIP --- src/BlocktrailSDK.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BlocktrailSDK.php b/src/BlocktrailSDK.php index cfcd444..3e0a2f2 100644 --- a/src/BlocktrailSDK.php +++ b/src/BlocktrailSDK.php @@ -201,7 +201,7 @@ public function getDataRestClient() { * @param RestClientInterface $restClient */ public function setRestClient(RestClientInterface $restClient) { - $this->client = $restClient; + $this->blocktrailClient = $restClient; } /** From 436d46b0113ad0a4d62c48bde5af60db91c66cee Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 11:05:27 +0200 Subject: [PATCH 08/18] curl --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7d641cd..3887373 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,8 @@ script: - sh -c 'if [ "${PHPCS}" = "true" ]; then export RUN_PHPUNIT=false; fi' - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit --stop-on-failure -c ./phpunit.xml; fi' + - curl -v 'https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY' + # run phpcs when enabled - sh -c 'if [ "${PHPCS}" = "true" ]; then ./vendor/bin/phpcs --standard=./phpcs.xml -n ./src/ && echo "PHPCS OK"; fi' From 90b95ea397af8807fa9270225685f72d73100ca0 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 11:07:25 +0200 Subject: [PATCH 09/18] curl --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3887373..6811021 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,10 +25,11 @@ script: - if [[ "${RUN_PHPUNIT}" != "false" ]]; then export RUN_PHPUNIT="true"; fi - - sh -c 'if [ "${PHPCS}" = "true" ]; then export RUN_PHPUNIT=false; fi' - - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit --stop-on-failure -c ./phpunit.xml; fi' - curl -v 'https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY' + + - sh -c 'if [ "${PHPCS}" = "true" ]; then export RUN_PHPUNIT=false; fi' + - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit --stop-on-failure -c ./phpunit.xml; fi' # run phpcs when enabled - sh -c 'if [ "${PHPCS}" = "true" ]; then ./vendor/bin/phpcs --standard=./phpcs.xml -n ./src/ && echo "PHPCS OK"; fi' From 3bec29f5066feb6ba0c846f4fc83f5e21ae79d03 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 11:11:04 +0200 Subject: [PATCH 10/18] DBG --- .travis.yml | 2 +- tests/IntegrationTests/IntegrationTestBase.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6811021..9816692 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ script: fi - curl -v 'https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY' - + - sh -c 'if [ "${PHPCS}" = "true" ]; then export RUN_PHPUNIT=false; fi' - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit --stop-on-failure -c ./phpunit.xml; fi' diff --git a/tests/IntegrationTests/IntegrationTestBase.php b/tests/IntegrationTests/IntegrationTestBase.php index df87d95..a429eb7 100644 --- a/tests/IntegrationTests/IntegrationTestBase.php +++ b/tests/IntegrationTests/IntegrationTestBase.php @@ -26,7 +26,7 @@ public function setupBlocktrailSDK($network = 'BTC', $testnet = false, $apiVersi $client = new BlocktrailSDK($apiKey, $apiSecret, $network, $testnet, $apiVersion, $apiEndpoint); $client->setVerboseErrors(true); -// $client->setCurlDebugging(true); + $client->setCurlDebugging(true); return $client; } From 904a9cce439f7b6fd5bfc662f415d8fe73017b28 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 11:31:21 +0200 Subject: [PATCH 11/18] cacert --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9816692..d4b73e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ cache: before_install: # disable XDEBUG unless specified to be used - sh -c 'if [ "${WITH_XDEBUG}" = "false" ]; then phpenv config-rm xdebug.ini && echo "xdebug disabled" || exit 0; fi' + - wget https://curl.haxx.se/ca/cacert.pem -O /tmp/cacert.pem + - echo "curl.cainfo=/tmp/cacert.pem" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; install: - composer update From 253caecc6485bebcbfbb4d1d5fad3510bc2ac3ce Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 11:40:38 +0200 Subject: [PATCH 12/18] evil inc --- .travis.yml | 3 +-- src/Connection/RestClient.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d4b73e7..b5c32f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,6 @@ cache: before_install: # disable XDEBUG unless specified to be used - sh -c 'if [ "${WITH_XDEBUG}" = "false" ]; then phpenv config-rm xdebug.ini && echo "xdebug disabled" || exit 0; fi' - - wget https://curl.haxx.se/ca/cacert.pem -O /tmp/cacert.pem - - echo "curl.cainfo=/tmp/cacert.pem" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; install: - composer update @@ -63,6 +61,7 @@ notifications: env: global: + - BLOCKTRAIL_SDK_NO_SSL_VERIFY=1 - BLOCKTRAIL_SDK_THROTTLE_BTCCOM=0.01 - secure: d4a4n3bdFO/TZGBnEO0zOarL7kTkh4WyH6U+sEQoqe7Wbs1OIN014JXqYZZn2TDQ8kKjxKGi63TiDvh2wvKF+B7dlOo3/WvqNOEA48eVmRI3/LQsAAcmUNIK6YnBN2YgIy0kl8ycDvx0W7hK/A6FFmbnGTJnG6vnQ5NmBe1G3ug= - secure: ndOLTmYQgNKnr4qXt9/1TjYUDuGFn7K/v5L4urwIAxwD1jJYLHpntvhjTehQq34VX2uIQeDn0Zd4qyorpJxTI5Pmow51j5ZmtA0lS8TaFTwfWc+GZIG7UT3vcoHC5U0o54KJAlumSSncTMttPPfYsD7+8+8KYEjrOlXg6djq5fI= diff --git a/src/Connection/RestClient.php b/src/Connection/RestClient.php index 4c45eb2..09074ce 100644 --- a/src/Connection/RestClient.php +++ b/src/Connection/RestClient.php @@ -79,7 +79,7 @@ protected function createGuzzleClient(array $options = [], array $curlOptions = 'http_errors' => false, 'connect_timeout' => 3, 'timeout' => 20.0, // tmp until we have a good matrix of all the requests and their expect min/max time - 'verify' => true, + 'verify' => \getenv('BLOCKTRAIL_SDK_NO_SSL_VERIFY') ? false : true, 'proxy' => '', 'debug' => false, 'config' => array(), From 5623e74320801e1ce75d0ef694ee7d9d5665cfb7 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 14:24:52 +0200 Subject: [PATCH 13/18] curl issues --- .travis.yml | 61 ++--------------------------------------------------- curl.php | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 59 deletions(-) create mode 100644 curl.php diff --git a/.travis.yml b/.travis.yml index b5c32f9..58b1044 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,64 +8,7 @@ branches: only: - "master" -cache: - directories: - - $HOME/.composer/cache/files - -before_install: - # disable XDEBUG unless specified to be used - - sh -c 'if [ "${WITH_XDEBUG}" = "false" ]; then phpenv config-rm xdebug.ini && echo "xdebug disabled" || exit 0; fi' - -install: - - composer update - - sh -c 'if [ "${TEST_FRAMEWORK}" != "" ]; then composer global require fxp/composer-asset-plugin; fi' - script: - # run tests when phpcs is not enabled - - if [[ "${RUN_PHPUNIT}" != "false" ]]; then - export RUN_PHPUNIT="true"; - fi - + # DEBUG - curl -v 'https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY' - - - sh -c 'if [ "${PHPCS}" = "true" ]; then export RUN_PHPUNIT=false; fi' - - sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit --stop-on-failure -c ./phpunit.xml; fi' - - # run phpcs when enabled - - sh -c 'if [ "${PHPCS}" = "true" ]; then ./vendor/bin/phpcs --standard=./phpcs.xml -n ./src/ && echo "PHPCS OK"; fi' - - # run framework tests when enabled - - if [[ "${TEST_FRAMEWORK}" != "" ]]; then - if [[ "${TRAVIS_PULL_REQUEST}" = false ]]; then - git fetch --unshallow; - php framework-test.php dev-${TRAVIS_BRANCH} && echo "Framework integration OK"; - elif [[ "${TRAVIS_PULL_REQUEST_BRANCH}" != "" ]]; then - php framework-test.php dev-${TRAVIS_PULL_REQUEST_BRANCH} && echo "Framework integration OK"; - fi - fi - -after_script: - - | - if [ "${WITH_XDEBUG}" = "true" ]; then - wget https://scrutinizer-ci.com/ocular.phar && - php ocular.phar code-coverage:upload --format=php-clover clover.xml - fi - - | - if [ "${WITH_XDEBUG}" = "true" ]; then - vendor/bin/coveralls - fi - -notifications: - slack: - secure: fwLiJFbxPCyY6+REN3XZ06RlffSVua3tW5Ih/p4xwp2KXNdcCldWCmziVhf65JGYaRU20M4HEOOcD+IpOI/IjpNHjF/+6OmtUGfkT9OfDCmuElqyq8UT6ck+xfb7FVtK42zkx0U3xC1qCclbFjY1Rht7vt5VnGN7Q3qJ3ax7mcM= - -env: - global: - - BLOCKTRAIL_SDK_NO_SSL_VERIFY=1 - - BLOCKTRAIL_SDK_THROTTLE_BTCCOM=0.01 - - secure: d4a4n3bdFO/TZGBnEO0zOarL7kTkh4WyH6U+sEQoqe7Wbs1OIN014JXqYZZn2TDQ8kKjxKGi63TiDvh2wvKF+B7dlOo3/WvqNOEA48eVmRI3/LQsAAcmUNIK6YnBN2YgIy0kl8ycDvx0W7hK/A6FFmbnGTJnG6vnQ5NmBe1G3ug= - - secure: ndOLTmYQgNKnr4qXt9/1TjYUDuGFn7K/v5L4urwIAxwD1jJYLHpntvhjTehQq34VX2uIQeDn0Zd4qyorpJxTI5Pmow51j5ZmtA0lS8TaFTwfWc+GZIG7UT3vcoHC5U0o54KJAlumSSncTMttPPfYsD7+8+8KYEjrOlXg6djq5fI= - # BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT - - secure: "hJDN/XyeCJVi86R08zPN/xpI/7U+SU6tWmfnVp4wF6cyZmdDY6UztSa5FaS/RRh0mF2M66PoW00VxyoZ7dj0avZrZWwILYdBpwiIWCg2Ay7rkBGbE/kiWuMfOAVpf8x3zO/ArunJC7spiWW+amTFxk0sutkz84bLxyQahsPmVgE=" - matrix: - - PHPCS=false WITH_XDEBUG=true # default disable phpcs run and disable xdebug + - php curl.php diff --git a/curl.php b/curl.php new file mode 100644 index 0000000..1e14809 --- /dev/null +++ b/curl.php @@ -0,0 +1,22 @@ + true, + CURLOPT_CERTINFO => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_FAILONERROR => false, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_HEADER => true, +]); + +$result = curl_exec($ch); +$info = curl_getinfo($ch); + +curl_close($ch); + +var_dump($result); From dfa700983d6bef403103f07ed3c8c47d3bf3d4d9 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Mon, 30 Apr 2018 13:22:39 +0200 Subject: [PATCH 14/18] DBG --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 58b1044..94a3e3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,5 +10,6 @@ branches: script: # DEBUG + - php -i - curl -v 'https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY' - php curl.php From 6f7def1df57b9ed8102ef270a60e2eb118cf7fc4 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Mon, 30 Apr 2018 13:25:04 +0200 Subject: [PATCH 15/18] try ssl versions --- .travis.yml | 2 +- curl.php | 56 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 94a3e3c..e999692 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ script: # DEBUG - php -i - curl -v 'https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY' - - php curl.php + - php curl.php diff --git a/curl.php b/curl.php index 1e14809..798e726 100644 --- a/curl.php +++ b/curl.php @@ -1,22 +1,38 @@ true, - CURLOPT_CERTINFO => true, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_FAILONERROR => false, - CURLOPT_TIMEOUT => 10, - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_HEADER => true, -]); - -$result = curl_exec($ch); -$info = curl_getinfo($ch); - -curl_close($ch); - -var_dump($result); + +$sslVersions = [ + CURL_SSLVERSION_DEFAULT, + CURL_SSLVERSION_TLSv1, + CURL_SSLVERSION_TLSv1_0, + CURL_SSLVERSION_TLSv1_1, + CURL_SSLVERSION_TLSv1_2, + CURL_SSLVERSION_SSLv2, + CURL_SSLVERSION_SSLv3, +]; + +var_dump(curl_version()); + +foreach ($sslVersions as $sslVersion) { + $ch = curl_init("https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY"); + + curl_setopt_array($ch, [ + CURLOPT_VERBOSE => true, + CURLOPT_CERTINFO => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_FAILONERROR => false, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_HEADER => true, + CURLOPT_SSLVERSION => $sslVersion, + ]); + + $result = curl_exec($ch); + $info = curl_getinfo($ch); + + curl_close($ch); + + var_dump($result); +} From 517621241ba6dbaf1d32ac6d657dc86f6b47060c Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Mon, 30 Apr 2018 13:30:21 +0200 Subject: [PATCH 16/18] capath --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e999692..5f6ec55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ script: # DEBUG - php -i - curl -v 'https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY' - - php curl.php + - php -dopenssl.capath=/etc/ssl/certs/ curl.php From a0b0819a1e2ca30dfe867cb5ae9945e572afa53e Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Mon, 30 Apr 2018 13:32:13 +0200 Subject: [PATCH 17/18] capath --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5f6ec55..a35e92d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ script: # DEBUG - php -i - curl -v 'https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY' - - php -dopenssl.capath=/etc/ssl/certs/ curl.php + - php -dopenssl.capath=/etc/ssl/certs/ -dcurl.capath=/etc/ssl/certs/ -dcapath=/etc/ssl/certs/ curl.php From ff05b40baeb73b6dda6710ea7b8950500074e06b Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Mon, 30 Apr 2018 13:33:28 +0200 Subject: [PATCH 18/18] dbg --- curl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/curl.php b/curl.php index 798e726..ba395cd 100644 --- a/curl.php +++ b/curl.php @@ -14,6 +14,7 @@ var_dump(curl_version()); foreach ($sslVersions as $sslVersion) { + var_dump($sslVersion); $ch = curl_init("https://api.blocktrail.com/v1/BTC/price?api_key=MY_APIKEY"); curl_setopt_array($ch, [