diff --git a/.gitignore b/.gitignore
index d5f17ca..6b84ac5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ composer.lock
dev/
clover*
*.phar
+./coverage/
diff --git a/.travis.yml b/.travis.yml
index 5a1bc44..6d2d0ba 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:
@@ -25,13 +21,14 @@ 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
+ - 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";
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';
+ - 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'
@@ -46,9 +43,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 +66,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
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"
}
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
diff --git a/src/BlocktrailSDK.php b/src/BlocktrailSDK.php
index f15f9bb..e4567dd 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}/";
}
@@ -201,7 +201,7 @@ public function getDataRestClient() {
* @param RestClientInterface $restClient
*/
public function setRestClient(RestClientInterface $restClient) {
- $this->client = $restClient;
+ $this->blocktrailClient = $restClient;
}
/**
@@ -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());
@@ -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/Connection/RestClient.php b/src/Connection/RestClient.php
index 4c45eb2..334b8d7 100644
--- a/src/Connection/RestClient.php
+++ b/src/Connection/RestClient.php
@@ -4,7 +4,9 @@
use Blocktrail\SDK\Blocktrail;
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;
@@ -45,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);
@@ -79,7 +81,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' => CaBundle::getBundledCaBundlePath(),
'proxy' => '',
'debug' => false,
'config' => array(),
@@ -142,9 +144,20 @@ 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]);
+ 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);
}
diff --git a/src/Throttler.php b/src/Throttler.php
index f44ceef..2c44d6a 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/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..d3f43b7
--- /dev/null
+++ b/tests/IntegrationTests/DataAPIIntegrationTest.php
@@ -0,0 +1,268 @@
+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',
+ '.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 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']);
+ $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') . "]");
- }
-}