DERO Payment Gateway Plugin for WooCommerce
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

450 lines
25KB

  1. <?php
  2. /**
  3. * Plugin Name: DERO WooCommerce Gateway
  4. * Plugin URI: https://github.com/Peppinux/dero-woocommerce-gateway
  5. * Description: DERO Payment Gateway for WooCommerce
  6. * Version: 1.0.0
  7. * Author: Peppinux
  8. * Author URI: https://github.com/Peppinux
  9. * License: MIT
  10. */
  11. defined('ABSPATH') || exit;
  12. define('DERO_GATEWAY_PLUGIN_DIR', plugin_dir_path(__FILE__));
  13. define('DERO_GATEWAY_PLUGIN_URL', plugin_dir_url(__FILE__));
  14. add_action('plugins_loaded', 'dero_gateway_init');
  15. require_once(DERO_GATEWAY_PLUGIN_DIR . '/lib/dero-wallet-rpc.php');
  16. require_once(DERO_GATEWAY_PLUGIN_DIR . '/lib/coingecko-api.php');
  17. require_once(DERO_GATEWAY_PLUGIN_DIR . '/lib/util/format-time.php');
  18. function dero_gateway_init() {
  19. if(!class_exists('WC_Payment_Gateway'))
  20. return;
  21. class DERO_Gateway extends WC_Payment_Gateway {
  22. function __construct() {
  23. $this->id = 'dero_gateway';
  24. $this->icon = apply_filters('woocommerce_gateway_icon', DERO_GATEWAY_PLUGIN_URL . '/assets/img/dero-icon.png');
  25. $this->has_fields = false;
  26. $this->method_title = __('DERO Gateway', 'dero_gateway');
  27. $this->method_description = __('DERO Payment Gateway for WooCommerce', 'dero_gateway');
  28. $this->has_fields = false;
  29. $this->init_form_fields();
  30. $this->init_settings();
  31. $this->enabled = $this->get_option('enabled');
  32. $this->title = $this->get_option('title');
  33. $this->description = $this->get_option('description');
  34. $this->discount = $this->get_option('discount');
  35. $this->order_valid_time = $this->get_option('order_valid_time');
  36. $this->confirmations = $this->get_option('confirmations');
  37. $this->wallet_host = $this->get_option('wallet_host');
  38. $this->wallet_port = $this->get_option('wallet_port');
  39. $this->wallet_login_required = $this->get_option('wallet_login_required');
  40. $this->wallet_username = $this->get_option('wallet_username');
  41. $this->wallet_password = $this->get_option('wallet_password');
  42. DERO_Wallet_RPC::setup($this->wallet_host, $this->wallet_port, $this->wallet_login_required, $this->wallet_username, $this->wallet_password);
  43. add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
  44. add_action('woocommerce_thankyou_' . $this->id, array($this, 'thankyou_page'));
  45. add_action('woocommerce_email_before_order_table', array($this, 'email_instructions'), 10, 3);
  46. }
  47. public function init_form_fields() {
  48. $this->form_fields = array(
  49. 'enabled' => array(
  50. 'title' => __('Enable/Disable', 'dero_gateway'),
  51. 'type' => 'checkbox',
  52. 'label' => __('Enable DERO Gateway Payment', 'dero_gateway'),
  53. 'default' => 'no'
  54. ),
  55. 'title' => array(
  56. 'title' => __('Title', 'dero_gateway'),
  57. 'type' => 'text',
  58. 'description' => __('Payment title which the user sees during checkout.', 'dero_gateway'),
  59. 'default' => __('DERO Payment Gateway', 'dero_gateway'),
  60. 'desc_tip' => true
  61. ),
  62. 'description' => array(
  63. 'title' => __('Description', 'dero_gateway'),
  64. 'type' => 'text',
  65. 'description' => __('Payment description which the user sees during checkout.', 'dero_gateway'),
  66. 'default' => __('Pay securely and privately using DERO. Payment details will be provided after checkout.', 'dero_gateway'),
  67. 'desc_tip' => true
  68. ),
  69. 'discount' => array(
  70. 'title' => __('Discount for using DERO', 'dero_gateway'),
  71. 'type' => __('number'),
  72. 'description' => __('Enter a percentage discount (e.g., 5 for 5%. Whole numbers only.) or leave this field empty if you do not wish to provide a discount.', 'dero_gateway'),
  73. 'default' => '0',
  74. 'desc_tip' => __('Discount for making a payment with DERO', 'dero_gateway')
  75. ),
  76. 'order_valid_time' => array(
  77. 'title' => __('Order valid time', 'dero_gateway'),
  78. 'type' => __('number'),
  79. 'description' => __('Enter the number of seconds that the funds must be received in after the order is placed (e.g., 3600 = 1 hour).', 'dero_gateway'),
  80. 'default' => '3600',
  81. 'desc_tip' => __('Amount of time the funds must be received in after the order is placed.', 'dero_gateway')
  82. ),
  83. 'confirmations' => array(
  84. 'title' => __('Number of confirmations', 'dero_gateway'),
  85. 'type' => __('number'),
  86. 'description' => __('Enter the number of confirmations that the transaction must have to be valid. Each confirmation should take around 12 seconds. 10 confirmations = 2 minutes.', 'dero_gateway'),
  87. 'default' => '10',
  88. 'desc_tip' => __('Number of confirmations that the transaction must have to be valid.', 'dero_gateway')
  89. ),
  90. 'wallet_host' => array(
  91. 'title' => __('DERO Wallet RPC Hostname/IP Address', 'dero_gateway'),
  92. 'type' => 'text',
  93. 'description' => __('Wallet RPC host used to connect to the wallet in order to verify transactions.', 'dero_gateway'),
  94. 'default' => __('127.0.0.1', 'dero_gateway'),
  95. 'desc_tip' => true
  96. ),
  97. 'wallet_port' => array(
  98. 'title' => __('DERO Wallet RPC Port', 'dero_gateway'),
  99. 'type' => __('number'),
  100. 'description' => __('Wallet RPC port used to connect to the wallet in order to verify transactions.', 'dero_gateway'),
  101. 'default' => 20209,
  102. 'desc_tip' => true
  103. ),
  104. 'wallet_login_required' => array(
  105. 'title' => __('Wallet RPC requires Login', 'dero_gateway'),
  106. 'type' => 'checkbox',
  107. 'label' => __('Enable Wallet RPC Login', 'dero_gateway'),
  108. 'default' => 'no'
  109. ),
  110. 'wallet_username' => array(
  111. 'title' => __('DERO Wallet RPC Username', 'dero_gateway'),
  112. 'type' => 'text',
  113. 'description' => __('Optional. Enter username only if wallet requires RPC Login and previous checkbox is checked.', 'dero_gateway'),
  114. 'default' => __('', 'dero_gateway'),
  115. 'desc_tip' => __('Wallet RPC username used to connect to the wallet in order to verify transactions.', 'dero_gateway')
  116. ),
  117. 'wallet_password' => array(
  118. 'title' => __('DERO Wallet RPC Password', 'dero_gateway'),
  119. 'type' => 'text',
  120. 'description' => __('Optional. Enter password only if wallet requires RPC Login and previous checkbox is checked.', 'dero_gateway'),
  121. 'default' => __('', 'dero_gateway'),
  122. 'desc_tip' => __('Wallet RPC password used to connect to the wallet in order to verify transactions.', 'dero_gateway')
  123. )
  124. );
  125. }
  126. public function process_payment($order_id) {
  127. global $woocommerce;
  128. $order = new WC_Order($order_id);
  129. $current_height = DERO_Wallet_RPC::get_height();
  130. if(!is_int($current_height) || !($current_height >= 0)) {
  131. $error_message = 'Could not get chain height.';
  132. wc_add_notice(__('DERO Payment Gateway error: ', 'dero_gateway') . $error_message, 'error');
  133. return;
  134. }
  135. global $wpdb;
  136. $table_name = $wpdb->prefix . 'dero_gateway_payments';
  137. $payment_id = '';
  138. do {
  139. $payment_id = bin2hex(openssl_random_pseudo_bytes(32));
  140. $query = $wpdb->prepare("SELECT COUNT(*) FROM $table_name WHERE payment_id=%s", $payment_id);
  141. $payment_id_found = $wpdb->get_var($query);
  142. } while($payment_id_found);
  143. $integrated_address = DERO_Wallet_RPC::make_integrated_address($payment_id);
  144. if(!is_string($integrated_address) || strlen($integrated_address) != 142) {
  145. $error_message = 'Could not make integrated address.';
  146. wc_add_notice(__('DERO Payment Gateway error: ', 'dero_gateway') . $error_message, 'error');
  147. return;
  148. }
  149. $currency = $order->get_currency();
  150. $supported_currencies = CoinGecko_API::get_supported_currencies();
  151. $exchange_rate = 0;
  152. if(in_array(strtolower($currency), $supported_currencies) || in_array(strtoupper($currency), $supported_currencies))
  153. $exchange_rate = CoinGecko_API::get_dero_exchange_rate($currency);
  154. else {
  155. $error_message = 'Currency ' . $currency . ' not supported by CoinGecko API.';
  156. wc_add_notice(__('DERO Payment Gateway error: ', 'dero_gateway') . $error_message, 'error');
  157. return;
  158. }
  159. $fiat_total = $order->get_total();
  160. $dero_total = $fiat_total / $exchange_rate;
  161. $discount = 0;
  162. if($this->discount > 0 && $this->discount <= 100) {
  163. $discount = $this->discount;
  164. $dero_total = $dero_total - ($dero_total * $discount / 100);
  165. }
  166. $prepared_statement_params = array(
  167. $order_id,
  168. $currency,
  169. $exchange_rate,
  170. $fiat_total,
  171. $discount,
  172. $dero_total
  173. );
  174. $query = $wpdb->prepare("INSERT INTO $table_name (order_id, currency, exchange_rate, fiat_total, discount_percentage, dero_total) VALUES (%d, %s, %f, %f, %d, %f)", $prepared_statement_params);
  175. $wpdb->query($query);
  176. $prepared_statement_params = array(
  177. $payment_id,
  178. $integrated_address,
  179. $current_height,
  180. $order_id
  181. );
  182. $query = $wpdb->prepare("UPDATE $table_name SET payment_id=%s, integrated_address=%s, status='on-hold', creation_time=NOW(), height_at_creation=%d WHERE order_id=%d", $prepared_statement_params); // This separated query is needed for order re-paying. The previous INSERT INTO wouldn't update the values of an already existing order.
  183. $wpdb->query($query);
  184. $order->update_status('on-hold', __('Awaiting DERO payment', 'dero_gateway'));
  185. $order->reduce_order_stock();
  186. $woocommerce->cart->empty_cart();
  187. return array(
  188. 'result' => 'success',
  189. 'redirect' => $this->get_return_url($order)
  190. );
  191. }
  192. public function thankyou_page($order_id) {
  193. global $wpdb;
  194. $table_name = $wpdb->prefix . 'dero_gateway_payments';
  195. $query = $wpdb->prepare("SELECT integrated_address, currency, exchange_rate, fiat_total, discount_percentage, dero_total, status, received_payment_txid, TIMESTAMPDIFF(SECOND, creation_time, NOW()) as seconds_passed FROM $table_name WHERE order_id=%d", $order_id);
  196. $result = $wpdb->get_results($query)[0];
  197. $status = ucfirst($result->status);
  198. if($result->status == 'on-hold' || $result->status == 'pending') {
  199. $remaining_seconds = $this->order_valid_time - $result->seconds_passed;
  200. $time_format = format_time($remaining_seconds);
  201. if($remaining_seconds > 0)
  202. $status = "Awaiting payment. Your order will expire in <i>$time_format</i> if payment is not received. Refresh this page to get updated status.";
  203. else
  204. $status = "Your order is about to expire. Place another one to complete your purchase.";
  205. } else if($result->status == 'expired')
  206. $status = 'Expired. Place another order to complete your purchase.';
  207. $discount_section = "";
  208. if((int)$result->discount_percentage > 0)
  209. $discount_section = "<li class='woocommerce-order-overview__order order'>
  210. Discount for paying with DERO: <strong>$result->discount_percentage%</strong>
  211. </li>";
  212. $integrated_address_section = "";
  213. if($result->status == 'on-hold' || $result->status == 'pending')
  214. $integrated_address_section = "<li class='woocommerce-order-overview__order order'>
  215. <span class='detail-title'>Pay to (integrated address):</span> <span class='detail-content'><strong><span id='dero-integrated-address'>$result->integrated_address</span></strong></span>
  216. <button class='clipboard-button' title='Copy to clipboard' data-clipboard-target='#dero-integrated-address'><svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 512 512' version='1'><path d='M504 118c-6-6-12-8-20-8H365c-11 0-23 3-36 11V27c0-7-3-14-8-19s-12-8-20-8H183c-8 0-16 2-25 6-10 4-17 8-22 13L19 136c-5 5-9 12-13 22-4 9-6 17-6 25v192c0 7 3 14 8 19s12 8 19 8h156v82c0 8 2 14 8 20 5 5 12 8 19 8h274c8 0 14-3 20-8 5-6 8-12 8-20V137c0-8-3-14-8-19zm-175 52v86h-85l85-86zM146 61v85H61l85-85zm56 185c-5 5-10 12-14 21-3 9-5 18-5 25v73H37V183h118c8 0 14-3 20-8 5-6 8-12 8-20V37h109v118l-90 91zm273 229H219V292h119c8 0 14-2 19-8 6-5 8-11 8-19V146h110v329z'/></svg></button>
  217. <button class='qrcode-button' title='Show QR Code' onclick='toggleQRCode()'><svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 512 512' version='1'><path d='M0 512h233V279H0zm47-186h139v139H47z'/><path d='M93 372h47v47H93zm279 93h47v47h-47zm93 0h47v47h-47z'/><path d='M465 326h-46v-47H279v233h47V372h46v47h140V279h-47zM0 233h233V0H0zM47 47h139v139H47z'/><path d='M93 93h47v47H93zM279 0v233h233V0zm186 186H326V47h139z'/><path d='M372 93h47v47h-47z'/></svg></button>
  218. <div id='address-qrcode'></div>
  219. </li>";
  220. $txid_section = "";
  221. if($result->received_payment_txid != null)
  222. $txid_section = "<li class='woocommerce-order-overview__order order'>
  223. Payment TXID: <strong>$result->received_payment_txid</strong>
  224. </li>";
  225. $instructions = "<style>
  226. .detail-title {
  227. display: block;
  228. }
  229. .detail-content {
  230. display: inline-block;
  231. word-break: break-word;
  232. }
  233. .clipboard-button, .qrcode-button {
  234. display: inline-block;
  235. margin-left: 10px;
  236. font-size: 0.5em;
  237. border-radius: 10%;
  238. }
  239. #address-qrcode {
  240. display: none;
  241. margin-top: 20px;
  242. }
  243. </style>
  244. <section class='woocommerce-order-details'>
  245. <h2 class='woocommerce-order-details__title'>DERO Payment Details</h2>
  246. <ul class='woocommerce-order-overview woocommerce-thankyou-order-details order_details'>
  247. <li class='woocommerce-order-overview__order order'>
  248. Status: <strong>$status</strong>
  249. </li>
  250. <li class='woocommerce-order-overview__order order'>
  251. Fiat total: <strong>$result->fiat_total $result->currency</strong>
  252. </li>
  253. <li class='woocommerce-order-overview__order order'>
  254. Exchange rate: <strong>1 DERO = $result->exchange_rate $result->currency</strong>
  255. </li>
  256. $discount_section
  257. <li class='woocommerce-order-overview__order order'>
  258. <span class='detail-title'>Total:</span> <span class='detail-content'><strong><span id='dero-total'>$result->dero_total</span> DERO</strong></span>
  259. <button class='clipboard-button' title='Copy to clipboard' data-clipboard-target='#dero-total'><svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 512 512' version='1'><path d='M504 118c-6-6-12-8-20-8H365c-11 0-23 3-36 11V27c0-7-3-14-8-19s-12-8-20-8H183c-8 0-16 2-25 6-10 4-17 8-22 13L19 136c-5 5-9 12-13 22-4 9-6 17-6 25v192c0 7 3 14 8 19s12 8 19 8h156v82c0 8 2 14 8 20 5 5 12 8 19 8h274c8 0 14-3 20-8 5-6 8-12 8-20V137c0-8-3-14-8-19zm-175 52v86h-85l85-86zM146 61v85H61l85-85zm56 185c-5 5-10 12-14 21-3 9-5 18-5 25v73H37V183h118c8 0 14-3 20-8 5-6 8-12 8-20V37h109v118l-90 91zm273 229H219V292h119c8 0 14-2 19-8 6-5 8-11 8-19V146h110v329z'/></svg></button>
  260. </li>
  261. $integrated_address_section
  262. $txid_section
  263. </ul>
  264. </section>
  265. <script type='text/javascript'>
  266. jQuery(document).ready(function() {
  267. // Clipboard
  268. var clipboardButtons = document.querySelectorAll('button.clipboard-button');
  269. var clipboard = new ClipboardJS(clipboardButtons);
  270. clipboard.on('success', function(e) {
  271. console.log('Text copied to clipboard.');
  272. });
  273. clipboard.on('error', function(e) {
  274. console.log('Error occured while trying to copy text to clipboard.');
  275. });
  276. // QR Code
  277. new QRCode(document.getElementById('address-qrcode'), '$result->integrated_address');
  278. });
  279. function toggleQRCode() {
  280. jQuery('#address-qrcode').toggle();
  281. }
  282. </script>";
  283. echo $instructions;
  284. }
  285. public function email_instructions($order, $sent_to_admin, $plain_text = false) {
  286. if(!$sent_to_admin && $order->get_payment_method() === $this->id && $order->has_status('on-hold')) {
  287. global $wpdb;
  288. $table_name = $wpdb->prefix . 'dero_gateway_payments';
  289. $query = $wpdb->prepare("SELECT integrated_address, dero_total, TIMESTAMPDIFF(SECOND, creation_time, NOW()) as seconds_passed FROM $table_name WHERE order_id=%d", $order->get_id());
  290. $result = $wpdb->get_results($query)[0];
  291. $remaining_seconds = $this->order_valid_time - $result->seconds_passed;
  292. $time_format = format_time($remaining_seconds);
  293. echo wp_kses_post(wpautop(wptexturize('You need to send <strong>' . $result->dero_total . ' DERO</strong> to the following address within <strong>' . $time_format . '</strong> to complete your purchase.<br>DERO Payment Integrated Address: <strong>' . $result->integrated_address . '</strong><br>More details can be found in the order page.' . PHP_EOL)));
  294. }
  295. }
  296. }
  297. function dero_gateway($methods) {
  298. $methods[] = 'DERO_Gateway';
  299. return $methods;
  300. }
  301. add_filter('woocommerce_payment_gateways', 'dero_gateway');
  302. }
  303. function dero_cron_add_one_minute($schedules) {
  304. $schedules['one_minute'] = array(
  305. 'interval' => 60,
  306. 'display' => __('Once every minute')
  307. );
  308. return $schedules;
  309. }
  310. add_filter('cron_schedules', 'dero_cron_add_one_minute');
  311. function check_dero_payments() {
  312. global $wpdb;
  313. $table_name = $wpdb->prefix . 'dero_gateway_payments';
  314. $results = $wpdb->get_results("SELECT order_id, payment_id, dero_total, height_at_creation, TIMESTAMPDIFF(SECOND, creation_time, NOW()) as seconds_passed FROM $table_name WHERE status='on-hold' OR status='pending'");
  315. global $woocommerce;
  316. $wc_gateways = new WC_Payment_Gateways();
  317. $dero_gateway = $wc_gateways->get_available_payment_gateways()['dero_gateway'];
  318. $order_valid_time = $dero_gateway->get_option('order_valid_time');
  319. $required_confirmations = $dero_gateway->get_option('confirmations');
  320. foreach($results as $result) {
  321. $order = new WC_Order($result->order_id);
  322. if($result->seconds_passed > $order_valid_time) {
  323. if($order->get_status() == "on-hold" || $order->get_status() == "pending")
  324. $order->update_status('failed', __('Payment not received in time', 'dero_gateway'));
  325. $query = $wpdb->prepare("UPDATE $table_name SET status=%s WHERE order_id=%d", array('expired', $result->order_id));
  326. $wpdb->query($query);
  327. } else {
  328. $payments = DERO_Wallet_RPC::get_bulk_payments($result->payment_id, $result->height_at_creation);
  329. if($payments != null) {
  330. $dero_total = $result->dero_total * pow(10, 12); // Convert DERO got from the db from float to int in order to make it comparable to the value returned by wallet RPC.
  331. $payment = null;
  332. foreach($payments as $p) {
  333. if(json_encode($p['amount']) >= $dero_total) {
  334. $payment = $p;
  335. break;
  336. }
  337. }
  338. $txid = null;
  339. if($payment != null) {
  340. $payment_confirmations = DERO_Wallet_RPC::get_height() - $result->height_at_creation;
  341. $txid = $payment['tx_hash'];
  342. if($payment_confirmations >= $required_confirmations)
  343. $order->payment_complete();
  344. } else {
  345. if($order->get_status() == "on-hold" || $order->get_status() == "pending")
  346. $order->update_status('failed', __('Amount of DERO sent not matching with order total.', 'dero_gateway'));
  347. }
  348. $query = $wpdb->prepare("UPDATE $table_name SET status=%s, received_payment_txid=%s WHERE order_id=%d", array($order->get_status(), $txid, $result->order_id));
  349. $wpdb->query($query);
  350. }
  351. }
  352. }
  353. }
  354. add_action('check_dero_payments_cron', 'check_dero_payments');
  355. function dero_activate() {
  356. global $wpdb;
  357. require_once(ABSPATH . '/wp-admin/includes/upgrade.php');
  358. $charset_collate = $wpdb->get_charset_collate();
  359. $table_name = $wpdb->prefix . 'dero_gateway_payments';
  360. if($wpdb->get_var("show tables like $table_name") != $table_name) {
  361. $sql = "CREATE TABLE $table_name (
  362. order_id BIGINT UNSIGNED NOT NULL,
  363. payment_id VARCHAR(128) NOT NULL,
  364. integrated_address VARCHAR(256) NOT NULL,
  365. currency VARCHAR(8) NOT NULL,
  366. exchange_rate DOUBLE UNSIGNED NOT NULL,
  367. fiat_total DOUBLE UNSIGNED NOT NULL,
  368. discount_percentage TINYINT UNSIGNED NOT NULL,
  369. dero_total DOUBLE UNSIGNED NOT NULL,
  370. status VARCHAR(16) NOT NULL,
  371. height_at_creation BIGINT NOT NULL,
  372. creation_time TIMESTAMP NOT NULL DEFAULT NOW(),
  373. received_payment_txid VARCHAR(256) NULL DEFAULT NULL,
  374. PRIMARY KEY (order_id)
  375. ) $charset_collate;";
  376. dbDelta($sql);
  377. }
  378. if(!wp_next_scheduled('check_dero_payments_cron'))
  379. wp_schedule_event(time(), 'one_minute', 'check_dero_payments_cron');
  380. }
  381. register_activation_hook(__FILE__, 'dero_activate');
  382. function dero_deactivate() {
  383. wp_clear_scheduled_hook('check_dero_payments_cron');
  384. }
  385. register_deactivation_hook(__FILE__, 'dero_deactivate');
  386. function dero_enqueue_scripts() {
  387. wp_enqueue_script('dero-qrcode-js', DERO_GATEWAY_PLUGIN_URL . 'assets/js/qrcode.min.js');
  388. wp_enqueue_script('dero-clipboard-js', DERO_GATEWAY_PLUGIN_URL . 'assets/js/clipboard.min.js');
  389. }
  390. add_action('wp_enqueue_scripts', 'dero_enqueue_scripts');
  391. function dero_accepted_here_shortcode() {
  392. return '<img src="' . DERO_GATEWAY_PLUGIN_URL . 'assets/img/dero-accepted-here.png" />';
  393. }
  394. add_shortcode('dero-accepted-here', 'dero_accepted_here_shortcode');
  395. ?>