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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 1543a8435b61337f94419c970e9b1b5b04b72ec1 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Fri, 20 Apr 2018 11:40:38 +0200 Subject: [PATCH 08/20] evil inc --- .travis.yml | 1 + src/Connection/RestClient.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7d641cd..106aba5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,6 +58,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 e0e1362c6f0165b45839d13ded6b3b1b15e2c772 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Fri, 27 Jul 2018 12:30:04 +0100 Subject: [PATCH 09/20] no evil in here --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 106aba5..7d641cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,7 +58,6 @@ 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= From 0f1ba334866ef3828f6f89ed5813b328c6f52223 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 30 Jul 2018 14:55:57 +0100 Subject: [PATCH 10/20] debug first failing http integration test --- src/Connection/RestClient.php | 2 +- src/Throttler.php | 16 +++++++-- .../DataAPIIntegrationTest.php | 36 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/Connection/RestClient.php b/src/Connection/RestClient.php index 09074ce..542d42e 100644 --- a/src/Connection/RestClient.php +++ b/src/Connection/RestClient.php @@ -142,7 +142,7 @@ public function setProxy($proxy) { */ public function request($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) { $this->throttler->waitForThrottle(); - + echo "Fired request at " . microtime(true) . PHP_EOL; $request = $this->buildRequest($method, $endpointUrl, $queryString, $body, $auth, $contentMD5Mode, $timeout); $response = $this->guzzle->send($request, ['auth' => $auth, 'timeout' => $timeout]); diff --git a/src/Throttler.php b/src/Throttler.php index f44ceef..80f5380 100644 --- a/src/Throttler.php +++ b/src/Throttler.php @@ -61,12 +61,24 @@ public function waitForThrottle() { return; } - $diff = $this->interval - (\microtime(true) - $this->lastTime); + $now = \microtime(true); + $diff = $this->interval - ($now - $this->lastTime); + + echo "Throttle routine\n"; + echo "* interval is {$this->interval}\n"; + echo "* now is {$now}\n"; + echo "* lastTime is {$this->lastTime}\n"; + echo "* wait time should be $diff\n"; if ($diff > 0) { - usleep((int)ceil($diff * 1000 * 1000)); + $usleep = (int)ceil($diff * 1000 * 1000); + echo "do sleep ($diff, $usleep)\n"; + usleep($usleep); } + if ($this->lastTime) { + echo "Real diff ".(microtime(true)-$this->lastTime).PHP_EOL; + } $this->lastTime = \microtime(true); } diff --git a/tests/IntegrationTests/DataAPIIntegrationTest.php b/tests/IntegrationTests/DataAPIIntegrationTest.php index adb6bb5..d3f43b7 100644 --- a/tests/IntegrationTests/DataAPIIntegrationTest.php +++ b/tests/IntegrationTests/DataAPIIntegrationTest.php @@ -6,6 +6,37 @@ class DataAPIIntegrationTest extends IntegrationTestBase { + /** + * @var int + */ + static protected $testEnd; + + private function logging($log) { + echo $log; + } + public function setUp() + { + parent::setUp(); // TODO: Change the autogenerated stub + if (self::$testEnd) { + $this->logging("started non-first integration test\n"); + $secondSleep = (microtime(true)-self::$testEnd)*1e6; + + } else { + $this->logging("started first integration test\n"); + $secondSleep = 1*1e6; + } + $this->logging("so sleep for $secondSleep\n"); + + usleep($secondSleep); + } + + public function tearDown() + { + parent::tearDown(); // TODO: Change the autogenerated stub + self::$testEnd = microtime(true); + $this->logging("ended integration test\n"); + } + protected static $txExceptFields = [ '.data.confirmations', '.data.estimated_value', '.data.estimated_change', '.data.estimated_change_address', '.data.time', '.data.block_time', '.data.block_hash', @@ -19,15 +50,20 @@ public function testRestClient() { } public function testAddress() { + echo "setup\n"; $client = $this->setupBlocktrailSDK(); //address info + echo "address\n"; $address = $client->address("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief"); + echo "done address \n"; $this->assertEqualsExceptKeys(\json_decode(\file_get_contents(__DIR__ . "/../data/address.3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief.json"), true), $address, []); //address transactions + echo "addresstxs \n"; $response = $client->addressTransactions("3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief", $page = 1, $limit = 20); + echo "done addresstxs \n"; $expectedResponse = \json_decode(\file_get_contents(__DIR__ . "/../data/addressTxs.3EU8LRmo5PgcSwnkn6Msbqc8BKNoQ7Xief.json"), true); self::sortByHash($expectedResponse['data']); self::sortByHash($response['data']); From 08f876a71db218dd3a17d3a4461b6355412fab85 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 30 Jul 2018 15:10:43 +0100 Subject: [PATCH 11/20] test out composer ca bundle --- composer.json | 3 ++- src/Connection/RestClient.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 4139529..06ea4a2 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,8 @@ "dompdf/dompdf" : "0.6.*", "endroid/qrcode": "1.5.*", "blocktrail/cryptojs-aes-php": "0.1.*", - "spomky-labs/php-aes-gcm": "v1.2.0" + "spomky-labs/php-aes-gcm": "v1.2.0", + "composer/ca-bundle": "^1.1" }, "require-dev": { "phpunit/phpunit": "5.*", diff --git a/src/Connection/RestClient.php b/src/Connection/RestClient.php index 542d42e..7852d64 100644 --- a/src/Connection/RestClient.php +++ b/src/Connection/RestClient.php @@ -4,6 +4,7 @@ use Blocktrail\SDK\Blocktrail; use Blocktrail\SDK\Throttler; +use Composer\CaBundle\CaBundle; use GuzzleHttp\Client as Guzzle; use GuzzleHttp\Handler\CurlHandler; use GuzzleHttp\HandlerStack; @@ -79,7 +80,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' => \getenv('BLOCKTRAIL_SDK_NO_SSL_VERIFY') ? false : true, + 'verify' => CaBundle::getBundledCaBundlePath(), 'proxy' => '', 'debug' => false, 'config' => array(), From 5ef24b7321a6edfa098d1cfff5ab302983c0c7f0 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 15:34:26 +0200 Subject: [PATCH 12/20] whitespace trigger build --- src/Throttler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Throttler.php b/src/Throttler.php index 80f5380..2c44d6a 100644 --- a/src/Throttler.php +++ b/src/Throttler.php @@ -64,7 +64,7 @@ public function waitForThrottle() { $now = \microtime(true); $diff = $this->interval - ($now - $this->lastTime); - echo "Throttle routine\n"; + echo "Throttle routine \n"; echo "* interval is {$this->interval}\n"; echo "* now is {$now}\n"; echo "* lastTime is {$this->lastTime}\n"; From 69089ec190dc4e9d0ed888a58d81f1f74b30436b Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 15:39:08 +0200 Subject: [PATCH 13/20] ls ca certs locations --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7d641cd..3cb8a23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ install: - sh -c 'if [ "${TEST_FRAMEWORK}" != "" ]; then composer global require fxp/composer-asset-plugin; fi' script: + - ls -lsaht /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt # run tests when phpcs is not enabled - if [[ "${RUN_PHPUNIT}" != "false" ]]; then export RUN_PHPUNIT="true"; From 63a158eb59799ef332029ede7cd0a76ba58d21bc Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 15:43:01 +0200 Subject: [PATCH 14/20] debug guzzlehttp --- composer.json | 3 +-- src/Connection/RestClient.php | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 06ea4a2..4139529 100644 --- a/composer.json +++ b/composer.json @@ -46,8 +46,7 @@ "dompdf/dompdf" : "0.6.*", "endroid/qrcode": "1.5.*", "blocktrail/cryptojs-aes-php": "0.1.*", - "spomky-labs/php-aes-gcm": "v1.2.0", - "composer/ca-bundle": "^1.1" + "spomky-labs/php-aes-gcm": "v1.2.0" }, "require-dev": { "phpunit/phpunit": "5.*", diff --git a/src/Connection/RestClient.php b/src/Connection/RestClient.php index 7852d64..29330de 100644 --- a/src/Connection/RestClient.php +++ b/src/Connection/RestClient.php @@ -80,9 +80,9 @@ 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' => CaBundle::getBundledCaBundlePath(), + //'verify' => CaBundle::getBundledCaBundlePath(), 'proxy' => '', - 'debug' => false, + 'debug' => true, 'config' => array(), 'auth' => '', )); From d1378acb0cb44d81d4daa1ae6f5c4b5ad0bf2a82 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 15:51:58 +0200 Subject: [PATCH 15/20] debug certs --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 3cb8a23..6d2d0ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ install: script: - ls -lsaht /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt + - echo -n | openssl s_client -showcerts -connect api.blocktrail.com:443 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' # run tests when phpcs is not enabled - if [[ "${RUN_PHPUNIT}" != "false" ]]; then export RUN_PHPUNIT="true"; From 581128177442aa72efcb3908c0417584e15f7aa3 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 15:57:05 +0200 Subject: [PATCH 16/20] why not.. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6d2d0ba..df09743 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ install: script: - ls -lsaht /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt + - cat /etc/ssl/certs/ca-certificates.crt - echo -n | openssl s_client -showcerts -connect api.blocktrail.com:443 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' # run tests when phpcs is not enabled - if [[ "${RUN_PHPUNIT}" != "false" ]]; then From c280b20e0942796e6f50300ba8257cc5af53e45b Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 16:16:52 +0200 Subject: [PATCH 17/20] more debugging --- src/Connection/RestClient.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Connection/RestClient.php b/src/Connection/RestClient.php index 29330de..5a6ec97 100644 --- a/src/Connection/RestClient.php +++ b/src/Connection/RestClient.php @@ -6,6 +6,7 @@ use Blocktrail\SDK\Throttler; use Composer\CaBundle\CaBundle; use GuzzleHttp\Client as Guzzle; +use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Handler\CurlHandler; use GuzzleHttp\HandlerStack; use HttpSignatures\Context; @@ -145,7 +146,18 @@ public function request($method, $endpointUrl, $queryString = null, $body = null $this->throttler->waitForThrottle(); echo "Fired request at " . microtime(true) . PHP_EOL; $request = $this->buildRequest($method, $endpointUrl, $queryString, $body, $auth, $contentMD5Mode, $timeout); - $response = $this->guzzle->send($request, ['auth' => $auth, 'timeout' => $timeout]); + try { + $response = $this->guzzle->send($request, ['auth' => $auth, 'timeout' => $timeout]); + } catch (RequestException $e) { + $debugR = $e->getRequest(); + print_r($debugR->getMethod()); + print_r($debugR->getUri()); + print_r($debugR->getRequestTarget()); + print_r($debugR->getHeaders()); + print_r($debugR->getBody()); + throw $e; + } + return $this->responseHandler($response); } From 05f83db308222e848299de2a1848a2318fd70901 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 16:22:11 +0200 Subject: [PATCH 18/20] less debugging --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index df09743..6d2d0ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,6 @@ install: script: - ls -lsaht /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt - - cat /etc/ssl/certs/ca-certificates.crt - echo -n | openssl s_client -showcerts -connect api.blocktrail.com:443 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' # run tests when phpcs is not enabled - if [[ "${RUN_PHPUNIT}" != "false" ]]; then From 792f7ca5877f9f132fcd33e385dfe22e90c4d846 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 18:32:31 +0200 Subject: [PATCH 19/20] try latest endpoint? --- src/BlocktrailSDK.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BlocktrailSDK.php b/src/BlocktrailSDK.php index 3e0a2f2..c382dab 100644 --- a/src/BlocktrailSDK.php +++ b/src/BlocktrailSDK.php @@ -80,7 +80,7 @@ public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = fa list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet); if (is_null($apiEndpoint)) { - $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com"; + $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://wallet-api.btc.com"; $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/"; } From 6422f141a7c31b783b39f0f49db7e0efb7cfda91 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 8 Aug 2018 19:19:38 +0200 Subject: [PATCH 20/20] wallet-api.btc.com --- src/BlocktrailSDK.php | 2 +- src/Connection/RestClient.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BlocktrailSDK.php b/src/BlocktrailSDK.php index c382dab..e4567dd 100644 --- a/src/BlocktrailSDK.php +++ b/src/BlocktrailSDK.php @@ -226,7 +226,7 @@ public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = $queryString = [ 'page' => $page, 'limit' => $limit, - 'sort_dir' => $sortDir + 'sort_dir' => $sortDir, ]; $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString)); return $this->converter->convertAddressTxs($response->body()); diff --git a/src/Connection/RestClient.php b/src/Connection/RestClient.php index 5a6ec97..334b8d7 100644 --- a/src/Connection/RestClient.php +++ b/src/Connection/RestClient.php @@ -47,7 +47,7 @@ public function __construct($apiEndpoint, $apiVersion, $apiKey, $apiSecret) { if ($throttle = \getenv('BLOCKTRAIL_SDK_THROTTLE_BTCCOM')) { $throttle = (float)$throttle; } else { - $throttle = 0.3; + $throttle = 0.33; } $this->throttler = Throttler::getInstance($this->apiEndpoint, $throttle); @@ -83,7 +83,7 @@ protected function createGuzzleClient(array $options = [], array $curlOptions = 'timeout' => 20.0, // tmp until we have a good matrix of all the requests and their expect min/max time //'verify' => CaBundle::getBundledCaBundlePath(), 'proxy' => '', - 'debug' => true, + 'debug' => false, 'config' => array(), 'auth' => '', ));