Rewrite plugin to v3.0

This commit is contained in:
mosu forge 2018-09-22 07:57:35 -07:00
parent 4c091a13f7
commit 4def1b771e
37 changed files with 4613 additions and 1685 deletions

View 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();

View 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),
)
);
}
}

View 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,
),
);

View 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;
}
}

View 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;
}
}

View 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 [];
}
}

View 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' : '');
}
}

View 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
View 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
View 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;
}
}