├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── electrum.php └── install-service.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kamil Monicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-electrum-class 2 | 3 | ![release](https://img.shields.io/github/release/Zaczero/php-electrum-class.svg) 4 | ![license](https://img.shields.io/github/license/Zaczero/php-electrum-class.svg) 5 | 6 | A simple, yet powerful Electrum class for PHP which allows you to receive cryptocurrency payments without any third party integrations nor KYC verification. Works with Linux, Windows and OSX installations. Latest SegWit address format is supported as well. 7 | 8 | ## 🌤️ Installation 9 | 10 | ### Install with composer (recommended) 11 | 12 | `composer require zaczero/php-electrum-class` 13 | 14 | *[Get composer here - getcomposer.org](https://getcomposer.org)* 15 | 16 | ### Install manually 17 | 18 | [Browse latest GitHub release](https://github.com/Zaczero/php-electrum-class/releases/latest) 19 | 20 | ## 🏁 Getting started 21 | 22 | ### Installing Electrum 23 | 24 | Let's start with setting up an electrum installation on your machine. Please follow the instructions at the [electrum.org/#download](https://electrum.org/#download). Alternatively if you are using Linux you can execute the following list of commands. Make sure to change the download URL to match the latest version of electrum. 25 | 26 | ```bash 27 | # Install dependencies 28 | sudo apt install python3-pyqt5 libsecp256k1-0 python3-cryptography 29 | 30 | # Download package 31 | wget https://download.electrum.org/3.3.8/Electrum-3.3.8.tar.gz 32 | 33 | # Extract package 34 | tar -xvf Electrum-3.3.8.tar.gz 35 | 36 | # Install electrum command 37 | sudo ln -s $(pwd)/Electrum-3.3.8/run_electrum /usr/bin/electrum 38 | 39 | # Check if everything works properly 40 | electrum help 41 | ``` 42 | 43 | ### Configuring RPC 44 | 45 | Now you have to set the username, password and port for the RPC connection. You can do that by running those commands. Make sure that the password is hard to guess and that the port is unreachable from behind the firewall. 46 | 47 | ```bash 48 | electrum setconfig rpcuser "user" 49 | electrum setconfig rpcpassword "S3CR3T_password" 50 | electrum setconfig rpcport 7777 51 | ``` 52 | 53 | #### [Testnet] Configuring RPC 54 | 55 | ```bash 56 | electrum setconfig rpcuser "user" --testnet 57 | electrum setconfig rpcpassword "S3CR3T_password" --testnet 58 | electrum setconfig rpcport 7777 --testnet 59 | ``` 60 | 61 | ### Creating wallet 62 | 63 | To create the wallet execute the command: 64 | 65 | * SegWit wallet 66 | 67 | ```bash 68 | electrum create --segwit 69 | ``` 70 | 71 | * Legacy wallet 72 | 73 | ```bash 74 | electrum create 75 | ``` 76 | 77 | #### [Testnet] Creating wallet 78 | 79 | ```bash 80 | electrum create --segwit --testnet 81 | # or 82 | electrum create --testnet 83 | ``` 84 | 85 | ### Starting Electrum in daemon mode 86 | 87 | There are two commands you have to run in order to have our electrum daemon function properly. 88 | 89 | ```bash 90 | # Start the daemon 91 | electrum daemon start 92 | 93 | # Load the wallet 94 | electrum daemon load_wallet 95 | ``` 96 | 97 | Please note that you will have to load the wallet every time you start the daemon. The same applies for the autostart procedure. 98 | 99 | #### [Testnet] Starting Electrum in daemon mode 100 | 101 | ```bash 102 | # Start the daemon 103 | electrum daemon start --testnet 104 | 105 | # Load the wallet 106 | electrum daemon load_wallet --testnet 107 | ``` 108 | 109 | ### (Optional linux-only) Create autostart entry 110 | 111 | The last step would be to make electrum daemon autostart itself on the system boot. You can achieve that by adding a `@reboot` entry to the cron service. To edit the cron tasks execute the following command. 112 | 113 | ```bash 114 | sudo crontab -e 115 | ``` 116 | 117 | Then simply create a reboot entry in a new line: 118 | 119 | ```bash 120 | @reboot electrum daemon start; electrum daemon load_wallet 121 | ``` 122 | 123 | #### [Testnet] Create autostart entry 124 | 125 | ```bash 126 | @reboot electrum daemon start --testnet; electrum daemon load_wallet --testnet 127 | ``` 128 | 129 | ## 🎡 Using PHP Electrum class 130 | 131 | First of all make sure to `require` the php-electrum-class file. Then you can initialize the class with the default constructor which requires *rpcuser* and *rpcpassword* variables. If you are planning to use the testnet please provide the testnet connection settings. Optionally you can also pass a custom *rpchost* and *rpcport* values *(by default it's localhost:7777)*. 132 | 133 | ```php 134 | require_once "electrum.php"; 135 | 136 | $rpcuser = "user"; 137 | $rpcpass = "CHANGE_ME_PASSWORD"; 138 | 139 | $electrum = new Electrum($rpcuser, $rpcpass); 140 | var_dump($electrum->getbalance()); 141 | ``` 142 | 143 | ### 📚 Class documentation 144 | 145 | * **btc2sat(float $btc) : float** 146 | * $btc - bitcoin value 147 | * return - satoshi value 148 | 149 | Converts bitcoin to satoshi unit. 150 | 151 | --- 152 | 153 | * **sat2btc(float $sat) : float** 154 | * $sat - satoshi value 155 | * return - bitcoin value 156 | 157 | Converts satoshi to bitcoin unit. 158 | 159 | --- 160 | 161 | * **broadcast(string $tx) : string** 162 | * $tx - hex-encoded transaction 163 | * return - transaction hash (txid) 164 | 165 | Broadcasts the hex-encoded transaction to the network. 166 | 167 | --- 168 | 169 | * **getfeerate(float $fee_level = 0.5) : string** 170 | * $fee_level - transaction priority *(range from 0.0 to 1.0)* 171 | * return - sat/byte fee rate 172 | 173 | Returns recommended sat/byte fee rate for chosen priority. $fee_level = 0 means that you don't care when the transaction will be going through. You just want to save as much as possible on the fee. $fee_level = 1 means that you want to process the transaction as soon as possible. 174 | 175 | --- 176 | 177 | * **createnewaddress() : string** 178 | * return - new receiving address 179 | 180 | Generates a new receiving address. 181 | 182 | --- 183 | 184 | * **getbalance(bool $confirmed_only = false) : float** 185 | * $confirmed_only - include only confirmed transactions 186 | * return - confirmed account balance 187 | 188 | This one is obvious. However please keep in mind that if you decide to include only confirmed transactions even if you send some amount from the wallet you will not see a change until the outgoing transaction gets confirmed. 189 | 190 | --- 191 | 192 | * **history(int $min_confirmations = 1, int $from_height = 1, &$last_height) : array** 193 | * $min_confirmations - only include transaction with X confirmations or above 194 | * $from_height - only include transaction from block X or above 195 | * &$last_height - returns lastly processed block height 196 | * return - an array of receiving addresses and total transactions value 197 | 198 | Iterates through all of the transactions which met the provided criteria and returns an array of addresses and total transaction value. Addresses are receiving addresses (not sending). Those are the same which got generated using `createnewaddress()` function. 199 | 200 | --- 201 | 202 | * **ismine(string $address) : bool** 203 | * $address - address to check 204 | * return - true or false 205 | 206 | Checks if provided address is owned by the local wallet. 207 | 208 | --- 209 | 210 | * **payto(string $destination, float $amount, float $amount_fee = 0.0) : string** 211 | * $destination - destination address to send to 212 | * $amount - amount to send in bitcoin unit 213 | * $amount_fee - fee amount in bitcoin unit *(0 is dynamic)* 214 | * return - hex-encoded transaction ready to broadcast 215 | 216 | Generates and signs a new transaction with provided parameters. 217 | 218 | --- 219 | 220 | * **payto_max(string $destination, float $amount_fee = 0.0) : string** 221 | * $destination - destination address to send to 222 | * $amount_fee - fee amount in bitcoin unit *(0 is dynamic)* 223 | * return - hex-encoded transaction ready to broadcast 224 | 225 | Generates and signs a new transaction with provided parameters. Sends all funds which are available. 226 | 227 | --- 228 | 229 | * **validateaddress(string $address) : bool** 230 | * $address - address which should be validated 231 | * return - true or false 232 | 233 | Checks if provided address is valid or not. 234 | 235 | ## 🏫 Example usage 236 | 237 | ### Creating a new receiving address 238 | 239 | Simply use a createnewaddress() function to generate the address and save it in the database alongside with the payment amount and the customer ID for later processing. A new address shall be generated for each payment request. 240 | 241 | ```php 242 | require_once "electrum.php"; 243 | 244 | $rpcuser = "user"; 245 | $rpcpass = "CHANGE_ME_PASSWORD"; 246 | 247 | $electrum = new Electrum($rpcuser, $rpcpass); 248 | 249 | $receive_address = $electrum->createnewaddress(); 250 | $price = 0.001; 251 | 252 | // pseudo code 253 | db_save_smart($receive_address, $price, $user_id); 254 | render_view(); 255 | ``` 256 | 257 | ### Processing payments (cron task) 258 | 259 | Iterate through receive_address->amount dictionary returned by history() function which contains all newly received payments. Then fetch the payment data by querying the database provided the receive_address. Add the amount to total received and then finalize the payment if total received is greater or equal than the required amount. 260 | 261 | Finally remember to save the $last_height returned by history() function so we don't process the same block twice or more. Please note that if there are no transactions returned then the $last_height will remain the same as provided initially. 262 | 263 | ```php 264 | require_once "electrum.php"; 265 | 266 | $rpcuser = "user"; 267 | $rpcpass = "CHANGE_ME_PASSWORD"; 268 | 269 | $electrum = new Electrum($rpcuser, $rpcpass); 270 | 271 | $min_confirmations = 1; 272 | $last_height = db_load("last_height"); 273 | $from_height = $last_height + 1; 274 | 275 | // iterate through all receive transactions 276 | foreach ($electrum->history( 277 | $min_confrimations, 278 | $from_height, 279 | $last_height) as $receive_address => $amount) { 280 | 281 | // fetch data by receive_address as a unique key 282 | db_where("receive_address", $receive_address); 283 | 284 | $price = db_load("price"); 285 | $user_id = db_load("user_id"); 286 | $amount_paid = db_load("amount_paid"); 287 | $completed = db_load("completed"); 288 | 289 | $amount_paid += $amount; 290 | 291 | // check if user paid the total amount 292 | if ($amount_paid >= $price && !$completed) { 293 | deliver_product($user_id); 294 | 295 | db_where("receive_address", $receive_address); 296 | db_save("amount_paid", $amount_paid); 297 | db_save("completed", true); 298 | } 299 | else { 300 | // wait for more money or already delivered 301 | db_where("receive_address", $receive_address); 302 | db_save("amount_paid", $amount_paid); 303 | } 304 | } 305 | 306 | // we have to store the last_height to make sure 307 | // we don't process the same transaction twice 308 | db_save($last_height); 309 | ``` 310 | 311 | ### Sending all funds to selected address 312 | 313 | ```php 314 | require_once "electrum.php"; 315 | 316 | $rpcuser = "user"; 317 | $rpcpass = "CHANGE_ME_PASSWORD"; 318 | 319 | $electrum = new Electrum($rpcuser, $rpcpass); 320 | 321 | // generate transaction for sending all available funds 322 | $tx = $electrum->payto_max("BTC_ADDRESS"); 323 | // and broadcast it to the network 324 | $txid = $electrum->broadcast($tx); 325 | 326 | // browse the transaction on blockchair.com 327 | $redirect_url = "https://blockchair.com/bitcoin/transaction/".$txid; 328 | header("Location: ".$redirect_url); 329 | ``` 330 | 331 | ### Sending funds to selected address with custom fee 332 | 333 | ```php 334 | require_once "electrum.php"; 335 | 336 | $rpcuser = "user"; 337 | $rpcpass = "CHANGE_ME_PASSWORD"; 338 | 339 | $electrum = new Electrum($rpcuser, $rpcpass); 340 | 341 | // get recommended fee rate as sat/byte 342 | // you can hard-code this value (eg. 1 will be 1 sat/byte) 343 | $fee_rate = $electrum->getfeerate(0.3); 344 | // generate a temporary transaction to estaminate the size 345 | // we don't broadcast it, just use for calculations 346 | $tx_tmp = $electrum->payto("BTC_ADDRESS", 0.2); 347 | // calculate a fee rate in bitcoin for given transaction 348 | // we divide by two because tx is hex-encoded this means 2 chars = 1 byte 349 | $fee = $electrum->sat2btc($fee_rate * strlen($tx_tmp) / 2); 350 | 351 | // send 0.2 BTC and include the bitcoin fee as a third parameter 352 | $tx = $electrum->payto("BTC_ADDRESS", 0.2, $fee); 353 | $txid = $electrum->broadcast($tx); 354 | 355 | // browse the transaction on blockchair.com 356 | $redirect_url = "https://blockchair.com/bitcoin/transaction/".$txid; 357 | header("Location: ".$redirect_url); 358 | ``` 359 | 360 | ## Footer 361 | 362 | ### 📧 Contact 363 | 364 | * Email: [kamil@monicz.pl](mailto:kamil@monicz.pl) 365 | 366 | ### 📃 License 367 | 368 | * [Zaczero/php-electrum-class](https://github.com/Zaczero/php-electrum-class/blob/master/LICENSE) 369 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zaczero/php-electrum-class", 3 | "description": "Accept Bitcoin payments with no third-party/KYC", 4 | "type": "library", 5 | "keywords": [ 6 | "bitcoin", 7 | "btc", 8 | "electrum", 9 | "cryptocurrency", 10 | "payments", 11 | "first-party", 12 | "no-kyc", 13 | "open-source" 14 | ], 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Kamil Monicz", 19 | "email": "kamil@monicz.pl", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": {}, 24 | "autoload": { 25 | "files": [ 26 | "electrum.php" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /electrum.php: -------------------------------------------------------------------------------- 1 | _rpcurl = "http://$rpcuser:$rpcpass@$rpchost:$rpcport"; 22 | $this->_rpcuser = $rpcuser; 23 | $this->_rpcpass = $rpcpass; 24 | $this->_rpcport = $rpcport; 25 | $this->_rpchost = $rpchost; 26 | } 27 | 28 | /** 29 | * Executes electrum command using RPC connection. 30 | * 31 | * @param string $method Command to be executed 32 | * @param array $params (optional) Command parameters 33 | * 34 | * @throws Exception If curl execution fails 35 | * @return mixed JSON-decoded response (as array) from electrum 36 | */ 37 | public function curl(string $method, array $params = []) { 38 | $data = [ 39 | "id" => "curltext", 40 | "method" => $method, 41 | "params" => $params, 42 | ]; 43 | 44 | $ch = curl_init($this->_rpcurl); 45 | curl_setopt($ch, CURLOPT_POST, 1); 46 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 47 | curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-type: application/json"]); 48 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 49 | 50 | $response = curl_exec($ch); 51 | 52 | if (curl_error($ch)) { 53 | throw new Exception(curl_error($ch)); 54 | } 55 | 56 | curl_close($ch); 57 | return json_decode($response, true)["result"]; 58 | } 59 | 60 | /** 61 | * Converts bitcoin to satoshi unit. 62 | * 63 | * @param float $btc Amount in bitcoin 64 | * 65 | * @return float Amount in satoshi 66 | */ 67 | public static function btc2sat(float $btc) : float { 68 | return $btc * 100000000; 69 | } 70 | 71 | /** 72 | * Coverts satoshi to bitcoin unit. 73 | * 74 | * @param float $sat Amount in satoshi 75 | * 76 | * @return float Amount in bitcoin 77 | */ 78 | public static function sat2btc(float $sat) : float { 79 | return $sat / 100000000; 80 | } 81 | 82 | /** 83 | * Broadcasts the hex-encoded transaction (TX) to the network. 84 | * 85 | * @param string $tx Hex-encoded transaction (TX) 86 | * 87 | * @return string Transaction hash (TXID) 88 | */ 89 | public function broadcast(string $tx) : string { 90 | $response = $this->curl("broadcast", [ 91 | "tx" => $tx, 92 | ]); 93 | 94 | return $response; 95 | } 96 | 97 | /** 98 | * Generates a new receiving address. 99 | * 100 | * @return string Generated bitcoin address 101 | */ 102 | public function createnewaddress() : string { 103 | $response = $this->curl("createnewaddress", []); 104 | 105 | return $response; 106 | } 107 | 108 | /** 109 | * Gets the wallet balance. 110 | * 111 | * @param bool $confirmed_only (optional) Should we include only confirmed funds 112 | * 113 | * @return float Wallet balance 114 | */ 115 | public function getbalance(bool $confirmed_only = false) : float { 116 | $response = $this->curl("getbalance", []); 117 | 118 | $total = 0.0; 119 | 120 | if (!$confirmed_only && key_exists("unconfirmed", $response)) $total += $response["unconfirmed"]; 121 | if (key_exists("confirmed", $response)) $total += $response["confirmed"]; 122 | 123 | return $total; 124 | } 125 | 126 | /** 127 | * Returns recommended sat/byte fee rate for selected priority. 128 | * 129 | * @param float $fee_level (optional) Priority of the transaction where 0.0 is the lowest and 1.0 is the highest 130 | * 131 | * @return float Estimated sat/byte fee rate 132 | */ 133 | public function getfeerate(float $fee_level = 0.5) : float { 134 | if ($fee_level < 0.0 || $fee_level > 1.0) throw new Exception("fee_level must be between 0.0 and 1.0"); 135 | 136 | $response = $this->curl("getfeerate", [ 137 | "fee_level" => $fee_level, 138 | ]); 139 | 140 | return floatval($response) / 1000; 141 | } 142 | 143 | /** 144 | * Iterates through all of the transactions which met the provided criteria and returns an array of addresses and total transaction value. 145 | * 146 | * @param int $min_confirmations (optional) Only include transaction with X confirmations or more 147 | * @param int $from_height (optional) Only include transaction since block X (inclusive) 148 | * @param int|null $last_height (optional) Returns last processed block height (if any) 149 | * 150 | * @return array An array of receiving addresses and total transactions value 151 | */ 152 | public function history(int $min_confirmations = 1, int $from_height = 1, &$last_height) : array { 153 | $result = []; 154 | $response = json_decode($this->curl("history", [ 155 | "show_addresses" => true, 156 | "show_fiat" => true, 157 | "show_fees" => true, 158 | "from_height" => $from_height, 159 | ]), true); 160 | 161 | foreach ($response["transactions"] as $transaction) { 162 | if ($transaction["incoming"] !== true || $transaction["height"] === 0) continue; 163 | if ($transaction["confirmations"] < $min_confirmations) break; 164 | 165 | foreach ($transaction["outputs"] as $output) { 166 | if ($this->ismine($output["address"])) { 167 | if (key_exists($output["address"], $result)) { 168 | $result[$output["address"]] += floatval($output["value"]); 169 | } 170 | else { 171 | $result[$output["address"]] = floatval($output["value"]); 172 | } 173 | } 174 | } 175 | 176 | $last_height = $transaction["height"]; 177 | } 178 | 179 | return $result; 180 | } 181 | 182 | /** 183 | * Checks if provided address is owned by the current wallet. 184 | * 185 | * @param string $address Address to check 186 | * 187 | * @return bool 188 | */ 189 | public function ismine(string $address) : bool { 190 | $response = $this->curl("ismine", [ 191 | "address" => $address, 192 | ]); 193 | 194 | return $response; 195 | } 196 | 197 | /** 198 | * Generates and signs a new transaction with provided parameters. 199 | * 200 | * @param string $destination Destination address to send to 201 | * @param float $amount Amount to send in bitcoin unit 202 | * @param float $amount_fee (optional) Fee amount in bitcoin unit (0.0 for dynamic) 203 | * 204 | * @return string Hex-encoded transaction (TX) ready to broadcast 205 | */ 206 | public function payto(string $destination, float $amount, float $amount_fee = 0.0) : string { 207 | if ($amount <= 0) return ""; 208 | if ($amount_fee >= 0.01) return ""; 209 | 210 | $param = [ 211 | "destination" => $destination, 212 | "amount" => $amount, 213 | ]; 214 | 215 | if ($amount_fee > 0.0) { 216 | $param["fee"] = $amount_fee; 217 | } 218 | 219 | $response = $this->curl("payto", $param); 220 | 221 | return $response["hex"]; 222 | } 223 | 224 | /** 225 | * Generates and signs a new transaction with provided parameters. Sends all funds available. 226 | * 227 | * @param string $destination Destination address to send to 228 | * @param float $amount_fee (optional) Fee amount in bitcoin unit (0.0 for dynamic) 229 | * 230 | * @return string Hex-encoded transaction (TX) ready to broadcast 231 | */ 232 | public function payto_max(string $destination, float $amount_fee = 0.0) : string { 233 | if ($amount_fee >= 0.01) return ""; 234 | 235 | $param = [ 236 | "destination" => $destination, 237 | "amount" => "!", 238 | ]; 239 | 240 | if ($amount_fee > 0.0) { 241 | $param["fee"] = $amount_fee; 242 | } 243 | 244 | $response = $this->curl("payto", $param); 245 | 246 | return $response["hex"]; 247 | } 248 | 249 | /** 250 | * Checks if provided address is valid or not. 251 | * 252 | * @param string $address Address to validate 253 | * 254 | * @return bool 255 | */ 256 | public function validateaddress(string $address) : bool { 257 | $response = $this->curl("validateaddress", [ 258 | "address" => $address, 259 | ]); 260 | 261 | return $response; 262 | } 263 | 264 | } -------------------------------------------------------------------------------- /install-service.sh: -------------------------------------------------------------------------------- 1 | cat > electrum.service <