diff --git a/src/TransactionBuilder.php b/src/TransactionBuilder.php index 15fe0e9..e23efde 100644 --- a/src/TransactionBuilder.php +++ b/src/TransactionBuilder.php @@ -132,6 +132,11 @@ public function shouldRandomizeChangeOuput() { * @return $this */ public function setFee($value) { + // using this 'dirty' way of checking for a float since there's no other reliable way in PHP + if (!is_int($value)) { + throw new \Exception("Fee should be in Satoshis (int) - can be 0"); + } + $this->fee = $value; return $this; diff --git a/src/Wallet.php b/src/Wallet.php index bbe6a48..3dbf9d8 100644 --- a/src/Wallet.php +++ b/src/Wallet.php @@ -498,10 +498,11 @@ public function doDiscovery($gap = 200) { * @param string $changeAddress (optional) change address to use (autogenerated if NULL) * @param bool $allowZeroConf * @param bool $randomizeChangeIdx randomize the location of the change (for increased privacy / anonimity) - * @return string the txid / transaction hash + * @param int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended! + * @return string the txid / transaction hash * @throws \Exception */ - public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true) { + public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $forceFee = null) { if ($this->locked) { throw new \Exception("Wallet needs to be unlocked to pay"); } @@ -517,6 +518,13 @@ public function pay(array $outputs, $changeAddress = null, $allowZeroConf = fals $txBuilder = new TransactionBuilder(); $txBuilder->randomizeChangeOutput($randomizeChangeIdx); + if ($forceFee !== null) { + $txBuilder->setFee($forceFee); + } else { + $txBuilder->validateChange($change); + $txBuilder->validateFee($fee); + } + foreach ($utxos as $utxo) { $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script']); } @@ -525,9 +533,6 @@ public function pay(array $outputs, $changeAddress = null, $allowZeroConf = fals $txBuilder->addRecipient($output['address'], $output['value']); } - $txBuilder->validateChange($change); - $txBuilder->validateFee($fee); - return $this->sendTx($txBuilder); } @@ -626,8 +631,9 @@ public function buildTx(TransactionBuilder $txBuilder) { // if change is not dust we need to add a change output if ($change > Blocktrail::DUST) { + $changeIdx = count($send); $changeAddress = $txBuilder->getChangeAddress() ?: $this->getNewAddress(); - $send[$changeAddress] = $change; + $send[$changeIdx] = ['address' => $changeAddress, 'value' => $change]; } else { // if change is dust we do nothing (implicitly it's added to the fee) $change = 0; diff --git a/src/WalletInterface.php b/src/WalletInterface.php index 031be9a..20c9ab3 100644 --- a/src/WalletInterface.php +++ b/src/WalletInterface.php @@ -145,9 +145,10 @@ public function doDiscovery($gap = 200); * @param string $changeAddress (optional) change address to use (autogenerated if NULL) * @param bool $allowZeroConf * @param bool $randomizeChangeIdx randomize the location of the change (for increased privacy / anonimity) + * @param int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended! * @return string the txid / transaction hash */ - public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true); + public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $forceFee = null); /** * build inputs and outputs lists for TransactionBuilder diff --git a/tests/WalletTest.php b/tests/WalletTest.php index fe4c37c..4c8827e 100644 --- a/tests/WalletTest.php +++ b/tests/WalletTest.php @@ -256,9 +256,29 @@ public function testWalletTransaction() { $this->assertTrue(BitcoinLib::validate_address($address, false, null)); $value = BlocktrailSDK::toSatoshi(0.0002); - $txHash = $wallet->pay([ - $address => $value, - ]); + $txHash = $wallet->pay([$address => $value,]); + + $this->assertTrue(!!$txHash); + + sleep(1); // sleep to wait for the TX to be processed + + try { + $tx = $client->transaction($txHash); + } catch (ObjectNotFound $e) { + $this->fail("404 for tx[{$txHash}] [" . gmdate('Y-m-d H:i:s') . "]"); + } + + $this->assertTrue(!!$tx, "check for tx[{$txHash}] [" . gmdate('Y-m-d H:i:s') . "]"); + $this->assertEquals($txHash, $tx['hash']); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0001), $tx['total_fee']); + $this->assertTrue(count($tx['outputs']) <= 2); + $this->assertTrue(in_array($value, array_column($tx['outputs'], 'value'))); + + /* + * do another TX but with a custom fee + */ + $value = BlocktrailSDK::toSatoshi(0.0002); + $txHash = $wallet->pay([$address => $value,], null, false, true, BlocktrailSDK::toSatoshi(0.0003)); $this->assertTrue(!!$txHash); @@ -272,6 +292,7 @@ public function testWalletTransaction() { $this->assertTrue(!!$tx, "check for tx[{$txHash}] [" . gmdate('Y-m-d H:i:s') . "]"); $this->assertEquals($txHash, $tx['hash']); + $this->assertEquals(BlocktrailSDK::toSatoshi(0.0003), $tx['total_fee']); $this->assertTrue(count($tx['outputs']) <= 2); $this->assertTrue(in_array($value, array_column($tx['outputs'], 'value'))); }