├── .github ├── ISSUE_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── images ├── HD-wallet.png ├── bitcoinPayManual_demo.gif └── bitcoinPay_demo.gif ├── resources ├── StoreCallback.php ├── StoreCheckout.php ├── bitcoinPay.css ├── bitcoinPay.js ├── bitcoinPay.php ├── bitcoinPay_conf.php └── bitcoinPay_light.css └── utilities └── generateKeys.php /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Background 2 | 3 | Describe your issue here. 4 | 5 | ### Your environment 6 | 7 | * which operating system? 8 | * any other relevant environment details? 9 | * are you running LightningTip behind a reverse proxy? 10 | 11 | ### Steps to reproduce 12 | 13 | Tell us how to reproduce this issue. Please provide stacktraces and links to code in question. 14 | 15 | ### Expected behaviour 16 | 17 | Tell us what should happen. 18 | 19 | ### Actual behaviour 20 | 21 | Tell us what happens instead. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | images/ 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at rob.clark56+github@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 michael1011, 2018 robclark56 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 | # bitcoinPay-PHP 2 | The files in this project will allow you to safely accept Bitcoin payments on your online PHP-based store (eStore). 3 | 4 | | Checkout Mode | Manual Mode | 5 | |---|---| 6 | |![bitcoinPay GIF](images/bitcoinPay_demo.gif)|![bitcoinPayManual GIF](images/bitcoinPayManual_demo.gif)| 7 | 8 | ## FEATURES ## 9 | * Support for: 10 | * Two Modes 11 | * Checkout Mode: eStore provides memo & fiat values 12 | * Manual Mode: User provides memo & fiat values 13 | * mainnet and testnet 14 | * Multiple fiat currencies 15 | * P2PKH addresses (e.g. 1xxxxxxxx). 16 | * Segwit support is not available as this is written. If/When Segwit address generation is supported at https://www.smartbit.com.au/api then this code (without change) will support Segwit. 17 | * Exchange Rate fluctuation protection. Protection in cases of late payment broadcasts and/or late transaction mining. 18 | * Each new payment to an unused bitcoin address. With support for multiple payments to same address. 19 | * QR Code Payment Request 20 | * Copy to clipboard 21 | * Error handling 22 | * Variable Confirmations. E.g. buying a low value sticker requires only 1 confirmation. Buying a car requires 6 confirmations. 23 | * Multiple wallets 24 | * Live exchange rate conversions between Fiat and BTC 25 | * Encryption protected messaging from bitcoinPay back to the eStore site. 26 | * CSS formatting 27 | 28 | ## CREDIT ## 29 | bitcoinPay-PHP is based on [LightningTip-PHP](https://github.com/robclark56/lightningtip-PHP), which in turn is based on [LightningTip](https://github.com/michael1011/lightningtip/blob/master/README.md) by [michael1011](https://github.com/michael1011/lightningtip). 30 | ## REQUIREMENTS ## 31 | A webserver that supports: 32 | 33 | * [PHP](http://www.php.net/), 34 | * [mySQL](https://www.mysql.com/), and 35 | * [cron jobs](https://en.wikipedia.org/wiki/Cron). 36 | ## SECURITY ## 37 | At no point do you enter any of your bitcoin private keys. No hacker can spend your bitcoins. 38 | 39 | You need to keep your webserver secure, as a hacker with sufficient privileges could exchange his/her xpub for yours and customers would start paying the hacker. 40 | ## ECOMMERCE EXAMPLE - CHECKOUT MODE ## 41 | The intended audience for this project is users that have an existing online eCommerce site (eStore). Typically the customer ends up at a _checkout confirmation_ webpage with some _Pay Now_ button(s). 42 | 43 | In this project we include a very simple dummy eStore checkout page that serves as an example of how to deploy _bitcoinPay_. 44 | 45 | ## DESIGN ## 46 | The basic flow is as follows: 47 | 48 | 1. Checkout Mode: 49 | 1. eStore displays a shopping cart page with a total payable (Fiat currency) 50 | 1. User clicks _Pay_ button => Redirected to PHP file which converts fiat value to BTC, and returns a confirmation page 51 | 1. Manual Mode 52 | 1. User enters Memo and fiat value. PHP/Javascript calculates BTC value. 53 | 1. User clicks _Get Payment Request_ => Javascript passes values to PHP file which responds with a Payment Request 54 | 1. The PHP file continuously monitors the blockchain for matching transactions 55 | 1. Customer makes payment with wallet 56 | 1. If/When payment has sufficient confirmations => PHP file sends a secure message back to eStore with payment status ('Paid' or 'Underpaid') and details. 57 | 1. eStore checks message validity, and then takes appropriate action for 2 possible cases: 'Paid' or 'Underpaid' 58 | 59 | ``` 60 | [eStore]<----- 'Paid'/'Underpaid'------\ 61 | | | 62 | | ^ 63 | \/ | 64 | [Web Browser,.js,.css]<----HTTP---->[.php]--[database] 65 | | | 66 | [QR] [Blockchain Explorer] 67 | | | 68 | \/ | 69 | [Bitcoin Wallet] -----------------[Blockchain] 70 | ``` 71 | ## EXTENDED PUBLIC KEYS ## 72 | This project takes advantage of the concept of _Extended Public Keys_ (xpub). For a full understanding, see [Mastering Bitcoin, Andreas M. Antonopoulos, Chapter 5](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05.asciidoc). 73 | 74 | ![HD Wallet Image](images/HD-wallet.png) 75 | 76 | The important things to note are: 77 | * An xpub can generate 78 | * ALL of the public keys & addresses in your wallet. 79 | * NONE of the private keys in your wallet, so can not be used to spend your bitcoins. 80 | * Each level of the tree in the above image has a different xpub. 81 | * The xpub at the master ('m') level can generate addresses for many different coins (Bitcoin, Litecoin,...). We do not want to use the xpub from this level. 82 | * The xpub from the Bitcoin (or Bitcoin-Testnet) level is what is needed for this project. 83 | 84 | ### How does bitcoinPay-PHP get the next receiving address from the xpub? ### 85 | There is an undocumented feature at the [smartbit.com.au API](https://www.smartbit.com.au/api). If you give an xpub to the _address_ API call, it returns the next un-used receiving address. 86 | 87 | [Try it!](https://api.smartbit.com.au/v1/blockchain/address/xpub6DFUsfUukGFu5E1rjZZpwGXVw8wUcrvhxzgFgCFCdyT3nxsbQoax9BLME3pY8j2j81ewhF95gbSRiBnmseGy69E2ZYKbHrmBjwtyXkGeSES) 88 | 89 | ### What the ? xpub, ypub, zpub, tpub, upub, vpub ### 90 | The 1st character of an Extended Public Key tells you what sort of wallet it comes from. As this is written, the [smartbit.com.au API](https://www.smartbit.com.au/api) supports only _xpub_ and _tpub_. 91 | 92 | | Address Type | mainnet | testnet| 93 | |----:|-------|-------| 94 | |P2PKH (eg) | xpub (1xxxxxx)| tpub (mxxxxxx)| 95 | |P2SH (eg)| ypub (3xxxxx)| upub (2xxxxx)| 96 | |Bech32 (eg)| zpub (bc1xxx)| vpub (tb1xxx)| 97 | 98 | [More info ...](https://support.samourai.io/article/49-xpub-s-ypub-s-zpub-s) 99 | 100 | ## MONITORING FOR PAYMENTS ## 101 | This is done by a [cron job](https://en.wikipedia.org/wiki/Cron). The timing logic is as below. _EXPIRY_SECONDS_ & _MINE_SECONDS_ are set in the configuration file. 102 | 103 | * __EXPIRY_SECONDS__ defines a time window that starts as soon as the Payment Pequest is generated, and ends EXPIRY_SECONDS later. For a payment to be received it must be broadcast to the blockchain within that window. It does not have to be confirmed within that window. If the payment is broadcast after EXPIRY_SECONDS, bitcoinPay will not track the payment. This window adds a degree of protection when the FIAT/BTC exchange rate is rapidly changing. 104 | * __MINE_SECONDS__ defines a time interval that starts as soon as the Payment Request is generated, and ends MINE_SECONDS later. A non-expired payment that is mined (include in a block) within this window, and has sufficient confirmations is accepted as PAID. This window protects for the case when the sender does not include sufficient miner fee and inclusion in the blockchain takes too long, again risking invoice under-payment in fiat value. 105 | 106 | The cron job runs periodically to check pending payments. `bitcoinPay.php`can be used as the file for that cron job, if: 107 | 108 | * called as a URL with one GET parameter as follows `https://my.estore.com/bitcoinPay/bitcoinPay.php?checksettled`, or 109 | * called from the command-line as follows: `$ php bitcoinPay.php checksettled` 110 | 111 | The logic used is as follows: 112 | 113 | |Transaction received within EXPIRY_SECONDS |Mined within MINE_SECONDS|Current Currency Value >= Invoice Currency Value| Result | 114 | | :---: | :---: | :---: | :---: | 115 | |Yes|Yes|True|Paid| 116 | |Yes|Yes|False|Paid| 117 | |Yes|No|True|Paid| 118 | |Yes|No|False|Underpaid| 119 | |No|N/A|N/A|Not Tracked| 120 | 121 | ## PREPARATION ## 122 | ### 1. Get your xpub & tpub ### 123 | Your wallet software will give your xpub/tpub. Examples shown below. 124 | 125 | 1. Coinomi: Select Bitcoin -> (3-dot menu) -> Account Details 126 | 1. Electrum: Open the wallet you want to receive funds into. Wallet --> Information. 127 | 1. Make your own: 128 | * Go to https://iancoleman.io/bip39/ 129 | * Generate __AND SAVE__ a new 12-word seed 130 | * Select Coin: __BTC-Bitcoin__ for mainnet, or __BTC-Bitcoin Testnet__ for testnet 131 | * Copy the _Account Extended Public Key_ (not the _BIP32 Extended Public Key_) 132 | 1. Other wallets: Check your documentation. 133 | 134 | ### 2. Generate Private/Public key pair ## 135 | To generate a Private/Public key pair, use one of these options: 136 | 137 | 1. Upload [generateKeys.php](https://github.com/robclark56/bitcoinPay-PHP/blob/master/utilities/generateKeys.php) to your host computer. Then run from the command line interface: `$ php generateKeys.php` 138 | 1. [http://travistidwell.com/jsencrypt/demo/](http://travistidwell.com/jsencrypt/demo/) (save page and run offline for extra safety) 139 | 140 | Save these keys locally for now. They will look something like this: 141 | ``` 142 | -----BEGIN RSA PRIVATE KEY----- 143 | MIICXQIBAAKBgQCQ6cZssvv0DNrh5qTDq3VnT8c41V34lTa5YFgE3itTEsxBFgUl 144 | [... lines deleted...] 145 | fqE1sl6cOF5yhsoYdQ2L0uJOqBS6rkqtbnN44pSzMDph 146 | -----END RSA PRIVATE KEY----- 147 | 148 | -----BEGIN PUBLIC KEY----- 149 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCQ6cZssvv0DNrh5qTDq3VnT8c4 150 | [... lines deleted...] 151 | U4UZulZEer8ss8l62QIDAQAB 152 | -----END PUBLIC KEY----- 153 | ``` 154 | 155 | ### 3. Create SQL Database ### 156 | You will need to create a mySQL database. Consult your host server documentation. 157 | 158 | For example, if you have access to cPanel, [these instructions](https://support.hostgator.com/articles/cpanel/how-do-i-create-a-mysql-database-a-user-and-then-delete-if-needed) can help. 159 | 160 | After you have created your database you should have this information: 161 | 162 | |-- Parameter --|------------------ Value ----------------|--- Comment ---| 163 | |---------|-----|-------| 164 | |User||Give _ALL PRIVILEGES_| 165 | |Password|||| 166 | |Host||Often is _localhost_| 167 | |Database name||| 168 | 169 | ## INSTALLATION ## 170 | * Create a folder on your webserver to host the bitcoinPay files. Consult your webserver documentation for details on where html files are stored. 171 | * e.g.: `.../public_html/bitcoinPay` 172 | * e.g.: `.../var/www/html/bitcoinPay` 173 | * e.g.: `.../htdocs/bitcoinPay` 174 | 175 | * Download the [latest release](https://github.com/robclark56/bitcoinPay-PHP/releases), and unzip. 176 | * Upload all files from the unzipped _resources_ folder to your webserver folder. __Note__: Due to JavaScript security, bitcoinPay.php must be hosted at the same domain as bitcoinPay.js 177 | * Edit files. 178 | * __bitcoinPay_conf.php__: Edit values as needed. Leave _WALLET_DEFAULT_ set to _wallet_testnet_. 179 | * __StoreCheckout.php__: Edit the CHANGE_ME section. 180 | * __StoreCallback.php__: Edit the CHANGE_ME section. 181 | * See note below on [Email Special Consideration](https://github.com/robclark56/bitcoinPay-PHP/blob/master/README.md#email-special-consideration) 182 | * __bitcoinPay.js__: Edit the CHANGE_ME section. 183 | * Create cron job to periodically check pending payments. Examples on how to run the cron job every 15 minutes are: 184 | * Servers with normal crontab-style cron jobs: 185 | * `*/15 * * * * /usr/bin/php /home/user/public_html/bitcoinPay/bitcoinPay.php checksettled` 186 | * Servers only allowing URL-style cron jobs: 187 | * Every 15 mins: `https://my.estore.com/bitcoinPay/bitcoinPay.php?checksettled` 188 | * Other: 189 | * Consult your documentation 190 | 191 | ### Email Special Consideration ### 192 | Some webserver hosts do not permit use of the PHP mail() function for security reasons. If you are in this category, there is a workaround available in bitcoinPay. 193 | 194 | * You will need to edit the _bitcoinPaySendEmail()_ function in these 2 files: 195 | * StoreCallback.php 196 | * bitcoinPay.php 197 | * Read the comments in the _bitcoinPaySendEmail()_ function in either of these files 198 | * Install PHPMailer in a folder called `PHPMailer` 199 | * Edit the _bitcoinPaySendEmail()_ function in the 2 files as below 200 | * Change ` if(false){ //false = use PHPMailer` 201 | * Edit all lines with CHANGE_ME 202 | 203 | ## TESTING - CHECKOUT MODE ## 204 | Use your browser to visit your URLs like this: 205 | 206 | * `https://my.estore.com/bitcoinPay/bitcoinPay.php?checksettled` 207 | * Note: This displays nothing if there are no pending payments, so _blank screen_ is a good response. The only point in trying this is to confirm there are no PHP configuration or syntax errors. 208 | * `https://my.estore.com/bitcoinPay/StoreCallback.php` 209 | * You should receive an email with this is the body: "Hacking Attempt???". This is the expected response when this file is called with the wrong, or no, POST parameters. 210 | * `https://my.estore.com/bitcoinPay/StoreCheckout.php` 211 | * `https://my.estore.com/bitcoinPay/StoreCheckout.php?order_id=100` 212 | * `https://my.estore.com/bitcoinPay/StoreCheckout.php?wallet=wallet_testnet` 213 | * `https://my.estore.com/bitcoinPay/StoreCheckout.php?wallet=wallet_mainnet` 214 | * `https://my.estore.com/bitcoinPay/StoreCheckout.php?wallet=wallet_mainnet&order_id=100` 215 | 216 | or you can check my test site here: 217 | 218 | (_https_ not used as this is hosted on a free web server without SSL certificates. You will not be entering any sensitive data.) 219 | 220 | * [Order for USD 80.00 (mainnet)](http://raspibolt.epizy.com/bitcoinPay/StoreCheckout.php?wallet=wallet_mainnet) CAREFUL: Don't send me real BTC. 221 | * [Order for USD 5.00 (testnet)](http://raspibolt.epizy.com/bitcoinPay/StoreCheckout.php?wallet=wallet_testnet&order_id=100) 222 | 223 | ## TESTING - MANUAL MODE ## 224 | Confirm Checkout Mode (above) is working. 225 | 226 | Use your browser to visit your URL like this: 227 | 228 | * `https://my.estore.com/bitcoinPay/bitcoinPay.php` 229 | 230 | or you can check my test site here: 231 | 232 | * [mainnet](http://raspibolt.epizy.com/bitcoinPay/bitcoinPay.php?wallet=wallet_mainnet) CAREFUL: Don't send me real BTC. 233 | * [testnet](http://raspibolt.epizy.com/bitcoinPay/bitcoinPay.php?wallet=wallet_testnet) 234 | 235 | ## LIVE USAGE ## 236 | 237 | ### 1a. Checkout Mode: Create a page on your eStore with a form something like this: ### 238 | ```php 239 | 247 |
248 | 249 | 250 | 251 | 253 | 254 | 255 | 257 | 258 |
Order Number: 252 |
Order Total: 256 |
259 | 260 | 261 | 262 |
263 | ``` 264 | ### 1b. Manual Mode ### 265 | Just visit your url like: `https://my.estore.com/bitcoinPay/bitcoinPay.php` 266 | ### 2. Process Payment Notifications ### 267 | Edit _StoreCallback.php_ and change these two sections as appropriate for your eStore. 268 | ```php 269 | case 'fullyPaid': 270 | //Add code here to process fully paid order 271 | break; 272 | 273 | case 'underPaid': 274 | //Add code here to process under-paid order 275 | break; 276 | ``` 277 | ### 3. Edit __bitcoinPay_conf.php__ to set the default wallet to your mainnet wallet. ### 278 | ```php 279 | define('DEFAULT_WALLET' ,'wallet_mainnet'); 280 | ``` 281 | 282 | ### 4. There is a light theme available for bitcoinPay. ### 283 | If you want to use it, uncomment this line in your bitcoinPay.php file: 284 | ``` 285 | 286 | ``` 287 | ### 5. Do not use bitcoinPay on XHTML sites ### 288 | That causes some weird scaling issues. 289 | 290 | ## LOCK DOWN SECURITY ## 291 | ### 1. Set files to Read Only ### 292 | Example using the shell command line: 293 | ``` 294 | $ cd 295 | $ chmod 0444 * 296 | $ cd .. 297 | $ chmod 0555 298 | ``` 299 | 300 | --- 301 | If want to tip me, you can use my [LightningTip](https://github.com/robclark56/lightningtip-PHP "lightningTip-PHP") as below. 302 | (_https_ not used as these are hosted on a free web server without SSL certificates. You will not be entering any sensitive data.) 303 | * [mainnet](http://raspibolt.epizy.com/LT/lightningTip.php) 304 | * [testnet](http://raspibolt.epizy.com/LT/lightningTip.php?testnet=1) 305 | 306 | -------------------------------------------------------------------------------- /images/HD-wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robclark56/bitcoinPay-PHP/36e3894fc01ccb0a1024b9dc037a12a33c1c63dc/images/HD-wallet.png -------------------------------------------------------------------------------- /images/bitcoinPayManual_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robclark56/bitcoinPay-PHP/36e3894fc01ccb0a1024b9dc037a12a33c1c63dc/images/bitcoinPayManual_demo.gif -------------------------------------------------------------------------------- /images/bitcoinPay_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robclark56/bitcoinPay-PHP/36e3894fc01ccb0a1024b9dc037a12a33c1c63dc/images/bitcoinPay_demo.gif -------------------------------------------------------------------------------- /resources/StoreCallback.php: -------------------------------------------------------------------------------- 1 | fullyPaid 12 | [data] => Array( 13 | [id] => 54 14 | [address] => mgjQF......AyEASgW 15 | [BTC] => 0.01060340 16 | [memo] => Order 42 17 | [currency] => USD 18 | [amount] => 80 19 | [minConfirmations] => 5 20 | [callback] => https://my.estore.com/bitcoinPay/StoreCallback.php 21 | [btc] => 0.01060340 22 | [confirmations] => 6 23 | [txid] => 34859e48e7106.....1d36788f5708a9 24 | [first_seen_gmt] => 2018-06-01 06:43:03 25 | [settled] => 1 26 | ) 27 | [hash] => 19e2d328f20701c3....2b90a8316f47b26d 28 | 29 | 30 | Security: 31 | To counter man-in-the-middle attacks, the [hash] must be verfied as ([data][address], hashed with PRIVATE KEY). 32 | 33 | */ 34 | 35 | // --- START CHANGE_ME ------------------------------------------ 36 | 37 | define('EMAIL_TO' ,'me@my.estore.com'); //Leave blank to disable email notification 38 | define('EMAIL_TO_NAME' ,'Manager'); 39 | define('EMAIL_FROM' ,'me@my.estore.com'); 40 | define('EMAIL_FROM_NAME','eStore Callback'); 41 | 42 | $eStorePubKey = 43 | '-----BEGIN PUBLIC KEY----- 44 | [... lines removed ...] 45 | -----END PUBLIC KEY-----' 46 | ; 47 | 48 | // ---- END CHANGE_ME -------------------------------------------- 49 | 50 | 51 | // Decrypt hash 52 | openssl_public_decrypt(hex2bin($_POST['hash']), $decrypt_hash, $eStorePubKey); 53 | 54 | // Check and process 55 | if($_POST['data']['address'] && $decrypt_hash === $_POST['data']['address']){ 56 | switch($_POST['status']){ 57 | case 'fullyPaid': 58 | $message = $_POST['status']; 59 | //Add code here to process fully paid order 60 | break; 61 | 62 | case 'underPaid': 63 | $message = $_POST['status']; 64 | //Add code here to process under-paid order 65 | break; 66 | 67 | default: 68 | $message = 'Unknown status:'.$_POST['status']; 69 | } 70 | } else { 71 | $message = 'Hacking Attempt???'; 72 | } 73 | 74 | // Notify 75 | bitcoinPaySendEmail(EMAIL_TO,EMAIL_TO_NAME,__FILE__,"$message\n\nPOST:".print_r($_POST,1)); 76 | 77 | ////////////////////////////////////////////////////////////////////////////// 78 | 79 | function bitcoinPaySendEmail($to,$to_name,$subject,$body){ 80 | // By default, this function uses the built in PHP mail() function. 81 | // If your hosting service does not allow PHP mail(), then PHPMailer may work for you. 82 | // See more info here: https://infinityfree.net/support/how-to-send-email-with-gmail-smtp/ 83 | // Note: The PHPMailer instructions work with more than just gmail. 84 | // 85 | if(empty($to)) return; 86 | 87 | if(true){ //false = use PHPMailer 88 | mail("$to_name <$to>",$subject,$body,"From: ".EMAIL_FROM_NAME." <".EMAIL_FROM.">"); 89 | } else { 90 | date_default_timezone_set('CHANGE_ME'); //eg 'Australia/Perth' 91 | require '../PHPMailer/PHPMailerAutoload.php'; //CHANGE_ME if needed 92 | $mail = new PHPMailer; 93 | $mail->isSMTP(); 94 | $mail->Host = 'CHANGE_ME'; // Which SMTP server to use. 95 | $mail->Port = CHANGE_ME; // Which port to use, 587 is the default port for TLS security. 96 | $mail->SMTPSecure = 'tls'; // Which security method to use. TLS is most secure. 97 | $mail->SMTPAuth = true; // Whether you need to login. This is almost always required. 98 | $mail->Username = 'CHANGE_ME'; 99 | $mail->Password = 'CHANGE_ME'; 100 | $mail->setFrom(EMAIL_FROM, EMAIL_FROM_NAME); 101 | $mail->addAddress($to, $name); 102 | $mail->Subject = $subject; 103 | $mail->Body = $body; 104 | $mail->send(); 105 | } 106 | } 107 | 108 | ?> 109 | -------------------------------------------------------------------------------- /resources/StoreCheckout.php: -------------------------------------------------------------------------------- 1 | 50, 29 | 'price_ea' => 0.10, 30 | 'desc' =>'HODL Sticker' 31 | ); 32 | 33 | $orders[42]['products'][] = 34 | array( 35 | 'qty' => 1, 36 | 'price_ea' => 10.00, 37 | 'desc' =>'Pan Galactic Gargle Blaster' 38 | ); 39 | $orders[42]['products'][] = 40 | array( 41 | 'qty' => 2, 42 | 'price_ea' => 5.00, 43 | 'desc' =>'Book of Vogon Poetry' 44 | ); 45 | $orders[42]['products'][] = 46 | array( 47 | 'qty' => 3, 48 | 'price_ea' => 20.00, 49 | 'desc' =>'HODL Teeshirts' 50 | ); 51 | ?> 52 | 53 | 54 | 55 | 56 | Store Checkout 57 | 61 | 62 | 63 | 64 | 65 |
66 | 67 |
68 |

All Electric Bitcoin Emporium

69 | 70 |

Your Shopping Cart - Order

71 | 72 | 73 | "; 83 | } 84 | echo ''; 85 | echo '
QtyDescriptionPrice eachPrice Total
$qty$desc$ $price$ $total
Total$ '.number_format($grand_total,2).'
'; 86 | ?> 87 |
" method="post"> 88 | 89 | 90 | 91 | 92 | 93 |
94 |
95 | ';?> 96 |
97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /resources/bitcoinPay.css: -------------------------------------------------------------------------------- 1 | #bitcoinPay { 2 | width: 12em; 3 | padding: 1em; 4 | background-color: #212121; 5 | border-radius: 4px; 6 | color: #F5F5F5; 7 | font-size: 20px; 8 | font-family: Arial, Helvetica, sans-serif; 9 | text-align: center; 10 | } 11 | 12 | 13 | .bitcoinPayCurrency { 14 | float: left; 15 | margin-top: 0.9em; 16 | margin-left: 0.5em; 17 | width: 2em; 18 | } 19 | 20 | .bitcoinPayInput { 21 | display: inline-block; 22 | padding: 6px 10px; 23 | margin-top: 0.5em; 24 | border: none; 25 | border-radius: 4px; 26 | font-size: 15px; 27 | color: #212121; 28 | background-color: #F5F5F5; 29 | outline: none; 30 | resize: none; 31 | overflow-y: hidden; 32 | } 33 | 34 | .bitcoinPayButton { 35 | padding: 0.4em 1em; 36 | font-size: 17px; 37 | color: #212121; 38 | background-color: #FFC83D; 39 | border: none; 40 | border-radius: 4px; 41 | outline: none; 42 | cursor: pointer; 43 | } 44 | 45 | .bitcoinPayButton:focus { 46 | outline: none; 47 | } 48 | 49 | .bitcoinPayButton::-moz-focus-inner { 50 | outline: none; 51 | border: 0; 52 | } 53 | 54 | #bitcoinPayLogo { 55 | margin-top: 0; 56 | margin-bottom: 0.6em; 57 | font-size: 25px; 58 | } 59 | 60 | #bitcoinPayInputs { 61 | margin-top: 0.8em; 62 | } 63 | 64 | #bitcoinPayMessage { 65 | min-height: 55px; 66 | margin-top: 0.5em; 67 | padding: 8px 10px; 68 | display: inline-block; 69 | box-sizing: border-box; 70 | text-align: left; 71 | font-family: Arial, Helvetica, sans-serif; 72 | width: 100%; 73 | } 74 | 75 | /* Hack for using placeholders on divs */ 76 | #bitcoinPayMessage:empty:before { 77 | content: attr(placeholder); 78 | color: gray; 79 | } 80 | 81 | #bitcoinPayFiatInput { 82 | width: 9em; 83 | float: right; 84 | } 85 | 86 | #bitcoinPayBTC { 87 | width: 9em; 88 | float: right; 89 | } 90 | 91 | #bitcoinPayGetInvoice { 92 | margin-top: 1em; 93 | } 94 | 95 | #bitcoinPayError { 96 | font-size: 17px; 97 | color: #F44336; 98 | } 99 | 100 | #bitcoinPayInvoice { 101 | margin-top: 1em; 102 | margin-bottom: 0.5em; 103 | } 104 | 105 | #bitcoinPayQR { 106 | margin-bottom: 0.8em; 107 | } 108 | 109 | #bitcoinPayTools { 110 | height: 100px; 111 | } 112 | 113 | #bitcoinPayCopy { 114 | border-right: 1px solid #F5F5F5; 115 | border-top-right-radius: 0; 116 | border-bottom-right-radius: 0; 117 | float: left; 118 | } 119 | 120 | #bitcoinPayOpen { 121 | border-top-left-radius: 0; 122 | border-bottom-left-radius: 0; 123 | float: left; 124 | } 125 | 126 | #bitcoinPayExpiry { 127 | padding: 0.3em 0; 128 | float: right; 129 | } 130 | 131 | #bitcoinPayFinished { 132 | margin-bottom: 0.2em; 133 | display: block; 134 | } 135 | 136 | .spinner { 137 | width: 12px; 138 | height: 12px; 139 | display: inline-block; 140 | border: 3px solid #F5F5F5; 141 | border-top: 3px solid #212121; 142 | border-radius: 50%; 143 | animation: spin 1.5s linear infinite; 144 | } 145 | 146 | @keyframes spin { 147 | 0% { 148 | transform: rotate(0deg); 149 | } 150 | 100% { 151 | transform: rotate(360deg); 152 | } 153 | } 154 | 155 | #bitcoinPay.testnet { 156 | background-color: #27b09d; 157 | } 158 | -------------------------------------------------------------------------------- /resources/bitcoinPay.js: -------------------------------------------------------------------------------- 1 | // bitcoinPay.js 2 | // Javascript for bitcoinPay (see: https://github.com/robclark56/bitcoinPay-PHP) 3 | // 4 | 5 | ///////// CHANGE ME //////////// 6 | // e.g. If bitcoinPay.php is available at https://my.domain/bitcoinPay/bitcoinPay.php, 7 | // Then set UrlFilePath to: "/bitcoinPay/bitcoinPay.php" 8 | var UrlFilePath = "/bitcoinPay/bitcoinPay.php"; 9 | ///////// END CHANGE ME //////// 10 | 11 | 12 | var requestUrl = window.location.protocol + "//" + window.location.hostname + UrlFilePath; 13 | var bitcoinLogo = '₿'; 14 | 15 | // To prohibit multiple requests at the same time 16 | var running = false; 17 | 18 | var invoice; 19 | var qrCode; 20 | var defaultGetInvoice; 21 | 22 | // Data capacities for QR codes with mode byte and error correction level L (7%) 23 | // Shortest invoice: 194 characters 24 | // Longest invoice: 1223 characters (as far as I know) 25 | var qrCodeDataCapacities = [ 26 | {"typeNumber": 9, "capacity": 230}, 27 | {"typeNumber": 10, "capacity": 271}, 28 | {"typeNumber": 11, "capacity": 321}, 29 | {"typeNumber": 12, "capacity": 367}, 30 | {"typeNumber": 13, "capacity": 425}, 31 | {"typeNumber": 14, "capacity": 458}, 32 | {"typeNumber": 15, "capacity": 520}, 33 | {"typeNumber": 16, "capacity": 586}, 34 | {"typeNumber": 17, "capacity": 644}, 35 | {"typeNumber": 18, "capacity": 718}, 36 | {"typeNumber": 19, "capacity": 792}, 37 | {"typeNumber": 20, "capacity": 858}, 38 | {"typeNumber": 21, "capacity": 929}, 39 | {"typeNumber": 22, "capacity": 1003}, 40 | {"typeNumber": 23, "capacity": 1091}, 41 | {"typeNumber": 24, "capacity": 1171}, 42 | {"typeNumber": 25, "capacity": 1273} 43 | ]; 44 | 45 | // TODO: solve this without JavaScript 46 | // Fixes weird bug which moved the button up one pixel when its content was changed 47 | window.onload = function () { 48 | var button = document.getElementById("bitcoinPayGetInvoice"); 49 | button.style.height = (button.clientHeight + 1) + "px"; 50 | button.style.width = (button.clientWidth + 1) + "px"; 51 | }; 52 | 53 | var wallet= getVal('wallet'); 54 | console.log('wallet = ' + wallet); 55 | if ( wallet !== null ) { 56 | requestUrl = requestUrl + '?wallet=' + wallet; 57 | console.log('requestUrl = ' + requestUrl ); 58 | } 59 | 60 | // TODO: show invoice even if JavaScript is disabled 61 | // TODO: fix scaling on phones 62 | // TODO: show price in dollar? 63 | function getPayReq() { 64 | if (running === false) { 65 | running = true; 66 | 67 | var payValue = document.getElementById("bitcoinPayAmount"); 68 | 69 | 70 | if (payValue.value !== "") { 71 | if (!isNaN(payValue.value)) { 72 | var request = new XMLHttpRequest(); 73 | 74 | request.onreadystatechange = function () { 75 | if (request.readyState === 4) { 76 | console.log("RESPONSE: " + request.responseText); 77 | try { 78 | var json = JSON.parse(request.responseText); 79 | if (request.status === 200 && json.Error === null) { 80 | console.log("Got invoice: " + json.Invoice); 81 | console.log("Invoice expires in: " + json.Expiry); 82 | console.log("Starting listening for invoice to get settled"); 83 | listenInvoiceSettled(json.Address,json.BTC,json.Memo); 84 | invoice = json.Invoice; 85 | 86 | // Update UI 87 | var wrapper = document.getElementById("bitcoinPay"); 88 | wrapper.innerHTML = "Please make this Bitcoin Payment"; 89 | wrapper.innerHTML += ""; 90 | wrapper.innerHTML += "
"; 91 | wrapper.innerHTML += "
" + 92 | "" + 93 | "" + 94 | "" + 95 | "
"; 96 | starTimer(json.Expiry, document.getElementById("bitcoinPayExpiry")); 97 | 98 | // Fixes bug which caused the content of #bitcoinPayTools to be visually outside of #bitcoinPay 99 | document.getElementById("bitcoinPayTools").style.height = document.getElementById("bitcoinPayCopy").clientHeight + "px"; 100 | document.getElementById("bitcoinPayOpen").onclick = function () { 101 | location.href = json.Invoice; 102 | }; 103 | showQRCode(); 104 | running = false; 105 | } else { 106 | showErrorMessage(json.Error); 107 | } 108 | } catch (exception) { 109 | console.error(exception); 110 | showErrorMessage("Failed to reach backend"); 111 | } 112 | } 113 | }; 114 | console.log('RequestURL = ' + requestUrl ); 115 | request.open("POST", requestUrl , true); 116 | request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 117 | var memo = document.getElementById("bitcoinPayMessage").value; 118 | var currency = document.getElementById("bitcoinPayCurrency").value; 119 | var currencyAmount = document.getElementById("bitcoinPayCurrencyAmount").value; 120 | var callback = document.getElementById("bitcoinPayCallback").value; 121 | var params = "Action=getinvoice&amount=" + parseInt(payValue.value) + "&message=" + encodeURIComponent(memo); 122 | params += '¤cyAmount=' + parseFloat(currencyAmount); 123 | params += '¤cy=' + encodeURIComponent(currency); 124 | params += '&callback=' + encodeURIComponent(callback); 125 | console.log(params); 126 | request.send(params); 127 | var button = document.getElementById("bitcoinPayGetInvoice"); 128 | defaultGetInvoice = button.innerHTML; 129 | button.innerHTML = "
"; 130 | } else { 131 | showErrorMessage("Payment amount must be a number"); 132 | } 133 | } else { 134 | showErrorMessage("No amount set"); 135 | } 136 | 137 | } else { 138 | console.warn("Last request still pending"); 139 | } 140 | } 141 | 142 | function listenInvoiceSettled(address,BTC,memo) { 143 | var interval = setInterval(function () { 144 | var request = new XMLHttpRequest(); 145 | 146 | //Prevent multiple calls for same invoice settled over slow networks. 147 | var IsSettled = false; 148 | if ( IsSettled == true) { 149 | return; 150 | } 151 | console.log('listenInvoiceSettled BTC:' + BTC + ' Address:' + address + ' Memo:' + memo); 152 | request.onreadystatechange = function () { 153 | if (request.readyState === 4 && request.status === 200) { 154 | console.log("RESPONSE: " + request.responseText); 155 | var json = JSON.parse(request.responseText); 156 | console.log('settled = ' + json.settled); 157 | if (json.settled) { 158 | console.log("Invoice settled with " + json.confirmations + " confirmations"); 159 | IsSettled = true; 160 | clearInterval(interval); 161 | showThankYouScreen(); 162 | } 163 | } 164 | }; 165 | 166 | request.open("POST", requestUrl , true); 167 | request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 168 | var params = "Action=checksettled&btc=" + BTC + "&address=" + encodeURIComponent(address) + "&memo=" + encodeURIComponent(memo) ; 169 | //console.log('PARAMS:' + params); 170 | request.send(params); 171 | 172 | }, 10000); 173 | 174 | } 175 | 176 | function showThankYouScreen() { 177 | var wrapper = document.getElementById("bitcoinPay"); 178 | wrapper.innerHTML = ''; 179 | wrapper.innerHTML += 'Payment Notification Received'; 180 | wrapper.innerHTML += '

Thank you!

'; 181 | wrapper.innerHTML += '

You can close this window as we wait for the Payment Confirmations.

'; 182 | } 183 | 184 | function starTimer(duration, element) { 185 | showTimer(duration, element); 186 | 187 | var interval = setInterval(function () { 188 | if (duration > 1) { 189 | duration--; 190 | showTimer(duration, element); 191 | } else { 192 | showExpired(); 193 | clearInterval(interval); 194 | } 195 | 196 | }, 1000); 197 | 198 | } 199 | 200 | function showTimer(duration, element) { 201 | var seconds = Math.floor(duration % 60); 202 | var minutes = Math.floor((duration / 60) % 60); 203 | var hours = Math.floor((duration / (60 * 60)) % 24); 204 | 205 | seconds = addLeadingZeros(seconds); 206 | minutes = addLeadingZeros(minutes); 207 | 208 | if (hours > 0) { 209 | element.innerHTML = hours + ":" + minutes + ":" + seconds; 210 | 211 | } else { 212 | element.innerHTML = minutes + ":" + seconds; 213 | } 214 | 215 | } 216 | 217 | function showExpired() { 218 | var wrapper = document.getElementById("bitcoinPay"); 219 | wrapper.innerHTML = '

'; 220 | wrapper.innerHTML += 'Your payment request expired!'; 221 | } 222 | 223 | function addLeadingZeros(value) { 224 | return ("0" + value).slice(-2); 225 | } 226 | 227 | function showQRCode() { 228 | var element = document.getElementById("bitcoinPayQR"); 229 | 230 | createQRCode(); 231 | 232 | element.innerHTML = qrCode; 233 | var size = document.getElementById("bitcoinPayInvoice").clientWidth + "px"; 234 | var qrElement = element.children[0]; 235 | qrElement.style.height = size; 236 | qrElement.style.width = size; 237 | } 238 | 239 | function createQRCode() { 240 | var invoiceLength = invoice.length; 241 | 242 | // Just in case an invoice bigger than expected gets created 243 | var typeNumber = 26; 244 | for (var i = 0; i < qrCodeDataCapacities.length; i++) { 245 | var dataCapacity = qrCodeDataCapacities[i]; 246 | if (invoiceLength < dataCapacity.capacity) { 247 | typeNumber = dataCapacity.typeNumber; 248 | break; 249 | } 250 | 251 | } 252 | 253 | console.log("Creating QR code with type number: " + typeNumber); 254 | 255 | var qr = qrcode(typeNumber, "L"); 256 | 257 | qr.addData(invoice); 258 | qr.make(); 259 | 260 | qrCode = qr.createImgTag(6, 6); 261 | } 262 | 263 | function copyInvoiceToClipboard() { 264 | var element = document.getElementById("bitcoinPayInvoice"); 265 | 266 | element.select(); 267 | document.execCommand('copy'); 268 | console.log("Copied invoice to clipboard"); 269 | } 270 | 271 | function showErrorMessage(message) { 272 | running = false; 273 | console.error(message); 274 | var error = document.getElementById("bitcoinPayError"); 275 | error.parentElement.style.marginTop = "0.5em"; 276 | error.innerHTML = message; 277 | var button = document.getElementById("bitcoinPayGetInvoice"); 278 | 279 | // Only necessary if it has a child (div with class spinner) 280 | if (button.children.length !== 0) { 281 | button.innerHTML = defaultGetInvoice; 282 | } 283 | 284 | } 285 | 286 | function divRestorePlaceholder(element) { 287 | //
and

mean that there is no user input 288 | if (element.innerHTML === "
" || element.innerHTML === "

") { 289 | element.innerHTML = ""; 290 | } 291 | } 292 | 293 | function getVal(str) { 294 | var v = window.location.search.match(new RegExp('(?:[\?\&]'+str+'=)([^&]+)')); 295 | return v ? v[1] : null; 296 | } 297 | 298 | function updateBTC(exchRate) { 299 | var fiat = document.getElementById('bitcoinPayFiatInput').value; 300 | var BTC = fiat/exchRate; 301 | var MSG = document.getElementById('bitcoinPayMessage').value; 302 | var INV = document.getElementById('bitcoinPayGetInvoice'); 303 | 304 | if((fiat) && (MSG)&&(MSG.trim()!='')){ 305 | INV.style.visibility='visible'; 306 | } else { 307 | INV.style.visibility='hidden'; 308 | } 309 | document.getElementById('bitcoinPayBTC').value = BTC.toFixed(8); 310 | document.getElementById('bitcoinPayAmount').value = BTC.toFixed(8) * 100000000; 311 | document.getElementById('bitcoinPayCurrencyAmount').value = fiat; 312 | } 313 | -------------------------------------------------------------------------------- /resources/bitcoinPay.php: -------------------------------------------------------------------------------- 1 | 'Paid' or 'underPaid' 98 | [isTestnet] => true or false 99 | [data] => Array( 100 | [id] => internal database id. Can be ignored. 101 | [wallet_name] => e.g. wallet_testnet 102 | [address] => The bitcoin address that received the payment 103 | [BTC] => The bitcoin value received 104 | [memo] => e.g. Order 42 105 | [currency] => e.g. USD 106 | [amount] => Fiat value of invoice. e.g. 80 (for $80) 107 | [minConfirmations] => Minimum confirmations required for this value transaction 108 | [callback] => e.g. https://my.estore.com/bitcoinPay/StoreCallback.php 109 | [gmt_request] => The GMT time the payment request was generated. e.g. 2018-06-04 07:55:00 110 | [gmt_first_seen] => The GMT time transaction was broadcast to the blockchain e.g. 2018-06-04 07:57:48 111 | [gmt_expiry_limit] => The GMT time the payment request expired. e.g. 2018-06-04 08:10:00\ 112 | [gmt_mined] => The GMT time the transaction was mined. e.g. 2018-06-04 08:15:36 113 | [gmt_mine_limit] => The GMT time by which the payment must have been mined by. e.g. 2018-06-04 10:55:00 114 | [confirmations] => Confirmations at this time 115 | [txid] => Transaction ID. e.g. e0aa695a827...f57424887243 116 | [settled] => true = Payment complete. (Should never be false) 117 | ) 118 | [hash] => ([data][address], encrypted with the Private Key) e.g. 5678077bf0cbd...58a08b709ae3c6. 119 | To check these data are from the correct source: 120 | ([data][address]) must equal ([hash], decrypted with the Public Key). 121 | [fiatValue] => Array ( 122 | [original] => The original invoice value in fiat. e.g. 80 123 | [now] => The value of the BTC when this callback was sent. 79.8221771016 124 | ) 125 | 126 | */ 127 | 128 | include "bitcoinPay_conf.php"; 129 | define('CHECK_SETTLED','checksettled'); 130 | 131 | if( ($argv[1] == CHECK_SETTLED) //called with command line argument (e.g. cron job) 132 | || isset($_GET[CHECK_SETTLED]) //called as URL (testing) 133 | ){ 134 | //Mode 2 (cron) 135 | $_POST['Action'] = CHECK_SETTLED; 136 | chdir(WORKING_DIRECTORY); 137 | } else { 138 | if(empty($_POST)){ 139 | //Allow Input 140 | $allowInput = true; 141 | $callback = DEFAULT_CALLBACK; 142 | } else { 143 | $amount =$_POST['amount']; 144 | $amount_format =$_POST['amount_format']; 145 | $memo =$_POST['memo']; 146 | $currency =$_POST['currency']; 147 | $currencyAmount =$_POST['currencyAmount']; 148 | $address =$_POST['address']; 149 | $btc =$_POST['btc']; 150 | $callback =$_POST['callback']; 151 | } 152 | if(empty($currency)) $currency = DEFAULT_CURRENCY; 153 | 154 | if($currency){ 155 | $ExchRate = getExchRate($currency); 156 | if(empty($ExchRate)){ 157 | ?> 158 | 159 | 160 | bitcoin Pay Error 161 |

Error

Unable to get Exchange Rate 162 | 163 | getPaymentRequest($_POST['message'],$_POST['amount']); 179 | if(empty($PR['error'])){ 180 | $DB = new bpDatabase; 181 | $DB->newPR($PR['address'],$PR['payment_request'], $_POST['message'],$currency,$currencyAmount,$PR['BTC'], 182 | EXPIRY_SECONDS, MINE_SECONDS, $Wallet->getMinConfirmations($PR['BTC']), 183 | $callback, $walletName 184 | ); 185 | } 186 | //Comment out next 1 line if you do not want to receive GetInvoice notifications 187 | bitcoinPaySendEmail(EMAIL_TO, EMAIL_TO_NAME,'[bitcoinPay] GetInvoice',print_r($PR,1)."\n\nFile:".__FILE__); 188 | 189 | echo json_encode(array 190 | ( 191 | 'Error' =>$PR['error'], 192 | 'Invoice' =>$PR['payment_request'], 193 | 'Address' =>$PR['address'], 194 | 'BTC' =>$PR['BTC'], 195 | 'Memo' =>$_POST['message'], 196 | 'Expiry' =>EXPIRY_SECONDS 197 | ) 198 | ); 199 | exit; 200 | 201 | //MODE 1, CASE 4 ($address & $btc set) 202 | //MODE 2 ($address & $btc not set) 203 | case CHECK_SETTLED: 204 | $CS = checkSettled($Wallet,$address,$btc); 205 | //If txid exists then payment has been broadcast to blockchain 206 | if($CS['txid']){ 207 | $DB = new bpDatabase; 208 | $DB->settledPR($address,$btc,$memo,$CS['confirmations']); 209 | } 210 | if($CS['error']){ 211 | echo json_encode($CS); 212 | } else { 213 | echo json_encode(array 214 | ( 215 | 'settled' =>!empty($CS['txid']), 216 | 'confirmations'=>$CS['confirmations'] 217 | ) 218 | ); 219 | } 220 | exit; 221 | 222 | default: 223 | if(empty($_POST['memo']) && !$allowInput) exit; 224 | 225 | // fall through to displaying the HTML 226 | } 227 | 228 | //MODE 1, CASE 1 229 | $testnet = $Wallet->isTestnet(); 230 | ?> 231 | 232 | 233 | 234 | 235 | bitcoinPay 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 |
> 245 |

246 | 247 | Pay with Bitcoin 248 |
249 | >
254 |
255 |
256 |
257 | > 262 |
263 |
264 |
265 |
266 |
267 | " onchange="updateBTC(this.value)" disabled> 268 |
269 |
270 |
271 | 272 | 273 | 274 | 275 | 276 |
277 |
278 |
279 | 280 | 281 | 282 | checkPayment($address,$btc); 290 | if($CP && !$CP['success']){ 291 | return array('error' =>"Unable to check address $address"); 292 | } 293 | //echo "CP = ".print_r($CP,1); 294 | // bitcoinPaySendEmail(EMAIL_TO, EMAIL_TO_NAME,'[bitcoinPay] checksettled',print_r($CP,1)); 295 | $transactions = $CP['address']['transactions']; 296 | if(!$transactions) { 297 | return array( 'result' =>"no transactions"); 298 | } 299 | foreach($transactions as $transaction){ 300 | //echo "\n".print_r($transaction['outputs'],1); 301 | if(!$transaction['outputs']){ 302 | return array('result' =>"no outputs"); 303 | exit; 304 | } 305 | foreach($transaction['outputs'] as $output){ 306 | if($output['addresses'][0]==$address && $output['value']==$btc){ 307 | return array( 308 | 'address' =>$address, 309 | 'BTC' =>$btc, 310 | 'confirmations' =>$transaction['confirmations'], 311 | 'txid' =>$transaction['txid'], 312 | 'gmt_first_seen'=>gmdate('Y-m-d H:i:s', $transaction['first_seen']), 313 | 'gmt_mined' =>gmdate('Y-m-d H:i:s', $transaction['time']), 314 | 'settled' =>$Wallet->isSettled($transaction['confirmations'],$btc) 315 | ); 316 | } 317 | } 318 | } 319 | } else { 320 | //MODE 2 321 | //Query all unpaid payments 322 | //Typically only called as a cron job; never from the javascript. 323 | // Phase 1: Look for new payments that have been broadcast within EXPIRY_SECONDS window 324 | // Phase 2: Look for broadcast payments to see if confirmed yet. 325 | 326 | $DB = new bpDatabase; 327 | 328 | ///// Phase 1 329 | $NewTransactions = $DB->getNewTransactions(); 330 | 331 | if($NewTransactions)echo "NewTransactions:".print_r($NewTransactions,1)."\n\n"; 332 | //See if matching transaction exists on blockchain for each NewTransaction 333 | if($NewTransactions){ 334 | foreach($NewTransactions as $NT){ 335 | //It is possible that each transaction is for a different wallet. 336 | if(isset($Wallet)) unset($Wallet); 337 | $Wallet = new Wallet($xpub[$NT['wallet_name']]); 338 | 339 | $CS = checkSettled($Wallet, $NT['address'],$NT['BTC']); 340 | echo print_r($CS,1); 341 | if(isset($CS['gmt_first_seen'])){ 342 | $DB->setConfirmations($NT['id'], 0); 343 | if(strtotime($NT['gmt_expiry']) > strtotime($CS['gmt_first_seen'])) { 344 | //Transaction broadcast inside Expiry Window 345 | $DB->setExpired($NT['id'],0); 346 | } else { 347 | //Transaction broadcast outside Expiry Window 348 | $DB->setExpired($NT['id'],1); 349 | } 350 | } else { 351 | //Not Broadcast (yet) 352 | if($NT['expired']) $DB->setExpired($NT['id'],1); 353 | } 354 | } 355 | } 356 | 357 | ///// Phase 2 358 | //See if any payments that have been broadcast have sufficient confirmations 359 | $PendingTransactions = $DB->getPendingTransactions(); 360 | if($PendingTransactions) echo "\nPendingTransactions = ".print_r($PendingTransactions,1); 361 | if(empty($PendingTransactions)) exit; 362 | 363 | foreach($PendingTransactions as $PT){ 364 | //It is possible that each transaction is for a different wallet. 365 | if(isset($Wallet)) unset($Wallet); 366 | $Wallet = new Wallet($xpub[$PT['wallet_name']]); 367 | /* eg $PT 368 | [id] => 54 369 | [address] => mgjQFqyr....HzAyEASgW 370 | [BTC] => 0.01060340 371 | [memo] => Order 42 372 | [currency] => USD 373 | [amount] => 80 374 | [minConfirmations] => 5 375 | [callback] => https://my.estore.com/bitcoinPay/StoreCallback.php 376 | [gmt_mine_limit] => 2018-06-01 07:49:37 377 | */ 378 | $CS = checkSettled($Wallet, $PT['address'],$PT['BTC']); 379 | /* eg $CS 380 | [address] => mgjQFqyrWNihreGHo4331KgEgHzAyEASgW 381 | [BTC] => 0.01060340 382 | [confirmations] => 893 383 | [txid] => 34859e48e71067db......b5d1d36788f5708a9 384 | [gmt_first_seen] => 2018-06-01 06:43:03 385 | [gmt_mined] => 2018-06-01 06:44:38 386 | [settled] => 1 387 | */ 388 | echo "CS: ".print_r($CS,1); 389 | 390 | if($CS['confirmations'] >= $PT['minConfirmations']){ 391 | $DB->setConfirmations($PT['id'], $CS['confirmations']); 392 | $CurrentFiatValue = getExchRate($PT['currency']) * $PT['BTC']; 393 | $OriginalFiatValue = $PT['amount']; 394 | //Check if mined within Mine Time 395 | $statusText = 'fullyPaid'; //default 396 | if(strtotime($PT['gmt_mine_limit']) < strtotime($CS['gmt_mined'])) { 397 | //Mined after Mine time 398 | //Check if Fiat value is above or below original transaction value 399 | if ($CurrentFiatValue < $OriginalFiatValue) $statusText ='underPaid'; 400 | } 401 | 402 | openssl_private_encrypt($PT[address], $encrypted_address, ESTORE_PRIV_KEY); 403 | CallBack($PT['callback'], 404 | array('status' => $statusText, 405 | 'isTestnet' => $Wallet->isTestnet(), 406 | 'data' => array_merge($PT,$CS), 407 | 'hash' => bin2hex($encrypted_address), 408 | 'fiatValue' => array('original'=>$OriginalFiatValue,'now'=>$CurrentFiatValue) 409 | ) 410 | ); 411 | } 412 | } 413 | exit; 414 | } 415 | } 416 | 417 | 418 | function CallBack($URL, $data){ 419 | //Used to callback to the originating eCommerce store 420 | echo "\nCallBack($URL,$json\n\n"; 421 | if(empty($URL)) return; 422 | 423 | $opts = array('http' => 424 | array( 425 | 'method' => 'POST', 426 | 'header' => 'Content-type: application/x-www-form-urlencoded', 427 | 'content' => http_build_query($data) 428 | ) 429 | ); 430 | $context = stream_context_create($opts); 431 | @file_get_contents($URL, false, $context); 432 | } 433 | 434 | function getExchRate($currency){ 435 | static $ExchRate; 436 | 437 | if(!$ExchRate[$currency]) { 438 | $response = json_decode(@file_get_contents("https://bitpay.com/api/rates/BTC/$currency")); 439 | if($response) $ExchRate[$currency] = $response->rate; 440 | } 441 | return $ExchRate[$currency]; 442 | } 443 | 444 | function bitcoinPaySendEmail($to,$to_name,$subject,$body){ 445 | // By default, this function uses the built in PHP mail() function. 446 | // If your hosting service does not allow PHP mail(), then PHPMailer may work for you. 447 | // See more info here: https://infinityfree.net/support/how-to-send-email-with-gmail-smtp/ 448 | // Note: The PHPMailer instructions work with more than just gmail. 449 | // 450 | if(empty($to)) return; 451 | 452 | if(true){ //false = use PHPMailer 453 | mail("$to_name <$to>",$subject,$body,"From: ".EMAIL_FROM_NAME." <".EMAIL_FROM.">"); 454 | } else { 455 | date_default_timezone_set('CHANGE_ME'); //eg 'Australia/Perth' 456 | require '../PHPMailer/PHPMailerAutoload.php'; //CHANGE_ME if needed 457 | $mail = new PHPMailer; 458 | $mail->isSMTP(); 459 | $mail->Host = 'CHANGE_ME'; // Which SMTP server to use. 460 | $mail->Port = CHANGE_ME; // Which port to use, 587 is the default port for TLS security. 461 | $mail->SMTPSecure = 'tls'; // Which security method to use. TLS is most secure. 462 | $mail->SMTPAuth = true; // Whether you need to login. This is almost always required. 463 | $mail->Username = 'CHANGE_ME'; 464 | $mail->Password = 'CHANGE_ME'; 465 | $mail->setFrom(EMAIL_FROM, EMAIL_FROM_NAME); 466 | $mail->addAddress($to, $name); 467 | $mail->Subject = $subject; 468 | $mail->Body = $body; 469 | $mail->send(); 470 | } 471 | } 472 | 473 | ///////// CLASSES /////////////////// 474 | 475 | class bpDatabase { 476 | private $db_table='bpRequests'; 477 | private $mysqli; 478 | 479 | function __construct(){ 480 | //Open DB Link 481 | $mysqli = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_BASE); 482 | if ($mysqli->connect_errno) { 483 | bitcoinPaySendEmail(EMAIL_TO, EMAIL_TO_NAME, 484 | '[bitcoinPay] Failed to connect to DB', 485 | "Error Number: $mysqli->connect_errno\nError Message: $mysqli->connect_error\n\nHOST:".DB_HOST."\nUSER:".DB_USER."\nBASE:".DB_BASE 486 | ); 487 | return; 488 | } 489 | 490 | $this->mysqli = $mysqli; 491 | //Create Table if needed. 492 | $sql = "CREATE TABLE IF NOT EXISTS $this->db_table ( 493 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 494 | `wallet_name` TINYTEXT NOT NULL, 495 | `address` varchar(255) NOT NULL, 496 | `memo` varchar(30) NOT NULL, 497 | `BTC` decimal (10,8) NOT NULL, 498 | `currency` CHAR(3) NOT NULL COMMENT '3 char currency code', 499 | `amount` FLOAT NOT NULL COMMENT 'Invoice amount in local currency', 500 | `payment_request` TEXT NOT NULL, 501 | `expiry_seconds` INT(6) NOT NULL COMMENT 'seconds allowed for transaction broadcast', 502 | `mine_seconds` INT(11) NOT NULL COMMENT 'seconds allowed for transaction to be mined' , 503 | `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 504 | `confirmations` INT(6) NULL DEFAULT NULL COMMENT 'null=not received, 0,1,... = received with x confirmations', 505 | `minConfirmations` INT(6) NOT NULL, 506 | `expired` BOOLEAN NULL DEFAULT NULL COMMENT 'NULL=unknown, 0=Expired with txid, 1=Expired with no TXID', 507 | `callback` TEXT NULL DEFAULT NULL COMMENT 'Callback URL when confirmed', 508 | PRIMARY KEY (`id`) 509 | )"; 510 | mysqli_query($this->mysqli,$sql); 511 | }//_construct 512 | 513 | function __destruct(){ 514 | mysqli_close($this->mysqli); 515 | } 516 | 517 | public function newPR($address,$payment_request,$message,$currency,$amount,$BTC,$expiry_seconds,$mine_seconds,$minConfirmations,$callback,$walletName){ 518 | $message = str_replace ( "'" , "''" , $message) ; //SQL: escape single quotes if present 519 | $sql = "INSERT into $this->db_table (wallet_name,address,payment_request,memo,currency,amount,BTC,expiry_seconds,mine_seconds,minConfirmations,callback) "; 520 | $sql .= "VALUES ('$walletName','$address','$payment_request','$message','$currency','$amount','$BTC','$expiry_seconds','$mine_seconds','$minConfirmations','$callback')"; 521 | mysqli_query($this->mysqli,$sql); 522 | } 523 | 524 | public function settledPR($address, $btc, $memo, $confirmations){ 525 | $sql ="UPDATE $this->db_table SET confirmations = '$confirmations' WHERE address = '$address' AND BTC = '$btc' AND memo = '$memo'"; 526 | //bitcoinPaySendEmail(EMAIL_TO, EMAIL_TO_NAME,'[bitcoinPay] settled',__FILE__."\n$sql"); 527 | mysqli_query($this->mysqli,$sql); 528 | } 529 | 530 | public function getPendingTransactions(){ 531 | $sql = 'SET time_zone = "+00:00";'; //GMT 532 | mysqli_query($this->mysqli,$sql); 533 | 534 | $sql = "SELECT id, wallet_name, address, BTC, memo, currency, amount, minConfirmations, callback, timestamp as gmt_request, ". 535 | "TIMESTAMPADD(second,expiry_seconds,timestamp) as gmt_expiry_limit, ". 536 | "TIMESTAMPADD(second,".MINE_SECONDS.",timestamp) as gmt_mine_limit ". 537 | "FROM $this->db_table ". 538 | "WHERE expired = 0 AND confirmations < minConfirmations "; 539 | 540 | $q = mysqli_query($this->mysqli,$sql); 541 | while($row = mysqli_fetch_assoc($q)){ 542 | $PT[] = $row; 543 | } 544 | return $PT; 545 | } 546 | 547 | public function getNewTransactions(){ 548 | //Return transactions that have not expired & unknown txid 549 | $sql = 'SET time_zone = "+00:00";'; //GMT 550 | mysqli_query($this->mysqli,$sql); 551 | 552 | $sql = "SELECT * , TIMESTAMPADD(second,expiry_seconds,timestamp) as gmt_expiry ". 553 | ", TIMESTAMPDIFF(SECOND, timestamp, CURRENT_TIMESTAMP) - expiry_seconds as ExpiredSeconds ". 554 | "FROM $this->db_table ". 555 | "WHERE expired IS NULL " 556 | ; 557 | 558 | $q = mysqli_query($this->mysqli,$sql); 559 | while($row = mysqli_fetch_assoc($q)){ 560 | $NT[] = array('expired' => $row['ExpiredSeconds'] > 0, 561 | 'gmt_payment_request' => $row['timestamp'], 562 | 'gmt_expiry' => $row['gmt_expiry'], 563 | 'id' => $row['id'], 564 | 'address' => $row['address'], 565 | 'BTC' => $row['BTC'], 566 | 'wallet_name' => $row['wallet_name'] 567 | ); 568 | } 569 | return $NT; 570 | } 571 | 572 | public function setSettled($id, $confirmations){ 573 | $sql = "UPDATE $this->db_table SET confirmations = '$confirmations' WHERE id = '$id'"; 574 | } 575 | 576 | 577 | public function setExpired($id, $expired){ 578 | $sql = "UPDATE $this->db_table SET expired = '$expired' WHERE id='$id'"; 579 | mysqli_query($this->mysqli,$sql); 580 | } 581 | 582 | public function setConfirmations($id, $confirmations){ 583 | $sql = "UPDATE $this->db_table SET confirmations= '$confirmations' WHERE id='$id'"; 584 | mysqli_query($this->mysqli,$sql); 585 | } 586 | 587 | } 588 | 589 | class Wallet{ 590 | private $baseurl = 'https://api.smartbit.com.au/v1/blockchain'; 591 | private $testurl = 'https://testnet-api.smartbit.com.au/v1/blockchain'; 592 | private $xpub; 593 | 594 | function __construct($xpub) { 595 | $this->xpub = $xpub; 596 | } 597 | 598 | public function isTestnet() { 599 | return !( stripos( $this->xpub , 'xpub' ) === 0 || 600 | stripos( $this->xpub , 'ypub' ) === 0 || 601 | stripos( $this->xpub , 'zpub' ) === 0 602 | ); 603 | } 604 | 605 | public function getPaymentRequest($memo='',$satoshi=0){ 606 | $URL = ($this->isTestnet()?$this->testurl:$this->baseurl)."/address/$this->xpub?tx=0"; 607 | $Response = json_decode(@file_get_contents($URL),true); 608 | if(!$Response || $Response['success'] == false){ 609 | $PR['error'] ="Unable to get payment address"; 610 | } else { 611 | $PR['address'] = $Response['address']['extkey_next_receiving_address']; 612 | $PR['BTC']=$satoshi/100000000; 613 | $PR['payment_request'] = "bitcoin:".$PR['address']; 614 | $PR['payment_request'] .= "?amount=".$PR['BTC']; 615 | $PR['payment_request'] .= "&message=".rawurlencode($memo); 616 | $PR['payment_request'] .= "&label=".rawurlencode($memo); 617 | $PR['memo']=$memo; 618 | } 619 | return $PR; 620 | } 621 | 622 | public function checkPayment($address,$btc){ 623 | $URL = ($this->isTestnet()?$this->testurl:$this->baseurl)."/address/$address"; 624 | return json_decode(@file_get_contents($URL),true); 625 | } 626 | 627 | public function getMinConfirmations($BTC){ 628 | if($BTC == 0) return 6; //Should not happen 629 | if($BTC < 0.000001) return 0; //dust 630 | if($BTC < 0.00001) return 1; //sticker 631 | if($BTC < 0.0001) return 2; //coffee 632 | if($BTC < 0.001) return 3; //lunch 633 | if($BTC < 0.01) return 4; //Week's Groceries 634 | if($BTC < 0.1) return 5; //Big TV 635 | return 6; //Small Car or more 636 | } 637 | 638 | public function isSettled ($confirmations,$BTC){ 639 | return $confirmations >= $this->getMinConfirmations($BTC); 640 | } 641 | } 642 | ?> 643 | -------------------------------------------------------------------------------- /resources/bitcoinPay_conf.php: -------------------------------------------------------------------------------- 1 | 65 | -------------------------------------------------------------------------------- /resources/bitcoinPay_light.css: -------------------------------------------------------------------------------- 1 | #bitcoinPay > a { 2 | font-weight: 600 !important; 3 | } 4 | 5 | #bitcoinPay { 6 | background-color: #FFFFFF !important; 7 | 8 | border: 2px #BDBDBD solid !important; 9 | 10 | color: #FFB300 !important; 11 | 12 | } 13 | 14 | .bitcoinPayInput { 15 | border: 1px #BDBDBD solid !important; 16 | 17 | background-color: #FFFFFF !important; 18 | } 19 | 20 | .bitcoinPayButton { 21 | color: #FFFFFF !important; 22 | 23 | background-color: #FFB300 !important; 24 | } 25 | 26 | #bitcoinPayError { 27 | color: #D50000 !important; 28 | 29 | font-weight: 600 !important; 30 | } 31 | 32 | #bitcoinPayCopy { 33 | border-right: 1px solid #FFFFFF !important; 34 | } 35 | 36 | #bitcoinPayExpiry { 37 | font-weight: 600 !important; 38 | } 39 | -------------------------------------------------------------------------------- /utilities/generateKeys.php: -------------------------------------------------------------------------------- 1 | "sha256", 11 | "private_key_bits" => 1024, 12 | "private_key_type" => OPENSSL_KEYTYPE_RSA, 13 | ); 14 | // Create the private and public key 15 | $res = openssl_pkey_new($config); 16 | openssl_pkey_export($res, $privKey); 17 | echo "$privKey\n"; 18 | // Extract the public key from $res to $pubKey 19 | $pubKey = openssl_pkey_get_details($res); 20 | $pubKey = $pubKey["key"]; 21 | echo $pubKey; 22 | ?> 23 | --------------------------------------------------------------------------------