Beispiel #1
0
function BWWC__get_bitcoin_address_for_payment__electrum($electrum_mpk, $order_info)
{
    global $wpdb;
    // status = "unused", "assigned", "used"
    $btc_addresses_table_name = $wpdb->prefix . 'bwwc_btc_addresses';
    $origin_id = 'electrum.mpk.' . md5($electrum_mpk);
    $bwwc_settings = BWWC__get_settings();
    $funds_received_value_expires_in_secs = $bwwc_settings['funds_received_value_expires_in_mins'] * 60;
    $assigned_address_expires_in_secs = $bwwc_settings['assigned_address_expires_in_mins'] * 60;
    $clean_address = NULL;
    $current_time = time();
    if ($bwwc_settings['reuse_expired_addresses']) {
        $reuse_expired_addresses_freshb_query_part = "OR (`status`='assigned'\r\n      \t\tAND (('{$current_time}' - `assigned_at`) > '{$assigned_address_expires_in_secs}')\r\n      \t\tAND (('{$current_time}' - `received_funds_checked_at`) < '{$funds_received_value_expires_in_secs}')\r\n      \t\t)";
    } else {
        $reuse_expired_addresses_freshb_query_part = "";
    }
    //-------------------------------------------------------
    // Quick scan for ready-to-use address
    // NULL == not found
    // Retrieve:
    //     'unused'   - with fresh zero balances
    //     'assigned' - expired, with fresh zero balances (if 'reuse_expired_addresses' is true)
    //
    // Hence - any returned address will be clean to use.
    $query = "SELECT `btc_address` FROM `{$btc_addresses_table_name}`\r\n         WHERE `origin_id`='{$origin_id}'\r\n         AND `total_received_funds`='0'\r\n         AND (`status`='unused' {$reuse_expired_addresses_freshb_query_part})\r\n         ORDER BY `index_in_wallet` ASC\r\n         LIMIT 1;";
    // Try to use lower indexes first
    $clean_address = $wpdb->get_var($query);
    //-------------------------------------------------------
    if (!$clean_address) {
        //-------------------------------------------------------
        // Find all unused addresses belonging to this mpk with possibly (to be verified right after) zero balances
        // Array(rows) or NULL
        // Retrieve:
        //    'unused'    - with old zero balances
        //    'unknown'   - ALL
        //    'assigned'  - expired with old zero balances (if 'reuse_expired_addresses' is true)
        //
        // Hence - any returned address with freshened balance==0 will be clean to use.
        if ($bwwc_settings['reuse_expired_addresses']) {
            $reuse_expired_addresses_oldb_query_part = "OR (`status`='assigned'\r\n\t      \t\tAND (('{$current_time}' - `assigned_at`) > '{$assigned_address_expires_in_secs}')\r\n\t      \t\tAND (('{$current_time}' - `received_funds_checked_at`) > '{$funds_received_value_expires_in_secs}')\r\n\t      \t\t)";
        } else {
            $reuse_expired_addresses_oldb_query_part = "";
        }
        $query = "SELECT * FROM `{$btc_addresses_table_name}`\r\n            WHERE `origin_id`='{$origin_id}'\r\n\t         \tAND `total_received_funds`='0'\r\n            AND (\r\n               `status`='unused'\r\n               OR `status`='unknown'\r\n               {$reuse_expired_addresses_oldb_query_part}\r\n               )\r\n            ORDER BY `index_in_wallet` ASC;";
        // Try to use lower indexes first
        $addresses_to_verify_for_zero_balances_rows = $wpdb->get_results($query, ARRAY_A);
        if (!is_array($addresses_to_verify_for_zero_balances_rows)) {
            $addresses_to_verify_for_zero_balances_rows = array();
        }
        //-------------------------------------------------------
        //-------------------------------------------------------
        // Try to re-verify balances of existing addresses (with old or non-existing balances) before reverting to slow operation of generating new address.
        //
        $blockchains_api_failures = 0;
        foreach ($addresses_to_verify_for_zero_balances_rows as $address_to_verify_for_zero_balance_row) {
            // http://blockexplorer.com/q/getreceivedbyaddress/1H9uAP3x439YvQDoKNGgSYCg3FmrYRzpD2
            // http://blockchain.info/q/getreceivedbyaddress/1H9uAP3x439YvQDoKNGgSYCg3FmrYRzpD2 [?confirmations=6]
            //
            $address_to_verify_for_zero_balance = $address_to_verify_for_zero_balance_row['btc_address'];
            $ret_info_array = BWWC__getreceivedbyaddress_info($address_to_verify_for_zero_balance, 0, $bwwc_settings['blockchain_api_timeout_secs']);
            if ($ret_info_array['balance'] === false) {
                $blockchains_api_failures++;
                if ($blockchains_api_failures >= $bwwc_settings['max_blockchains_api_failures']) {
                    // Allow no more than 3 contigious blockchains API failures. After which return error reply.
                    $ret_info_array = array('result' => 'error', 'message' => $ret_info_array['message'], 'host_reply_raw' => $ret_info_array['host_reply_raw'], 'generated_bitcoin_address' => false);
                    return $ret_info_array;
                }
            } else {
                if ($ret_info_array['balance'] == 0) {
                    // Update DB with balance and timestamp, mark address as 'assigned' and return this address as clean.
                    $clean_address = $address_to_verify_for_zero_balance;
                    break;
                } else {
                    // Balance at this address suddenly became non-zero!
                    // It means either order was paid after expiration or "unknown" address suddenly showed up with non-zero balance or payment was sent to this address outside of this online store business.
                    // Mark it as 'revalidate' so cron job would check if that's possible delayed payment.
                    //
                    $address_meta = BWWC_unserialize_address_meta(@$address_to_verify_for_zero_balance_row['address_meta']);
                    if (isset($address_meta['orders'][0])) {
                        $new_status = 'revalidate';
                    } else {
                        $new_status = 'used';
                    }
                    // No orders were ever placed to this address. Likely payment was sent to this address outside of this online store business.
                    $current_time = time();
                    $query = "UPDATE `{$btc_addresses_table_name}`\r\n\t\t\t         SET\r\n\t\t\t            `status`='{$new_status}',\r\n\t\t\t            `total_received_funds` = '{$ret_info_array['balance']}',\r\n\t\t\t            `received_funds_checked_at`='{$current_time}'\r\n\t\t\t        WHERE `btc_address`='{$address_to_verify_for_zero_balance}';";
                    $ret_code = $wpdb->query($query);
                }
            }
        }
        //-------------------------------------------------------
    }
    //-------------------------------------------------------
    if (!$clean_address) {
        // Still could not find unused virgin address. Time to generate it from scratch.
        /*
        Returns:
           $ret_info_array = array (
              'result'                      => 'success', // 'error'
              'message'                     => '', // Failed to find/generate bitcoin address',
              'host_reply_raw'              => '', // Error. No host reply availabe.',
              'generated_bitcoin_address'   => '1FVai2j2FsFvCbgsy22ZbSMfUd3HLUHvKx', // false,
              );
        */
        $ret_addr_array = BWWC__generate_new_bitcoin_address_for_electrum_wallet($bwwc_settings, $electrum_mpk);
        if ($ret_addr_array['result'] == 'success') {
            $clean_address = $ret_addr_array['generated_bitcoin_address'];
        }
    }
    //-------------------------------------------------------
    //-------------------------------------------------------
    if ($clean_address) {
        /*
                 $order_info =
                 array (
                    'order_id'     => $order_id,
                    'order_total'  => $order_total_in_btc,
                    'order_datetime'  => date('Y-m-d H:i:s T'),
                    'requested_by_ip' => @$_SERVER['REMOTE_ADDR'],
                    );
        */
        /*
        $address_meta =
           array (
              'orders' =>
                 array (
                    // All orders placed on this address in reverse chronological order
                    array (
                       'order_id'     => $order_id,
                       'order_total'  => $order_total_in_btc,
                       'order_datetime'  => date('Y-m-d H:i:s T'),
                       'requested_by_ip' => @$_SERVER['REMOTE_ADDR'],
                    ),
                    array (
                       ...
                    ),
                 ),
              'other_meta_info' => array (...)
           );
        */
        // Prepare `address_meta` field for this clean address.
        $address_meta = $wpdb->get_var("SELECT `address_meta` FROM `{$btc_addresses_table_name}` WHERE `btc_address`='{$clean_address}'");
        $address_meta = BWWC_unserialize_address_meta($address_meta);
        if (!isset($address_meta['orders']) || !is_array($address_meta['orders'])) {
            $address_meta['orders'] = array();
        }
        array_unshift($address_meta['orders'], $order_info);
        // Prepend new order to array of orders
        if (count($address_meta['orders']) > 10) {
            array_pop($address_meta['orders']);
        }
        // Do not keep history of more than 10 unfullfilled orders per address.
        $address_meta_serialized = BWWC_serialize_address_meta($address_meta);
        // Update DB with balance and timestamp, mark address as 'assigned' and return this address as clean.
        //
        $current_time = time();
        $remote_addr = $order_info['requested_by_ip'];
        $query = "UPDATE `{$btc_addresses_table_name}`\r\n         SET\r\n            `total_received_funds` = '0',\r\n            `received_funds_checked_at`='{$current_time}',\r\n            `status`='assigned',\r\n            `assigned_at`='{$current_time}',\r\n            `last_assigned_to_ip`='{$remote_addr}',\r\n            `address_meta`='{$address_meta_serialized}'\r\n        WHERE `btc_address`='{$clean_address}';";
        $ret_code = $wpdb->query($query);
        $ret_info_array = array('result' => 'success', 'message' => "", 'host_reply_raw' => "", 'generated_bitcoin_address' => $clean_address);
        return $ret_info_array;
    }
    //-------------------------------------------------------
    $ret_info_array = array('result' => 'error', 'message' => 'Failed to find/generate bitcoin address. ' . $ret_addr_array['message'], 'host_reply_raw' => $ret_addr_array['host_reply_raw'], 'generated_bitcoin_address' => false);
    return $ret_info_array;
}
function BWWC_cron_job_worker($hardcron = false)
{
    global $wpdb;
    $bwwc_settings = BWWC__get_settings();
    if (@$bwwc_settings['gateway_settings']['service_provider'] != 'electrum-wallet') {
        return;
        // Only active electrum wallet as a service provider needs cron job
    }
    // status = "unused", "assigned", "used"
    $btc_addresses_table_name = $wpdb->prefix . 'bwwc_btc_addresses';
    $funds_received_value_expires_in_secs = $bwwc_settings['funds_received_value_expires_in_mins'] * 60;
    $assigned_address_expires_in_secs = $bwwc_settings['assigned_address_expires_in_mins'] * 60;
    $confirmations_required = $bwwc_settings['gateway_settings']['confirmations'];
    $clean_address = NULL;
    $current_time = time();
    // Search for completed orders (addresses that received full payments for their orders) ...
    // NULL == not found
    // Retrieve:
    //     'assigned'   - unexpired, with old balances (due for revalidation. Fresh balances and still 'assigned' means no [full] payment received yet)
    //     'revalidate' - all
    //        order results by most recently assigned
    $query = "SELECT * FROM `{$btc_addresses_table_name}`\n      WHERE\n      (\n        (`status`='assigned' AND (('{$current_time}' - `assigned_at`) < '{$assigned_address_expires_in_secs}'))\n        OR\n        (`status`='revalidate')\n      )\n      AND (('{$current_time}' - `received_funds_checked_at`) > '{$funds_received_value_expires_in_secs}')\n      ORDER BY `received_funds_checked_at` ASC;";
    // Check the ones that haven't been checked for longest time
    $rows_for_balance_check = $wpdb->get_results($query, ARRAY_A);
    if (is_array($rows_for_balance_check)) {
        $count_rows_for_balance_check = count($rows_for_balance_check);
    } else {
        $count_rows_for_balance_check = 0;
    }
    if (is_array($rows_for_balance_check)) {
        $ran_cycles = 0;
        foreach ($rows_for_balance_check as $row_for_balance_check) {
            $ran_cycles++;
            // To limit number of cycles per soft cron job.
            // Prepare 'address_meta' for use.
            $address_meta = BWWC_unserialize_address_meta(@$row_for_balance_check['address_meta']);
            $last_order_info = @$address_meta['orders'][0];
            $row_id = $row_for_balance_check['id'];
            // Retrieve current balance at address.
            $balance_info_array = BWWC__getreceivedbyaddress_info($row_for_balance_check['btc_address'], $confirmations_required, $bwwc_settings['blockchain_api_timeout_secs']);
            if ($balance_info_array['result'] == 'success') {
                /*
                $balance_info_array = array (
                					'result'                      => 'success',
                					'message'                     => "",
                					'host_reply_raw'              => "",
                					'balance'                     => $funds_received,
                					);
                */
                // Refresh 'received_funds_checked_at' field
                $current_time = time();
                $query = "UPDATE `{$btc_addresses_table_name}`\n             SET\n                `total_received_funds` = '{$balance_info_array['balance']}',\n                `received_funds_checked_at`='{$current_time}'\n            WHERE `id`='{$row_id}';";
                $ret_code = $wpdb->query($query);
                if ($balance_info_array['balance'] > 0) {
                    if ($row_for_balance_check['status'] == 'revalidate') {
                        // Address with suddenly appeared balance. Check if that is matching to previously-placed [likely expired] order
                        if (!$last_order_info || !@$last_order_info['order_id'] || !@$balance_info_array['balance'] || !@$last_order_info['order_total']) {
                            // No proper metadata present. Mark this address as 'xused' (used by unknown entity outside of this application) and be done with it forever.
                            $query = "UPDATE `{$btc_addresses_table_name}`\n                   SET\n                      `status` = 'xused'\n                  WHERE `id`='{$row_id}';";
                            $ret_code = $wpdb->query($query);
                            continue;
                        } else {
                            // Metadata for this address is present. Mark this address as 'assigned' and treat it like that further down...
                            $query = "UPDATE `{$btc_addresses_table_name}`\n                   SET\n                      `status` = 'assigned'\n                  WHERE `id`='{$row_id}';";
                            $ret_code = $wpdb->query($query);
                        }
                    }
                    BWWC__log_event(__FILE__, __LINE__, "Cron job: NOTE: Detected non-zero balance at address: '{$row_for_balance_check['btc_address']}, order ID = '{$last_order_info['order_id']}'. Detected balance ='{$balance_info_array['balance']}'.");
                    if ($balance_info_array['balance'] < $last_order_info['order_total']) {
                        BWWC__log_event(__FILE__, __LINE__, "Cron job: NOTE: balance at address: '{$row_for_balance_check['btc_address']}' (BTC '{$balance_info_array['balance']}') is not yet sufficient to complete it's order (order ID = '{$last_order_info['order_id']}'). Total required: '{$last_order_info['order_total']}'. Will wait for more funds to arrive...");
                    }
                } else {
                }
                // Note: to be perfectly safe against late-paid orders, we need to:
                //	Scan '$address_meta['orders']' for first UNPAID order that is exactly matching amount at address.
                if ($balance_info_array['balance'] >= $last_order_info['order_total']) {
                    // Process full payment event
                    /*
                    $address_meta =
                       array (
                          'orders' =>
                             array (
                                // All orders placed on this address in reverse chronological order
                                array (
                                   'order_id'     => $order_id,
                                   'order_total'  => $order_total_in_btc,
                                   'order_datetime'  => date('Y-m-d H:i:s T'),
                                   'requested_by_ip' => @$_SERVER['REMOTE_ADDR'],
                                ),
                                array (
                                   ...
                                ),
                             ),
                          'other_meta_info' => array (...)
                       );
                    */
                    // Last order was fully paid! Complete it...
                    BWWC__log_event(__FILE__, __LINE__, "Cron job: NOTE: Full payment for order ID '{$last_order_info['order_id']}' detected at address: '{$row_for_balance_check['btc_address']}' (BTC '{$balance_info_array['balance']}'). Total was required for this order: '{$last_order_info['order_total']}'. Processing order ...");
                    // Update order' meta info
                    $address_meta['orders'][0]['paid'] = true;
                    // Process and complete the order within WooCommerce (send confirmation emails, etc...)
                    BWWC__process_payment_completed_for_order($last_order_info['order_id'], $balance_info_array['balance']);
                    // Update address' record
                    $address_meta_serialized = BWWC_serialize_address_meta($address_meta);
                    // Update DB - mark address as 'used'.
                    //
                    $current_time = time();
                    // Note: `total_received_funds` and `received_funds_checked_at` are already updated above.
                    //
                    $query = "UPDATE `{$btc_addresses_table_name}`\n\t             SET\n\t                `status`='used',\n\t                `address_meta`='{$address_meta_serialized}'\n\t            WHERE `id`='{$row_id}';";
                    $ret_code = $wpdb->query($query);
                    BWWC__log_event(__FILE__, __LINE__, "Cron job: SUCCESS: Order ID '{$last_order_info['order_id']}' successfully completed.");
                    // This is not needed here. Let it process as many orders as are paid for in the same loop.
                    // Maybe to be moved there --> //..// (to avoid soft-cron checking of balance of hundreds of addresses in a same loop)
                    //
                    // 	        //	Return here to avoid overloading too many processing needs to one random visitor.
                    // 	        //	Then it means no more than one order can be processed per 2.5 minutes (or whatever soft cron schedule is).
                    // 	        //	Hard cron is immune to this limitation.
                    // 	        if (!$hardcron && $ran_cycles >= $bwwc_settings['soft_cron_max_loops_per_run'])
                    // 	        {
                    // 	        	return;
                    // 	        }
                }
            } else {
                BWWC__log_event(__FILE__, __LINE__, "Cron job: Warning: Cannot retrieve balance for address: '{$row_for_balance_check['btc_address']}: " . $balance_info_array['message']);
            }
            //..//
        }
    }
    // Process all 'revalidate' addresses here.
    // ...
    //-----------------------------------------------------
    // Pre-generate new bitcoin address for electrum wallet
    // Try to retrieve mpk from copy of settings.
    if ($hardcron) {
        $electrum_mpk = @$bwwc_settings['gateway_settings']['electrum_master_public_key'];
        if ($electrum_mpk && @$bwwc_settings['gateway_settings']['service_provider'] == 'electrum-wallet') {
            // Calculate number of unused addresses belonging to currently active electrum wallet
            $origin_id = 'electrum.mpk.' . md5($electrum_mpk);
            $current_time = time();
            $assigned_address_expires_in_secs = $bwwc_settings['assigned_address_expires_in_mins'] * 60;
            if ($bwwc_settings['reuse_expired_addresses']) {
                $reuse_expired_addresses_query_part = "OR (`status`='assigned' AND (('{$current_time}' - `assigned_at`) > '{$assigned_address_expires_in_secs}'))";
            } else {
                $reuse_expired_addresses_query_part = "";
            }
            // Calculate total number of currently unused addresses in a system. Make sure there aren't too many.
            // NULL == not found
            // Retrieve:
            //     'unused'   - with fresh zero balances
            //     'assigned' - expired, with fresh zero balances (if 'reuse_expired_addresses' is true)
            //
            // Hence - any returned address will be clean to use.
            $query = "SELECT COUNT(*) as `total_unused_addresses` FROM `{$btc_addresses_table_name}`\n           WHERE `origin_id`='{$origin_id}'\n           AND `total_received_funds`='0'\n           AND (`status`='unused' {$reuse_expired_addresses_query_part})\n           ";
            $total_unused_addresses = $wpdb->get_var($query);
            if ($total_unused_addresses < $bwwc_settings['max_unused_addresses_buffer']) {
                BWWC__generate_new_bitcoin_address_for_electrum_wallet($bwwc_settings, $electrum_mpk);
            }
        }
    }
    //-----------------------------------------------------
}