diff --git a/.gitignore b/.gitignore index a681dcf..e8f8c86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ *.xml *.iml + +# Emacs +*~ +\#* +.\#* \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1c38054..1dba848 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2017-2018 Monero Integrations +Copyright (c) 2018, Ryo Currency Project +Portions Copyright (c) 2017-2018, Monero Integrations Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 087fd6a..aea0a99 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,107 @@ -# MoneroWP -A WooCommerce extension for accepting Monero +# Monero Gateway for WooCommerce -## Dependencies -This plugin is rather simple but there are a few things that need to be set up beforehand. +## Features -* A web server! Ideally with the most recent versions of PHP and mysql +* Payment validation done through either `monero-wallet-rpc` or the [xmrchain.net blockchain explorer](https://xmrchain.net/). +* Validates payments with `cron`, so does not require users to stay on the order confirmation page for their order to validate. +* Order status updates are done through AJAX instead of Javascript page reloads. +* Customers can pay with multiple transactions and are notified as soon as transactions hit the mempool. +* Configurable block confirmations, from `0` for zero confirm to `60` for high ticket purchases. +* Live price updates every minute; total amount due is locked in after the order is placed for a configurable amount of time (default 60 minutes) so the price does not change after order has been made. +* Hooks into emails, order confirmation page, customer order history page, and admin order details page. +* View all payments received to your wallet with links to the blockchain explorer and associated orders. +* Optionally display all prices on your store in terms of Monero. +* Shortcodes! Display exchange rates in numerous currencies. -* A Monero wallet. You can find the official wallet [here](https://getmonero.org/downloads/) +## Requirements -* [WordPress](https://wordpress.org) -WordPress is the backend tool that is needed to use WooCommerce and this Monero plugin +* Monero wallet to receive payments - [GUI](https://github.com/monero-project/monero-gui/releases) - [CLI](https://github.com/monero-project/monero/releases) - [Paper](https://moneroaddress.org/) +* [BCMath](http://php.net/manual/en/book.bc.php) - A PHP extension used for arbitrary precision maths -* [WooCommerce](https://woocommerce.com) -This Monero plugin is an extension of WooCommerce, which works with WordPress +## Installing the plugin -* [BCMath](http://php.net/manual/en/book.bc.php) -A PHP extension used for arbitrary precision maths +* Download the plugin from the [releases page](https://github.com/monero-integrations/monerowp) or clone with `git clone https://github.com/monero-integrations/monerowp` +* Unzip or place the `monero-woocommerce-gateway` folder in the `wp-content/plugins` directory. +* Activate "Monero Woocommerce Gateway" in your WordPress admin dashboard. +* It is highly recommended that you use native cronjobs instead of WordPress's "Poor Man's Cron" by adding `define('DISABLE_WP_CRON', true);` into your `wp-config.php` file and adding `* * * * * wget -q -O - https://yourstore.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1` to your crontab. -## Step 1: Activating the plugin -* Downloading: First of all, you will need to download the plugin. You can download the latest release as a .zip file from https://github.com/monero-integrations/monerowp/releases If you wish, you can also download the latest source code from GitHub. This can be done with the command `git clone https://github.com/monero-integrations/monerowp.git` or can be downloaded as a zip file from the GitHub web page. +## Option 1: Use your wallet address and viewkey -* Unzip the file monerowp_release.zip if you downloaded the zip from the releases page [here](https://github.com/monero-integrations/monerowp/releases). +This is the easiest way to start accepting Monero on your website. You'll need: -* Put the plugin in the correct directory: You will need to put the folder named `monero` from this repo/unzipped release into the WordPress plugins directory. This can be found at `path/to/wordpress/folder/wp-content/plugins` +* Your Monero wallet address starting with `4` +* Your wallet's secret viewkey -* Activate the plugin from the WordPress admin panel: Once you login to the admin panel in WordPress, click on "Installed Plugins" under "Plugins". Then simply click "Activate" where it says "Monero - WooCommerce Gateway" +Then simply select the `viewkey` option in the settings page and paste your address and viewkey. You're all set! -## Step 2 Option 1: Use your wallet address and viewkey +Note on privacy: when you validate transactions with your private viewkey, your viewkey is sent to (but not stored on) xmrchain.net over HTTPS. This could potentially allow an attacker to see your incoming, but not outgoing, transactions if they were to get his hands on your viewkey. Even if this were to happen, your funds would still be safe and it would be impossible for somebody to steal your money. For maximum privacy use your own `monero-wallet-rpc` instance. -* Get your Monero wallet address starting with '4' -* Get your wallet secret viewkey from your wallet +## Option 2: Using `monero-wallet-rpc` -A note on privacy: When you validate transactions with your private viewkey, your viewkey is sent to (but not stored on) xmrchain.net over HTTPS. This could potentially allow an attacker to see your incoming, but not outgoing, transactions if he were to get his hands on your viewkey. Even if this were to happen, your funds would still be safe and it would be impossible for somebody to steal your money. For maximum privacy use your own monero-wallet-rpc instance. +The most secure way to accept Monero on your website. You'll need: -## Step 2 Option 2: Get a Monero daemon to connect to +* Root access to your webserver +* Latest [Monero-currency binaries](https://github.com/monero-project/monero/releases) -### Option 1: Running a full node yourself +After downloading (or compiling) the Monero binaries on your server, install the [systemd unit files](https://github.com/monero-integrations/monerowp/tree/master/assets/systemd-unit-files) or run `monerod` and `monero-wallet-rpc` with `screen` or `tmux`. You can skip running `monerod` by using a remote node with `monero-wallet-rpc` by adding `--daemon-address node.moneroworld.com:18089` to the `monero-wallet-rpc.service` file. -To do this: start the Monero daemon on your server and leave it running in the background. This can be accomplished by running `./monerod` inside your Monero downloads folder. The first time that you start your node, the Monero daemon will download and sync the entire Monero blockchain. This can take several hours and is best done on a machine with at least 4GB of ram, an SSD hard drive (with at least 40GB of free space), and a high speed internet connection. +Note on security: using this option, while the most secure, requires you to run the Monero wallet RPC program on your server. Best practice for this is to use a view-only wallet since otherwise your server would be running a hot-wallet and a security breach could allow hackers to empty your funds. -### Option 2: Connecting to a remote node -The easiest way to find a remote node to connect to is to visit [moneroworld.com](https://moneroworld.com/#nodes) and use one of the nodes offered. It is probably easiest to use node.moneroworld.com:18089 which will automatically connect you to a random node. +## Configuration -### Setup your Monero wallet-rpc +* `Enable / Disable` - Turn on or off Monero gateway. (Default: Disable) +* `Title` - Name of the payment gateway as displayed to the customer. (Default: Monero Gateway) +* `Discount for using Monero` - Percentage discount applied to orders for paying with Monero. Can also be negative to apply a surcharge. (Default: 0) +* `Order valid time` - Number of seconds after order is placed that the transaction must be seen in the mempool. (Default: 3600 [1 hour]) +* `Number of confirmations` - Number of confirmations the transaction must recieve before the order is marked as complete. Use `0` for nearly instant confirmation. (Default: 5) +* `Confirmation Type` - Confirm transactions with either your viewkey, or by using `monero-wallet-rpc`. (Default: viewkey) +* `Monero Address` (if confirmation type is viewkey) - Your public Monero address starting with 4. (No default) +* `Secret Viewkey` (if confirmation type is viewkey) - Your *private* viewkey (No default) +* `Monero wallet RPC Host/IP` (if confirmation type is `monero-wallet-rpc`) - IP address where the wallet rpc is running. It is highly discouraged to run the wallet anywhere other than the local server! (Default: 127.0.0.1) +* `Monero wallet RPC port` (if confirmation type is `monero-wallet-rpc`) - Port the wallet rpc is bound to with the `--rpc-bind-port` argument. (Default 18080) +* `Testnet` - Check this to change the blockchain explorer links to the testnet explorer. (Default: unchecked) +* `SSL warnings` - Check this to silence SSL warnings. (Default: unchecked) +* `Show QR Code` - Show payment QR codes. There is no Monero software that can read QR codes at this time (Default: unchecked) +* `Show Prices in Monero` - Convert all prices on the frontend to Monero. Experimental feature, only use if you do not accept any other payment option. (Default: unchecked) +* `Display Decimals` (if show prices in Monero is enabled) - Number of decimals to round prices to on the frontend. The final order amount will not be rounded and will be displayed down to the nanoMonero. (Default: 12) -* Setup a Monero wallet using the monero-wallet-cli tool. If you do not know how to do this you can learn about it at [getmonero.org](https://getmonero.org/resources/user-guides/monero-wallet-cli.html) +## Shortcodes -* [Create a view-only wallet from that wallet for security.](https://monero.stackexchange.com/questions/3178/how-to-create-a-view-only-wallet-for-the-gui/4582#4582) +This plugin makes available two shortcodes that you can use in your theme. -* Start the Wallet RPC and leave it running in the background. This can be accomplished by running `./monero-wallet-rpc --rpc-bind-port 18082 --disable-rpc-login --log-level 2 --wallet-file /path/viewOnlyWalletFile` where "/path/viewOnlyWalletFile" is the wallet file for your view-only wallet. If you wish to use a remote node you can add the `--daemon-address` flag followed by the address of the node. `--daemon-address node.moneroworld.com:18089` for example. +#### Live price shortcode -## Step 4: Setup Monero Gateway in WooCommerce +This will display the price of Monero in the selected currency. If no currency is provided, the store's default currency will be used. -* Navigate to the "settings" panel in the WooCommerce widget in the WordPress admin panel. +``` +[monero-price] +[monero-price currency="BTC"] +[monero-price currency="USD"] +[monero-price currency="CAD"] +[monero-price currency="EUR"] +[monero-price currency="GBP"] +``` +Will display: +``` +1 XMR = 123.68000 USD +1 XMR = 0.01827000 BTC +1 XMR = 123.68000 USD +1 XMR = 168.43000 CAD +1 XMR = 105.54000 EUR +1 XMR = 94.84000 GBP +``` -* Click on "Checkout" -* Select "Monero GateWay" +#### Monero accepted here badge -* Check the box labeled "Enable this payment gateway" +This will display a badge showing that you accept Monero-currency. -* Check either "Use ViewKey" or "Use monero-wallet-rpc" +`[monero-accepted-here]` -If You chose to use viewkey: +![Monero Accepted Here](/assets/images/monero-accepted-here.png?raw=true "Monero Accepted Here") -* Enter your Monero wallet address in the box labeled "Monero Address". If you do not know your address, you can run the `address` command in your Monero wallet +## Donations -* Enter your secret viewkey in the box labeled "ViewKey" +monero-integrations: 44krVcL6TPkANjpFwS2GWvg1kJhTrN7y9heVeQiDJ3rP8iGbCd5GeA4f3c2NKYHC1R4mCgnW7dsUUUae2m9GiNBGT4T8s2X -If you chose to use monero-wallet-rpc: - -* Enter your Monero wallet address in the box labeled "Monero Address". If you do not know your address, you can run the `address` command in your Monero wallet - -* Enter the IP address of your server in the box labeled "Monero wallet RPC Host/IP" - -* Enter the port number of the Wallet RPC in the box labeled "Monero wallet RPC port" (will be `18082` if you used the above example). - -Finally: - -* Click on "Save changes" - -## Donating to the Devs :) -XMR Address : `44krVcL6TPkANjpFwS2GWvg1kJhTrN7y9heVeQiDJ3rP8iGbCd5GeA4f3c2NKYHC1R4mCgnW7dsUUUae2m9GiNBGT4T8s2X` +mosu-forge: 4A6BQp7do5MTxpCguq1kAS27yMLpbHcf89Ha2a8Shayt2vXkCr6QRpAXr1gLYRV5esfzoK3vLJTm5bDWk5gKmNrT6s6xZep diff --git a/assets/css/monero-gateway-order-page.css b/assets/css/monero-gateway-order-page.css new file mode 100644 index 0000000..b4c51f3 --- /dev/null +++ b/assets/css/monero-gateway-order-page.css @@ -0,0 +1,78 @@ +#monero_payment_messages > span { + display:none; +} +.monero_details_row { + display: flex !important; + align-items: center; + margin:0 -8px; +} +.monero_details_row > * { + padding:0 8px; +} +.monero_details_left { +} +.monero_details_main { + flex-grow: 1; + word-break:break-all; +} +.monero_details_right.button-row { + display:flex; + margin-top: 5px; + align-self: self-start; +} +.monero_details_right.button-row button { + width: 32px; + height: 32px; + padding: 6px 2px; + margin: 0 4px; + line-height:28px; + text-align:center; +} +#monero_integrated_address { + line-height: 16px; +} +#monero_qr_code_container { + position:fixed; + top:0; + left:0; + right:0; + bottom:0; + z-index:9999; + background:rgba(0,0,0,0.5); +} +#monero_qr_code { + position: absolute; + width: 256px; + height: 256px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + box-sizing: content-box; + padding: 20px; + background: white; + border-radius: 5px; +} +#monero_toast { + position: fixed; + z-index: 999; + top: 32px; + right: 12px; +} +#monero_toast > div { + display: block; + position: relative; + overflow: hidden; + margin-top: 10px; + margin-right: 10px; + padding: 20px; + width: 300px; + border-radius: 3px; + color: white; + right: -400px; +} +#monero_toast > div.success { + background: rgba(68, 190, 117, 0.8); +} +#monero_toast > div.error { + background: rgba(195, 60, 60, 0.8); +} \ No newline at end of file diff --git a/assets/images/monero-accepted-here.png b/assets/images/monero-accepted-here.png new file mode 100644 index 0000000..27ba0c3 Binary files /dev/null and b/assets/images/monero-accepted-here.png differ diff --git a/monero/assets/monero_icon.png b/assets/images/monero-icon-admin.png similarity index 100% rename from monero/assets/monero_icon.png rename to assets/images/monero-icon-admin.png diff --git a/assets/images/monero-icon.png b/assets/images/monero-icon.png new file mode 100644 index 0000000..65240e1 Binary files /dev/null and b/assets/images/monero-icon.png differ diff --git a/assets/js/clipboard.js b/assets/js/clipboard.js new file mode 100644 index 0000000..5e4822a --- /dev/null +++ b/assets/js/clipboard.js @@ -0,0 +1,939 @@ +/*! + * clipboard.js v2.0.0 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else if(typeof exports === 'object') + exports["ClipboardJS"] = factory(); + else + root["ClipboardJS"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 3); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (global, factory) { + if (true) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [module, __webpack_require__(7)], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports !== "undefined") { + factory(module, require('select')); + } else { + var mod = { + exports: {} + }; + factory(mod, global.select); + global.clipboardAction = mod.exports; + } +})(this, function (module, _select) { + 'use strict'; + + var _select2 = _interopRequireDefault(_select); + + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + } + + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + var _createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; + }(); + + var ClipboardAction = function () { + /** + * @param {Object} options + */ + function ClipboardAction(options) { + _classCallCheck(this, ClipboardAction); + + this.resolveOptions(options); + this.initSelection(); + } + + /** + * Defines base properties passed from constructor. + * @param {Object} options + */ + + + _createClass(ClipboardAction, [{ + key: 'resolveOptions', + value: function resolveOptions() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.action = options.action; + this.container = options.container; + this.emitter = options.emitter; + this.target = options.target; + this.text = options.text; + this.trigger = options.trigger; + + this.selectedText = ''; + } + }, { + key: 'initSelection', + value: function initSelection() { + if (this.text) { + this.selectFake(); + } else if (this.target) { + this.selectTarget(); + } + } + }, { + key: 'selectFake', + value: function selectFake() { + var _this = this; + + var isRTL = document.documentElement.getAttribute('dir') == 'rtl'; + + this.removeFake(); + + this.fakeHandlerCallback = function () { + return _this.removeFake(); + }; + this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true; + + this.fakeElem = document.createElement('textarea'); + // Prevent zooming on iOS + this.fakeElem.style.fontSize = '12pt'; + // Reset box model + this.fakeElem.style.border = '0'; + this.fakeElem.style.padding = '0'; + this.fakeElem.style.margin = '0'; + // Move element out of screen horizontally + this.fakeElem.style.position = 'absolute'; + this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px'; + // Move element to the same position vertically + var yPosition = window.pageYOffset || document.documentElement.scrollTop; + this.fakeElem.style.top = yPosition + 'px'; + + this.fakeElem.setAttribute('readonly', ''); + this.fakeElem.value = this.text; + + this.container.appendChild(this.fakeElem); + + this.selectedText = (0, _select2.default)(this.fakeElem); + this.copyText(); + } + }, { + key: 'removeFake', + value: function removeFake() { + if (this.fakeHandler) { + this.container.removeEventListener('click', this.fakeHandlerCallback); + this.fakeHandler = null; + this.fakeHandlerCallback = null; + } + + if (this.fakeElem) { + this.container.removeChild(this.fakeElem); + this.fakeElem = null; + } + } + }, { + key: 'selectTarget', + value: function selectTarget() { + this.selectedText = (0, _select2.default)(this.target); + this.copyText(); + } + }, { + key: 'copyText', + value: function copyText() { + var succeeded = void 0; + + try { + succeeded = document.execCommand(this.action); + } catch (err) { + succeeded = false; + } + + this.handleResult(succeeded); + } + }, { + key: 'handleResult', + value: function handleResult(succeeded) { + this.emitter.emit(succeeded ? 'success' : 'error', { + action: this.action, + text: this.selectedText, + trigger: this.trigger, + clearSelection: this.clearSelection.bind(this) + }); + } + }, { + key: 'clearSelection', + value: function clearSelection() { + if (this.trigger) { + this.trigger.focus(); + } + + window.getSelection().removeAllRanges(); + } + }, { + key: 'destroy', + value: function destroy() { + this.removeFake(); + } + }, { + key: 'action', + set: function set() { + var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy'; + + this._action = action; + + if (this._action !== 'copy' && this._action !== 'cut') { + throw new Error('Invalid "action" value, use either "copy" or "cut"'); + } + }, + get: function get() { + return this._action; + } + }, { + key: 'target', + set: function set(target) { + if (target !== undefined) { + if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) { + if (this.action === 'copy' && target.hasAttribute('disabled')) { + throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute'); + } + + if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) { + throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes'); + } + + this._target = target; + } else { + throw new Error('Invalid "target" value, use a valid Element'); + } + } + }, + get: function get() { + return this._target; + } + }]); + + return ClipboardAction; + }(); + + module.exports = ClipboardAction; +}); + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +var is = __webpack_require__(6); +var delegate = __webpack_require__(5); + +/** + * Validates all params and calls the right + * listener function based on its target type. + * + * @param {String|HTMLElement|HTMLCollection|NodeList} target + * @param {String} type + * @param {Function} callback + * @return {Object} + */ +function listen(target, type, callback) { + if (!target && !type && !callback) { + throw new Error('Missing required arguments'); + } + + if (!is.string(type)) { + throw new TypeError('Second argument must be a String'); + } + + if (!is.fn(callback)) { + throw new TypeError('Third argument must be a Function'); + } + + if (is.node(target)) { + return listenNode(target, type, callback); + } + else if (is.nodeList(target)) { + return listenNodeList(target, type, callback); + } + else if (is.string(target)) { + return listenSelector(target, type, callback); + } + else { + throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList'); + } +} + +/** + * Adds an event listener to a HTML element + * and returns a remove listener function. + * + * @param {HTMLElement} node + * @param {String} type + * @param {Function} callback + * @return {Object} + */ +function listenNode(node, type, callback) { + node.addEventListener(type, callback); + + return { + destroy: function() { + node.removeEventListener(type, callback); + } + } +} + +/** + * Add an event listener to a list of HTML elements + * and returns a remove listener function. + * + * @param {NodeList|HTMLCollection} nodeList + * @param {String} type + * @param {Function} callback + * @return {Object} + */ +function listenNodeList(nodeList, type, callback) { + Array.prototype.forEach.call(nodeList, function(node) { + node.addEventListener(type, callback); + }); + + return { + destroy: function() { + Array.prototype.forEach.call(nodeList, function(node) { + node.removeEventListener(type, callback); + }); + } + } +} + +/** + * Add an event listener to a selector + * and returns a remove listener function. + * + * @param {String} selector + * @param {String} type + * @param {Function} callback + * @return {Object} + */ +function listenSelector(selector, type, callback) { + return delegate(document.body, selector, type, callback); +} + +module.exports = listen; + + +/***/ }), +/* 2 */ +/***/ (function(module, exports) { + +function E () { + // Keep this empty so it's easier to inherit from + // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3) +} + +E.prototype = { + on: function (name, callback, ctx) { + var e = this.e || (this.e = {}); + + (e[name] || (e[name] = [])).push({ + fn: callback, + ctx: ctx + }); + + return this; + }, + + once: function (name, callback, ctx) { + var self = this; + function listener () { + self.off(name, listener); + callback.apply(ctx, arguments); + }; + + listener._ = callback + return this.on(name, listener, ctx); + }, + + emit: function (name) { + var data = [].slice.call(arguments, 1); + var evtArr = ((this.e || (this.e = {}))[name] || []).slice(); + var i = 0; + var len = evtArr.length; + + for (i; i < len; i++) { + evtArr[i].fn.apply(evtArr[i].ctx, data); + } + + return this; + }, + + off: function (name, callback) { + var e = this.e || (this.e = {}); + var evts = e[name]; + var liveEvents = []; + + if (evts && callback) { + for (var i = 0, len = evts.length; i < len; i++) { + if (evts[i].fn !== callback && evts[i].fn._ !== callback) + liveEvents.push(evts[i]); + } + } + + // Remove event from queue to prevent memory leak + // Suggested by https://github.com/lazd + // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910 + + (liveEvents.length) + ? e[name] = liveEvents + : delete e[name]; + + return this; + } +}; + +module.exports = E; + + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (global, factory) { + if (true) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [module, __webpack_require__(0), __webpack_require__(2), __webpack_require__(1)], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports !== "undefined") { + factory(module, require('./clipboard-action'), require('tiny-emitter'), require('good-listener')); + } else { + var mod = { + exports: {} + }; + factory(mod, global.clipboardAction, global.tinyEmitter, global.goodListener); + global.clipboard = mod.exports; + } +})(this, function (module, _clipboardAction, _tinyEmitter, _goodListener) { + 'use strict'; + + var _clipboardAction2 = _interopRequireDefault(_clipboardAction); + + var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter); + + var _goodListener2 = _interopRequireDefault(_goodListener); + + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + } + + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + var _createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; + }(); + + function _possibleConstructorReturn(self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return call && (typeof call === "object" || typeof call === "function") ? call : self; + } + + function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; + } + + var Clipboard = function (_Emitter) { + _inherits(Clipboard, _Emitter); + + /** + * @param {String|HTMLElement|HTMLCollection|NodeList} trigger + * @param {Object} options + */ + function Clipboard(trigger, options) { + _classCallCheck(this, Clipboard); + + var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this)); + + _this.resolveOptions(options); + _this.listenClick(trigger); + return _this; + } + + /** + * Defines if attributes would be resolved using internal setter functions + * or custom functions that were passed in the constructor. + * @param {Object} options + */ + + + _createClass(Clipboard, [{ + key: 'resolveOptions', + value: function resolveOptions() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.action = typeof options.action === 'function' ? options.action : this.defaultAction; + this.target = typeof options.target === 'function' ? options.target : this.defaultTarget; + this.text = typeof options.text === 'function' ? options.text : this.defaultText; + this.container = _typeof(options.container) === 'object' ? options.container : document.body; + } + }, { + key: 'listenClick', + value: function listenClick(trigger) { + var _this2 = this; + + this.listener = (0, _goodListener2.default)(trigger, 'click', function (e) { + return _this2.onClick(e); + }); + } + }, { + key: 'onClick', + value: function onClick(e) { + var trigger = e.delegateTarget || e.currentTarget; + + if (this.clipboardAction) { + this.clipboardAction = null; + } + + this.clipboardAction = new _clipboardAction2.default({ + action: this.action(trigger), + target: this.target(trigger), + text: this.text(trigger), + container: this.container, + trigger: trigger, + emitter: this + }); + } + }, { + key: 'defaultAction', + value: function defaultAction(trigger) { + return getAttributeValue('action', trigger); + } + }, { + key: 'defaultTarget', + value: function defaultTarget(trigger) { + var selector = getAttributeValue('target', trigger); + + if (selector) { + return document.querySelector(selector); + } + } + }, { + key: 'defaultText', + value: function defaultText(trigger) { + return getAttributeValue('text', trigger); + } + }, { + key: 'destroy', + value: function destroy() { + this.listener.destroy(); + + if (this.clipboardAction) { + this.clipboardAction.destroy(); + this.clipboardAction = null; + } + } + }], [{ + key: 'isSupported', + value: function isSupported() { + var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut']; + + var actions = typeof action === 'string' ? [action] : action; + var support = !!document.queryCommandSupported; + + actions.forEach(function (action) { + support = support && !!document.queryCommandSupported(action); + }); + + return support; + } + }]); + + return Clipboard; + }(_tinyEmitter2.default); + + /** + * Helper function to retrieve attribute value. + * @param {String} suffix + * @param {Element} element + */ + function getAttributeValue(suffix, element) { + var attribute = 'data-clipboard-' + suffix; + + if (!element.hasAttribute(attribute)) { + return; + } + + return element.getAttribute(attribute); + } + + module.exports = Clipboard; +}); + +/***/ }), +/* 4 */ +/***/ (function(module, exports) { + +var DOCUMENT_NODE_TYPE = 9; + +/** + * A polyfill for Element.matches() + */ +if (typeof Element !== 'undefined' && !Element.prototype.matches) { + var proto = Element.prototype; + + proto.matches = proto.matchesSelector || + proto.mozMatchesSelector || + proto.msMatchesSelector || + proto.oMatchesSelector || + proto.webkitMatchesSelector; +} + +/** + * Finds the closest parent that matches a selector. + * + * @param {Element} element + * @param {String} selector + * @return {Function} + */ +function closest (element, selector) { + while (element && element.nodeType !== DOCUMENT_NODE_TYPE) { + if (typeof element.matches === 'function' && + element.matches(selector)) { + return element; + } + element = element.parentNode; + } +} + +module.exports = closest; + + +/***/ }), +/* 5 */ +/***/ (function(module, exports, __webpack_require__) { + +var closest = __webpack_require__(4); + +/** + * Delegates event to a selector. + * + * @param {Element} element + * @param {String} selector + * @param {String} type + * @param {Function} callback + * @param {Boolean} useCapture + * @return {Object} + */ +function _delegate(element, selector, type, callback, useCapture) { + var listenerFn = listener.apply(this, arguments); + + element.addEventListener(type, listenerFn, useCapture); + + return { + destroy: function() { + element.removeEventListener(type, listenerFn, useCapture); + } + } +} + +/** + * Delegates event to a selector. + * + * @param {Element|String|Array} [elements] + * @param {String} selector + * @param {String} type + * @param {Function} callback + * @param {Boolean} useCapture + * @return {Object} + */ +function delegate(elements, selector, type, callback, useCapture) { + // Handle the regular Element usage + if (typeof elements.addEventListener === 'function') { + return _delegate.apply(null, arguments); + } + + // Handle Element-less usage, it defaults to global delegation + if (typeof type === 'function') { + // Use `document` as the first parameter, then apply arguments + // This is a short way to .unshift `arguments` without running into deoptimizations + return _delegate.bind(null, document).apply(null, arguments); + } + + // Handle Selector-based usage + if (typeof elements === 'string') { + elements = document.querySelectorAll(elements); + } + + // Handle Array-like based usage + return Array.prototype.map.call(elements, function (element) { + return _delegate(element, selector, type, callback, useCapture); + }); +} + +/** + * Finds closest match and invokes callback. + * + * @param {Element} element + * @param {String} selector + * @param {String} type + * @param {Function} callback + * @return {Function} + */ +function listener(element, selector, type, callback) { + return function(e) { + e.delegateTarget = closest(e.target, selector); + + if (e.delegateTarget) { + callback.call(element, e); + } + } +} + +module.exports = delegate; + + +/***/ }), +/* 6 */ +/***/ (function(module, exports) { + +/** + * Check if argument is a HTML element. + * + * @param {Object} value + * @return {Boolean} + */ +exports.node = function(value) { + return value !== undefined + && value instanceof HTMLElement + && value.nodeType === 1; +}; + +/** + * Check if argument is a list of HTML elements. + * + * @param {Object} value + * @return {Boolean} + */ +exports.nodeList = function(value) { + var type = Object.prototype.toString.call(value); + + return value !== undefined + && (type === '[object NodeList]' || type === '[object HTMLCollection]') + && ('length' in value) + && (value.length === 0 || exports.node(value[0])); +}; + +/** + * Check if argument is a string. + * + * @param {Object} value + * @return {Boolean} + */ +exports.string = function(value) { + return typeof value === 'string' + || value instanceof String; +}; + +/** + * Check if argument is a function. + * + * @param {Object} value + * @return {Boolean} + */ +exports.fn = function(value) { + var type = Object.prototype.toString.call(value); + + return type === '[object Function]'; +}; + + +/***/ }), +/* 7 */ +/***/ (function(module, exports) { + +function select(element) { + var selectedText; + + if (element.nodeName === 'SELECT') { + element.focus(); + + selectedText = element.value; + } + else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { + var isReadOnly = element.hasAttribute('readonly'); + + if (!isReadOnly) { + element.setAttribute('readonly', ''); + } + + element.select(); + element.setSelectionRange(0, element.value.length); + + if (!isReadOnly) { + element.removeAttribute('readonly'); + } + + selectedText = element.value; + } + else { + if (element.hasAttribute('contenteditable')) { + element.focus(); + } + + var selection = window.getSelection(); + var range = document.createRange(); + + range.selectNodeContents(element); + selection.removeAllRanges(); + selection.addRange(range); + + selectedText = selection.toString(); + } + + return selectedText; +} + +module.exports = select; + + +/***/ }) +/******/ ]); +}); \ No newline at end of file diff --git a/assets/js/clipboard.min.js b/assets/js/clipboard.min.js new file mode 100644 index 0000000..b00ee51 --- /dev/null +++ b/assets/js/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.0 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=3)}([function(t,e,n){var o,r,i;!function(a,c){r=[t,n(7)],o=c,void 0!==(i="function"==typeof o?o.apply(e,r):o)&&(t.exports=i)}(0,function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var o=function(t){return t&&t.__esModule?t:{default:t}}(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,o.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,o.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":r(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=a})},function(t,e,n){function o(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.fn(n))throw new TypeError("Third argument must be a Function");if(c.node(t))return r(t,e,n);if(c.nodeList(t))return i(t,e,n);if(c.string(t))return a(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function r(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.removeEventListener(e,n)}}}function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.addEventListener(e,n)}),{destroy:function(){Array.prototype.forEach.call(t,function(t){t.removeEventListener(e,n)})}}}function a(t,e,n){return u(document.body,t,e,n)}var c=n(6),u=n(5);t.exports=o},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){function o(){r.off(t,o),e.apply(n,arguments)}var r=this;return o._=e,this.on(t,o,n)},emit:function(t){var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;for(o;o0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===d(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,f.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(s.default);t.exports=p})},function(t,e){function n(t,e){for(;t&&t.nodeType!==o;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}var o=9;if("undefined"!=typeof Element&&!Element.prototype.matches){var r=Element.prototype;r.matches=r.matchesSelector||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector}t.exports=n},function(t,e,n){function o(t,e,n,o,r){var a=i.apply(this,arguments);return t.addEventListener(n,a,r),{destroy:function(){t.removeEventListener(n,a,r)}}}function r(t,e,n,r,i){return"function"==typeof t.addEventListener?o.apply(null,arguments):"function"==typeof n?o.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return o(t,e,n,r,i)}))}function i(t,e,n,o){return function(n){n.delegateTarget=a(n.target,e),n.delegateTarget&&o.call(t,n)}}var a=n(4);t.exports=r},function(t,e){e.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},e.nodeList=function(t){var n=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in t&&(0===t.length||e.node(t[0]))},e.string=function(t){return"string"==typeof t||t instanceof String},e.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e){function n(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}t.exports=n}])}); \ No newline at end of file diff --git a/assets/js/monero-gateway-order-page.js b/assets/js/monero-gateway-order-page.js new file mode 100644 index 0000000..da7ca24 --- /dev/null +++ b/assets/js/monero-gateway-order-page.js @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2018, Ryo Currency Project +*/ +function monero_showNotification(message, type='success') { + var toast = jQuery('
' + message + '
'); + jQuery('#monero_toast').append(toast); + toast.animate({ "right": "12px" }, "fast"); + setInterval(function() { + toast.animate({ "right": "-400px" }, "fast", function() { + toast.remove(); + }); + }, 2500) +} +function monero_showQR(show=true) { + jQuery('#monero_qr_code_container').toggle(show); +} +function monero_fetchDetails() { + var data = { + '_': jQuery.now(), + 'order_id': monero_details.order_id + }; + jQuery.get(monero_ajax_url, data, function(response) { + if (typeof response.error !== 'undefined') { + console.log(response.error); + } else { + monero_details = response; + monero_updateDetails(); + } + }); +} + +function monero_updateDetails() { + + var details = monero_details; + + jQuery('#monero_payment_messages').children().hide(); + switch(details.status) { + case 'unpaid': + jQuery('.monero_payment_unpaid').show(); + jQuery('.monero_payment_expire_time').html(details.order_expires); + break; + case 'partial': + jQuery('.monero_payment_partial').show(); + jQuery('.monero_payment_expire_time').html(details.order_expires); + break; + case 'paid': + jQuery('.monero_payment_paid').show(); + jQuery('.monero_confirm_time').html(details.time_to_confirm); + jQuery('.button-row button').prop("disabled",true); + break; + case 'confirmed': + jQuery('.monero_payment_confirmed').show(); + jQuery('.button-row button').prop("disabled",true); + break; + case 'expired': + jQuery('.monero_payment_expired').show(); + jQuery('.button-row button').prop("disabled",true); + break; + case 'expired_partial': + jQuery('.monero_payment_expired_partial').show(); + jQuery('.button-row button').prop("disabled",true); + break; + } + + jQuery('#monero_exchange_rate').html('1 XMR = '+details.rate_formatted+' '+details.currency); + jQuery('#monero_total_amount').html(details.amount_total_formatted); + jQuery('#monero_total_paid').html(details.amount_paid_formatted); + jQuery('#monero_total_due').html(details.amount_due_formatted); + + jQuery('#monero_integrated_address').html(details.integrated_address); + + if(monero_show_qr) { + var qr = jQuery('#monero_qr_code').html(''); + new QRCode(qr.get(0), details.qrcode_uri); + } + + if(details.txs.length) { + jQuery('#monero_tx_table').show(); + jQuery('#monero_tx_none').hide(); + jQuery('#monero_tx_table tbody').html(''); + for(var i=0; i < details.txs.length; i++) { + var tx = details.txs[i]; + var height = tx.height == 0 ? 'N/A' : tx.height; + var row = ''+ + ''+ + ''+ + ''+tx.txid+''+ + ''+ + ''+height+''+ + ''+tx.amount_formatted+' Monero'+ + ''; + + jQuery('#monero_tx_table tbody').append(row); + } + } else { + jQuery('#monero_tx_table').hide(); + jQuery('#monero_tx_none').show(); + } + + // Show state change notifications + var new_txs = details.txs; + var old_txs = monero_order_state.txs; + if(new_txs.length != old_txs.length) { + for(var i = 0; i < new_txs.length; i++) { + var is_new_tx = true; + for(var j = 0; j < old_txs.length; j++) { + if(new_txs[i].txid == old_txs[j].txid && new_txs[i].amount == old_txs[j].amount) { + is_new_tx = false; + break; + } + } + if(is_new_tx) { + monero_showNotification('Transaction received for '+new_txs[i].amount_formatted+' Monero'); + } + } + } + + if(details.status != monero_order_state.status) { + switch(details.status) { + case 'paid': + monero_showNotification('Your order has been paid in full'); + break; + case 'confirmed': + monero_showNotification('Your order has been confirmed'); + break; + case 'expired': + case 'expired_partial': + monero_showNotification('Your order has expired', 'error'); + break; + } + } + + monero_order_state = { + status: monero_details.status, + txs: monero_details.txs + }; + +} +jQuery(document).ready(function($) { + if (typeof monero_details !== 'undefined') { + monero_order_state = { + status: monero_details.status, + txs: monero_details.txs + }; + setInterval(monero_fetchDetails, 30000); + monero_updateDetails(); + new ClipboardJS('.clipboard').on('success', function(e) { + e.clearSelection(); + if(e.trigger.disabled) return; + switch(e.trigger.getAttribute('data-clipboard-target')) { + case '#monero_integrated_address': + monero_showNotification('Copied destination address!'); + break; + case '#monero_total_due': + monero_showNotification('Copied total amount due!'); + break; + } + e.clearSelection(); + }); + } +}); \ No newline at end of file diff --git a/assets/js/qrcode.js b/assets/js/qrcode.js new file mode 100644 index 0000000..5507c15 --- /dev/null +++ b/assets/js/qrcode.js @@ -0,0 +1,614 @@ +/** + * @fileoverview + * - Using the 'QRCode for Javascript library' + * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. + * - this library has no dependencies. + * + * @author davidshimjs + * @see http://www.d-project.com/ + * @see http://jeromeetienne.github.com/jquery-qrcode/ + */ +var QRCode; + +(function () { + //--------------------------------------------------------------------- + // QRCode for JavaScript + // + // Copyright (c) 2009 Kazuhiko Arase + // + // URL: http://www.d-project.com/ + // + // Licensed under the MIT license: + // http://www.opensource.org/licenses/mit-license.php + // + // The word "QR Code" is registered trademark of + // DENSO WAVE INCORPORATED + // http://www.denso-wave.com/qrcode/faqpatent-e.html + // + //--------------------------------------------------------------------- + function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE; + this.data = data; + this.parsedData = []; + + // Added to support UTF-8 Characters + for (var i = 0, l = this.data.length; i < l; i++) { + var byteArray = []; + var code = this.data.charCodeAt(i); + + if (code > 0x10000) { + byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); + byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); + byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[3] = 0x80 | (code & 0x3F); + } else if (code > 0x800) { + byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); + byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[2] = 0x80 | (code & 0x3F); + } else if (code > 0x80) { + byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); + byteArray[1] = 0x80 | (code & 0x3F); + } else { + byteArray[0] = code; + } + + this.parsedData.push(byteArray); + } + + this.parsedData = Array.prototype.concat.apply([], this.parsedData); + + if (this.parsedData.length != this.data.length) { + this.parsedData.unshift(191); + this.parsedData.unshift(187); + this.parsedData.unshift(239); + } + } + + QR8bitByte.prototype = { + getLength: function (buffer) { + return this.parsedData.length; + }, + write: function (buffer) { + for (var i = 0, l = this.parsedData.length; i < l; i++) { + buffer.put(this.parsedData[i], 8); + } + } + }; + + function QRCodeModel(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = []; + } + + QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} + return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} + if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} + this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} + return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} + for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} + for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} + this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} + var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} + this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} + row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" + +buffer.getLengthInBits() + +">" + +totalDataCount*8 + +")");} + if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} + while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} + while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD1,8);} + return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} + var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} + return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} + return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} + return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} + for(var row=0;row=256){n-=255;} + return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} + if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} + this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; + + function _isSupportCanvas() { + return typeof CanvasRenderingContext2D != "undefined"; + } + + // android 2.x doesn't support Data-URI spec + function _getAndroid() { + var android = false; + var sAgent = navigator.userAgent; + + if (/android/i.test(sAgent)) { // android + android = true; + var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); + + if (aMat && aMat[1]) { + android = parseFloat(aMat[1]); + } + } + + return android; + } + + var svgDrawer = (function() { + + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + + this.clear(); + + function makeSVG(tag, attrs) { + var el = document.createElementNS('http://www.w3.org/2000/svg', tag); + for (var k in attrs) + if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); + return el; + } + + var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); + svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); + _el.appendChild(svg); + + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})); + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + if (oQRCode.isDark(row, col)) { + var child = makeSVG("use", {"x": String(col), "y": String(row)}); + child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") + svg.appendChild(child); + } + } + } + }; + Drawing.prototype.clear = function () { + while (this._el.hasChildNodes()) + this._el.removeChild(this._el.lastChild); + }; + return Drawing; + })(); + + var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; + + // Drawing in DOM by using Table tag + var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + var aHTML = ['']; + + for (var row = 0; row < nCount; row++) { + aHTML.push(''); + + for (var col = 0; col < nCount; col++) { + aHTML.push(''); + } + + aHTML.push(''); + } + + aHTML.push('
'); + _el.innerHTML = aHTML.join(''); + + // Fix the margin values as real size. + var elTable = _el.childNodes[0]; + var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; + var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; + + if (nLeftMarginTable > 0 && nTopMarginTable > 0) { + elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; + } + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._el.innerHTML = ''; + }; + + return Drawing; + })() : (function () { // Drawing in Canvas + function _onMakeImage() { + this._elImage.src = this._elCanvas.toDataURL("image/png"); + this._elImage.style.display = "block"; + this._elCanvas.style.display = "none"; + } + + // Android 2.1 bug workaround + // http://code.google.com/p/android/issues/detail?id=5141 + if (this._android && this._android <= 2.1) { + var factor = 1 / window.devicePixelRatio; + var drawImage = CanvasRenderingContext2D.prototype.drawImage; + CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { + if (("nodeName" in image) && /img/i.test(image.nodeName)) { + for (var i = arguments.length - 1; i >= 1; i--) { + arguments[i] = arguments[i] * factor; + } + } else if (typeof dw == "undefined") { + arguments[1] *= factor; + arguments[2] *= factor; + arguments[3] *= factor; + arguments[4] *= factor; + } + + drawImage.apply(this, arguments); + }; + } + + /** + * Check whether the user's browser supports Data URI or not + * + * @private + * @param {Function} fSuccess Occurs if it supports Data URI + * @param {Function} fFail Occurs if it doesn't support Data URI + */ + function _safeSetDataURI(fSuccess, fFail) { + var self = this; + self._fFail = fFail; + self._fSuccess = fSuccess; + + // Check it just once + if (self._bSupportDataURI === null) { + var el = document.createElement("img"); + var fOnError = function() { + self._bSupportDataURI = false; + + if (self._fFail) { + self._fFail.call(self); + } + }; + var fOnSuccess = function() { + self._bSupportDataURI = true; + + if (self._fSuccess) { + self._fSuccess.call(self); + } + }; + + el.onabort = fOnError; + el.onerror = fOnError; + el.onload = fOnSuccess; + el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. + return; + } else if (self._bSupportDataURI === true && self._fSuccess) { + self._fSuccess.call(self); + } else if (self._bSupportDataURI === false && self._fFail) { + self._fFail.call(self); + } + }; + + /** + * Drawing QRCode by using canvas + * + * @constructor + * @param {HTMLElement} el + * @param {Object} htOption QRCode Options + */ + var Drawing = function (el, htOption) { + this._bIsPainted = false; + this._android = _getAndroid(); + + this._htOption = htOption; + this._elCanvas = document.createElement("canvas"); + this._elCanvas.width = htOption.width; + this._elCanvas.height = htOption.height; + el.appendChild(this._elCanvas); + this._el = el; + this._oContext = this._elCanvas.getContext("2d"); + this._bIsPainted = false; + this._elImage = document.createElement("img"); + this._elImage.alt = "Scan me!"; + this._elImage.style.display = "none"; + this._el.appendChild(this._elImage); + this._bSupportDataURI = null; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _elImage = this._elImage; + var _oContext = this._oContext; + var _htOption = this._htOption; + + var nCount = oQRCode.getModuleCount(); + var nWidth = _htOption.width / nCount; + var nHeight = _htOption.height / nCount; + var nRoundedWidth = Math.round(nWidth); + var nRoundedHeight = Math.round(nHeight); + + _elImage.style.display = "none"; + this.clear(); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + var bIsDark = oQRCode.isDark(row, col); + var nLeft = col * nWidth; + var nTop = row * nHeight; + _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.lineWidth = 1; + _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.fillRect(nLeft, nTop, nWidth, nHeight); + + // 안티 앨리어싱 방지 처리 + _oContext.strokeRect( + Math.floor(nLeft) + 0.5, + Math.floor(nTop) + 0.5, + nRoundedWidth, + nRoundedHeight + ); + + _oContext.strokeRect( + Math.ceil(nLeft) - 0.5, + Math.ceil(nTop) - 0.5, + nRoundedWidth, + nRoundedHeight + ); + } + } + + this._bIsPainted = true; + }; + + /** + * Make the image from Canvas if the browser supports Data URI. + */ + Drawing.prototype.makeImage = function () { + if (this._bIsPainted) { + _safeSetDataURI.call(this, _onMakeImage); + } + }; + + /** + * Return whether the QRCode is painted or not + * + * @return {Boolean} + */ + Drawing.prototype.isPainted = function () { + return this._bIsPainted; + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); + this._bIsPainted = false; + }; + + /** + * @private + * @param {Number} nNumber + */ + Drawing.prototype.round = function (nNumber) { + if (!nNumber) { + return nNumber; + } + + return Math.floor(nNumber * 1000) / 1000; + }; + + return Drawing; + })(); + + /** + * Get the type by string length + * + * @private + * @param {String} sText + * @param {Number} nCorrectLevel + * @return {Number} type + */ + function _getTypeNumber(sText, nCorrectLevel) { + var nType = 1; + var length = _getUTF8Length(sText); + + for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { + var nLimit = 0; + + switch (nCorrectLevel) { + case QRErrorCorrectLevel.L : + nLimit = QRCodeLimitLength[i][0]; + break; + case QRErrorCorrectLevel.M : + nLimit = QRCodeLimitLength[i][1]; + break; + case QRErrorCorrectLevel.Q : + nLimit = QRCodeLimitLength[i][2]; + break; + case QRErrorCorrectLevel.H : + nLimit = QRCodeLimitLength[i][3]; + break; + } + + if (length <= nLimit) { + break; + } else { + nType++; + } + } + + if (nType > QRCodeLimitLength.length) { + throw new Error("Too long data"); + } + + return nType; + } + + function _getUTF8Length(sText) { + var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); + return replacedText.length + (replacedText.length != sText ? 3 : 0); + } + + /** + * @class QRCode + * @constructor + * @example + * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); + * + * @example + * var oQRCode = new QRCode("test", { + * text : "http://naver.com", + * width : 128, + * height : 128 + * }); + * + * oQRCode.clear(); // Clear the QRCode. + * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. + * + * @param {HTMLElement|String} el target element or 'id' attribute of element. + * @param {Object|String} vOption + * @param {String} vOption.text QRCode link data + * @param {Number} [vOption.width=256] + * @param {Number} [vOption.height=256] + * @param {String} [vOption.colorDark="#000000"] + * @param {String} [vOption.colorLight="#ffffff"] + * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] + */ + QRCode = function (el, vOption) { + this._htOption = { + width : 256, + height : 256, + typeNumber : 4, + colorDark : "#000000", + colorLight : "#ffffff", + correctLevel : QRErrorCorrectLevel.H + }; + + if (typeof vOption === 'string') { + vOption = { + text : vOption + }; + } + + // Overwrites options + if (vOption) { + for (var i in vOption) { + this._htOption[i] = vOption[i]; + } + } + + if (typeof el == "string") { + el = document.getElementById(el); + } + + if (this._htOption.useSVG) { + Drawing = svgDrawer; + } + + this._android = _getAndroid(); + this._el = el; + this._oQRCode = null; + this._oDrawing = new Drawing(this._el, this._htOption); + + if (this._htOption.text) { + this.makeCode(this._htOption.text); + } + }; + + /** + * Make the QRCode + * + * @param {String} sText link data + */ + QRCode.prototype.makeCode = function (sText) { + this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); + this._oQRCode.addData(sText); + this._oQRCode.make(); + this._el.title = sText; + this._oDrawing.draw(this._oQRCode); + this.makeImage(); + }; + + /** + * Make the Image from Canvas element + * - It occurs automatically + * - Android below 3 doesn't support Data-URI spec. + * + * @private + */ + QRCode.prototype.makeImage = function () { + if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { + this._oDrawing.makeImage(); + } + }; + + /** + * Clear the QRCode + */ + QRCode.prototype.clear = function () { + this._oDrawing.clear(); + }; + + /** + * @name QRCode.CorrectLevel + */ + QRCode.CorrectLevel = QRErrorCorrectLevel; +})(); diff --git a/assets/js/qrcode.min.js b/assets/js/qrcode.min.js new file mode 100644 index 0000000..993e88f --- /dev/null +++ b/assets/js/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/assets/systemd-unit-files/monero-wallet-rpc.service b/assets/systemd-unit-files/monero-wallet-rpc.service new file mode 100644 index 0000000..9818c4c --- /dev/null +++ b/assets/systemd-unit-files/monero-wallet-rpc.service @@ -0,0 +1,14 @@ +[Unit] +Description=Monero Wallet RPC +After=network.target monerod.service + +[Service] +User=moneroservices +Group=moneroservices +WorkingDirectory=/opt/monero-wallets +Type=simple +ExecStart=/opt/monero-bin/monero-wallet-rpc --wallet-file /opt/monero-wallets/woocommerce --rpc-bind-port 18080 --password-file /opt/monero-wallets/woocommerce.password --disable-rpc-login --log-file /var/log/monero-wallet.log +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/assets/systemd-unit-files/monerod.service b/assets/systemd-unit-files/monerod.service new file mode 100644 index 0000000..65e56b7 --- /dev/null +++ b/assets/systemd-unit-files/monerod.service @@ -0,0 +1,14 @@ +[Unit] +Description=Monero Full Node +After=network.target + +[Service] +User=moneroservices +Group=moneroservices +WorkingDirectory=/opt/monero-data-dir +Type=simple +LimitNOFILE=65535 +ExecStart=/usr/bin/monerod --log-file /var/log/monerod.log --data-dir /opt/monero-data-dir --non-interactive +Restart=always +[Install] +WantedBy=multi-user.target diff --git a/include/admin/class-monero-admin-interface.php b/include/admin/class-monero-admin-interface.php new file mode 100644 index 0000000..930f84e --- /dev/null +++ b/include/admin/class-monero-admin-interface.php @@ -0,0 +1,133 @@ +prepare_items(); + $payments_list->display(); + } + + /** + * Monero settings page + */ + public function settings_page() { + WC_Admin_Settings::output(); + } + + public function settings_page_init() { + global $current_tab, $current_section; + + $current_section = 'monero_gateway'; + $current_tab = 'checkout'; + + // Include settings pages. + WC_Admin_Settings::get_settings_pages(); + + // Save settings if data has been posted. + if (apply_filters("woocommerce_save_settings_{$current_tab}_{$current_section}", !empty($_POST))) { + WC_Admin_Settings::save(); + } + + // Add any posted messages. + if (!empty($_GET['wc_error'])) { + WC_Admin_Settings::add_error(wp_kses_post(wp_unslash($_GET['wc_error']))); + } + + if (!empty($_GET['wc_message'])) { + WC_Admin_Settings::add_message(wp_kses_post(wp_unslash($_GET['wc_message']))); + } + + do_action('woocommerce_settings_page_init'); + } + +} + +return new Monero_Admin_Interface(); diff --git a/include/admin/class-monero-admin-payments-list.php b/include/admin/class-monero-admin-payments-list.php new file mode 100644 index 0000000..40ea59c --- /dev/null +++ b/include/admin/class-monero-admin-payments-list.php @@ -0,0 +1,276 @@ + 'payment', + 'plural' => 'payments', + 'ajax' => false + )); + } + + function extra_tablenav($which) { + if ($which == "top") { + $hidden_fields = wp_nonce_field() . wp_referer_field(); + $tab_info = array( + 'all' => array(), + 'pending' => array(), + 'paid' => array(), + 'confirmed' => array(), + 'expired' => array(), + ); + foreach($tab_info as $type=>&$info) { + $info['active'] = ''; + $info['count'] = $this->get_item_count($type); + } + if(isset($_GET['type'])) { + switch($_GET['type']) { + case 'all': + $tab_info['all']['active'] = 'class="current" aria-current="page"'; + break; + case 'pending': + $tab_info['pending']['active'] = 'class="current" aria-current="page"'; + break; + case 'paid': + $tab_info['paid']['active'] = 'class="current" aria-current="page"'; + break; + case 'confirmed': + $tab_info['confirmed']['active'] = 'class="current" aria-current="page"'; + break; + case 'expired': + $tab_info['expired']['active'] = 'class="current" aria-current="page"'; + break; + } + } else { + $tab_info['all']['active'] = 'class="current" aria-current="page"'; + } + if(Monero_Gateway::get_confirm_type() == 'monero-wallet-rpc') { + $balance = Monero_Gateway::admin_balance_info(); + $balance_info = << + Wallet height: {$balance['height']}
+ Your balance is: {$balance['balance']}
+ Unlocked balance: {$balance['unlocked_balance']}
+ + +HTML; + } else { + $balance_info = ''; + } + echo << +

Monero Payments

+ $balance_info +
+ + +

Monero Payments List

+ + + +HTML; + } else if ($which == "bottom") { + echo '
'; + } + } + + /** + * Get column value. + * + * @param mixed $item Item being displayed. + * @param string $column_name Column name. + */ + public function column_default($item, $column_name) { + + switch($column_name) { + case 'col_order_id': + echo $this->get_order_link($item->order_id); + break; + case 'col_payment_id': + echo $item->payment_id; + break; + case 'col_txid': + $url = MONERO_GATEWAY_EXPLORER_URL.'/tx/'.$item->txid; + echo ''.$item->txid.''; + break; + case 'col_height': + echo $item->height; + break; + case 'col_amount': + echo Monero_Gateway::format_monero($item->amount).' Monero'; + break; + } + } + + protected function get_order_link($order_id) { + $order = new WC_Order($order_id); + $buyer = ''; + + if($order->get_billing_first_name() || $order->get_billing_last_name()) { + $buyer = trim(sprintf(_x('%1$s %2$s', 'full name', 'woocommerce'), $order->get_billing_first_name(), $order->get_billing_last_name())); + } else if ($order->get_billing_company()) { + $buyer = trim($order->get_billing_company()); + } else if ($order->get_customer_id()) { + $user = get_user_by('id', $order->get_customer_id()); + $buyer = ucwords($user->display_name); + } + + return '#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . ''; + + } + + function get_columns() { + return $columns= array( + 'col_order_id' => __('Order'), + 'col_payment_id' => __('Payment ID'), + 'col_txid' => __('Txid'), + 'col_height' => __('Height'), + 'col_amount' => __('Amount'), + ); + } + + public function get_sortable_columns() { + return array(); + return $sortable = array( + 'col_order_id' => 'col_order_id', + 'col_payment_id' => 'payment_id', + 'col_txid' => 'txid', + 'col_height' => 'height', + 'col_amount' => 'amount', + ); + } + + function prepare_items() { + + $this->_column_headers = array($this->get_columns(), array(), $this->get_sortable_columns()); + $current_page = absint($this->get_pagenum()); + + $per_page = 25; + + $this->get_items($current_page, $per_page); + + } + + public function no_items() { + esc_html_e('No Monero payments found', 'monero_gateway'); + } + + protected function get_filter_vars() { + $type = isset($_GET['type']) ? $_GET['type'] : null; + return (object) array( + 'type' => $type, + ); + } + + protected function get_item_count($type) { + global $wpdb; + $table_name_1 = $wpdb->prefix.'monero_gateway_quotes'; + $table_name_2 = $wpdb->prefix.'monero_gateway_quotes_txids'; + $query_where = ' WHERE 1=1 '.$this->get_clause_type($type); + $query = "SELECT COUNT(*) AS count FROM {$table_name_2} t2 LEFT JOIN $table_name_1 t1 ON t2.payment_id = t1.payment_id {$query_where}"; + $item_count = $wpdb->get_var($query); + if(is_null($item_count)) $item_count = 0; + return $item_count; + } + + protected function get_clause_type($type) { + global $wpdb; + switch($type) { + case 'pending': + $query_where = $wpdb->prepare(' AND pending = 1 AND paid = 0 ', array()); + break; + case 'paid': + $query_where = $wpdb->prepare(' AND paid = 1 AND confirmed = 0 ', array()); + break; + case 'confirmed': + $query_where = $wpdb->prepare(' AND confirmed = 1 ', array()); + break; + case 'expired': + $query_where = $wpdb->prepare(' AND paid = 0 AND pending = 0 ', array()); + break; + case 'all': + default: + $query_where = ' '; + } + return $query_where; + } + + public function get_items($current_page, $per_page) { + global $wpdb; + + $this->items = array(); + $filters = $this->get_filter_vars(); + + $table_name_1 = $wpdb->prefix.'monero_gateway_quotes'; + $table_name_2 = $wpdb->prefix.'monero_gateway_quotes_txids'; + + $query_where = ' WHERE 1=1 '; + + $query_where .= $this->get_clause_type($filters->type); + + $query_order = $wpdb->prepare('ORDER BY id DESC LIMIT %d, %d;', ($current_page-1)*$per_page, $per_page); + + $query = "SELECT t1.order_id, t1.confirmed, t1.paid, t1.pending, t2.* FROM {$table_name_2} t2 LEFT JOIN $table_name_1 t1 ON t2.payment_id = t1.payment_id {$query_where} {$query_order}"; + + $this->items = $wpdb->get_results($query); + + $max_items = $this->get_item_count($filters->type); + + $this->set_pagination_args( + array( + 'total_items' => $max_items, + 'per_page' => $per_page, + 'total_pages' => ceil($max_items/$per_page), + ) + ); + } + +} diff --git a/include/admin/monero-gateway-admin-settings.php b/include/admin/monero-gateway-admin-settings.php new file mode 100644 index 0000000..0a02d40 --- /dev/null +++ b/include/admin/monero-gateway-admin-settings.php @@ -0,0 +1,114 @@ + array( + 'title' => __('Enable / Disable', 'monero_gateway'), + 'label' => __('Enable this payment gateway', 'monero_gateway'), + 'type' => 'checkbox', + 'default' => 'no' + ), + 'title' => array( + 'title' => __('Title', 'monero_gateway'), + 'type' => 'text', + 'desc_tip' => __('Payment title the customer will see during the checkout process.', 'monero_gateway'), + 'default' => __('Monero Gateway', 'monero_gateway') + ), + 'description' => array( + 'title' => __('Description', 'monero_gateway'), + 'type' => 'textarea', + 'desc_tip' => __('Payment description the customer will see during the checkout process.', 'monero_gateway'), + 'default' => __('Pay securely using Monero. You will be provided payment details after checkout.', 'monero_gateway') + ), + 'discount' => array( + 'title' => __('Discount for using Monero', 'monero_gateway'), + 'desc_tip' => __('Provide a discount to your customers for making a private payment with Monero', 'monero_gateway'), + 'description' => __('Enter a percentage discount (i.e. 5 for 5%) or leave this empty if you do not wish to provide a discount', 'monero_gateway'), + 'type' => __('number'), + 'default' => '0' + ), + 'valid_time' => array( + 'title' => __('Order valid time', 'monero_gateway'), + 'desc_tip' => __('Amount of time order is valid before expiring', 'monero_gateway'), + 'description' => __('Enter the number of seconds that the funds must be received in after order is placed. 3600 seconds = 1 hour', 'monero_gateway'), + 'type' => __('number'), + 'default' => '3600' + ), + 'confirms' => array( + 'title' => __('Number of confirmations', 'monero_gateway'), + 'desc_tip' => __('Number of confirms a transaction must have to be valid', 'monero_gateway'), + 'description' => __('Enter the number of confirms that transactions must have. Enter 0 to zero-confim. Each confirm will take approximately four minutes', 'monero_gateway'), + 'type' => __('number'), + 'default' => '5' + ), + 'confirm_type' => array( + 'title' => __('Confirmation Type', 'monero_gateway'), + 'desc_tip' => __('Select the method for confirming transactions', 'monero_gateway'), + 'description' => __('Select the method for confirming transactions', 'monero_gateway'), + 'type' => 'select', + 'options' => array( + 'viewkey' => __('viewkey', 'monero_gateway'), + 'monero-wallet-rpc' => __('monero-wallet-rpc', 'monero_gateway') + ), + 'default' => 'viewkey' + ), + 'monero_address' => array( + 'title' => __('Monero Address', 'monero_gateway'), + 'label' => __('Useful for people that have not a daemon online'), + 'type' => 'text', + 'desc_tip' => __('Monero Wallet Address (MoneroL)', 'monero_gateway') + ), + 'viewkey' => array( + 'title' => __('Secret Viewkey', 'monero_gateway'), + 'label' => __('Secret Viewkey'), + 'type' => 'text', + 'desc_tip' => __('Your secret Viewkey', 'monero_gateway') + ), + 'daemon_host' => array( + 'title' => __('Monero wallet RPC Host/IP', 'monero_gateway'), + 'type' => 'text', + 'desc_tip' => __('This is the Daemon Host/IP to authorize the payment with', 'monero_gateway'), + 'default' => '127.0.0.1', + ), + 'daemon_port' => array( + 'title' => __('Monero wallet RPC port', 'monero_gateway'), + 'type' => __('number'), + 'desc_tip' => __('This is the Wallet RPC port to authorize the payment with', 'monero_gateway'), + 'default' => '18080', + ), + 'testnet' => array( + 'title' => __(' Testnet', 'monero_gateway'), + 'label' => __(' Check this if you are using testnet ', 'monero_gateway'), + 'type' => 'checkbox', + 'description' => __('Advanced usage only', 'monero_gateway'), + 'default' => 'no' + ), + 'onion_service' => array( + 'title' => __(' SSL warnings ', 'monero_gateway'), + 'label' => __(' Check to Silence SSL warnings', 'monero_gateway'), + 'type' => 'checkbox', + 'description' => __('Check this box if you are running on an Onion Service (Suppress SSL errors)', 'monero_gateway'), + 'default' => 'no' + ), + 'show_qr' => array( + 'title' => __('Show QR Code', 'monero_gateway'), + 'label' => __('Show QR Code', 'monero_gateway'), + 'type' => 'checkbox', + 'description' => __('Enable this to show a QR code after checkout with payment details.'), + 'default' => 'no' + ), + 'use_monero_price' => array( + 'title' => __('Show Prices in Monero', 'monero_gateway'), + 'label' => __('Show Prices in Monero', 'monero_gateway'), + 'type' => 'checkbox', + 'description' => __('Enable this to convert ALL prices on the frontend to Monero (experimental)'), + 'default' => 'no' + ), + 'use_monero_price_decimals' => array( + 'title' => __('Display Decimals', 'monero_gateway'), + 'type' => __('number'), + 'description' => __('Number of decimal places to display on frontend. Upon checkout exact price will be displayed.'), + 'default' => 12, + ), +); diff --git a/include/class-monero-base58.php b/include/class-monero-base58.php new file mode 100644 index 0000000..0818996 --- /dev/null +++ b/include/class-monero-base58.php @@ -0,0 +1,354 @@ +hex_to_bin(): Invalid input type (must be a string)'); + } + if (strlen($hex) % 2 != 0) { + throw new Exception('base58->hex_to_bin(): Invalid input length (must be even)'); + } + + $res = array_fill(0, strlen($hex) / 2, 0); + for ($i = 0; $i < strlen($hex) / 2; $i++) { + $res[$i] = intval(substr($hex, $i * 2, $i * 2 + 2 - $i * 2), 16); + } + return $res; + } + + /** + * + * Convert a binary array to a hexadecimal string + * + * @param array $bin A binary array to convert to a hexadecimal string + * @return string + * + */ + private function bin_to_hex($bin) { + if (gettype($bin) != 'array') { + throw new Exception('base58->bin_to_hex(): Invalid input type (must be an array)'); + } + + $res = []; + for ($i = 0; $i < count($bin); $i++) { + $res[] = substr('0'.dechex($bin[$i]), -2); + } + return join($res); + } + + /** + * + * Convert a string to a binary array + * + * @param string $str A string to convert to a binary array + * @return array + * + */ + private function str_to_bin($str) { + if (gettype($str) != 'string') { + throw new Exception('base58->str_to_bin(): Invalid input type (must be a string)'); + } + + $res = array_fill(0, strlen($str), 0); + for ($i = 0; $i < strlen($str); $i++) { + $res[$i] = ord($str[$i]); + } + return $res; + } + + /** + * + * Convert a binary array to a string + * + * @param array $bin A binary array to convert to a string + * @return string + * + */ + private function bin_to_str($bin) { + if (gettype($bin) != 'array') { + throw new Exception('base58->bin_to_str(): Invalid input type (must be an array)'); + } + + $res = array_fill(0, count($bin), 0); + for ($i = 0; $i < count($bin); $i++) { + $res[$i] = chr($bin[$i]); + } + return preg_replace('/[[:^print:]]/', '', join($res)); // preg_replace necessary to strip errant non-ASCII characters eg. '' + } + + /** + * + * Convert a UInt8BE (one unsigned big endian byte) array to UInt64 + * + * @param array $data A UInt8BE array to convert to UInt64 + * @return number + * + */ + private function uint8_be_to_64($data) { + if (gettype($data) != 'array') { + throw new Exception ('base58->uint8_be_to_64(): Invalid input type (must be an array)'); + } + + $res = 0; + $i = 0; + switch (9 - count($data)) { + case 1: + $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); + case 2: + $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); + case 3: + $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); + case 4: + $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); + case 5: + $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); + case 6: + $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); + case 7: + $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); + case 8: + $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); + break; + default: + throw new Exception('base58->uint8_be_to_64: Invalid input length (1 <= count($data) <= 8)'); + } + return $res; + } + + /** + * + * Convert a UInt64 (unsigned 64 bit integer) to a UInt8BE array + * + * @param number $num A UInt64 number to convert to a UInt8BE array + * @param integer $size Size of array to return + * @return array + * + */ + private function uint64_to_8_be($num, $size) { + if (gettype($num) != ('integer' || 'double')) { + throw new Exception ('base58->uint64_to_8_be(): Invalid input type ($num must be a number)'); + } + if (gettype($size) != 'integer') { + throw new Exception ('base58->uint64_to_8_be(): Invalid input type ($size must be an integer)'); + } + if ($size < 1 || $size > 8) { + throw new Exception ('base58->uint64_to_8_be(): Invalid size (1 <= $size <= 8)'); + } + + $res = array_fill(0, $size, 0); + for ($i = $size - 1; $i >= 0; $i--) { + $res[$i] = bcmod($num, bcpow(2, 8)); + $num = bcdiv($num, bcpow(2, 8)); + } + return $res; + } + + /** + * + * Convert a hexadecimal (Base16) array to a Base58 string + * + * @param array $data + * @param array $buf + * @param number $index + * @return array + * + */ + private function encode_block($data, $buf, $index) { + if (gettype($data) != 'array') { + throw new Exception('base58->encode_block(): Invalid input type ($data must be an array)'); + } + if (gettype($buf) != 'array') { + throw new Exception('base58->encode_block(): Invalid input type ($buf must be an array)'); + } + if (gettype($index) != ('integer' || 'double')) { + throw new Exception('base58->encode_block(): Invalid input type ($index must be a number)'); + } + if (count($data) < 1 or count($data) > self::$full_encoded_block_size) { + throw new Exception('base58->encode_block(): Invalid input length (1 <= count($data) <= 8)'); + } + + $num = self::uint8_be_to_64($data); + $i = self::$encoded_block_sizes[count($data)] - 1; + while ($num > 0) { + $remainder = bcmod($num, 58); + $num = bcdiv($num, 58); + $buf[$index + $i] = ord(self::$alphabet[$remainder]); + $i--; + } + return $buf; + } + + /** + * + * Encode a hexadecimal (Base16) string to Base58 + * + * @param string $hex A hexadecimal (Base16) string to convert to Base58 + * @return string + * + */ + public function encode($hex) { + if (gettype($hex) != 'string') { + throw new Exception ('base58->encode(): Invalid input type (must be a string)'); + } + + $data = self::hex_to_bin($hex); + if (count($data) == 0) { + return ''; + } + + $full_block_count = floor(count($data) / self::$full_block_size); + $last_block_size = count($data) % self::$full_block_size; + $res_size = $full_block_count * self::$full_encoded_block_size + self::$encoded_block_sizes[$last_block_size]; + + $res = array_fill(0, $res_size, 0); + for ($i = 0; $i < $res_size; $i++) { + $res[$i] = self::$alphabet[0]; + } + + for ($i = 0; $i < $full_block_count; $i++) { + $res = self::encode_block(array_slice($data, $i * self::$full_block_size, ($i * self::$full_block_size + self::$full_block_size) - ($i * self::$full_block_size)), $res, $i * self::$full_encoded_block_size); + } + + if ($last_block_size > 0) { + $res = self::encode_block(array_slice($data, $full_block_count * self::$full_block_size, $full_block_count * self::$full_block_size + $last_block_size), $res, $full_block_count * self::$full_encoded_block_size); + } + + return self::bin_to_str($res); + } + + /** + * + * Convert a Base58 input to hexadecimal (Base16) + * + * @param array $data + * @param array $buf + * @param integer $index + * @return array + * + */ + private function decode_block($data, $buf, $index) { + if (gettype($data) != 'array') { + throw new Exception('base58->decode_block(): Invalid input type ($data must be an array)'); + } + if (gettype($buf) != 'array') { + throw new Exception('base58->decode_block(): Invalid input type ($buf must be an array)'); + } + if (gettype($index) != ('integer' || 'double')) { + throw new Exception('base58->decode_block(): Invalid input type ($index must be a number)'); + } + + $res_size = self::index_of(self::$encoded_block_sizes, count($data)); + if ($res_size <= 0) { + throw new Exception('base58->decode_block(): Invalid input length ($data must be a value from base58::$encoded_block_sizes)'); + } + + $res_num = 0; + $order = 1; + for ($i = count($data) - 1; $i >= 0; $i--) { + $digit = strpos(self::$alphabet, chr($data[$i])); + if ($digit < 0) { + throw new Exception("base58->decode_block(): Invalid character ($digit \"{$digit}\" not found in base58::$alphabet)"); + } + + $product = bcadd(bcmul($order, $digit), $res_num); + if ($product > bcpow(2, 64)) { + throw new Exception('base58->decode_block(): Integer overflow ($product exceeds the maximum 64bit integer)'); + } + + $res_num = $product; + $order = bcmul($order, 58); + } + if ($res_size < self::$full_block_size && bcpow(2, 8 * $res_size) <= 0) { + throw new Exception('base58->decode_block(): Integer overflow (bcpow(2, 8 * $res_size) exceeds the maximum 64bit integer)'); + } + + $tmp_buf = self::uint64_to_8_be($res_num, $res_size); + for ($i = 0; $i < count($tmp_buf); $i++) { + $buf[$i + $index] = $tmp_buf[$i]; + } + return $buf; + } + + /** + * + * Decode a Base58 string to hexadecimal (Base16) + * + * @param string $hex A Base58 string to convert to hexadecimal (Base16) + * @return string + * + */ + public function decode($enc) { + if (gettype($enc) != 'string') { + throw new Exception ('base58->decode(): Invalid input type (must be a string)'); + } + + $enc = self::str_to_bin($enc); + if (count($enc) == 0) { + return ''; + } + $full_block_count = floor(bcdiv(count($enc), self::$full_encoded_block_size)); + $last_block_size = bcmod(count($enc), self::$full_encoded_block_size); + $last_block_decoded_size = self::index_of(self::$encoded_block_sizes, $last_block_size); + + $data_size = $full_block_count * self::$full_block_size + $last_block_decoded_size; + + $data = array_fill(0, $data_size, 0); + for ($i = 0; $i < $full_block_count; $i++) { + $data = self::decode_block(array_slice($enc, $i * self::$full_encoded_block_size, ($i * self::$full_encoded_block_size + self::$full_encoded_block_size) - ($i * self::$full_encoded_block_size)), $data, $i * self::$full_block_size); + } + + if ($last_block_size > 0) { + $data = self::decode_block(array_slice($enc, $full_block_count * self::$full_encoded_block_size, $full_block_count * self::$full_encoded_block_size + $last_block_size), $data, $full_block_count * self::$full_block_size); + } + + return self::bin_to_hex($data); + } + + /** + * + * Search an array for a value + * Source: https://stackoverflow.com/a/30994678 + * + * @param array $haystack An array to search + * @param string $needle A string to search for + * @return number The index of the element found (or -1 for no match) + * + */ + private function index_of($haystack, $needle) { + if (gettype($haystack) != 'array') { + throw new Exception ('base58->decode(): Invalid input type ($haystack must be an array)'); + } + // if (gettype($needle) != 'string') { + // throw new Exception ('base58->decode(): Invalid input type ($needle must be a string)'); + // } + + foreach ($haystack as $key => $value) if ($value === $needle) return $key; + return -1; + } +} diff --git a/include/class-monero-cryptonote.php b/include/class-monero-cryptonote.php new file mode 100644 index 0000000..9028c95 --- /dev/null +++ b/include/class-monero-cryptonote.php @@ -0,0 +1,312 @@ +ed25519 = new ed25519(); + $this->base58 = new Monero_base58(); + $this->address_prefix = MONERO_GATEWAY_ADDRESS_PREFIX; + $this->address_prefix_integrated = MONERO_GATEWAY_ADDRESS_PREFIX_INTEGRATED; + } + + /* + * @param string Hex encoded string of the data to hash + * @return string Hex encoded string of the hashed data + * + */ + public function keccak_256($message) + { + $keccak256 = SHA3::init (SHA3::KECCAK_256); + $keccak256->absorb (hex2bin($message)); + return bin2hex ($keccak256->squeeze (32)) ; + } + + /* + * @return string A hex encoded string of 32 random bytes + * + */ + public function gen_new_hex_seed() + { + $bytes = random_bytes(32); + return bin2hex($bytes); + } + + public function sc_reduce($input) + { + $integer = $this->ed25519->decodeint(hex2bin($input)); + + $modulo = bcmod($integer , $this->ed25519->l); + + $result = bin2hex($this->ed25519->encodeint($modulo)); + return $result; + } + + /* + * Hs in the cryptonote white paper + * + * @param string Hex encoded data to hash + * + * @return string A 32 byte encoded integer + */ + public function hash_to_scalar($data) + { + $hash = $this->keccak_256($data); + $scalar = $this->sc_reduce($hash); + return $scalar; + } + + /* + * Derive a deterministic private view key from a private spend key + * @param string A private spend key represented as a 32 byte hex string + * + * @return string A deterministic private view key represented as a 32 byte hex string + */ + public function derive_viewkey($spendkey) + { + return $this->hash_to_scalar($spendkey); + } + + /* + * Generate a pair of random private keys + * + * @param string A hex string to be used as a seed (this should be random) + * + * @return array An array containing a private spend key and a deterministic view key + */ + public function gen_private_keys($seed) + { + $spendkey = $this->sc_reduce($seed); + $viewkey = $this->derive_viewkey($spendkey); + $result = array("spendkey" => $spendkey, + "viewkey" => $viewkey); + + return $result; + } + + /* + * Get a public key from a private key on the ed25519 curve + * + * @param string a 32 byte hex encoded private key + * + * @return string a 32 byte hex encoding of a point on the curve to be used as a public key + */ + public function pk_from_sk($privKey) + { + $keyInt = $this->ed25519->decodeint(hex2bin($privKey)); + $aG = $this->ed25519->scalarmult_base($keyInt); + return bin2hex($this->ed25519->encodepoint($aG)); + } + + /* + * Generate key derivation + * + * @param string a 32 byte hex encoding of a point on the ed25519 curve used as a public key + * @param string a 32 byte hex encoded private key + * + * @return string The hex encoded key derivation + */ + public function gen_key_derivation($public, $private) + { + $point = $this->ed25519->scalarmult($this->ed25519->decodepoint(hex2bin($public)), $this->ed25519->decodeint(hex2bin($private))); + $res = $this->ed25519->scalarmult($point, 8); + return bin2hex($this->ed25519->encodepoint($res)); + } + + public function encode_variant($data) + { + $orig = $data; + + if ($data < 0x80) + { + return bin2hex(pack('C', $data)); + } + + $encodedBytes = []; + while ($data > 0) + { + $encodedBytes[] = 0x80 | ($data & 0x7f); + $data >>= 7; + } + + $encodedBytes[count($encodedBytes)-1] &= 0x7f; + $bytes = call_user_func_array('pack', array_merge(array('C*'), $encodedBytes));; + return bin2hex($bytes); + } + + public function derivation_to_scalar($der, $index) + { + $encoded = $this->encode_variant($index); + $data = $der . $encoded; + return $this->hash_to_scalar($data); + } + + // this is a one way function used for both encrypting and decrypting 8 byte payment IDs + public function stealth_payment_id($payment_id, $tx_pub_key, $viewkey) + { + if(strlen($payment_id) != 16) + { + throw new Exception("Error: Incorrect payment ID size. Should be 8 bytes"); + } + $der = $this->gen_key_derivation($tx_pub_key, $viewkey); + $data = $der . '8d'; + $hash = $this->keccak_256($data); + $key = substr($hash, 0, 16); + $result = bin2hex(pack('H*',$payment_id) ^ pack('H*',$key)); + return $result; + } + + // takes transaction extra field as hex string and returns transaction public key 'R' as hex string + public function txpub_from_extra($extra) + { + $parsed = array_map("hexdec", str_split($extra, 2)); + + if($parsed[0] == 1) + { + return substr($extra, 2, 64); + } + + if($parsed[0] == 2) + { + if($parsed[0] == 2 || $parsed[2] == 1) + { + $offset = (($parsed[1] + 2) *2) + 2; + return substr($extra, (($parsed[1] + 2) *2) + 2, 64); + } + } + } + + public function derive_public_key($der, $index, $pub) + { + $scalar = $this->derivation_to_scalar($der, $index); + $sG = $this->ed25519->scalarmult_base($this->ed25519->decodeint(hex2bin($scalar))); + $pubPoint = $this->ed25519->decodepoint(hex2bin($pub)); + $key = $this->ed25519->encodepoint($this->ed25519->edwards($pubPoint, $sG)); + return bin2hex($key); + } + + /* + * Perform the calculation P = P' as described in the cryptonote whitepaper + * + * @param string 32 byte transaction public key R + * @param string 32 byte reciever private view key a + * @param string 32 byte reciever public spend key B + * @param int output index + * @param string output you want to check against P + */ + public function is_output_mine($txPublic, $privViewkey, $publicSpendkey, $index, $P) + { + $derivation = $this->gen_key_derivation($txPublic, $privViewkey); + $Pprime = $this->derive_public_key($derivation, $index, $publicSpendkey); + + if($P == $Pprime) + { + return true; + } + else + return false; + } + + /* + * Create a valid base58 encoded Monero address from public keys + * + * @param string Public spend key + * @param string Public view key + * + * @return string Base58 encoded Monero address + */ + public function encode_address($pSpendKey, $pViewKey) + { + $data = $this->address_prefix . $pSpendKey . $pViewKey; + $encoded = $this->base58->encode($data); + return $encoded; + } + + public function verify_checksum($address) + { + $decoded = $this->base58->decode($address); + $checksum = substr($decoded, -8); + $checksum_hash = $this->keccak_256(substr($decoded, 0, -8)); + $calculated = substr($checksum_hash, 0, 8); + return $checksum == $calculated; + } + +/* + * Decode a base58 encoded Monero address + * + * @param string A base58 encoded Monero address + * + * @return array An array containing the Address network byte, public spend key, and public view key + */ + public function decode_address($address) + { + $decoded = $this->base58->decode($address); + + if(!$this->verify_checksum($address)){ + throw new Exception("Error: invalid checksum"); + } + + $expected_prefix = $this->encode_variant($this->address_prefix); + $expected_prefix_length = strlen($expected_prefix); + + $network_byte = substr($decoded, 0, $expected_prefix_length); + $public_spendkey = substr($decoded, $expected_prefix_length, 64); + $public_viewkey = substr($decoded, 64+$expected_prefix_length, 64); + + return array( + "networkByte" => $network_byte, + "spendkey" => $public_spendkey, + "viewkey" => $public_viewkey + ); + } + + /* + * Get an integrated address from public keys and a payment id + * + * @param string A 32 byte hex encoded public spend key + * @param string A 32 byte hex encoded public view key + * @param string An 8 byte hex string to use as a payment id + */ + public function integrated_addr_from_keys($public_spendkey, $public_viewkey, $payment_id) + { + $prefix = $this->encode_variant($this->address_prefix_integrated); + $data = $prefix.$public_spendkey.$public_viewkey.$payment_id; + $checksum = substr($this->keccak_256($data), 0, 8); + $result = $this->base58->encode($data.$checksum); + return $result; + } + + /* + * Generate a Monero address from seed + * + * @param string Hex string to use as seed + * + * @return string A base58 encoded Monero address + */ + public function address_from_seed($hex_seed) + { + $private_keys = $this->gen_private_keys($hex_seed); + $private_viewkey = $private_keys["viewkey"]; + $private_spendkey = $private_keys["spendkey"]; + + $public_spendkey = $this->pk_from_sk($private_spendkey); + $public_viewkey = $this->pk_from_sk($private_viewkey); + + $address = $this->encode_address($public_spendkey, $public_viewkey); + return $address; + } +} diff --git a/include/class-monero-explorer-tools.php b/include/class-monero-explorer-tools.php new file mode 100644 index 0000000..e43b00f --- /dev/null +++ b/include/class-monero-explorer-tools.php @@ -0,0 +1,90 @@ +url = $testnet ? MONERO_GATEWAY_TESTNET_EXPLORER_URL : MONERO_GATEWAY_MAINNET_EXPLORER_URL; + } + + private function call_api($endpoint) + { + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_URL => $this->url . $endpoint, + )); + $data = curl_exec($curl); + curl_close($curl); + return json_decode($data, true); + } + + public function get_last_block_height() + { + $data = $this->call_api('/api/networkinfo'); + if($data['status'] == 'success') + return $data['data']['height'] - 1; + else + return 0; + } + + public function getheight() + { + return $this->get_last_block_height(); + } + + public function get_txs_from_block($height) + { + $data = $this->call_api("/api/search/$height"); + if($data['status'] == 'success') + return $data['data']['txs']; + else + return []; + } + + public function get_outputs($address, $viewkey) + { + $data = $this->call_api("/api/outputsblocks?address=$address&viewkey=$viewkey&limit=5&mempool=1"); + if($data['status'] == 'success') + return $data['data']['outputs']; + else + return []; + } + + public function check_tx($tx_hash, $address, $viewkey) + { + $data = $this->call_api("/api/outputs?txhash=$tx_hash&address=$address&viewkey=$viewkey&txprove=0"); + if($data['status'] == 'success') { + foreach($data['data']['outputs'] as $output) { + if($output['match']) + return true; + } + } else { + return false; + } + } + + function get_mempool_txs() + { + $data = $this->call_api('/api/mempool'); + if($data['status'] == 'success') + return $data['txs']; + else + return []; + } + +} diff --git a/include/class-monero-gateway.php b/include/class-monero-gateway.php new file mode 100644 index 0000000..7cda884 --- /dev/null +++ b/include/class-monero-gateway.php @@ -0,0 +1,789 @@ +'); + } + + function __construct($add_action=true) + { + $this->id = self::$_id; + $this->method_title = __(self::$_method_title, 'monero_gateway'); + $this->method_description = __(self::$_method_description, 'monero_gateway'); + $this->has_fields = false; + $this->supports = array( + 'products', + 'subscriptions', + 'subscription_cancellation', + 'subscription_suspension', + 'subscription_reactivation', + 'subscription_amount_changes', + 'subscription_date_changes', + 'subscription_payment_method_change' + ); + + $this->enabled = $this->get_option('enabled') == 'yes'; + + $this->init_form_fields(); + $this->init_settings(); + + self::$_title = $this->settings['title']; + $this->title = $this->settings['title']; + $this->description = $this->settings['description']; + self::$discount = $this->settings['discount']; + self::$valid_time = $this->settings['valid_time']; + self::$confirms = $this->settings['confirms']; + self::$confirm_type = $this->settings['confirm_type']; + self::$address = $this->settings['monero_address']; + self::$viewkey = $this->settings['viewkey']; + self::$host = $this->settings['daemon_host']; + self::$port = $this->settings['daemon_port']; + self::$testnet = $this->settings['testnet'] == 'yes'; + self::$onion_service = $this->settings['onion_service'] == 'yes'; + self::$show_qr = $this->settings['show_qr'] == 'yes'; + self::$use_monero_price = $this->settings['use_monero_price'] == 'yes'; + self::$use_monero_price_decimals = $this->settings['use_monero_price_decimals']; + + $explorer_url = self::$testnet ? MONERO_GATEWAY_TESTNET_EXPLORER_URL : MONERO_GATEWAY_MAINNET_EXPLORER_URL; + defined('MONERO_GATEWAY_EXPLORER_URL') || define('MONERO_GATEWAY_EXPLORER_URL', $explorer_url); + + if($add_action) + add_action('woocommerce_update_options_payment_gateways_'.$this->id, array($this, 'process_admin_options')); + + // Initialize helper classes + self::$cryptonote = new Monero_Cryptonote(); + if(self::$confirm_type == 'monero-wallet-rpc') { + require_once('class-monero-wallet-rpc.php'); + self::$monero_wallet_rpc = new Monero_Wallet_Rpc(self::$host, self::$port); + } else { + require_once('class-monero-explorer-tools.php'); + self::$monero_explorer_tools = new Monero_Explorer_Tools(self::$testnet); + } + + self::$log = new WC_Logger(); + } + + public function init_form_fields() + { + $this->form_fields = include 'admin/monero-gateway-admin-settings.php'; + } + + public function validate_monero_address_field($key,$address) + { + if($this->settings['confirm_type'] == 'viewkey') { + if (strlen($address) == 95 && substr($address, 0, 1) == '4') + if(self::$cryptonote->verify_checksum($address)) + return $address; + self::$_errors[] = 'Monero address is invalid'; + } + return $address; + } + + public function validate_viewkey_field($key,$viewkey) + { + if($this->settings['confirm_type'] == 'viewkey') { + if(preg_match('/^[a-z0-9]{64}$/i', $viewkey)) { + return $viewkey; + } else { + self::$_errors[] = 'Viewkey is invalid'; + return ''; + } + } + return $viewkey; + } + + public function validate_confirms_field($key,$confirms) + { + if($confirms >= 0 && $confirms <= 60) + return $confirms; + self::$_errors[] = 'Number of confirms must be between 0 and 60'; + } + + public function validate_valid_time_field($key,$valid_time) + { + if($valid_time >= 600 && $valid_time < 86400*7) + return $valid_time; + self::$_errors[] = 'Order valid time must be between 600 (10 minutes) and 604800 (1 week)'; + } + + public function admin_options() + { + $confirm_type = self::$confirm_type; + if($confirm_type === 'monero-wallet-rpc') + $balance = self::admin_balance_info(); + + $settings_html = $this->generate_settings_html(array(), false); + $errors = array_merge(self::$_errors, $this->admin_php_module_check(), $this->admin_ssl_check()); + include MONERO_GATEWAY_PLUGIN_DIR . '/templates/monero-gateway/admin/settings-page.php'; + } + + public static function admin_balance_info() + { + if(!is_admin()) { + return array( + 'height' => 'Not Available', + 'balance' => 'Not Available', + 'unlocked_balance' => 'Not Available', + ); + } + $wallet_amount = self::$monero_wallet_rpc->getbalance(); + $height = self::$monero_wallet_rpc->getheight(); + if (!isset($wallet_amount)) { + self::$_errors[] = 'Cannot connect to monero-wallet-rpc'; + self::$log->add('Monero_Payments', '[ERROR] Cannot connect to monero-wallet-rpc'); + return array( + 'height' => 'Not Available', + 'balance' => 'Not Available', + 'unlocked_balance' => 'Not Available', + ); + } else { + return array( + 'height' => $height, + 'balance' => self::format_monero($wallet_amount['balance']).' Monero', + 'unlocked_balance' => self::format_monero($wallet_amount['unlocked_balance']).' Monero' + ); + } + } + + protected function admin_ssl_check() + { + $errors = array(); + if ($this->enabled && !self::$onion_service) + if (get_option('woocommerce_force_ssl_checkout') == 'no') + $errors[] = sprintf('%s is enabled and WooCommerce is not forcing the SSL certificate on your checkout page. Please ensure that you have a valid SSL certificate and that you are forcing the checkout pages to be secured.', self::$_method_title, admin_url('admin.php?page=wc-settings&tab=checkout')); + return $errors; + } + + protected function admin_php_module_check() + { + $errors = array(); + if(!extension_loaded('bcmath')) + $errors[] = 'PHP extension bcmath must be installed'; + return $errors; + } + + public function process_payment($order_id) + { + global $wpdb; + $table_name = $wpdb->prefix.'monero_gateway_quotes'; + + $order = wc_get_order($order_id); + + // Generate a unique payment id + do { + $payment_id = bin2hex(openssl_random_pseudo_bytes(8)); + $query = $wpdb->prepare("SELECT COUNT(*) FROM $table_name WHERE payment_id=%s", array($payment_id)); + $payment_id_used = $wpdb->get_var($query); + } while ($payment_id_used); + + $currency = $order->get_currency(); + $rate = self::get_live_rate($currency); + $fiat_amount = $order->get_total(''); + $monero_amount = 1e8 * $fiat_amount / $rate; + + if(self::$discount) + $monero_amount = $monero_amount - $monero_amount * self::$discount / 100; + + $monero_amount = intval($monero_amount * MONERO_GATEWAY_ATOMIC_UNITS_POW); + + $query = $wpdb->prepare("INSERT INTO $table_name (order_id, payment_id, currency, rate, amount) VALUES (%d, %s, %s, %d, %d)", array($order_id, $payment_id, $currency, $rate, $monero_amount)); + $wpdb->query($query); + + $order->update_status('on-hold', __('Awaiting offline payment', 'monero_gateway')); + $order->reduce_order_stock(); // Reduce stock levels + WC()->cart->empty_cart(); // Remove cart + + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url($order) + ); + } + + /* + * function for verifying payments + * This cron runs every 30 seconds + */ + public static function do_update_event() + { + global $wpdb; + + // Get Live Price + $currencies = implode(',', self::$currencies); + $api_link = 'https://min-api.cryptocompare.com/data/price?fsym=XMR&tsyms='.$currencies.'&extraParams=monero_woocommerce'; + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_URL => $api_link, + )); + $resp = curl_exec($curl); + curl_close($curl); + $price = json_decode($resp, true); + + if(!isset($price['Response']) || $price['Response'] != 'Error') { + $table_name = $wpdb->prefix.'monero_gateway_live_rates'; + foreach($price as $currency=>$rate) { + // shift decimal eight places for precise int storage + $rate = intval($rate * 1e8); + $query = $wpdb->prepare("INSERT INTO $table_name (currency, rate, updated) VALUES (%s, %d, NOW()) ON DUPLICATE KEY UPDATE rate=%d, updated=NOW()", array($currency, $rate, $rate)); + $wpdb->query($query); + } + } + + // Get current network/wallet height + if(self::$confirm_type == 'monero-wallet-rpc') + $height = self::$monero_wallet_rpc->getheight(); + else + $height = self::$monero_explorer_tools->getheight(); + set_transient('monero_gateway_network_height', $height); + + // Get pending payments + $table_name_1 = $wpdb->prefix.'monero_gateway_quotes'; + $table_name_2 = $wpdb->prefix.'monero_gateway_quotes_txids'; + + $query = $wpdb->prepare("SELECT *, $table_name_1.payment_id AS payment_id, $table_name_1.amount AS amount_total, $table_name_2.amount AS amount_paid, NOW() as now FROM $table_name_1 LEFT JOIN $table_name_2 ON $table_name_1.payment_id = $table_name_2.payment_id WHERE pending=1", array()); + $rows = $wpdb->get_results($query); + + $pending_payments = array(); + + // Group the query into distinct orders by payment_id + foreach($rows as $row) { + if(!isset($pending_payments[$row->payment_id])) + $pending_payments[$row->payment_id] = array( + 'quote' => null, + 'txs' => array() + ); + $pending_payments[$row->payment_id]['quote'] = $row; + if($row->txid) + $pending_payments[$row->payment_id]['txs'][] = $row; + } + + // Loop through each pending payment and check status + foreach($pending_payments as $pending) { + $quote = $pending['quote']; + $old_txs = $pending['txs']; + $order_id = $quote->order_id; + $order = wc_get_order($order_id); + $payment_id = self::sanatize_id($quote->payment_id); + $amount_monero = $quote->amount_total; + + if(self::$confirm_type == 'monero-wallet-rpc') + $new_txs = self::check_payment_rpc($payment_id); + else + $new_txs = self::check_payment_explorer($payment_id); + + foreach($new_txs as $new_tx) { + $is_new_tx = true; + foreach($old_txs as $old_tx) { + if($new_tx['txid'] == $old_tx->txid && $new_tx['amount'] == $old_tx->amount_paid) { + $is_new_tx = false; + break; + } + } + if($is_new_tx) { + $old_txs[] = (object) $new_tx; + } + + $query = $wpdb->prepare("INSERT INTO $table_name_2 (payment_id, txid, amount, height) VALUES (%s, %s, %d, %d) ON DUPLICATE KEY UPDATE height=%d", array($payment_id, $new_tx['txid'], $new_tx['amount'], $new_tx['height'], $new_tx['height'])); + $wpdb->query($query); + } + + $txs = $old_txs; + $heights = array(); + $amount_paid = 0; + foreach($txs as $tx) { + $amount_paid += $tx->amount; + $heights[] = $tx->height; + } + + $paid = $amount_paid > $amount_monero - MONERO_GATEWAY_ATOMIC_UNIT_THRESHOLD; + + if($paid) { + if(self::$confirms == 0) { + $confirmed = true; + } else { + $highest_block = max($heights); + if($height - $highest_block >= self::$confirms && !in_array(0, $heights)) { + $confirmed = true; + } else { + $confirmed = false; + } + } + } else { + $confirmed = false; + } + + if($paid && $confirmed) { + self::$log->add('Monero_Payments', "[SUCCESS] Payment has been confirmed for order id $order_id and payment id $payment_id"); + $query = $wpdb->prepare("UPDATE $table_name_1 SET confirmed=1,paid=1,pending=0 WHERE payment_id=%s", array($payment_id)); + $wpdb->query($query); + + unset(self::$payment_details[$order_id]); + + if(self::is_virtual_in_cart($order_id) == true){ + $order->update_status('completed', __('Payment has been received.', 'monero_gateway')); + } else { + $order->update_status('processing', __('Payment has been received.', 'monero_gateway')); + } + + } else if($paid) { + self::$log->add('Monero_Payments', "[SUCCESS] Payment has been received for order id $order_id and payment id $payment_id"); + $query = $wpdb->prepare("UPDATE $table_name_1 SET paid=1 WHERE payment_id=%s", array($payment_id)); + $wpdb->query($query); + + unset(self::$payment_details[$order_id]); + + } else { + $timestamp_created = new DateTime($quote->created); + $timestamp_now = new DateTime($quote->now); + $order_age_seconds = $timestamp_now->getTimestamp() - $timestamp_created->getTimestamp(); + if($order_age_seconds > self::$valid_time) { + self::$log->add('Monero_Payments', "[FAILED] Payment has expired for order id $order_id and payment id $payment_id"); + $query = $wpdb->prepare("UPDATE $table_name_1 SET pending=0 WHERE payment_id=%s", array($payment_id)); + $wpdb->query($query); + + unset(self::$payment_details[$order_id]); + + $order->update_status('cancelled', __('Payment has expired.', 'monero_gateway')); + } + } + } + } + + protected static function check_payment_rpc($payment_id) + { + $txs = array(); + $payments = self::$monero_wallet_rpc->get_all_payments($payment_id); + foreach($payments as $payment) { + $txs[] = array( + 'amount' => $payment['amount'], + 'txid' => $payment['tx_hash'], + 'height' => $payment['block_height'] + ); + } + return $txs; + } + + public static function check_payment_explorer($payment_id) + { + $txs = array(); + $outputs = self::$monero_explorer_tools->get_outputs(self::$address, self::$viewkey); + foreach($outputs as $payment) { + if($payment['payment_id'] == $payment_id) { + $txs[] = array( + 'amount' => $payment['amount'], + 'txid' => $payment['tx_hash'], + 'height' => $payment['block_no'] + ); + } + } + return $txs; + } + + protected static function get_payment_details($order_id) + { + if(!is_integer($order_id)) + $order_id = $order_id->get_id(); + + if(isset(self::$payment_details[$order_id])) + return self::$payment_details[$order_id]; + + global $wpdb; + $table_name_1 = $wpdb->prefix.'monero_gateway_quotes'; + $table_name_2 = $wpdb->prefix.'monero_gateway_quotes_txids'; + $query = $wpdb->prepare("SELECT *, $table_name_1.payment_id AS payment_id, $table_name_1.amount AS amount_total, $table_name_2.amount AS amount_paid, NOW() as now FROM $table_name_1 LEFT JOIN $table_name_2 ON $table_name_1.payment_id = $table_name_2.payment_id WHERE order_id=%d", array($order_id)); + $details = $wpdb->get_results($query); + if (count($details)) { + $txs = array(); + $heights = array(); + $amount_paid = 0; + foreach($details as $tx) { + if(!isset($tx->txid)) + continue; + $txs[] = array( + 'txid' => $tx->txid, + 'height' => $tx->height, + 'amount' => $tx->amount_paid, + 'amount_formatted' => self::format_monero($tx->amount_paid) + ); + $amount_paid += $tx->amount_paid; + $heights[] = $tx->height; + } + + usort($txs, function($a, $b) { + if($a['height'] == 0) return -1; + return $b['height'] - $a['height']; + }); + + if(count($heights) && !in_array(0, $heights)) { + $height = get_transient('monero_gateway_network_height'); + $highest_block = max($heights); + $confirms = $height - $highest_block; + $blocks_to_confirm = self::$confirms - $confirms; + } else { + $blocks_to_confirm = self::$confirms; + } + $time_to_confirm = self::format_seconds_to_time($blocks_to_confirm * MONERO_GATEWAY_DIFFICULTY_TARGET); + + $amount_total = $details[0]->amount_total; + $amount_due = max(0, $amount_total - $amount_paid); + + $timestamp_created = new DateTime($details[0]->created); + $timestamp_now = new DateTime($details[0]->now); + + $order_age_seconds = $timestamp_now->getTimestamp() - $timestamp_created->getTimestamp(); + $order_expires_seconds = self::$valid_time - $order_age_seconds; + + $address = self::$address; + $payment_id = self::sanatize_id($details[0]->payment_id); + + if(self::$confirm_type == 'monero-wallet-rpc') { + $array_integrated_address = self::$monero_wallet_rpc->make_integrated_address($payment_id); + if (isset($array_integrated_address['integrated_address'])) { + $integrated_addr = $array_integrated_address['integrated_address']; + } else { + self::$log->add('Monero_Gateway', '[ERROR] Unable get integrated address'); + return '[ERROR] Unable get integrated address'; + } + } else { + if ($address) { + $decoded_address = self::$cryptonote->decode_address($address); + $pub_spendkey = $decoded_address['spendkey']; + $pub_viewkey = $decoded_address['viewkey']; + $integrated_addr = self::$cryptonote->integrated_addr_from_keys($pub_spendkey, $pub_viewkey, $payment_id); + } else { + self::$log->add('Monero_Gateway', '[ERROR] Merchant has not set Monero address'); + return '[ERROR] Merchant has not set Monero address'; + } + } + + $status = ''; + $paid = $details[0]->paid == 1; + $confirmed = $details[0]->confirmed == 1; + $pending = $details[0]->pending == 1; + + if($confirmed) { + $status = 'confirmed'; + } else if($paid) { + $status = 'paid'; + } else if($pending && $order_expires_seconds > 0) { + if(count($txs)) { + $status = 'partial'; + } else { + $status = 'unpaid'; + } + } else { + if(count($txs)) { + $status = 'expired_partial'; + } else { + $status = 'expired'; + } + } + + $qrcode_uri = 'monero:'.$address.'?tx_amount='.$amount_due.'&tx_payment_id='.$payment_id; + $my_order_url = wc_get_endpoint_url('view-order', $order_id, wc_get_page_permalink('myaccount')); + + $payment_details = array( + 'order_id' => $order_id, + 'payment_id' => $payment_id, + 'integrated_address' => $integrated_addr, + 'qrcode_uri' => $qrcode_uri, + 'my_order_url' => $my_order_url, + 'rate' => $details[0]->rate, + 'rate_formatted' => sprintf('%.8f', $details[0]->rate / 1e8), + 'currency' => $details[0]->currency, + 'amount_total' => $amount_total, + 'amount_paid' => $amount_paid, + 'amount_due' => $amount_due, + 'amount_total_formatted' => self::format_monero($amount_total), + 'amount_paid_formatted' => self::format_monero($amount_paid), + 'amount_due_formatted' => self::format_monero($amount_due), + 'status' => $status, + 'created' => $details[0]->created, + 'order_age' => $order_age_seconds, + 'order_expires' => self::format_seconds_to_time($order_expires_seconds), + 'blocks_to_confirm' => $blocks_to_confirm, + 'time_to_confirm' => $time_to_confirm, + 'txs' => $txs + ); + self::$payment_details[$order_id] = $payment_details; + return $payment_details; + } else { + return '[ERROR] Quote not found'; + } + + } + + public static function get_payment_details_ajax() { + + $user = wp_get_current_user(); + if($user === 0) + self::ajax_output(array('error' => '[ERROR] User not logged in')); + + $order_id = preg_replace("/[^0-9]+/", "", $_GET['order_id']); + $order = wc_get_order( $order_id ); + + if($order->user_id != $user->ID) + self::ajax_output(array('error' => '[ERROR] Order does not belong to this user')); + + if($order->get_payment_method() != self::$_id) + self::ajax_output(array('error' => '[ERROR] Order not paid for with Monero')); + + $details = self::get_payment_details($order); + if(!is_array($details)) + self::ajax_output(array('error' => $details)); + + self::ajax_output($details); + + } + public static function ajax_output($response) { + ob_clean(); + header('Content-type: application/json'); + echo json_encode($response); + wp_die(); + } + + public static function admin_order_page($post) + { + $order = wc_get_order($post->ID); + if($order->get_payment_method() != self::$_id) + return; + + $method_title = self::$_title; + $details = self::get_payment_details($order); + if(!is_array($details)) { + $error = $details; + include MONERO_GATEWAY_PLUGIN_DIR . '/templates/monero-gateway/admin/order-history-error-page.php'; + return; + } + include MONERO_GATEWAY_PLUGIN_DIR . '/templates/monero-gateway/admin/order-history-page.php'; + } + + public static function customer_order_page($order) + { + if(is_integer($order)) { + $order_id = $order; + $order = wc_get_order($order_id); + } else { + $order_id = $order->get_id(); + } + + if($order->get_payment_method() != self::$_id) + return; + + $method_title = self::$_title; + $details = self::get_payment_details($order_id); + if(!is_array($details)) { + $error = $details; + include MONERO_GATEWAY_PLUGIN_DIR . '/templates/monero-gateway/customer/order-error-page.php'; + return; + } + $show_qr = self::$show_qr; + $details_json = json_encode($details); + $ajax_url = WC_AJAX::get_endpoint('monero_gateway_payment_details'); + include MONERO_GATEWAY_PLUGIN_DIR . '/templates/monero-gateway/customer/order-page.php'; + } + + public static function customer_order_email($order) + { + if(is_integer($order)) { + $order_id = $order; + $order = wc_get_order($order_id); + } else { + $order_id = $order->get_id(); + } + + if($order->get_payment_method() != self::$_id) + return; + + $method_title = self::$_title; + $details = self::get_payment_details($order_id); + if(!is_array($details)) { + include MONERO_GATEWAY_PLUGIN_DIR . '/templates/monero-gateway/customer/order-email-error-block.php'; + return; + } + include MONERO_GATEWAY_PLUGIN_DIR . '/templates/monero-gateway/customer/order-email-block.php'; + } + + public static function get_id() + { + return self::$_id; + } + + public static function get_confirm_type() + { + return self::$confirm_type; + } + + public static function use_qr_code() + { + return self::$show_qr; + } + + public static function use_monero_price() + { + return self::$use_monero_price; + } + + + public static function convert_wc_price($price, $currency) + { + $rate = self::get_live_rate($currency); + $monero_amount = intval(MONERO_GATEWAY_ATOMIC_UNITS_POW * 1e8 * $price / $rate) / MONERO_GATEWAY_ATOMIC_UNITS_POW; + $monero_amount_formatted = sprintf('%.'.self::$use_monero_price_decimals.'f', $monero_amount); + + return << + $monero_amount_formatted + XMR + + +HTML; + } + + public static function convert_wc_price_order($price_html, $order) + { + if($order->get_payment_method() != self::$_id) + return $price_html; + + $order_id = $order->get_id(); + $payment_details = self::get_payment_details($order_id); + if(!is_array($payment_details)) + return $price_html; + + // Experimental regex, may fail with other custom price formatters + $match_ok = preg_match('/data-price="([^"]*)"/', $price_html, $matches); + if($match_ok !== 1) // regex failed + return $price_html; + + $price = array_pop($matches); + $currency = $payment_details['currency']; + $rate = $payment_details['rate']; + $monero_amount = intval(MONERO_GATEWAY_ATOMIC_UNITS_POW * 1e8 * $price / $rate) / MONERO_GATEWAY_ATOMIC_UNITS_POW; + $monero_amount_formatted = sprintf('%.'.MONERO_GATEWAY_ATOMIC_UNITS.'f', $monero_amount); + + return << + $monero_amount_formatted + XMR + + +HTML; + } + + public static function get_live_rate($currency) + { + if(isset(self::$rates[$currency])) + return self::$rates[$currency]; + + global $wpdb; + $table_name = $wpdb->prefix.'monero_gateway_live_rates'; + $query = $wpdb->prepare("SELECT rate FROM $table_name WHERE currency=%s", array($currency)); + + $rate = $wpdb->get_row($query)->rate; + self::$rates[$currency] = $rate; + + return $rate; + } + + protected static function sanatize_id($payment_id) + { + // Limit payment id to alphanumeric characters + $sanatized_id = preg_replace("/[^a-zA-Z0-9]+/", "", $payment_id); + return $sanatized_id; + } + + protected static function is_virtual_in_cart($order_id) + { + $order = wc_get_order($order_id); + $items = $order->get_items(); + $cart_size = count($items); + $virtual_items = 0; + + foreach ( $items as $item ) { + $product = new WC_Product( $item['product_id'] ); + if ($product->is_virtual()) { + $virtual_items += 1; + } + } + return $virtual_items == $cart_size; + } + + public static function format_monero($atomic_units) { + return sprintf(MONERO_GATEWAY_ATOMIC_UNITS_SPRINTF, $atomic_units / MONERO_GATEWAY_ATOMIC_UNITS_POW); + } + + public static function format_seconds_to_time($seconds) + { + $units = array(); + + $dtF = new \DateTime('@0'); + $dtT = new \DateTime("@$seconds"); + $diff = $dtF->diff($dtT); + + $d = $diff->format('%a'); + $h = $diff->format('%h'); + $m = $diff->format('%i'); + + if($d == 1) + $units[] = "$d day"; + else if($d > 1) + $units[] = "$d days"; + + if($h == 0 && $d != 0) + $units[] = "$h hours"; + else if($h == 1) + $units[] = "$h hour"; + else if($h > 0) + $units[] = "$h hours"; + + if($m == 1) + $units[] = "$m minute"; + else + $units[] = "$m minutes"; + + return implode(', ', $units) . ($seconds < 0 ? ' ago' : ''); + } + +} diff --git a/monero/library.php b/include/class-monero-wallet-rpc.php similarity index 67% rename from monero/library.php rename to include/class-monero-wallet-rpc.php index 8a75108..5940dda 100644 --- a/monero/library.php +++ b/include/class-monero-wallet-rpc.php @@ -1,7 +1,6 @@ * http://implix.com * Modified to work with monero-rpc wallet by Serhack and cryptochangements + * Modified to work with monero-wallet-rpc wallet by mosu-forge */ -class Monero_Library + +defined( 'ABSPATH' ) || exit; + +class Monero_Wallet_Rpc { - protected $url = null, $is_debug = false, $parameters_structure = 'array'; + protected $url = null, $is_debug = false; protected $curl_options = array( CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_TIMEOUT => 8 @@ -36,7 +39,7 @@ class Monero_Library { $this->validate(false === extension_loaded('curl'), 'The curl extension must be loaded to use this class!'); $this->validate(false === extension_loaded('json'), 'The json extension must be loaded to use this class!'); - + $this->host = $pHost; $this->port = $pPort; $this->url = $pHost . ':' . $pPort . '/json_rpc'; @@ -45,7 +48,7 @@ class Monero_Library public function validate($pFailed, $pErrMsg) { if ($pFailed) { - echo $pErrMsg; + if(is_admin()) echo $pErrMsg; } } @@ -55,25 +58,12 @@ class Monero_Library return $this; } - /* public function setParametersStructure($pParametersStructure) - { - if (in_array($pParametersStructure, array('array', 'object'))) - { - $this->parameters_structure = $pParametersStructure; - } - else - { - throw new UnexpectedValueException('Invalid parameters structure type.'); - } - return $this; - } */ - public function setCurlOptions($pOptionsArray) { if (is_array($pOptionsArray)) { $this->curl_options = $pOptionsArray + $this->curl_options; } else { - echo 'Invalid options type.'; + if(is_admin()) echo 'Invalid options type.'; } return $this; } @@ -81,13 +71,7 @@ class Monero_Library public function _print($json) { $json_encoded = json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - echo $json_encoded; - } - - public function address() - { - $address = $this->_run('getaddress'); - return $address; + if(is_admin()) echo $json_encoded; } public function _run($method, $params = null) @@ -99,23 +83,29 @@ class Monero_Library private function request($pMethod, $pParams) { static $requestId = 0; + // generating uniuqe id per process $requestId++; + // check if given params are correct $this->validate(false === is_scalar($pMethod), 'Method name has no scalar value'); - // $this->validate(false === is_array($pParams), 'Params must be given as array'); - // send params as an object or an array - //$pParams = ($this->parameters_structure == 'object') ? $pParams[0] : array_values($pParams); + // Request (method invocation) $request = json_encode(array('jsonrpc' => '2.0', 'method' => $pMethod, 'params' => $pParams, 'id' => $requestId)); + // if is_debug mode is true then add url and request to is_debug $this->debug('Url: ' . $this->url . "\r\n", false); $this->debug('Request: ' . $request . "\r\n", false); + + // Response (method invocation) $responseMessage = $this->getResponse($request); + // if is_debug mode is true then add response to is_debug and display it $this->debug('Response: ' . $responseMessage . "\r\n", true); + // decode and create array ( can be object, just set to false ) $responseDecoded = json_decode($responseMessage, true); + // check if decoding json generated any errors $jsonErrorMsg = $this->getJsonLastErrorMsg(); $this->validate(!is_null($jsonErrorMsg), $jsonErrorMsg . ': ' . $responseMessage); @@ -149,7 +139,7 @@ class Monero_Library $endTime = array_sum(explode(' ', microtime())); // performance summary $debug .= 'Request time: ' . round($endTime - $startTime, 3) . ' s Memory usage: ' . round(memory_get_usage() / 1024) . " kb\r\n"; - echo nl2br($debug); + if(is_admin()) echo nl2br($debug); // send output immediately flush(); // clean static @@ -177,14 +167,18 @@ class Monero_Library } // send the request $response = curl_exec($ch); + // check http status code $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (isset($this->httpErrors[$httpCode])) { - echo 'Response Http Error - ' . $this->httpErrors[$httpCode]; + if(is_admin()) + echo 'Response Http Error - ' . $this->httpErrors[$httpCode]; } + // check for curl error if (0 < curl_errno($ch)) { - echo '[ERROR] Failed to connect to monero-wallet-rpc at ' . $this->host . ' port '. $this->port .'
'; + if(is_admin()) + echo '[ERROR] Failed to connect to monero-wallet-rpc at ' . $this->host . ' port '. $this->port .'
'; } // close the connection curl_close($ch); @@ -219,12 +213,18 @@ class Monero_Library } } - /* + /* * The following functions can all be called to interact with the Monero RPC wallet * They will majority of them will return the result as an array * Example: $daemon->address(); where $daemon is an instance of this class, will return the wallet address as string within an array */ + public function address() + { + $address = $this->_run('getaddress'); + return $address; + } + public function getbalance() { $balance = $this->_run('getbalance'); @@ -234,7 +234,7 @@ class Monero_Library public function getheight() { $height = $this->_run('getheight'); - return $height; + return $height['height']; } public function incoming_transfer($type) @@ -271,7 +271,7 @@ class Monero_Library public function split_integrated_address($integrated_address) { if (!isset($integrated_address)) { - echo "Error: Integrated_Address mustn't be null"; + if(is_admin()) echo "Error: Integrated_Address must not be null"; } else { $split_params = array('integrated_address' => $integrated_address); $split_methods = $this->_run('split_integrated_address', $split_params); @@ -281,8 +281,8 @@ class Monero_Library public function make_uri($address, $amount, $recipient_name = null, $description = null) { - // If I pass 1, it will be 0.0000001 xmr. Then - $new_amount = $amount * 100000000; + // Convert to atomic units + $new_amount = $amount * MONERO_GATEWAY_ATOMIC_UNITS_POW; $uri_params = array('address' => $address, 'amount' => $new_amount, 'payment_id' => '', 'recipient_name' => $recipient_name, 'tx_description' => $description); $uri = $this->_run('make_uri', $uri_params); @@ -296,9 +296,9 @@ class Monero_Library return $parsed_uri; } - public function transfer($amount, $address, $mixin = 4) + public function transfer($amount, $address, $mixin = 12) { - $new_amount = $amount * 1000000000000; + $new_amount = $amount * MONERO_GATEWAY_ATOMIC_UNITS_POW; $destinations = array('amount' => $new_amount, 'address' => $address); $transfer_parameters = array('destinations' => array($destinations), 'mixin' => $mixin, 'get_tx_key' => true, 'unlock_time' => 0, 'payment_id' => ''); $transfer_method = $this->_run('transfer', $transfer_parameters); @@ -309,128 +309,44 @@ class Monero_Library { $get_payments_parameters = array('payment_id' => $payment_id); $get_payments = $this->_run('get_payments', $get_payments_parameters); - return $get_payments; + if(isset($get_payments['payments'])) + return $get_payments['payments']; + else + return array(); } - public function get_bulk_payments($payment_id, $min_block_height) + public function get_pool_payments($payment_id) { - $get_bulk_payments_parameters = array('payment_id' => $payment_id, 'min_block_height' => $min_block_height); - $get_bulk_payments = $this->_run('get_bulk_payments', $get_bulk_payments_parameters); - return $get_bulk_payments; - } -} - -class NodeTools -{ - private $url; - public function __construct($testnet = false) - { - if(!testnet) - { - $this->url = 'https://xmrchain.net'; - } - if(testnet) - { - $this->url = 'https://testnet.xmrchain.net'; - } - } - - public function get_last_block_height() - { - $curl = curl_init(); - - curl_setopt_array($curl, array( - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_URL => $this->url . 'api/networkinfo', - )); - $resp = curl_exec($curl); - curl_close($curl); - - $array = json_decode($resp, true); - return $array['data']['height'] - 1; - } - - public function get_txs_from_block($height) - { - $curl = curl_init(); - - curl_setopt_array($curl, array( - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_URL => $this->url . '/api/search/' . $height, - )); - $resp = curl_exec($curl); - curl_close($curl); - - $array = json_decode($resp, true); - - return $array['data']['txs']; - } - - public function get_outputs($address, $viewkey, $zero_conf = false) - { - $curl = curl_init(); - - if(!$zero_conf) - { - curl_setopt_array($curl, array( - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_URL => $this->url . '/api/outputsblocks?address=' . $address . '&viewkey=' . $viewkey . '&limit=5&mempool=0', - )); - } - - // also look in mempool if accepting zero confirmation transactions - if($zero_conf) - { - curl_setopt_array($curl, array( - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_URL => $this->url . '/api/outputsblocks?address=' . $address . '&viewkey=' . $viewkey . '&limit=5&mempool=1', - )); + $get_payments_parameters = array('pool' => true); + $get_payments = $this->_run('get_transfers', $get_payments_parameters); + + if(!isset($get_payments['pool'])) + return array(); + + $payments = array(); + foreach($get_payments['pool'] as $payment) { + if($payment['double_spend_seen'])continue; + if($payment['payment_id'] == $payment_id) { + $payment['tx_hash'] = $payment['txid']; + $payment['block_height'] = $payment['height']; + $payments[] = $payment; + } } - - $resp = curl_exec($curl); - curl_close($curl); - - $array = json_decode($resp, true); - - return $array['data']['outputs']; + + return $payments; } - - public function check_tx($tx_hash, $address, $viewKey) + + public function get_all_payments($payment_id) { - $curl = curl_init(); - curl_setopt_array($curl, array( - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_URL => $this-url . '/api/outputs?txhash=' .$tx_hash . '&address='. $address . '&viewkey='. $viewKey .'&txprove=0', - )); - $resp = curl_exec($curl); - curl_close($curl); - $array = json_decode($resp, true); - $output_count = count($array['data']['outputs']); - $i = 0; - while($i < $output_count) - { - if($array['data']['outputs'][$i]['match']) - { - return $array['data']['outputs'][$i]; - } - - $i++; - } - + $confirmed_payments = $this->get_payments($payment_id); + $pool_payments = $this->get_pool_payments($payment_id); + return array_merge($pool_payments, $confirmed_payments); } - - function get_mempool_txs() + + public function get_bulk_payments($payment_id, $min_block_height) { - $curl = curl_init(); - - curl_setopt_array($curl, array( - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_URL => $this->url . '/api/mempool', - )); - $resp = curl_exec($curl); - curl_close($curl); - $array = json_decode($resp, true); - return $array; + $get_bulk_payments_parameters = array('payment_id' => $payment_id, 'min_block_height' => $min_block_height); + $get_bulk_payments = $this->_run('get_bulk_payments', $get_bulk_payments_parameters); + return $get_bulk_payments; } - } diff --git a/monero/include/SHA3.php b/include/crypto/SHA3.php similarity index 99% rename from monero/include/SHA3.php rename to include/crypto/SHA3.php index 385561f..aa3b263 100644 --- a/monero/include/SHA3.php +++ b/include/crypto/SHA3.php @@ -23,7 +23,6 @@ vim: ts=4 noet ai */ @file */ - /** SHA-3 (FIPS-202) for PHP strings (byte arrays) (PHP 5.2.1+) PHP 7.0 computes SHA-3 about 4 times faster than PHP 5.2 - 5.6 (on x86_64) @@ -34,6 +33,9 @@ vim: ts=4 noet ai */ This uses PHP's native byte strings. Supports 32-bit as well as 64-bit systems. Also for LE vs. BE systems. */ + +defined( 'ABSPATH' ) || exit; + class SHA3 { const SHA3_224 = 1; const SHA3_256 = 2; diff --git a/monero/include/ed25519.php b/include/crypto/ed25519.php similarity index 99% rename from monero/include/ed25519.php rename to include/crypto/ed25519.php index e6d443b..0a270f7 100644 --- a/monero/include/ed25519.php +++ b/include/crypto/ed25519.php @@ -30,6 +30,9 @@ * * @link http://ed25519.cr.yp.to/software.html Other ED25519 implementations this is referenced from */ + +defined( 'ABSPATH' ) || exit; + class ed25519 { public $b; diff --git a/monero-woocommerce-gateway.php b/monero-woocommerce-gateway.php new file mode 100644 index 0000000..10accac --- /dev/null +++ b/monero-woocommerce-gateway.php @@ -0,0 +1,249 @@ +'.__('Settings', 'monero_gateway').'' + ); + return array_merge($plugin_links, $links); + } + + add_filter('cron_schedules', 'monero_cron_add_one_minute'); + function monero_cron_add_one_minute($schedules) { + $schedules['one_minute'] = array( + 'interval' => 60, + 'display' => __('Once every minute', 'monero_gateway') + ); + return $schedules; + } + + add_action('wp', 'monero_activate_cron'); + function monero_activate_cron() { + if(!wp_next_scheduled('monero_update_event')) { + wp_schedule_event(time(), 'one_minute', 'monero_update_event'); + } + } + + add_action('monero_update_event', 'monero_update_event'); + function monero_update_event() { + Monero_Gateway::do_update_event(); + } + + add_action('woocommerce_thankyou_'.Monero_Gateway::get_id(), 'monero_order_confirm_page'); + add_action('woocommerce_order_details_after_order_table', 'monero_order_page'); + add_action('woocommerce_email_after_order_table', 'monero_order_email'); + + function monero_order_confirm_page($order_id) { + Monero_Gateway::customer_order_page($order_id); + } + function monero_order_page($order) { + if(!is_wc_endpoint_url('order-received')) + Monero_Gateway::customer_order_page($order); + } + function monero_order_email($order) { + Monero_Gateway::customer_order_email($order); + } + + add_action('wc_ajax_monero_gateway_payment_details', 'monero_get_payment_details_ajax'); + function monero_get_payment_details_ajax() { + Monero_Gateway::get_payment_details_ajax(); + } + + add_filter('woocommerce_currencies', 'monero_add_currency'); + function monero_add_currency($currencies) { + $currencies['Monero'] = __('Monero', 'monero_gateway'); + return $currencies; + } + + add_filter('woocommerce_currency_symbol', 'monero_add_currency_symbol', 10, 2); + function monero_add_currency_symbol($currency_symbol, $currency) { + switch ($currency) { + case 'Monero': + $currency_symbol = 'XMR'; + break; + } + return $currency_symbol; + } + + if(Monero_Gateway::use_monero_price()) { + + // This filter will replace all prices with amount in Monero (live rates) + add_filter('wc_price', 'monero_live_price_format', 10, 3); + function monero_live_price_format($price_html, $price_float, $args) { + if(!isset($args['currency']) || !$args['currency']) { + global $woocommerce; + $currency = strtoupper(get_woocommerce_currency()); + } else { + $currency = strtoupper($args['currency']); + } + return Monero_Gateway::convert_wc_price($price_float, $currency); + } + + // These filters will replace the live rate with the exchange rate locked in for the order + // We must be careful to hit all the hooks for price displays associated with an order, + // else the exchange rate can change dynamically (which it should for an order) + add_filter('woocommerce_order_formatted_line_subtotal', 'monero_order_item_price_format', 10, 3); + function monero_order_item_price_format($price_html, $item, $order) { + return Monero_Gateway::convert_wc_price_order($price_html, $order); + } + + add_filter('woocommerce_get_formatted_order_total', 'monero_order_total_price_format', 10, 2); + function monero_order_total_price_format($price_html, $order) { + return Monero_Gateway::convert_wc_price_order($price_html, $order); + } + + add_filter('woocommerce_get_order_item_totals', 'monero_order_totals_price_format', 10, 3); + function monero_order_totals_price_format($total_rows, $order, $tax_display) { + foreach($total_rows as &$row) { + $price_html = $row['value']; + $row['value'] = Monero_Gateway::convert_wc_price_order($price_html, $order); + } + return $total_rows; + } + + } + + add_action('wp_enqueue_scripts', 'monero_enqueue_scripts'); + function monero_enqueue_scripts() { + if(Monero_Gateway::use_monero_price()) + wp_dequeue_script('wc-cart-fragments'); + if(Monero_Gateway::use_qr_code()) + wp_enqueue_script('monero-qr-code', MONERO_GATEWAY_PLUGIN_URL.'assets/js/qrcode.min.js'); + + wp_enqueue_script('monero-clipboard-js', MONERO_GATEWAY_PLUGIN_URL.'assets/js/clipboard.min.js'); + wp_enqueue_script('monero-gateway', MONERO_GATEWAY_PLUGIN_URL.'assets/js/monero-gateway-order-page.js'); + wp_enqueue_style('monero-gateway', MONERO_GATEWAY_PLUGIN_URL.'assets/css/monero-gateway-order-page.css'); + } + + // [monero-price currency="USD"] + // currency: BTC, GBP, etc + // if no none, then default store currency + function monero_price_func( $atts ) { + global $woocommerce; + $a = shortcode_atts( array( + 'currency' => get_woocommerce_currency() + ), $atts ); + + $currency = strtoupper($a['currency']); + $rate = Monero_Gateway::get_live_rate($currency); + if($currency == 'BTC') + $rate_formatted = sprintf('%.8f', $rate / 1e8); + else + $rate_formatted = sprintf('%.5f', $rate / 1e8); + + return "1 XMR = $rate_formatted $currency"; + } + add_shortcode('monero-price', 'monero_price_func'); + + + // [monero-accepted-here] + function monero_accepted_func() { + return ''; + } + add_shortcode('monero-accepted-here', 'monero_accepted_func'); + +} + +register_deactivation_hook(__FILE__, 'monero_deactivate'); +function monero_deactivate() { + $timestamp = wp_next_scheduled('monero_update_event'); + wp_unschedule_event($timestamp, 'monero_update_event'); +} + +register_activation_hook(__FILE__, 'monero_install'); +function monero_install() { + global $wpdb; + require_once( ABSPATH . '/wp-admin/includes/upgrade.php' ); + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . "monero_gateway_quotes"; + if($wpdb->get_var("show tables like '$table_name'") != $table_name) { + $sql = "CREATE TABLE $table_name ( + order_id BIGINT(20) UNSIGNED NOT NULL, + payment_id VARCHAR(16) DEFAULT '' NOT NULL, + currency VARCHAR(6) DEFAULT '' NOT NULL, + rate BIGINT UNSIGNED DEFAULT 0 NOT NULL, + amount BIGINT UNSIGNED DEFAULT 0 NOT NULL, + paid TINYINT NOT NULL DEFAULT 0, + confirmed TINYINT NOT NULL DEFAULT 0, + pending TINYINT NOT NULL DEFAULT 1, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (order_id) + ) $charset_collate;"; + dbDelta($sql); + } + + $table_name = $wpdb->prefix . "monero_gateway_quotes_txids"; + if($wpdb->get_var("show tables like '$table_name'") != $table_name) { + $sql = "CREATE TABLE $table_name ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + payment_id VARCHAR(16) DEFAULT '' NOT NULL, + txid VARCHAR(64) DEFAULT '' NOT NULL, + amount BIGINT UNSIGNED DEFAULT 0 NOT NULL, + height MEDIUMINT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY (payment_id, txid, amount) + ) $charset_collate;"; + dbDelta($sql); + } + + $table_name = $wpdb->prefix . "monero_gateway_live_rates"; + if($wpdb->get_var("show tables like '$table_name'") != $table_name) { + $sql = "CREATE TABLE $table_name ( + currency VARCHAR(6) DEFAULT '' NOT NULL, + rate BIGINT UNSIGNED DEFAULT 0 NOT NULL, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (currency) + ) $charset_collate;"; + dbDelta($sql); + } +} diff --git a/monero/include/base58.php b/monero/include/base58.php deleted file mode 100644 index e0c1604..0000000 --- a/monero/include/base58.php +++ /dev/null @@ -1,354 +0,0 @@ -hex_to_bin(): Invalid input type (must be a string)'); - } - if (strlen($hex) % 2 != 0) { - throw new Exception('base58->hex_to_bin(): Invalid input length (must be even)'); - } - - $res = array_fill(0, strlen($hex) / 2, 0); - for ($i = 0; $i < strlen($hex) / 2; $i++) { - $res[$i] = intval(substr($hex, $i * 2, $i * 2 + 2 - $i * 2), 16); - } - return $res; - } - - /** - * - * Convert a binary array to a hexadecimal string - * - * @param array $bin A binary array to convert to a hexadecimal string - * @return string - * - */ - private function bin_to_hex($bin) { - if (gettype($bin) != 'array') { - throw new Exception('base58->bin_to_hex(): Invalid input type (must be an array)'); - } - - $res = []; - for ($i = 0; $i < count($bin); $i++) { - $res[] = substr('0'.dechex($bin[$i]), -2); - } - return join($res); - } - - /** - * - * Convert a string to a binary array - * - * @param string $str A string to convert to a binary array - * @return array - * - */ - private function str_to_bin($str) { - if (gettype($str) != 'string') { - throw new Exception('base58->str_to_bin(): Invalid input type (must be a string)'); - } - - $res = array_fill(0, strlen($str), 0); - for ($i = 0; $i < strlen($str); $i++) { - $res[$i] = ord($str[$i]); - } - return $res; - } - - /** - * - * Convert a binary array to a string - * - * @param array $bin A binary array to convert to a string - * @return string - * - */ - private function bin_to_str($bin) { - if (gettype($bin) != 'array') { - throw new Exception('base58->bin_to_str(): Invalid input type (must be an array)'); - } - - $res = array_fill(0, count($bin), 0); - for ($i = 0; $i < count($bin); $i++) { - $res[$i] = chr($bin[$i]); - } - return preg_replace('/[[:^print:]]/', '', join($res)); // preg_replace necessary to strip errant non-ASCII characters eg. '' - } - - /** - * - * Convert a UInt8BE (one unsigned big endian byte) array to UInt64 - * - * @param array $data A UInt8BE array to convert to UInt64 - * @return number - * - */ - private function uint8_be_to_64($data) { - if (gettype($data) != 'array') { - throw new Exception ('base58->uint8_be_to_64(): Invalid input type (must be an array)'); - } - - $res = 0; - $i = 0; - switch (9 - count($data)) { - case 1: - $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); - case 2: - $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); - case 3: - $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); - case 4: - $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); - case 5: - $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); - case 6: - $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); - case 7: - $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); - case 8: - $res = bcadd(bcmul($res, bcpow(2, 8)), $data[$i++]); - break; - default: - throw new Exception('base58->uint8_be_to_64: Invalid input length (1 <= count($data) <= 8)'); - } - return $res; - } - - /** - * - * Convert a UInt64 (unsigned 64 bit integer) to a UInt8BE array - * - * @param number $num A UInt64 number to convert to a UInt8BE array - * @param integer $size Size of array to return - * @return array - * - */ - private function uint64_to_8_be($num, $size) { - if (gettype($num) != ('integer' || 'double')) { - throw new Exception ('base58->uint64_to_8_be(): Invalid input type ($num must be a number)'); - } - if (gettype($size) != 'integer') { - throw new Exception ('base58->uint64_to_8_be(): Invalid input type ($size must be an integer)'); - } - if ($size < 1 || $size > 8) { - throw new Exception ('base58->uint64_to_8_be(): Invalid size (1 <= $size <= 8)'); - } - - $res = array_fill(0, $size, 0); - for ($i = $size - 1; $i >= 0; $i--) { - $res[$i] = bcmod($num, bcpow(2, 8)); - $num = bcdiv($num, bcpow(2, 8)); - } - return $res; - } - - /** - * - * Convert a hexadecimal (Base16) array to a Base58 string - * - * @param array $data - * @param array $buf - * @param number $index - * @return array - * - */ - private function encode_block($data, $buf, $index) { - if (gettype($data) != 'array') { - throw new Exception('base58->encode_block(): Invalid input type ($data must be an array)'); - } - if (gettype($buf) != 'array') { - throw new Exception('base58->encode_block(): Invalid input type ($buf must be an array)'); - } - if (gettype($index) != ('integer' || 'double')) { - throw new Exception('base58->encode_block(): Invalid input type ($index must be a number)'); - } - if (count($data) < 1 or count($data) > self::$full_encoded_block_size) { - throw new Exception('base58->encode_block(): Invalid input length (1 <= count($data) <= 8)'); - } - - $num = self::uint8_be_to_64($data); - $i = self::$encoded_block_sizes[count($data)] - 1; - while ($num > 0) { - $remainder = bcmod($num, 58); - $num = bcdiv($num, 58); - $buf[$index + $i] = ord(self::$alphabet[$remainder]); - $i--; - } - return $buf; - } - - /** - * - * Encode a hexadecimal (Base16) string to Base58 - * - * @param string $hex A hexadecimal (Base16) string to convert to Base58 - * @return string - * - */ - public function encode($hex) { - if (gettype($hex) != 'string') { - throw new Exception ('base58->encode(): Invalid input type (must be a string)'); - } - - $data = self::hex_to_bin($hex); - if (count($data) == 0) { - return ''; - } - - $full_block_count = floor(count($data) / self::$full_block_size); - $last_block_size = count($data) % self::$full_block_size; - $res_size = $full_block_count * self::$full_encoded_block_size + self::$encoded_block_sizes[$last_block_size]; - - $res = array_fill(0, $res_size, 0); - for ($i = 0; $i < $res_size; $i++) { - $res[$i] = self::$alphabet[0]; - } - - for ($i = 0; $i < $full_block_count; $i++) { - $res = self::encode_block(array_slice($data, $i * self::$full_block_size, ($i * self::$full_block_size + self::$full_block_size) - ($i * self::$full_block_size)), $res, $i * self::$full_encoded_block_size); - } - - if ($last_block_size > 0) { - $res = self::encode_block(array_slice($data, $full_block_count * self::$full_block_size, $full_block_count * self::$full_block_size + $last_block_size), $res, $full_block_count * self::$full_encoded_block_size); - } - - return self::bin_to_str($res); - } - - /** - * - * Convert a Base58 input to hexadecimal (Base16) - * - * @param array $data - * @param array $buf - * @param integer $index - * @return array - * - */ - private function decode_block($data, $buf, $index) { - if (gettype($data) != 'array') { - throw new Exception('base58->decode_block(): Invalid input type ($data must be an array)'); - } - if (gettype($buf) != 'array') { - throw new Exception('base58->decode_block(): Invalid input type ($buf must be an array)'); - } - if (gettype($index) != ('integer' || 'double')) { - throw new Exception('base58->decode_block(): Invalid input type ($index must be a number)'); - } - - $res_size = self::index_of(self::$encoded_block_sizes, count($data)); - if ($res_size <= 0) { - throw new Exception('base58->decode_block(): Invalid input length ($data must be a value from base58::$encoded_block_sizes)'); - } - - $res_num = 0; - $order = 1; - for ($i = count($data) - 1; $i >= 0; $i--) { - $digit = strpos(self::$alphabet, chr($data[$i])); - if ($digit < 0) { - throw new Exception("base58->decode_block(): Invalid character ($digit \"{$digit}\" not found in base58::$alphabet)"); - } - - $product = bcadd(bcmul($order, $digit), $res_num); - if ($product > bcpow(2, 64)) { - throw new Exception('base58->decode_block(): Integer overflow ($product exceeds the maximum 64bit integer)'); - } - - $res_num = $product; - $order = bcmul($order, 58); - } - if ($res_size < self::$full_block_size && bcpow(2, 8 * $res_size) <= 0) { - throw new Exception('base58->decode_block(): Integer overflow (bcpow(2, 8 * $res_size) exceeds the maximum 64bit integer)'); - } - - $tmp_buf = self::uint64_to_8_be($res_num, $res_size); - for ($i = 0; $i < count($tmp_buf); $i++) { - $buf[$i + $index] = $tmp_buf[$i]; - } - return $buf; - } - - /** - * - * Decode a Base58 string to hexadecimal (Base16) - * - * @param string $hex A Base58 string to convert to hexadecimal (Base16) - * @return string - * - */ - public function decode($enc) { - if (gettype($enc) != 'string') { - throw new Exception ('base58->decode(): Invalid input type (must be a string)'); - } - - $enc = self::str_to_bin($enc); - if (count($enc) == 0) { - return ''; - } - $full_block_count = floor(bcdiv(count($enc), self::$full_encoded_block_size)); - $last_block_size = bcmod(count($enc), self::$full_encoded_block_size); - $last_block_decoded_size = self::index_of(self::$encoded_block_sizes, $last_block_size); - - $data_size = $full_block_count * self::$full_block_size + $last_block_decoded_size; - - $data = array_fill(0, $data_size, 0); - for ($i = 0; $i <= $full_block_count; $i++) { - $data = self::decode_block(array_slice($enc, $i * self::$full_encoded_block_size, ($i * self::$full_encoded_block_size + self::$full_encoded_block_size) - ($i * self::$full_encoded_block_size)), $data, $i * self::$full_block_size); - } - - if ($last_block_size > 0) { - $data = self::decode_block(array_slice($enc, $full_block_count * self::$full_encoded_block_size, $full_block_count * self::$full_encoded_block_size + $last_block_size), $data, $full_block_count * self::$full_block_size); - } - - return self::bin_to_hex($data); - } - - /** - * - * Search an array for a value - * Source: https://stackoverflow.com/a/30994678 - * - * @param array $haystack An array to search - * @param string $needle A string to search for - * @return number The index of the element found (or -1 for no match) - * - */ - private function index_of($haystack, $needle) { - if (gettype($haystack) != 'array') { - throw new Exception ('base58->decode(): Invalid input type ($haystack must be an array)'); - } - // if (gettype($needle) != 'string') { - // throw new Exception ('base58->decode(): Invalid input type ($needle must be a string)'); - // } - - foreach ($haystack as $key => $value) if ($value === $needle) return $key; - return -1; - } -} - -?> diff --git a/monero/include/cryptonote.php b/monero/include/cryptonote.php deleted file mode 100644 index a6dafaf..0000000 --- a/monero/include/cryptonote.php +++ /dev/null @@ -1,305 +0,0 @@ -ed25519 = new ed25519(); - $this->base58 = new base58(); - } - - /* - * @param string Hex encoded string of the data to hash - * @return string Hex encoded string of the hashed data - * - */ - public function keccak_256($message) - { - $keccak256 = SHA3::init (SHA3::KECCAK_256); - $keccak256->absorb (hex2bin($message)); - return bin2hex ($keccak256->squeeze (32)) ; - } - - /* - * @return string A hex encoded string of 32 random bytes - * - */ - public function gen_new_hex_seed() - { - $bytes = random_bytes(32); - return bin2hex($bytes); - } - - public function sc_reduce($input) - { - $integer = $this->ed25519->decodeint(hex2bin($input)); - - $modulo = bcmod($integer , $this->ed25519->l); - - $result = bin2hex($this->ed25519->encodeint($modulo)); - return $result; - } - - /* - * Hs in the cryptonote white paper - * - * @param string Hex encoded data to hash - * - * @return string A 32 byte encoded integer - */ - public function hash_to_scalar($data) - { - $hash = $this->keccak_256($data); - $scalar = $this->sc_reduce($hash); - return $scalar; - } - - /* - * Derive a deterministic private view key from a private spend key - * @param string A private spend key represented as a 32 byte hex string - * - * @return string A deterministic private view key represented as a 32 byte hex string - */ - public function derive_viewKey($spendKey) - { - return $this->hash_to_scalar($spendkey); - } - - /* - * Generate a pair of random private keys - * - * @param string A hex string to be used as a seed (this should be random) - * - * @return array An array containing a private spend key and a deterministic view key - */ - public function gen_private_keys($seed) - { - $spendKey = $this->sc_reduce($seed); - $viewKey = $this->derive_viewKey($spendKey); - $result = array("spendKey" => $spendKey, - "viewKey" => $viewKey); - - return $result; - } - - /* - * Get a public key from a private key on the ed25519 curve - * - * @param string a 32 byte hex encoded private key - * - * @return string a 32 byte hex encoding of a point on the curve to be used as a public key - */ - public function pk_from_sk($privKey) - { - $keyInt = $this->ed25519->decodeint(hex2bin($privKey)); - $aG = $this->ed25519->scalarmult_base($keyInt); - return bin2hex($this->ed25519->encodepoint($aG)); - } - - /* - * Generate key derivation - * - * @param string a 32 byte hex encoding of a point on the ed25519 curve used as a public key - * @param string a 32 byte hex encoded private key - * - * @return string The hex encoded key derivation - */ - public function gen_key_derivation($public, $private) - { - $point = $this->ed25519->scalarmult($this->ed25519->decodepoint(hex2bin($public)), $this->ed25519->decodeint(hex2bin($private))); - $res = $this->ed25519->scalarmult($point, 8); - return bin2hex($this->ed25519->encodepoint($res)); - } - - public function encode_varint($data) - { - $orig = $data; - - if ($data < 0x80) - { - return bin2hex(pack('C', $data)); - } - - $encodedBytes = []; - while ($data > 0) - { - $encodedBytes[] = 0x80 | ($data & 0x7f); - $data >>= 7; - } - - $encodedBytes[count($encodedBytes)-1] &= 0x7f; - $bytes = call_user_func_array('pack', array_merge(array('C*'), $encodedBytes));; - return bin2hex($bytes); - } - - public function derivation_to_scalar($der, $index) - { - $encoded = $this->encode_varint($index); - $data = $der . $encoded; - return $this->hash_to_scalar($data); - } - - // this is a one way function used for both encrypting and decrypting 8 byte payment IDs - public function stealth_payment_id($payment_id, $tx_pub_key, $viewkey) - { - if(strlen($payment_id) != 16) - { - throw new Exception("Error: Incorrect payment ID size. Should be 8 bytes"); - } - $der = $this->gen_key_derivation($tx_pub_key, $viewkey); - $data = $der . '8d'; - $hash = $this->keccak_256($data); - $key = substr($hash, 0, 16); - $result = bin2hex(pack('H*',$payment_id) ^ pack('H*',$key)); - return $result; - } - - // takes transaction extra field as hex string and returns transaction public key 'R' as hex string - public function txpub_from_extra($extra) - { - $parsed = array_map("hexdec", str_split($extra, 2)); - - if($parsed[0] == 1) - { - return substr($extra, 2, 64); - } - - if($parsed[0] == 2) - { - if($parsed[0] == 2 || $parsed[2] == 1) - { - $offset = (($parsed[1] + 2) *2) + 2; - return substr($extra, (($parsed[1] + 2) *2) + 2, 64); - } - } - } - - public function derive_public_key($der, $index, $pub) - { - $scalar = $this->derivation_to_scalar($der, $index); - $sG = $this->ed25519->scalarmult_base($this->ed25519->decodeint(hex2bin($scalar))); - $pubPoint = $this->ed25519->decodepoint(hex2bin($pub)); - $key = $this->ed25519->encodepoint($this->ed25519->edwards($pubPoint, $sG)); - return bin2hex($key); - } - - /* - * Perform the calculation P = P' as described in the cryptonote whitepaper - * - * @param string 32 byte transaction public key R - * @param string 32 byte reciever private view key a - * @param string 32 byte reciever public spend key B - * @param int output index - * @param string output you want to check against P - */ - public function is_output_mine($txPublic, $privViewkey, $publicSpendkey, $index, $P) - { - $derivation = $this->gen_key_derivation($txPublic, $privViewkey); - $Pprime = $this->derive_public_key($derivation, $index, $publicSpendkey); - - if($P == $Pprime) - { - return true; - } - else - return false; - } - - /* - * Create a valid base58 encoded Monero address from public keys - * - * @param string Public spend key - * @param string Public view key - * - * @return string Base58 encoded Monero address - */ - public function encode_address($pSpendKey, $pViewKey) - { - // mainnet network byte is 18 (0x12) - $data = "12" . $pSpendKey . $pViewKey; - $encoded = $this->base58->encode($data); - return $encoded; - } - - public function verify_checksum($address) - { - $decoded = $this->base58->decode($address); - $checksum = substr($decoded, -8); - $checksum_hash = $this->keccak_256(substr($decoded, 0, 130)); - $calculated = substr($checksum_hash, 0, 8); - if($checksum == $calculated){ - return true; - } - else - return false; - } - - /* - * Decode a base58 encoded Monero address - * - * @param string A base58 encoded Monero address - * - * @return array An array containing the Address network byte, public spend key, and public view key - */ - public function decode_address($address) - { - $decoded = $this->base58->decode($address); - - if(!$this->verify_checksum($address)){ - throw new Exception("Error: invalid checksum"); - } - - $network_byte = substr($decoded, 0, 2); - $public_spendKey = substr($decoded, 2, 64); - $public_viewKey = substr($decoded, 66, 64); - - $result = array("networkByte" => $network_byte, - "spendKey" => $public_spendKey, - "viewKey" => $public_viewKey); - return $result; - } - - /* - * Get an integrated address from public keys and a payment id - * - * @param string A 32 byte hex encoded public spend key - * @param string A 32 byte hex encoded public view key - * @param string An 8 byte hex string to use as a payment id - */ - public function integrated_addr_from_keys($public_spendkey, $public_viewkey, $payment_id) - { - // 0x13 is the mainnet network byte for integrated addresses - $data = "13".$public_spendkey.$public_viewkey.$payment_id; - $checksum = substr($this->keccak_256($data), 0, 8); - $result = $this->base58->encode($data.$checksum); - return $result; - } - - /* - * Generate a Monero address from seed - * - * @param string Hex string to use as seed - * - * @return string A base58 encoded Monero address - */ - public function address_from_seed($hex_seed) - { - $private_keys = $this->gen_private_keys($hex_seed); - $private_viewKey = $private_keys["viewKey"]; - $private_spendKey = $private_keys["spendKey"]; - - $public_spendKey = $this->pk_from_sk($private_spendKey); - $public_viewKey = $this->pk_from_sk($private_viewKey); - - $address = $this->encode_address($public_spendKey, $public_viewKey); - return $address; - } - } - diff --git a/monero/include/monero_payments.php b/monero/include/monero_payments.php deleted file mode 100644 index 1e44cec..0000000 --- a/monero/include/monero_payments.php +++ /dev/null @@ -1,748 +0,0 @@ -id = "monero_gateway"; - $this->method_title = __("Monero GateWay", 'monero_gateway'); - $this->method_description = __("Monero Payment Gateway Plug-in for WooCommerce. You can find more information about this payment gateway on our website. You'll need a daemon online for your address.", 'monero_gateway'); - $this->title = __("Monero Gateway", 'monero_gateway'); - $this->version = "2.0"; - // - $this->icon = apply_filters('woocommerce_offline_icon', ''); - $this->has_fields = false; - - $this->log = new WC_Logger(); - - $this->init_form_fields(); - $this->host = $this->get_option('daemon_host'); - $this->port = $this->get_option('daemon_port'); - $this->address = $this->get_option('monero_address'); - $this->viewKey = $this->get_option('viewKey'); - $this->discount = $this->get_option('discount'); - $this->accept_zero_conf = $this->get_option('zero_conf'); - - $this->use_viewKey = $this->get_option('use_viewKey'); - $this->use_rpc = $this->get_option('use_rpc'); - - $env = $this->get_option('environment'); - - if($this->use_viewKey == 'yes') - { - $this->non_rpc = true; - } - if($this->use_rpc == 'yes') - { - $this->non_rpc = false; - } - if($this->accept_zero_conf == 'yes') - { - $this->zero_confirm = true; - } - - if($env == 'yes') - { - $this->testnet = true; - } - - // After init_settings() is called, you can get the settings and load them into variables, e.g: - // $this->title = $this->get_option('title' ); - $this->init_settings(); - - // Turn these settings into variables we can use - foreach ($this->settings as $setting_key => $value) { - $this->$setting_key = $value; - } - - add_action('admin_notices', array($this, 'do_ssl_check')); - add_action('admin_notices', array($this, 'validate_fields')); - add_action('woocommerce_thankyou_' . $this->id, array($this, 'instruction')); - if (is_admin()) { - /* Save Settings */ - add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options')); - add_filter('woocommerce_currencies', array($this,'add_my_currency')); - add_filter('woocommerce_currency_symbol', array($this,'add_my_currency_symbol'), 10, 2); - add_action('woocommerce_email_before_order_table', array($this, 'email_instructions'), 10, 2); - } - $this->monero_daemon = new Monero_Library($this->host, $this->port); - $this->cryptonote = new Cryptonote(); - - $this->supports = array( 'subscriptions', 'products' ); - $this->supports = array( - 'products', - 'subscriptions', - 'subscription_cancellation', - 'subscription_suspension', - 'subscription_reactivation', - 'subscription_amount_changes', - 'subscription_date_changes', - 'subscription_payment_method_change' - ); - } - - public function get_icon() - { - return apply_filters('woocommerce_gateway_icon', ""); - } - - public function init_form_fields() - { - $this->form_fields = array( - 'enabled' => array( - 'title' => __('Enable / Disable', 'monero_gateway'), - 'label' => __('Enable this payment gateway', 'monero_gateway'), - 'type' => 'checkbox', - 'default' => 'no' - ), - - 'title' => array( - 'title' => __('Title', 'monero_gateway'), - 'type' => 'text', - 'desc_tip' => __('Payment title the customer will see during the checkout process.', 'monero_gateway'), - 'default' => __('Monero XMR Payment', 'monero_gateway') - ), - 'description' => array( - 'title' => __('Description', 'monero_gateway'), - 'type' => 'textarea', - 'desc_tip' => __('Payment description the customer will see during the checkout process.', 'monero_gateway'), - 'default' => __('Pay securely using XMR.', 'monero_gateway') - - ), - 'use_viewKey' => array( - 'title' => __('Use ViewKey', 'monero_gateway'), - 'label' => __(' Verify Transaction with ViewKey ', 'monero_gateway'), - 'type' => 'checkbox', - 'description' => __('Fill in the Address and ViewKey fields to verify transactions with your ViewKey', 'monero_gateway'), - 'default' => 'no' - ), - 'monero_address' => array( - 'title' => __('Monero Address', 'monero_gateway'), - 'label' => __('Useful for people that have not a daemon online'), - 'type' => 'text', - 'desc_tip' => __('Monero Wallet Address', 'monero_gateway') - ), - 'viewKey' => array( - 'title' => __('Secret ViewKey', 'monero_gateway'), - 'label' => __('Secret ViewKey'), - 'type' => 'text', - 'desc_tip' => __('Your secret ViewKey', 'monero_gateway') - ), - 'use_rpc' => array( - 'title' => __('Use monero-wallet-rpc', 'monero_gateway'), - 'label' => __(' Verify transactions with the monero-wallet-rpc ', 'monero_gateway'), - 'type' => 'checkbox', - 'description' => __('This must be setup seperately', 'monero_gateway'), - 'default' => 'no' - ), - 'daemon_host' => array( - 'title' => __('Monero wallet RPC Host/ IP', 'monero_gateway'), - 'type' => 'text', - 'desc_tip' => __('This is the Daemon Host/IP to authorize the payment with port', 'monero_gateway'), - 'default' => 'localhost', - ), - 'daemon_port' => array( - 'title' => __('Monero wallet RPC port', 'monero_gateway'), - 'type' => 'text', - 'desc_tip' => __('This is the Daemon Host/IP to authorize the payment with port', 'monero_gateway'), - 'default' => '18080', - ), - 'discount' => array( - 'title' => __('% discount for using XMR', 'monero_gateway'), - - 'desc_tip' => __('Provide a discount to your customers for making a private payment with XMR!', 'monero_gateway'), - 'description' => __('Do you want to spread the word about Monero? Offer a small discount! Leave this empty if you do not wish to provide a discount', 'monero_gateway'), - 'type' => __('number'), - 'default' => '5' - - ), - 'environment' => array( - 'title' => __(' Testnet', 'monero_gateway'), - 'label' => __(' Check this if you are using testnet ', 'monero_gateway'), - 'type' => 'checkbox', - 'description' => __('Check this box if you are using testnet', 'monero_gateway'), - 'default' => 'no' - ), - 'zero_conf' => array( - 'title' => __(' Accept 0 conf txs', 'monero_gateway'), - 'label' => __(' Accept 0-confirmation transactions ', 'monero_gateway'), - 'type' => 'checkbox', - 'description' => __('This is faster but less secure', 'monero_gateway'), - 'default' => 'no' - ), - 'onion_service' => array( - 'title' => __(' SSL warnings ', 'monero_gateway'), - 'label' => __(' Check to Silence SSL warnings', 'monero_gateway'), - 'type' => 'checkbox', - 'description' => __('Check this box if you are running on an Onion Service (Suppress SSL errors)', 'monero_gateway'), - 'default' => 'no' - ), - ); - } - - public function add_my_currency($currencies) - { - $currencies['XMR'] = __('Monero', 'woocommerce'); - return $currencies; - } - - public function add_my_currency_symbol($currency_symbol, $currency) - { - switch ($currency) { - case 'XMR': - $currency_symbol = 'XMR'; - break; - } - return $currency_symbol; - } - - public function admin_options() - { - $this->log->add('Monero_gateway', '[SUCCESS] Monero Settings OK'); - echo "

Monero Payment Gateway

"; - - echo "

Welcome to Monero Extension for WooCommerce. Getting started: Make a connection with daemon Contact Me"; - echo "

"; - - if(!$this->non_rpc) // only try to get balance data if using wallet-rpc - $this->getamountinfo(); - - echo "
"; - echo "
"; - $this->generate_settings_html(); - echo "
"; - echo "

Learn more about using monero-wallet-rpc here and viewkeys here

"; - } - - public function getamountinfo() - { - $wallet_amount = $this->monero_daemon->getbalance(); - if (!isset($wallet_amount)) { - $this->log->add('Monero_gateway', '[ERROR] Cannot connect to monero-wallet-rpc'); - echo "
Your balance is: Not Available
"; - echo "Unlocked balance: Not Available"; - } - else - { - $real_wallet_amount = $wallet_amount['balance'] / 1000000000000; - $real_amount_rounded = round($real_wallet_amount, 6); - - $unlocked_wallet_amount = $wallet_amount['unlocked_balance'] / 1000000000000; - $unlocked_amount_rounded = round($unlocked_wallet_amount, 6); - - echo "Your balance is: " . $real_amount_rounded . " XMR
"; - echo "Unlocked balance: " . $unlocked_amount_rounded . " XMR
"; - } - } - - public function process_payment($order_id) - { - $order = wc_get_order($order_id); - $order->update_status('on-hold', __('Awaiting offline payment', 'monero_gateway')); - // Reduce stock levels - $order->reduce_order_stock(); - - // Remove cart - WC()->cart->empty_cart(); - - // Return thank you redirect - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url($order) - ); - - } - - // Submit payment and handle response - - public function validate_fields() - { - if ($this->check_monero() != TRUE) { - echo "

Your Monero Address doesn't look valid. Have you checked it?

"; - } - if(!$this->check_viewKey()) - { - echo "

Your ViewKey doesn't look valid. Have you checked it?

"; - } - if($this->check_checkedBoxes()) - { - echo "

You must choose to either use monero-wallet-rpc or a ViewKey, not both

"; - } - - } - - - // Validate fields - - public function check_monero() - { - $monero_address = $this->settings['monero_address']; - if (strlen($monero_address) == 95 && substr($monero_address, 1)) - { - if($this->cryptonote->verify_checksum($monero_address)) - { - return true; - } - } - return false; - } - public function check_viewKey() - { - if($this->use_viewKey == 'yes') - { - if (strlen($this->viewKey) == 64) { - return true; - } - return false; - } - return true; - } - public function check_checkedBoxes() - { - if($this->use_viewKey == 'yes') - { - if($this->use_rpc == 'yes') - { - return true; - } - } - else - return false; - } - - public function is_virtual_in_cart($order_id) - { - $order = wc_get_order( $order_id ); - $items = $order->get_items(); - $cart_size = count($items); - $virtual_items = 0; - - foreach ( $items as $item ) { - $product = new WC_Product( $item['product_id'] ); - if ( $product->is_virtual() ) { - $virtual_items += 1; - } - } - if($virtual_items == $cart_size) - { - return true; - } - else{ - return false; - } - } - - public function instruction($order_id) - { - if($this->non_rpc) - { - echo ""; - $order = wc_get_order($order_id); - $amount = floatval(preg_replace('#[^\d.]#', '', $order->get_total())); - $payment_id = $this->set_paymentid_cookie(8); - $currency = $order->get_currency(); - $amount_xmr2 = $this->changeto($amount, $currency, $payment_id); - $address = $this->address; - - $order->update_meta_data( "Payment ID", $payment_id); - $order->update_meta_data( "Amount requested (XMR)", $amount_xmr2); - $order->save(); - - if (!isset($address)) { - // If there isn't address (merchant missed that field!), $address will be the Monero address for donating :) - $address = "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A"; - } - - - - $decoded_address = $this->cryptonote->decode_address($address); - $pub_spendKey = $decoded_address['spendKey']; - $pub_viewKey = $decoded_address['viewKey']; - - $integrated_addr = $this->cryptonote->integrated_addr_from_keys($pub_spendKey, $pub_viewKey, $payment_id); - - $uri = urlencode("monero:".$address."?tx_amount=".$amount_xmr2."&tx_payment_id=".$payment_id); - $this->verify_non_rpc($payment_id, $amount_xmr2, $order_id, $this->zero_confirm); - if($this->confirmed == false) - { - echo "

We are waiting for your transaction to be confirmed

"; - } - if($this->confirmed) - { - echo "

Your transaction has been successfully confirmed!

"; - } - - echo " - - - - - - - - - - -
- -
- -
- -

MONERO PAYMENT

-
- - -
-
- Send: -
".$amount_xmr2."
-
-
- To this address: -
".$integrated_addr."
-
-
- Or scan QR: -
-
-
-
- - - - -
- -
- - - "; - - echo " - "; - } - else - { - $order = wc_get_order($order_id); - $amount = floatval(preg_replace('#[^\d.]#', '', $order->get_total())); - $payment_id = $this->set_paymentid_cookie(8); - $currency = $order->get_currency(); - $amount_xmr2 = $this->changeto($amount, $currency, $payment_id); - - $order->update_meta_data( "Payment ID", $payment_id); - $order->update_meta_data( "Amount requested (XMR)", $amount_xmr2); - $order->save(); - - $uri = urlencode("monero:".$address."?tx_amount=".$amount_xmr2."&tx_payment_id=".$payment_id); - $array_integrated_address = $this->monero_daemon->make_integrated_address($payment_id); - if (!isset($array_integrated_address)) { - $this->log->add('Monero_Gateway', '[ERROR] Unable get integrated address'); - // Seems that we can't connect with daemon, then set array_integrated_address, little hack - $array_integrated_address["integrated_address"] = $address; - } - $message = $this->verify_payment($payment_id, $amount_xmr2, $order); - if ($this->confirmed) { - $color = "006400"; - } else { - $color = "DC143C"; - } - echo "

" . $message . "

"; - - echo " - - - - - - - - - - -
- -
- -
- -

MONERO PAYMENT

-
- - -
-
- Send: -
".$amount_xmr2."
-
-
- To this address: -
".$array_integrated_address['integrated_address']."
-
-
- Or scan QR: -
-
-
-
- - - - -
- -
- - - "; - - echo " - "; - } - } - - private function set_paymentid_cookie($size) - { - if (!isset($_COOKIE['payment_id'])) { - $payment_id = bin2hex(openssl_random_pseudo_bytes($size)); - setcookie('payment_id', $payment_id, time() + 2700); - } - else{ - $payment_id = $this->sanatize_id($_COOKIE['payment_id']); - } - return $payment_id; - } - - public function sanatize_id($payment_id) - { - // Limit payment id to alphanumeric characters - $sanatized_id = preg_replace("/[^a-zA-Z0-9]+/", "", $payment_id); - return $sanatized_id; - } - - public function changeto($amount, $currency, $payment_id) - { - global $wpdb; - // This will create a table named whatever the payment id is inside the database "WordPress" - $create_table = "CREATE TABLE IF NOT EXISTS $payment_id ( - rate INT - )"; - $wpdb->query($create_table); - $rows_num = $wpdb->get_results("SELECT count(*) as count FROM $payment_id"); - if ($rows_num[0]->count > 0) // Checks if the row has already been created or not - { - $stored_rate = $wpdb->get_results("SELECT rate FROM $payment_id"); - - $stored_rate_transformed = $stored_rate[0]->rate / 100; //this will turn the stored rate back into a decimaled number - - if (isset($this->discount)) { - $sanatized_discount = preg_replace('/[^0-9]/', '', $this->discount); - $discount_decimal = $sanatized_discount / 100; - $new_amount = $amount / $stored_rate_transformed; - $discount = $new_amount * $discount_decimal; - $final_amount = $new_amount - $discount; - $rounded_amount = round($final_amount, 12); - } else { - $new_amount = $amount / $stored_rate_transformed; - $rounded_amount = round($new_amount, 12); //the Monero wallet can't handle decimals smaller than 0.000000000001 - } - } else // If the row has not been created then the live exchange rate will be grabbed and stored - { - $xmr_live_price = $this->retriveprice($currency); - $live_for_storing = $xmr_live_price * 100; //This will remove the decimal so that it can easily be stored as an integer - - $wpdb->query("INSERT INTO $payment_id (rate) VALUES ($live_for_storing)"); - if(isset($this->discount)) - { - $new_amount = $amount / $xmr_live_price; - $discount = $new_amount * $this->discount / 100; - $discounted_price = $new_amount - $discount; - $rounded_amount = round($discounted_price, 12); - } - else - { - $new_amount = $amount / $xmr_live_price; - $rounded_amount = round($new_amount, 12); - } - } - - return $rounded_amount; - } - - - // Check if we are forcing SSL on checkout pages - // Custom function not required by the Gateway - - public function retriveprice($currency) - { - $api_link = 'https://min-api.cryptocompare.com/data/price?fsym=XMR&tsyms=BTC,USD,EUR,CAD,INR,GBP,COP,SGD' . ',' . $currency . '&extraParams=monero_woocommerce'; - $xmr_price = file_get_contents($api_link); - $price = json_decode($xmr_price, TRUE); - if (!isset($price)) { - $this->log->add('Monero_Gateway', '[ERROR] Unable to get the price of Monero'); - } - switch ($currency) { - case 'USD': - return $price['USD']; - case 'EUR': - return $price['EUR']; - case 'CAD': - return $price['CAD']; - case 'GBP': - return $price['GBP']; - case 'INR': - return $price['INR']; - case 'COP': - return $price['COP']; - case 'SGD': - return $price['SGD']; - case $currency: - return $price[$currency]; - case 'XMR': - $price = '1'; - return $price; - } - } - - private function on_verified($payment_id, $amount_atomic_units, $order_id) - { - $message = "Payment has been received and confirmed. Thanks!"; - $this->log->add('Monero_gateway', '[SUCCESS] Payment has been recorded. Congratulations!'); - $this->confirmed = true; - $order = wc_get_order($order_id); - - if($this->is_virtual_in_cart($order_id) == true){ - $order->update_status('completed', __('Payment has been received.', 'monero_gateway')); - } - else{ - $order->update_status('processing', __('Payment has been received.', 'monero_gateway')); // Show payment id used for order - } - global $wpdb; - $wpdb->query("DROP TABLE $payment_id"); // Drop the table from database after payment has been confirmed as it is no longer needed - - $this->reloadTime = 3000000000000; // Greatly increase the reload time as it is no longer needed - return $message; - } - - public function verify_payment($payment_id, $amount, $order_id) - { - /* - * function for verifying payments - * Check if a payment has been made with this payment id then notify the merchant - */ - $message = "We are waiting for your payment to be confirmed"; - $amount_atomic_units = $amount * 1000000000000; - $get_payments_method = $this->monero_daemon->get_payments($payment_id); - if (isset($get_payments_method["payments"][0]["amount"])) { - if ($get_payments_method["payments"][0]["amount"] >= $amount_atomic_units) - { - $message = $this->on_verified($payment_id, $amount_atomic_units, $order_id); - } - if ($get_payments_method["payments"][0]["amount"] < $amount_atomic_units) - { - $totalPayed = $get_payments_method["payments"][0]["amount"]; - $outputs_count = count($get_payments_method["payments"]); // number of outputs recieved with this payment id - $output_counter = 1; - - while($output_counter < $outputs_count) - { - $totalPayed += $get_payments_method["payments"][$output_counter]["amount"]; - $output_counter++; - } - if($totalPayed >= $amount_atomic_units) - { - $message = $this->on_verified($payment_id, $amount_atomic_units, $order_id); - } - } - } - return $message; - } - public function last_block_seen($height) // sometimes 2 blocks are mined within a few seconds of each other. Make sure we don't miss one - { - if (!isset($_COOKIE['last_seen_block'])) - { - setcookie('last_seen_block', $height, time() + 2700); - return 0; - } - else{ - $cookie_block = $_COOKIE['last_seen_block']; - $difference = $height - $cookie_block; - setcookie('last_seen_block', $height, time() + 2700); - return $difference; - } - } - - public function verify_non_rpc($payment_id, $amount, $order_id, $accept_zero_conf = false) - { - $tools = new NodeTools($this->testnet); - - $amount_atomic_units = $amount * 1000000000000; - - $outputs = $tools->get_outputs($this->address, $this->viewKey, $accept_zero_conf); - $outs_count = count($outputs); - - $i = 0; - $tx_hash; - if($outs_count != 0) - { - while($i < $outs_count ) - { - if($outputs[$i]['payment_id'] == $payment_id) - { - if($outputs[$i]['amount'] >= $amount_atomic_units) - { - $this->on_verified($payment_id, $amount_atomic_units, $order_id); - return true; - } - } - $i++; - } - } - return false; - } - - public function do_ssl_check() - { - if ($this->enabled == "yes" && !$this->get_option('onion_service')) { - if (get_option('woocommerce_force_ssl_checkout') == "no") { - echo "

" . sprintf(__("%s is enabled and WooCommerce is not forcing the SSL certificate on your checkout page. Please ensure that you have a valid SSL certificate and that you are forcing the checkout pages to be secured."), $this->method_title, admin_url('admin.php?page=wc-settings&tab=checkout')) . "

"; - } - } - } - - public function connect_daemon() - { - $host = $this->settings['daemon_host']; - $port = $this->settings['daemon_port']; - $monero_library = new Monero($host, $port); - if ($monero_library->works() == true) { - echo "

Everything works! Congratulations and welcome to Monero.

"; - - } else { - $this->log->add('Monero_gateway', '[ERROR] Plugin cannot reach wallet RPC.'); - echo "

Error with connection of daemon, see documentation!

"; - } - } -} diff --git a/monero/monero_gateway.php b/monero/monero_gateway.php deleted file mode 100644 index 35e3a04..0000000 --- a/monero/monero_gateway.php +++ /dev/null @@ -1,65 +0,0 @@ -' . __('Settings', 'monero_payment') . '', - ); - - return array_merge($plugin_links, $links); -} - -add_action('admin_menu', 'monero_create_menu'); -function monero_create_menu() -{ - add_menu_page( - __('Monero', 'textdomain'), - 'Monero', - 'manage_options', - 'admin.php?page=wc-settings&tab=checkout§ion=monero_gateway', - '', - plugins_url('monero/assets/monero_icon.png'), - 56 // Position on menu, woocommerce has 55.5, products has 55.6 - - ); -} - - diff --git a/readme.txt b/readme.txt index 2c71579..ee5ac56 100644 --- a/readme.txt +++ b/readme.txt @@ -1,9 +1,9 @@ === Monero WooCommerce Extension === -Contributors: serhack +Contributors: serhack, mosu-forge Donate link: http://monerointegrations.com/donate.html Tags: monero, woocommerce, integration, payment, merchant, cryptocurrency, accept monero, monero woocommerce Requires at least: 4.0 -Tested up to: 4.8 +Tested up to: 4.9.8 Stable tag: trunk License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -50,6 +50,9 @@ An extension to WooCommerce for accepting Monero as payment in your store. = 0.2 = * Bug fixes += 0.3 = +* Complete rewrite of how the plugin handles payments + == Upgrade Notice == soon diff --git a/templates/monero-gateway/admin/order-history-error-page.php b/templates/monero-gateway/admin/order-history-error-page.php new file mode 100644 index 0000000..0017f52 --- /dev/null +++ b/templates/monero-gateway/admin/order-history-error-page.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/monero-gateway/admin/order-history-page.php b/templates/monero-gateway/admin/order-history-page.php new file mode 100644 index 0000000..cfdb47a --- /dev/null +++ b/templates/monero-gateway/admin/order-history-page.php @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Exchange rate1 XMR =
Total amount XMR
Total paid XMR
Total due XMR
Order age ago
Order exipires + +
Status + Confirmed'; + break; + case 'paid': + echo 'Paid, waiting confirmation'; + break; + case 'partial': + echo 'Partial payment made'; + break; + case 'unpaid': + echo 'Pending payment'; + break; + case 'expired_partial': + echo 'Expired, partial payment made'; + break; + case 'expired': + echo 'Expired'; + break; + } + ?> +
Payment id
Integrated address
+ + + + + + + + + + + + + + + +
TransactionsHeightAmount
+ + XMR
+ diff --git a/templates/monero-gateway/admin/settings-page.php b/templates/monero-gateway/admin/settings-page.php new file mode 100644 index 0000000..698234a --- /dev/null +++ b/templates/monero-gateway/admin/settings-page.php @@ -0,0 +1,54 @@ + +

Monero Gateway Error:

+ + +

Monero Gateway Settings

+ + +
+ '; + echo 'Your balance is: ' . $balance['balance'] . '
'; + echo 'Unlocked balance: ' . $balance['unlocked_balance'] . '
'; + ?> +
+ + + + +
+ +

Learn more about using the Monero payment gateway

+ + + + \ No newline at end of file diff --git a/templates/monero-gateway/customer/order-email-block.php b/templates/monero-gateway/customer/order-email-block.php new file mode 100644 index 0000000..57b1e85 --- /dev/null +++ b/templates/monero-gateway/customer/order-email-block.php @@ -0,0 +1,56 @@ + + +

+ +

+ +

Your order has been confirmed. Thank you for paying with Monero!

+ + + +

+ +

+ +

Your order has expired. Please place another order to complete your purchase.

+ + + +

+ +

+ +

Please pay the amount due to complete your transactions. Your order will expire in if payment is not received.

+ +
+ + + + + + + + + + + + +
+ PAY TO:
+ + + +
+ TOTAL DUE:
+ + XMR + +
+ EXCHANGE RATE:
+ + 1 XMR = + +
+
+ + \ No newline at end of file diff --git a/templates/monero-gateway/customer/order-email-error-block.php b/templates/monero-gateway/customer/order-email-error-block.php new file mode 100644 index 0000000..f07b613 --- /dev/null +++ b/templates/monero-gateway/customer/order-email-error-block.php @@ -0,0 +1,5 @@ +

+ +

+ +

Payment method not available, please contact the store owner for manual payment

diff --git a/templates/monero-gateway/customer/order-error-page.php b/templates/monero-gateway/customer/order-error-page.php new file mode 100644 index 0000000..2d62da2 --- /dev/null +++ b/templates/monero-gateway/customer/order-error-page.php @@ -0,0 +1,4 @@ +
+

+

Payment method not available, please contact the store owner for manual payment

+
\ No newline at end of file diff --git a/templates/monero-gateway/customer/order-page.php b/templates/monero-gateway/customer/order-page.php new file mode 100644 index 0000000..8e809a2 --- /dev/null +++ b/templates/monero-gateway/customer/order-page.php @@ -0,0 +1,100 @@ +
+

+ + + + + Please pay the amount due to complete your transactions. Your order will expire in if payment is not received. + + We have received partial payment. Please pay the remaining amount to complete your transactions. Your order will expire in if payment is not received. + + We have received your payment in full. Please wait while amount is confirmed. Approximate confirm time is .
You can check your payment status anytime in your account dashboard.
+ + Your order has been confirmed. Thank you for paying with Monero! + + Your order has expired. Please place another order to complete your purchase. + + Your order has expired. Please contact the store owner to receive refund on your partial payment. + +
+ +
    +
  • + Pay to: + + + + + + + + + +
  • +
  • + Total due: + + + XMR + + + + + +
  • +
  • + Total order amount: + + XMR + +
  • +
  • + Total paid: + + XMR + +
  • +
  • + Exchange rate: +
  • +
+ + + + + + + + + + + + + + + + + + +
+ +
+ +