mirror of
https://git.wownero.com/wownero/wownerowp.git
synced 2024-08-15 01:03:16 +00:00
Rewrite plugin to v3.0
This commit is contained in:
parent
4c091a13f7
commit
4def1b771e
37 changed files with 4613 additions and 1685 deletions
133
include/admin/class-monero-admin-interface.php
Normal file
133
include/admin/class-monero-admin-interface.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
/*
|
||||
* Copyright (c) 2018, Ryo Currency Project
|
||||
* Admin interface for Monero gateway
|
||||
* Authors: mosu-forge
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
require_once('class-monero-admin-payments-list.php');
|
||||
|
||||
if (class_exists('Monero_Admin_Interface', false)) {
|
||||
return new Monero_Admin_Interface();
|
||||
}
|
||||
|
||||
class Monero_Admin_Interface {
|
||||
|
||||
public function __construct() {
|
||||
add_action('add_meta_boxes', array($this, 'meta_boxes'));
|
||||
add_action('admin_menu', array($this, 'admin_menu'));
|
||||
add_action('admin_head', array( $this, 'admin_menu_update'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*/
|
||||
public function meta_boxes() {
|
||||
add_meta_box(
|
||||
'monero_admin_order_details',
|
||||
__('Monero Gateway','monero_gateway'),
|
||||
array($this, 'meta_box_order_details'),
|
||||
'shop_order',
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Meta box for order page
|
||||
*/
|
||||
public function meta_box_order_details($order) {
|
||||
Monero_Gateway::admin_order_page($order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add menu items.
|
||||
*/
|
||||
public function admin_menu() {
|
||||
add_menu_page(
|
||||
__('Monero', 'monero_gateway'),
|
||||
__('Monero', 'monero_gateway'),
|
||||
'manage_woocommerce',
|
||||
'monero_gateway',
|
||||
array($this, 'orders_page'),
|
||||
MONERO_GATEWAY_PLUGIN_URL.'/assets/images/monero-icon-admin.png',
|
||||
56 // Position on menu, woocommerce has 55.5, products has 55.6
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'monero_gateway',
|
||||
__('Payments', 'monero_gateway'),
|
||||
__('Payments', 'monero_gateway'),
|
||||
'manage_woocommerce',
|
||||
'monero_gateway_payments',
|
||||
array($this, 'payments_page')
|
||||
);
|
||||
|
||||
$settings_page = add_submenu_page(
|
||||
'monero_gateway',
|
||||
__('Settings', 'monero_gateway'),
|
||||
__('Settings', 'monero_gateway'),
|
||||
'manage_options',
|
||||
'monero_gateway_settings',
|
||||
array($this, 'settings_page')
|
||||
);
|
||||
add_action('load-'.$settings_page, array($this, 'settings_page_init'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicate sub-menu item
|
||||
*/
|
||||
public function admin_menu_update() {
|
||||
global $submenu;
|
||||
if (isset($submenu['monero_gateway'])) {
|
||||
unset($submenu['monero_gateway'][0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monero payments page
|
||||
*/
|
||||
public function payments_page() {
|
||||
$payments_list = new Monero_Admin_Payments_List();
|
||||
$payments_list->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();
|
276
include/admin/class-monero-admin-payments-list.php
Normal file
276
include/admin/class-monero-admin-payments-list.php
Normal file
|
@ -0,0 +1,276 @@
|
|||
<?php
|
||||
/*
|
||||
* Copyright (c) 2018, Ryo Currency Project
|
||||
* Admin interface for Monero gateway
|
||||
* Authors: mosu-forge
|
||||
*/
|
||||
|
||||
if(!class_exists('WP_List_Table')) {
|
||||
require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php');
|
||||
}
|
||||
|
||||
class Monero_Admin_Payments_List extends WP_List_Table {
|
||||
|
||||
function __construct() {
|
||||
parent::__construct(array(
|
||||
'singular'=> '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 = <<<HTML
|
||||
<div style="border:1px solid #ddd;padding:5px 10px;">
|
||||
Wallet height: {$balance['height']}</br>
|
||||
Your balance is: {$balance['balance']}</br>
|
||||
Unlocked balance: {$balance['unlocked_balance']}</br>
|
||||
</div>
|
||||
|
||||
HTML;
|
||||
} else {
|
||||
$balance_info = '';
|
||||
}
|
||||
echo <<<HTML
|
||||
<div class="wrap">
|
||||
<h1 class="wp-heading-inline">Monero Payments</h1>
|
||||
$balance_info
|
||||
<hr class="wp-header-end">
|
||||
<ul class="subsubsub">
|
||||
<li>
|
||||
<a href="?page=monero_gateway_payments&type=all" {$tab_info['all']['active']}>
|
||||
All <span class="count">({$tab_info['all']['count']})</span>
|
||||
</a> |
|
||||
</li>
|
||||
<li style="display:none">
|
||||
<a href="?page=monero_gateway_payments&type=pending" {$tab_info['pending']['active']}>
|
||||
Pending <span class="count">({$tab_info['pending']['count']})</span>
|
||||
</a> |
|
||||
</li>
|
||||
<li>
|
||||
<a href="?page=monero_gateway_payments&type=paid" {$tab_info['paid']['active']}>
|
||||
Received <span class="count">({$tab_info['paid']['count']})</span>
|
||||
</a> |
|
||||
</li>
|
||||
<li>
|
||||
<a href="?page=monero_gateway_payments&type=confirmed" {$tab_info['confirmed']['active']}>
|
||||
Confirmed <span class="count">({$tab_info['confirmed']['count']})</span>
|
||||
</a> |
|
||||
</li>
|
||||
<li>
|
||||
<a href="?page=monero_gateway_payments&type=expired" {$tab_info['expired']['active']}>
|
||||
Expired <span class="count">({$tab_info['expired']['count']})</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form id="monero-payments-filter" method="get" style="display:none">
|
||||
<p class="search-box">
|
||||
<label class="screen-reader-text" for="post-search-input">Search payments:</label>
|
||||
<input type="search" id="post-search-input" name="s" value="">
|
||||
<input type="submit" id="search-submit" class="button" value="Search payments">
|
||||
</p>
|
||||
$hidden_fields
|
||||
</form>
|
||||
<h2 class="screen-reader-text">Monero Payments List</h2>
|
||||
<style>
|
||||
#col_order_id { width: 150px; }
|
||||
#col_payment_id { width: 150px; }
|
||||
#col_height { width: 100px; }
|
||||
#col_amount { width: 150px; }
|
||||
</style>
|
||||
<table class="wp-list-table widefat fixed striped posts">
|
||||
|
||||
HTML;
|
||||
} else if ($which == "bottom") {
|
||||
echo '<br class="clear"></div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 '<a href="'.$url.'" target="_blank">'.$item->txid.'</a>';
|
||||
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 '<a href="' . esc_url( admin_url( 'post.php?post=' . absint( $order->get_id() ) ) . '&action=edit' ) . '" class="order-view"><strong>#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong></a>';
|
||||
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
114
include/admin/monero-gateway-admin-settings.php
Normal file
114
include/admin/monero-gateway-admin-settings.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
return 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 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,
|
||||
),
|
||||
);
|
354
include/class-monero-base58.php
Normal file
354
include/class-monero-base58.php
Normal file
|
@ -0,0 +1,354 @@
|
|||
<?php
|
||||
/*
|
||||
* base58.php
|
||||
*
|
||||
* PHP Base58 codec
|
||||
*
|
||||
* Based on https://github.com/MoneroPy/moneropy/base58.py and https://github.com/mymonero/mymonero-core-js/cryptonote_utils/cryptonote_base58.js
|
||||
*
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class Monero_base58 {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
static $alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
static $encoded_block_sizes = [0, 2, 3, 5, 6, 7, 9, 10, 11];
|
||||
static $full_block_size = 8;
|
||||
static $full_encoded_block_size = 11;
|
||||
|
||||
/**
|
||||
*
|
||||
* Convert a hexadecimal string to a binary array
|
||||
*
|
||||
* @param string $hex A hexadecimal string to convert to a binary array
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
private function hex_to_bin($hex) {
|
||||
if (gettype($hex) != 'string') {
|
||||
throw new Exception('base58->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;
|
||||
}
|
||||
}
|
312
include/class-monero-cryptonote.php
Normal file
312
include/class-monero-cryptonote.php
Normal file
|
@ -0,0 +1,312 @@
|
|||
<?php
|
||||
/*
|
||||
Copyright (c) 2018 Monero-Integrations
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
if(!class_exists('SHA3'))
|
||||
require_once('crypto/SHA3.php');
|
||||
|
||||
if(!class_exists('ed25519'))
|
||||
require_once('crypto/ed25519.php');
|
||||
|
||||
require_once('class-monero-base58.php');
|
||||
|
||||
class Monero_Cryptonote
|
||||
{
|
||||
protected $ed25519;
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
90
include/class-monero-explorer-tools.php
Normal file
90
include/class-monero-explorer-tools.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
/**
|
||||
* monero_explorer_tools.php
|
||||
*
|
||||
* Uses CURL to call API functions from the block explorer
|
||||
* https://xmrchain.net/
|
||||
*
|
||||
* @author Serhack
|
||||
* @author cryptochangements
|
||||
* @author mosu-forge
|
||||
*
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class Monero_Explorer_Tools
|
||||
{
|
||||
private $url;
|
||||
public function __construct($testnet = false)
|
||||
{
|
||||
$this->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 [];
|
||||
}
|
||||
|
||||
}
|
789
include/class-monero-gateway.php
Normal file
789
include/class-monero-gateway.php
Normal file
|
@ -0,0 +1,789 @@
|
|||
<?php
|
||||
/*
|
||||
* Main Gateway of Monero using either a local daemon or the explorer
|
||||
* Authors: Serhack, cryptochangements, mosu-forge
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
require_once('class-monero-cryptonote.php');
|
||||
|
||||
class Monero_Gateway extends WC_Payment_Gateway
|
||||
{
|
||||
private static $_id = 'monero_gateway';
|
||||
private static $_title = 'Monero Gateway';
|
||||
private static $_method_title = 'Monero Gateway';
|
||||
private static $_method_description = 'Monero Gateway Plug-in for WooCommerce.';
|
||||
private static $_errors = [];
|
||||
|
||||
private static $discount = false;
|
||||
private static $valid_time = null;
|
||||
private static $confirms = null;
|
||||
private static $confirm_type = null;
|
||||
private static $address = null;
|
||||
private static $viewkey = null;
|
||||
private static $host = null;
|
||||
private static $port = null;
|
||||
private static $testnet = false;
|
||||
private static $onion_service = false;
|
||||
private static $show_qr = false;
|
||||
private static $use_monero_price = false;
|
||||
private static $use_monero_price_decimals = MONERO_GATEWAY_ATOMIC_UNITS;
|
||||
|
||||
private static $cryptonote;
|
||||
private static $monero_wallet_rpc;
|
||||
private static $monero_explorer_tools;
|
||||
private static $log;
|
||||
|
||||
private static $currencies = array('BTC','USD','EUR','CAD','INR','GBP','COP','SGD','JPY');
|
||||
private static $rates = array();
|
||||
|
||||
private static $payment_details = array();
|
||||
|
||||
public function get_icon()
|
||||
{
|
||||
return apply_filters('woocommerce_gateway_icon', '<img src="'.MONERO_GATEWAY_PLUGIN_URL.'assets/images/monero-icon.png"/>');
|
||||
}
|
||||
|
||||
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 <a href="%s">forcing the checkout pages to be secured.</a>', 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 <<<HTML
|
||||
<span class="woocommerce-Price-amount amount" data-price="$price" data-currency="$currency"
|
||||
data-rate="$rate" data-rate-type="live">
|
||||
$monero_amount_formatted
|
||||
<span class="woocommerce-Price-currencySymbol">XMR</span>
|
||||
</span>
|
||||
|
||||
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 <<<HTML
|
||||
<span class="woocommerce-Price-amount amount" data-price="$price" data-currency="$currency"
|
||||
data-rate="$rate" data-rate-type="fixed">
|
||||
$monero_amount_formatted
|
||||
<span class="woocommerce-Price-currencySymbol">XMR</span>
|
||||
</span>
|
||||
|
||||
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' : '');
|
||||
}
|
||||
|
||||
}
|
352
include/class-monero-wallet-rpc.php
Normal file
352
include/class-monero-wallet-rpc.php
Normal file
|
@ -0,0 +1,352 @@
|
|||
<?php
|
||||
/**
|
||||
* monero_wallet_rpc
|
||||
*
|
||||
* Written using the JSON RPC specification -
|
||||
* http://json-rpc.org/wiki/specification
|
||||
*
|
||||
* @author Kacper Rowinski <krowinski@implix.com>
|
||||
* 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
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class Monero_Wallet_Rpc
|
||||
{
|
||||
protected $url = null, $is_debug = false;
|
||||
protected $curl_options = array(
|
||||
CURLOPT_CONNECTTIMEOUT => 8,
|
||||
CURLOPT_TIMEOUT => 8
|
||||
);
|
||||
protected $host;
|
||||
protected $port;
|
||||
private $httpErrors = array(
|
||||
400 => '400 Bad Request',
|
||||
401 => '401 Unauthorized',
|
||||
403 => '403 Forbidden',
|
||||
404 => '404 Not Found',
|
||||
405 => '405 Method Not Allowed',
|
||||
406 => '406 Not Acceptable',
|
||||
408 => '408 Request Timeout',
|
||||
500 => '500 Internal Server Error',
|
||||
502 => '502 Bad Gateway',
|
||||
503 => '503 Service Unavailable'
|
||||
);
|
||||
|
||||
public function __construct($pHost, $pPort)
|
||||
{
|
||||
$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';
|
||||
}
|
||||
|
||||
public function validate($pFailed, $pErrMsg)
|
||||
{
|
||||
if ($pFailed) {
|
||||
if(is_admin()) echo $pErrMsg;
|
||||
}
|
||||
}
|
||||
|
||||
public function setDebug($pIsDebug)
|
||||
{
|
||||
$this->is_debug = !empty($pIsDebug);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCurlOptions($pOptionsArray)
|
||||
{
|
||||
if (is_array($pOptionsArray)) {
|
||||
$this->curl_options = $pOptionsArray + $this->curl_options;
|
||||
} else {
|
||||
if(is_admin()) echo 'Invalid options type.';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function _print($json)
|
||||
{
|
||||
$json_encoded = json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if(is_admin()) echo $json_encoded;
|
||||
}
|
||||
|
||||
public function _run($method, $params = null)
|
||||
{
|
||||
$result = $this->request($method, $params);
|
||||
return $result; //the result is returned as an array
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// 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);
|
||||
// check if response is correct
|
||||
$this->validate(empty($responseDecoded['id']), 'Invalid response data structure: ' . $responseMessage);
|
||||
$this->validate($responseDecoded['id'] != $requestId, 'Request id: ' . $requestId . ' is different from Response id: ' . $responseDecoded['id']);
|
||||
if (isset($responseDecoded['error'])) {
|
||||
$errorMessage = 'Request have return error: ' . $responseDecoded['error']['message'] . '; ' . "\n" .
|
||||
'Request: ' . $request . '; ';
|
||||
if (isset($responseDecoded['error']['data'])) {
|
||||
$errorMessage .= "\n" . 'Error data: ' . $responseDecoded['error']['data'];
|
||||
}
|
||||
$this->validate(!is_null($responseDecoded['error']), $errorMessage);
|
||||
}
|
||||
return $responseDecoded['result'];
|
||||
}
|
||||
|
||||
protected function debug($pAdd, $pShow = false)
|
||||
{
|
||||
static $debug, $startTime;
|
||||
// is_debug off return
|
||||
if (false === $this->is_debug) {
|
||||
return;
|
||||
}
|
||||
// add
|
||||
$debug .= $pAdd;
|
||||
// get starttime
|
||||
$startTime = empty($startTime) ? array_sum(explode(' ', microtime())) : $startTime;
|
||||
if (true === $pShow and !empty($debug)) {
|
||||
// get endtime
|
||||
$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";
|
||||
if(is_admin()) echo nl2br($debug);
|
||||
// send output immediately
|
||||
flush();
|
||||
// clean static
|
||||
$debug = $startTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function & getResponse(&$pRequest)
|
||||
{
|
||||
// do the actual connection
|
||||
$ch = curl_init();
|
||||
if (!$ch) {
|
||||
throw new RuntimeException('Could\'t initialize a cURL session');
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_URL, $this->url);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $pRequest);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
|
||||
curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
if (!curl_setopt_array($ch, $this->curl_options)) {
|
||||
throw new RuntimeException('Error while setting curl options');
|
||||
}
|
||||
// send the request
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// check http status code
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if (isset($this->httpErrors[$httpCode])) {
|
||||
if(is_admin())
|
||||
echo 'Response Http Error - ' . $this->httpErrors[$httpCode];
|
||||
}
|
||||
|
||||
// check for curl error
|
||||
if (0 < curl_errno($ch)) {
|
||||
if(is_admin())
|
||||
echo '[ERROR] Failed to connect to monero-wallet-rpc at ' . $this->host . ' port '. $this->port .'</br>';
|
||||
}
|
||||
// close the connection
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
|
||||
//prints result as json
|
||||
|
||||
function getJsonLastErrorMsg()
|
||||
{
|
||||
if (!function_exists('json_last_error_msg')) {
|
||||
function json_last_error_msg()
|
||||
{
|
||||
static $errors = array(
|
||||
JSON_ERROR_NONE => 'No error',
|
||||
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
|
||||
JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch',
|
||||
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
|
||||
JSON_ERROR_SYNTAX => 'Syntax error',
|
||||
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'
|
||||
);
|
||||
$error = json_last_error();
|
||||
return array_key_exists($error, $errors) ? $errors[$error] : 'Unknown error (' . $error . ')';
|
||||
}
|
||||
}
|
||||
|
||||
// Fix PHP 5.2 error caused by missing json_last_error function
|
||||
if (function_exists('json_last_error')) {
|
||||
return json_last_error() ? json_last_error_msg() : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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');
|
||||
return $balance;
|
||||
}
|
||||
|
||||
public function getheight()
|
||||
{
|
||||
$height = $this->_run('getheight');
|
||||
return $height['height'];
|
||||
}
|
||||
|
||||
public function incoming_transfer($type)
|
||||
{
|
||||
$incoming_parameters = array('transfer_type' => $type);
|
||||
$incoming_transfers = $this->_run('incoming_transfers', $incoming_parameters);
|
||||
return $incoming_transfers;
|
||||
}
|
||||
|
||||
public function get_transfers($input_type, $input_value)
|
||||
{
|
||||
$get_parameters = array($input_type => $input_value);
|
||||
$get_transfers = $this->_run('get_transfers', $get_parameters);
|
||||
return $get_transfers;
|
||||
}
|
||||
|
||||
public function view_key()
|
||||
{
|
||||
$query_key = array('key_type' => 'view_key');
|
||||
$query_key_method = $this->_run('query_key', $query_key);
|
||||
return $query_key_method;
|
||||
}
|
||||
|
||||
public function make_integrated_address($payment_id)
|
||||
{
|
||||
$integrate_address_parameters = array('payment_id' => $payment_id);
|
||||
$integrate_address_method = $this->_run('make_integrated_address', $integrate_address_parameters);
|
||||
return $integrate_address_method;
|
||||
}
|
||||
|
||||
/* A payment id can be passed as a string
|
||||
A random payment id will be generated if one is not given */
|
||||
|
||||
public function split_integrated_address($integrated_address)
|
||||
{
|
||||
if (!isset($integrated_address)) {
|
||||
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);
|
||||
return $split_methods;
|
||||
}
|
||||
}
|
||||
|
||||
public function make_uri($address, $amount, $recipient_name = null, $description = null)
|
||||
{
|
||||
// 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);
|
||||
return $uri;
|
||||
}
|
||||
|
||||
public function parse_uri($uri)
|
||||
{
|
||||
$uri_parameters = array('uri' => $uri);
|
||||
$parsed_uri = $this->_run('parse_uri', $uri_parameters);
|
||||
return $parsed_uri;
|
||||
}
|
||||
|
||||
public function transfer($amount, $address, $mixin = 12)
|
||||
{
|
||||
$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);
|
||||
return $transfer_method;
|
||||
}
|
||||
|
||||
public function get_payments($payment_id)
|
||||
{
|
||||
$get_payments_parameters = array('payment_id' => $payment_id);
|
||||
$get_payments = $this->_run('get_payments', $get_payments_parameters);
|
||||
if(isset($get_payments['payments']))
|
||||
return $get_payments['payments'];
|
||||
else
|
||||
return array();
|
||||
}
|
||||
|
||||
public function get_pool_payments($payment_id)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
return $payments;
|
||||
}
|
||||
|
||||
public function get_all_payments($payment_id)
|
||||
{
|
||||
$confirmed_payments = $this->get_payments($payment_id);
|
||||
$pool_payments = $this->get_pool_payments($payment_id);
|
||||
return array_merge($pool_payments, $confirmed_payments);
|
||||
}
|
||||
|
||||
public function get_bulk_payments($payment_id, $min_block_height)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
324
include/crypto/SHA3.php
Normal file
324
include/crypto/SHA3.php
Normal file
|
@ -0,0 +1,324 @@
|
|||
<?php /* -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*-
|
||||
vim: ts=4 noet ai */
|
||||
|
||||
/**
|
||||
Streamable SHA-3 for PHP 5.2+, with no lib/ext dependencies!
|
||||
|
||||
Copyright © 2018 Desktopd Developers
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
@license LGPL-3+
|
||||
@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)
|
||||
|
||||
Based on the reference implementations, which are under CC-0
|
||||
Reference: http://keccak.noekeon.org/
|
||||
|
||||
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;
|
||||
const SHA3_384 = 3;
|
||||
const SHA3_512 = 4;
|
||||
|
||||
const SHAKE128 = 5;
|
||||
const SHAKE256 = 6;
|
||||
|
||||
const KECCAK_256 = 7;
|
||||
|
||||
|
||||
public static function init ($type = null) {
|
||||
switch ($type) {
|
||||
case self::SHA3_224: return new self (1152, 448, 0x06, 28);
|
||||
case self::SHA3_256: return new self (1088, 512, 0x06, 32);
|
||||
case self::SHA3_384: return new self (832, 768, 0x06, 48);
|
||||
case self::SHA3_512: return new self (576, 1024, 0x06, 64);
|
||||
case self::SHAKE128: return new self (1344, 256, 0x1f);
|
||||
case self::SHAKE256: return new self (1088, 512, 0x1f);
|
||||
case self::KECCAK_256: return new self (1088, 512, 0x01, 32);
|
||||
}
|
||||
|
||||
throw new Exception ('Invalid operation type');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Feed input to SHA-3 "sponge"
|
||||
*/
|
||||
public function absorb ($data) {
|
||||
if (self::PHASE_INPUT != $this->phase) {
|
||||
throw new Exception ('No more input accepted');
|
||||
}
|
||||
|
||||
$rateInBytes = $this->rateInBytes;
|
||||
$this->inputBuffer .= $data;
|
||||
while (strlen ($this->inputBuffer) >= $rateInBytes) {
|
||||
list ($input, $this->inputBuffer) = array (
|
||||
substr ($this->inputBuffer, 0, $rateInBytes)
|
||||
, substr ($this->inputBuffer, $rateInBytes));
|
||||
|
||||
$blockSize = $rateInBytes;
|
||||
for ($i = 0; $i < $blockSize; $i++) {
|
||||
$this->state[$i] = $this->state[$i] ^ $input[$i];
|
||||
}
|
||||
|
||||
$this->state = self::keccakF1600Permute ($this->state);
|
||||
$this->blockSize = 0;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
Get hash output
|
||||
*/
|
||||
public function squeeze ($length = null) {
|
||||
$outputLength = $this->outputLength; // fixed length output
|
||||
if ($length && 0 < $outputLength && $outputLength != $length) {
|
||||
throw new Exception ('Invalid length');
|
||||
}
|
||||
|
||||
if (self::PHASE_INPUT == $this->phase) {
|
||||
$this->finalizeInput ();
|
||||
}
|
||||
|
||||
if (self::PHASE_OUTPUT != $this->phase) {
|
||||
throw new Exception ('No more output allowed');
|
||||
}
|
||||
if (0 < $outputLength) {
|
||||
$this->phase = self::PHASE_DONE;
|
||||
return $this->getOutputBytes ($outputLength);
|
||||
}
|
||||
|
||||
$blockLength = $this->rateInBytes;
|
||||
list ($output, $this->outputBuffer) = array (
|
||||
substr ($this->outputBuffer, 0, $length)
|
||||
, substr ($this->outputBuffer, $length));
|
||||
$neededLength = $length - strlen ($output);
|
||||
$diff = $neededLength % $blockLength;
|
||||
if ($diff) {
|
||||
$readLength = (($neededLength - $diff) / $blockLength + 1)
|
||||
* $blockLength;
|
||||
} else {
|
||||
$readLength = $neededLength;
|
||||
}
|
||||
|
||||
$read = $this->getOutputBytes ($readLength);
|
||||
$this->outputBuffer .= substr ($read, $neededLength);
|
||||
return $output . substr ($read, 0, $neededLength);
|
||||
}
|
||||
|
||||
|
||||
// internally used
|
||||
const PHASE_INIT = 1;
|
||||
const PHASE_INPUT = 2;
|
||||
const PHASE_OUTPUT = 3;
|
||||
const PHASE_DONE = 4;
|
||||
|
||||
private $phase = self::PHASE_INIT;
|
||||
private $state; // byte array (string)
|
||||
private $rateInBytes; // positive integer
|
||||
private $suffix; // 8-bit unsigned integer
|
||||
private $inputBuffer = ''; // byte array (string): max length = rateInBytes
|
||||
private $outputLength = 0;
|
||||
private $outputBuffer = '';
|
||||
|
||||
|
||||
public function __construct ($rate, $capacity, $suffix, $length = 0) {
|
||||
if (1600 != ($rate + $capacity)) {
|
||||
throw new Error ('Invalid parameters');
|
||||
}
|
||||
if (0 != ($rate % 8)) {
|
||||
throw new Error ('Invalid rate');
|
||||
}
|
||||
|
||||
$this->suffix = $suffix;
|
||||
$this->state = str_repeat ("\0", 200);
|
||||
$this->blockSize = 0;
|
||||
|
||||
$this->rateInBytes = $rate / 8;
|
||||
$this->outputLength = $length;
|
||||
$this->phase = self::PHASE_INPUT;
|
||||
return;
|
||||
}
|
||||
|
||||
protected function finalizeInput () {
|
||||
$this->phase = self::PHASE_OUTPUT;
|
||||
|
||||
$input = $this->inputBuffer;
|
||||
$inputLength = strlen ($input);
|
||||
if (0 < $inputLength) {
|
||||
$blockSize = $inputLength;
|
||||
for ($i = 0; $i < $blockSize; $i++) {
|
||||
$this->state[$i] = $this->state[$i] ^ $input[$i];
|
||||
}
|
||||
|
||||
$this->blockSize = $blockSize;
|
||||
}
|
||||
|
||||
// Padding
|
||||
$rateInBytes = $this->rateInBytes;
|
||||
$this->state[$this->blockSize] = $this->state[$this->blockSize]
|
||||
^ chr ($this->suffix);
|
||||
if (($this->suffix & 0x80) != 0
|
||||
&& $this->blockSize == ($rateInBytes - 1)) {
|
||||
$this->state = self::keccakF1600Permute ($this->state);
|
||||
}
|
||||
$this->state[$rateInBytes - 1] = $this->state[$rateInBytes - 1] ^ "\x80";
|
||||
$this->state = self::keccakF1600Permute ($this->state);
|
||||
}
|
||||
|
||||
protected function getOutputBytes ($outputLength) {
|
||||
// Squeeze
|
||||
$output = '';
|
||||
while (0 < $outputLength) {
|
||||
$blockSize = min ($outputLength, $this->rateInBytes);
|
||||
$output .= substr ($this->state, 0, $blockSize);
|
||||
$outputLength -= $blockSize;
|
||||
if (0 < $outputLength) {
|
||||
$this->state = self::keccakF1600Permute ($this->state);
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
1600-bit state version of Keccak's permutation
|
||||
*/
|
||||
protected static function keccakF1600Permute ($state) {
|
||||
$lanes = str_split ($state, 8);
|
||||
$R = 1;
|
||||
$values = "\1\2\4\10\20\40\100\200";
|
||||
|
||||
for ($round = 0; $round < 24; $round++) {
|
||||
// θ step
|
||||
$C = array ();
|
||||
for ($x = 0; $x < 5; $x++) {
|
||||
// (x, 0) (x, 1) (x, 2) (x, 3) (x, 4)
|
||||
$C[$x] = $lanes[$x] ^ $lanes[$x + 5] ^ $lanes[$x + 10]
|
||||
^ $lanes[$x + 15] ^ $lanes[$x + 20];
|
||||
}
|
||||
for ($x = 0; $x < 5; $x++) {
|
||||
//$D = $C[($x + 4) % 5] ^ self::rotL64 ($C[($x + 1) % 5], 1);
|
||||
$D = $C[($x + 4) % 5] ^ self::rotL64One ($C[($x + 1) % 5]);
|
||||
for ($y = 0; $y < 5; $y++) {
|
||||
$idx = $x + 5 * $y; // x, y
|
||||
$lanes[$idx] = $lanes[$idx] ^ $D;
|
||||
}
|
||||
}
|
||||
unset ($C, $D);
|
||||
|
||||
// ρ and π steps
|
||||
$x = 1;
|
||||
$y = 0;
|
||||
$current = $lanes[1]; // x, y
|
||||
for ($t = 0; $t < 24; $t++) {
|
||||
list ($x, $y) = array ($y, (2 * $x + 3 * $y) % 5);
|
||||
$idx = $x + 5 * $y;
|
||||
list ($current, $lanes[$idx]) = array ($lanes[$idx]
|
||||
, self::rotL64 ($current
|
||||
, (($t + 1) * ($t + 2) / 2) % 64));
|
||||
}
|
||||
unset ($temp, $current);
|
||||
|
||||
// χ step
|
||||
$temp = array ();
|
||||
for ($y = 0; $y < 5; $y++) {
|
||||
for ($x = 0; $x < 5; $x++) {
|
||||
$temp[$x] = $lanes[$x + 5 * $y];
|
||||
}
|
||||
for ($x = 0; $x < 5; $x++) {
|
||||
$lanes[$x + 5 * $y] = $temp[$x]
|
||||
^ ((~ $temp[($x + 1) % 5]) & $temp[($x + 2) % 5]);
|
||||
|
||||
}
|
||||
}
|
||||
unset ($temp);
|
||||
|
||||
// ι step
|
||||
for ($j = 0; $j < 7; $j++) {
|
||||
$R = (($R << 1) ^ (($R >> 7) * 0x71)) & 0xff;
|
||||
if ($R & 2) {
|
||||
$offset = (1 << $j) - 1;
|
||||
$shift = $offset % 8;
|
||||
$octetShift = ($offset - $shift) / 8;
|
||||
$n = "\0\0\0\0\0\0\0\0";
|
||||
$n[$octetShift] = $values[$shift];
|
||||
|
||||
$lanes[0] = $lanes[0]
|
||||
^ $n;
|
||||
//^ self::rotL64 ("\1\0\0\0\0\0\0\0", (1 << $j) - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode ($lanes);
|
||||
}
|
||||
|
||||
protected static function rotL64_64 ($n, $offset) {
|
||||
return ($n << $offset) & ($n >> (64 - $offset));
|
||||
}
|
||||
|
||||
/**
|
||||
64-bit bitwise left rotation (Little endian)
|
||||
*/
|
||||
protected static function rotL64 ($n, $offset) {
|
||||
|
||||
//$n = (binary) $n;
|
||||
//$offset = ((int) $offset) % 64;
|
||||
//if (8 != strlen ($n)) throw new Exception ('Invalid number');
|
||||
//if ($offset < 0) throw new Exception ('Invalid offset');
|
||||
|
||||
$shift = $offset % 8;
|
||||
$octetShift = ($offset - $shift) / 8;
|
||||
$n = substr ($n, - $octetShift) . substr ($n, 0, - $octetShift);
|
||||
|
||||
$overflow = 0x00;
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$a = ord ($n[$i]) << $shift;
|
||||
$n[$i] = chr (0xff & $a | $overflow);
|
||||
$overflow = $a >> 8;
|
||||
}
|
||||
$n[0] = chr (ord ($n[0]) | $overflow);
|
||||
return $n;
|
||||
}
|
||||
|
||||
/**
|
||||
64-bit bitwise left rotation (Little endian)
|
||||
*/
|
||||
protected static function rotL64One ($n) {
|
||||
list ($n[0], $n[1], $n[2], $n[3], $n[4], $n[5], $n[6], $n[7])
|
||||
= array (
|
||||
chr (((ord ($n[0]) << 1) & 0xff) ^ (ord ($n[7]) >> 7))
|
||||
,chr (((ord ($n[1]) << 1) & 0xff) ^ (ord ($n[0]) >> 7))
|
||||
,chr (((ord ($n[2]) << 1) & 0xff) ^ (ord ($n[1]) >> 7))
|
||||
,chr (((ord ($n[3]) << 1) & 0xff) ^ (ord ($n[2]) >> 7))
|
||||
,chr (((ord ($n[4]) << 1) & 0xff) ^ (ord ($n[3]) >> 7))
|
||||
,chr (((ord ($n[5]) << 1) & 0xff) ^ (ord ($n[4]) >> 7))
|
||||
,chr (((ord ($n[6]) << 1) & 0xff) ^ (ord ($n[5]) >> 7))
|
||||
,chr (((ord ($n[7]) << 1) & 0xff) ^ (ord ($n[6]) >> 7)));
|
||||
return $n;
|
||||
}
|
||||
}
|
501
include/crypto/ed25519.php
Normal file
501
include/crypto/ed25519.php
Normal file
|
@ -0,0 +1,501 @@
|
|||
<?php
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2013 John Judy
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
//ini_set('xdebug.max_nesting_level', 0);
|
||||
/**
|
||||
* A PHP implementation of the Python ED25519 library
|
||||
*
|
||||
* @author johnj
|
||||
*
|
||||
* @link http://ed25519.cr.yp.to/software.html Other ED25519 implementations this is referenced from
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class ed25519
|
||||
{
|
||||
public $b;
|
||||
|
||||
public $q;
|
||||
|
||||
public $l;
|
||||
|
||||
public $d;
|
||||
|
||||
public $I;
|
||||
|
||||
public $By;
|
||||
|
||||
public $Bx;
|
||||
|
||||
public $B;
|
||||
|
||||
private $gmp; // Is the GMP extension available?
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->b = 256;
|
||||
$this->q = "57896044618658097711785492504343953926634992332820282019728792003956564819949"; //bcsub(bcpow(2, 255),19);
|
||||
$this->l = "7237005577332262213973186563042994240857116359379907606001950938285454250989"; //bcadd(bcpow(2,252),27742317777372353535851937790883648493);
|
||||
$this->d = "-4513249062541557337682894930092624173785641285191125241628941591882900924598840740"; //bcmul(-121665,$this->inv(121666));
|
||||
$this->I = "19681161376707505956807079304988542015446066515923890162744021073123829784752"; //$this->expmod(2, bcdiv((bcsub($this->q,1)),4),$this->q);
|
||||
$this->By = "46316835694926478169428394003475163141307993866256225615783033603165251855960"; //bcmul(4,$this->inv(5));
|
||||
$this->Bx = "15112221349535400772501151409588531511454012693041857206046113283949847762202"; //$this->xrecover($this->By);
|
||||
$this->B = array(
|
||||
"15112221349535400772501151409588531511454012693041857206046113283949847762202",
|
||||
"46316835694926478169428394003475163141307993866256225615783033603165251855960"
|
||||
); //array(bcmod($this->Bx,$this->q),bcmod($this->By,$this->q));
|
||||
|
||||
$this->gmp = extension_loaded('gmp');
|
||||
}
|
||||
|
||||
public function H($m)
|
||||
{
|
||||
return hash('sha512', $m, true);
|
||||
}
|
||||
|
||||
//((n % M) + M) % M //python modulus craziness
|
||||
public function pymod($x, $m)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$mod = gmp_mod($x, $m);
|
||||
if ($mod < 0) {
|
||||
$mod = gmp_add($mod, $m);
|
||||
}
|
||||
} else {
|
||||
$mod = bcmod($x, $m);
|
||||
if ($mod < 0) {
|
||||
$mod = bcadd($mod, $m);
|
||||
}
|
||||
}
|
||||
|
||||
return $mod;
|
||||
}
|
||||
|
||||
public function expmod($b, $e, $m)
|
||||
{
|
||||
//if($e==0){return 1;}
|
||||
if ($this->gmp) {
|
||||
$t = gmp_powm($b, $e, $m);
|
||||
if ($t < 0) {
|
||||
$t = gmp_add($t, $m);
|
||||
}
|
||||
} else {
|
||||
$t = bcpowmod($b, $e, $m);
|
||||
if ($t[0] === '-') {
|
||||
$t = bcadd($t, $m);
|
||||
}
|
||||
}
|
||||
|
||||
return $t;
|
||||
}
|
||||
|
||||
public function inv($x)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
return $this->expmod($x, gmp_sub($this->q, 2), $this->q);
|
||||
} else {
|
||||
return $this->expmod($x, bcsub($this->q, 2), $this->q);
|
||||
}
|
||||
}
|
||||
|
||||
public function xrecover($y)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$y2 = gmp_pow($y, 2);
|
||||
$xx = gmp_mul(gmp_sub($y2, 1), $this->inv(gmp_add(gmp_mul($this->d, $y2), 1)));
|
||||
$x = $this->expmod($xx, gmp_div(gmp_add($this->q, 3), 8, 0), $this->q);
|
||||
if ($this->pymod(gmp_sub(gmp_pow($x, 2), $xx), $this->q) != 0) {
|
||||
$x = $this->pymod(gmp_mul($x, $this->I), $this->q);
|
||||
}
|
||||
if (substr($x, -1)%2 != 0) {
|
||||
$x = gmp_sub($this->q, $x);
|
||||
}
|
||||
} else {
|
||||
$y2 = bcpow($y, 2);
|
||||
$xx = bcmul(bcsub($y2, 1), $this->inv(bcadd(bcmul($this->d, $y2), 1)));
|
||||
$x = $this->expmod($xx, bcdiv(bcadd($this->q, 3), 8, 0), $this->q);
|
||||
if ($this->pymod(bcsub(bcpow($x, 2), $xx), $this->q) != 0) {
|
||||
$x = $this->pymod(bcmul($x, $this->I), $this->q);
|
||||
}
|
||||
if (substr($x, -1)%2 != 0) {
|
||||
$x = bcsub($this->q, $x);
|
||||
}
|
||||
}
|
||||
|
||||
return $x;
|
||||
}
|
||||
|
||||
public function edwards($P, $Q)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
list($x1, $y1) = $P;
|
||||
list($x2, $y2) = $Q;
|
||||
$xmul = gmp_mul($x1, $x2);
|
||||
$ymul = gmp_mul($y1, $y2);
|
||||
$com = gmp_mul($this->d, gmp_mul($xmul, $ymul));
|
||||
$x3 = gmp_mul(gmp_add(gmp_mul($x1, $y2), gmp_mul($x2, $y1)), $this->inv(gmp_add(1, $com)));
|
||||
$y3 = gmp_mul(gmp_add($ymul, $xmul), $this->inv(gmp_sub(1, $com)));
|
||||
|
||||
return array($this->pymod($x3, $this->q), $this->pymod($y3, $this->q));
|
||||
} else {
|
||||
list($x1, $y1) = $P;
|
||||
list($x2, $y2) = $Q;
|
||||
$xmul = bcmul($x1, $x2);
|
||||
$ymul = bcmul($y1, $y2);
|
||||
$com = bcmul($this->d, bcmul($xmul, $ymul));
|
||||
$x3 = bcmul(bcadd(bcmul($x1, $y2), bcmul($x2, $y1)), $this->inv(bcadd(1, $com)));
|
||||
$y3 = bcmul(bcadd($ymul, $xmul), $this->inv(bcsub(1, $com)));
|
||||
|
||||
return array($this->pymod($x3, $this->q), $this->pymod($y3, $this->q));
|
||||
}
|
||||
}
|
||||
|
||||
public function scalarmult($P, $e)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
if ($e == 0) {
|
||||
return array(0, 1);
|
||||
}
|
||||
$Q = $this->scalarmult($P, gmp_div($e, 2, 0));
|
||||
$Q = $this->edwards($Q, $Q);
|
||||
if (substr($e, -1)%2 == 1) {
|
||||
$Q = $this->edwards($Q, $P);
|
||||
}
|
||||
} else {
|
||||
if ($e == 0) {
|
||||
return array(0, 1);
|
||||
}
|
||||
$Q = $this->scalarmult($P, bcdiv($e, 2, 0));
|
||||
$Q = $this->edwards($Q, $Q);
|
||||
if (substr($e, -1)%2 == 1) {
|
||||
$Q = $this->edwards($Q, $P);
|
||||
}
|
||||
}
|
||||
|
||||
return $Q;
|
||||
}
|
||||
|
||||
public function scalarloop($P, $e)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$temp = array();
|
||||
$loopE = $e;
|
||||
while ($loopE > 0) {
|
||||
array_unshift($temp, $loopE);
|
||||
$loopE = gmp_div($loopE, 2, 0);
|
||||
}
|
||||
$Q = array();
|
||||
foreach ($temp as $e) {
|
||||
if ($e == 1) {
|
||||
$Q = $this->edwards(array(0, 1), $P);
|
||||
} elseif (substr($e, -1)%2 == 1) {
|
||||
$Q = $this->edwards($this->edwards($Q, $Q), $P);
|
||||
} else {
|
||||
$Q = $this->edwards($Q, $Q);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$temp = array();
|
||||
$loopE = $e;
|
||||
while ($loopE > 0) {
|
||||
array_unshift($temp, $loopE);
|
||||
$loopE = bcdiv($loopE, 2, 0);
|
||||
}
|
||||
$Q = array();
|
||||
foreach ($temp as $e) {
|
||||
if ($e == 1) {
|
||||
$Q = $this->edwards(array(0, 1), $P);
|
||||
} elseif (substr($e, -1)%2 == 1) {
|
||||
$Q = $this->edwards($this->edwards($Q, $Q), $P);
|
||||
} else {
|
||||
$Q = $this->edwards($Q, $Q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $Q;
|
||||
}
|
||||
|
||||
public function bitsToString($bits)
|
||||
{
|
||||
$string = '';
|
||||
for ($i = 0; $i < $this->b/8; $i++) {
|
||||
$sum = 0;
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$bit = $bits[$i*8+$j];
|
||||
$sum += (int) $bit << $j;
|
||||
}
|
||||
$string .= chr($sum);
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
public function dec2bin_i($decimal_i)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$binary_i = '';
|
||||
do {
|
||||
$binary_i = substr($decimal_i, -1)%2 .$binary_i;
|
||||
$decimal_i = gmp_div($decimal_i, '2', 0);
|
||||
} while (gmp_cmp($decimal_i, '0'));
|
||||
} else {
|
||||
$binary_i = '';
|
||||
do {
|
||||
$binary_i = substr($decimal_i, -1)%2 .$binary_i;
|
||||
$decimal_i = bcdiv($decimal_i, '2', 0);
|
||||
} while (bccomp($decimal_i, '0'));
|
||||
}
|
||||
|
||||
return ($binary_i);
|
||||
}
|
||||
|
||||
public function encodeint($y)
|
||||
{
|
||||
$bits = substr(str_pad(strrev($this->dec2bin_i($y)), $this->b, '0', STR_PAD_RIGHT), 0, $this->b);
|
||||
|
||||
return $this->bitsToString($bits);
|
||||
}
|
||||
|
||||
public function encodepoint($P)
|
||||
{
|
||||
list($x, $y) = $P;
|
||||
$bits = substr(str_pad(strrev($this->dec2bin_i($y)), $this->b-1, '0', STR_PAD_RIGHT), 0, $this->b-1);
|
||||
$bits .= (substr($x, -1)%2 == 1 ? '1' : '0');
|
||||
|
||||
return $this->bitsToString($bits);
|
||||
}
|
||||
|
||||
public function bit($h, $i)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
return (ord($h[(int) gmp_div($i, 8, 0)]) >> substr($i, -3)%8) & 1;
|
||||
} else {
|
||||
return (ord($h[(int) bcdiv($i, 8, 0)]) >> substr($i, -3)%8) & 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the public key of a given private key
|
||||
*
|
||||
* @param string $sk the secret key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function publickey($sk)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$h = $this->H($sk);
|
||||
$sum = 0;
|
||||
for ($i = 3; $i < $this->b-2; $i++) {
|
||||
$sum = gmp_add($sum, gmp_mul(gmp_pow(2, $i), $this->bit($h, $i)));
|
||||
}
|
||||
$a = gmp_add(gmp_pow(2, $this->b-2), $sum);
|
||||
$A = $this->scalarmult($this->B, $a);
|
||||
$data = $this->encodepoint($A);
|
||||
} else {
|
||||
$h = $this->H($sk);
|
||||
$sum = 0;
|
||||
for ($i = 3; $i < $this->b-2; $i++) {
|
||||
$sum = bcadd($sum, bcmul(bcpow(2, $i), $this->bit($h, $i)));
|
||||
}
|
||||
$a = bcadd(bcpow(2, $this->b-2), $sum);
|
||||
$A = $this->scalarmult($this->B, $a);
|
||||
$data = $this->encodepoint($A);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function Hint($m)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$h = $this->H($m);
|
||||
$sum = 0;
|
||||
for ($i = 0; $i < $this->b*2; $i++) {
|
||||
$sum = gmp_add($sum, gmp_mul(gmp_pow(2, $i), $this->bit($h, $i)));
|
||||
}
|
||||
} else {
|
||||
$h = $this->H($m);
|
||||
$sum = 0;
|
||||
for ($i = 0; $i < $this->b*2; $i++) {
|
||||
$sum = bcadd($sum, bcmul(bcpow(2, $i), $this->bit($h, $i)));
|
||||
}
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
public function signature($m, $sk, $pk)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$h = $this->H($sk);
|
||||
$a = gmp_pow(2, (gmp_sub($this->b, 2)));
|
||||
for ($i = 3; $i < $this->b-2; $i++) {
|
||||
$a = gmp_add($a, gmp_mul(gmp_pow(2, $i), $this->bit($h, $i)));
|
||||
}
|
||||
$r = $this->Hint(substr($h, $this->b/8, ($this->b/4-$this->b/8)).$m);
|
||||
$R = $this->scalarmult($this->B, $r);
|
||||
$encR = $this->encodepoint($R);
|
||||
$S = $this->pymod(gmp_add($r, gmp_mul($this->Hint($encR.$pk.$m), $a)), $this->l);
|
||||
} else {
|
||||
$h = $this->H($sk);
|
||||
$a = bcpow(2, (bcsub($this->b, 2)));
|
||||
for ($i = 3; $i < $this->b-2; $i++) {
|
||||
$a = bcadd($a, bcmul(bcpow(2, $i), $this->bit($h, $i)));
|
||||
}
|
||||
$r = $this->Hint(substr($h, $this->b/8, ($this->b/4-$this->b/8)).$m);
|
||||
$R = $this->scalarmult($this->B, $r);
|
||||
$encR = $this->encodepoint($R);
|
||||
$S = $this->pymod(bcadd($r, bcmul($this->Hint($encR.$pk.$m), $a)), $this->l);
|
||||
}
|
||||
|
||||
return $encR.$this->encodeint($S);
|
||||
}
|
||||
|
||||
public function isoncurve($P)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
list($x, $y) = $P;
|
||||
$x2 = gmp_pow($x, 2);
|
||||
$y2 = gmp_pow($y, 2);
|
||||
|
||||
return $this->pymod(gmp_sub(gmp_sub(gmp_sub($y2, $x2), 1), gmp_mul($this->d, gmp_mul($x2, $y2))), $this->q) == 0;
|
||||
} else {
|
||||
list($x, $y) = $P;
|
||||
$x2 = bcpow($x, 2);
|
||||
$y2 = bcpow($y, 2);
|
||||
|
||||
return $this->pymod(bcsub(bcsub(bcsub($y2, $x2), 1), bcmul($this->d, bcmul($x2, $y2))), $this->q) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function decodeint($s)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$sum = 0;
|
||||
for ($i = 0; $i < $this->b; $i++) {
|
||||
$sum = gmp_add($sum, gmp_mul(gmp_pow(2, $i), $this->bit($s, $i)));
|
||||
}
|
||||
} else {
|
||||
$sum = 0;
|
||||
for ($i = 0; $i < $this->b; $i++) {
|
||||
$sum = bcadd($sum, bcmul(bcpow(2, $i), $this->bit($s, $i)));
|
||||
}
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
/*
|
||||
* def decodepoint(s):
|
||||
y = sum(2**i * bit(s,i) for i in range(0,b-1))
|
||||
x = xrecover(y)
|
||||
if x & 1 != bit(s,b-1): x = q-x
|
||||
P = [x,y]
|
||||
if not isoncurve(P): raise Exception("decoding point that is not on curve")
|
||||
return P
|
||||
|
||||
*/
|
||||
public function decodepoint($s)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
$y = 0;
|
||||
for ($i = 0; $i < $this->b-1; $i++) {
|
||||
$y = gmp_add($y, gmp_mul(gmp_pow(2, $i), $this->bit($s, $i)));
|
||||
}
|
||||
$x = $this->xrecover($y);
|
||||
if (substr($x, -1)%2 != $this->bit($s, $this->b-1)) {
|
||||
$x = gmp_sub($this->q, $x);
|
||||
}
|
||||
$P = array($x, $y);
|
||||
if (!$this->isoncurve($P)) {
|
||||
throw new \Exception("Decoding point that is not on curve");
|
||||
}
|
||||
} else {
|
||||
$y = 0;
|
||||
for ($i = 0; $i < $this->b-1; $i++) {
|
||||
$y = bcadd($y, bcmul(bcpow(2, $i), $this->bit($s, $i)));
|
||||
}
|
||||
$x = $this->xrecover($y);
|
||||
if (substr($x, -1)%2 != $this->bit($s, $this->b-1)) {
|
||||
$x = bcsub($this->q, $x);
|
||||
}
|
||||
$P = array($x, $y);
|
||||
if (!$this->isoncurve($P)) {
|
||||
throw new \Exception("Decoding point that is not on curve");
|
||||
}
|
||||
}
|
||||
|
||||
return $P;
|
||||
}
|
||||
|
||||
public function checkvalid($s, $m, $pk)
|
||||
{
|
||||
if (strlen($s) != $this->b/4) {
|
||||
throw new \Exception('Signature length is wrong');
|
||||
}
|
||||
if (strlen($pk) != $this->b/8) {
|
||||
throw new \Exception('Public key length is wrong: '.strlen($pk));
|
||||
}
|
||||
$R = $this->decodepoint(substr($s, 0, $this->b/8));
|
||||
try {
|
||||
$A = $this->decodepoint($pk);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
$S = $this->decodeint(substr($s, $this->b/8, $this->b/4));
|
||||
$h = $this->Hint($this->encodepoint($R).$pk.$m);
|
||||
|
||||
return $this->scalarmult($this->B, $S) == $this->edwards($R, $this->scalarmult($A, $h));
|
||||
}
|
||||
|
||||
// The code below is by the Monero-Integrations team
|
||||
|
||||
public function scalarmult_base($e)
|
||||
{
|
||||
if ($this->gmp) {
|
||||
if ($e == 0) {
|
||||
return array(0, 1);
|
||||
}
|
||||
$Q = $this->scalarmult($this->B, gmp_div($e, 2, 0));
|
||||
$Q = $this->edwards($Q, $Q);
|
||||
if (substr($e, -1)%2 == 1) {
|
||||
$Q = $this->edwards($Q, $this->B);
|
||||
}
|
||||
} else {
|
||||
if ($e == 0) {
|
||||
return array(0, 1);
|
||||
}
|
||||
$Q = $this->scalarmult($this->B, bcdiv($e, 2, 0));
|
||||
$Q = $this->edwards($Q, $Q);
|
||||
if (substr($e, -1)%2 == 1) {
|
||||
$Q = $this->edwards($Q, $this->B);
|
||||
}
|
||||
}
|
||||
|
||||
return $Q;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue