Issues (236)

src/pay/crypto.php (8 issues)

1
<?php
2
3
namespace BPT\pay;
4
5
use BPT\BPT;
6
use BPT\constants\callbackTypes;
7
use BPT\constants\cryptoCallbackActionTypes;
8
use BPT\constants\cryptoCallbackStatus;
9
use BPT\constants\cryptoStatus;
10
use BPT\constants\fields;
11
use BPT\constants\loggerTypes;
12
use BPT\database\mysql;
13
use BPT\exception\bptException;
14
use BPT\logger;
15
use BPT\pay\crypto\errorResponseInterface;
16
use BPT\pay\crypto\estimatePriceInterface;
17
use BPT\pay\crypto\estimateUpdateInterface;
18
use BPT\pay\crypto\invoicePaymentInterface;
19
use BPT\pay\crypto\invoiceResponseInterface;
20
use BPT\pay\crypto\ipnDataInterface;
21
use BPT\pay\crypto\paymentInterface;
22
use BPT\receiver\callback;
23
use BPT\telegram\request;
24
use BPT\tools\tools;
25
use BPT\types\cryptoCallback;
26
use CurlHandle;
27
use function BPT\object;
28
29
class crypto {
30
    private static string $ipn_secret = '';
31
32
    private static int $round_decimal = 4;
33
34
    const API_BASE = 'https://api.nowpayments.io/v1/';
35
36
    private static CurlHandle $session;
37
38
    public static function init (string $api_key = '', string $ipn_secret = '', int $round_decimal = 4): void {
39
        self::$ipn_secret = $ipn_secret;
40
        self::$round_decimal = $round_decimal;
41
        self::$session = curl_init();
42
        curl_setopt(self::$session, CURLOPT_RETURNTRANSFER, true);
43
        curl_setopt(self::$session, CURLOPT_SSL_VERIFYPEER, 1);
44
        curl_setopt(self::$session, CURLOPT_SSL_VERIFYHOST, 2);
45
        curl_setopt(self::$session, CURLOPT_HTTPHEADER, [
46
            'X-API-KEY: ' . $api_key,
47
            'Content-Type: application/json'
48
        ]);
49
    }
50
51
    private static function execute (string $method, string $endpoint, string|array $data = '') {
52
        if (is_array($data)) {
53
            foreach ($data as $key => $value) {
54
                if (empty($value)) {
55
                    unset($data[$key]);
56
                }
57
            }
58
        }
59
60
        $session = self::$session;
61
62
        switch ($method) {
63
            case 'GET':
64
                curl_setopt($session, CURLOPT_URL, self::API_BASE . $endpoint . (!empty($data) && is_array($data) ? ('?' . http_build_query($data)) : ''));
65
                break;
66
            case 'POST':
67
                curl_setopt($session, CURLOPT_POST, true);
68
                curl_setopt($session, CURLOPT_POSTFIELDS, json_encode($data));
69
                curl_setopt($session, CURLOPT_URL, self::API_BASE . $endpoint);
70
                break;
71
            default:
72
                return false;
73
        }
74
        return json_decode(curl_exec($session));
0 ignored issues
show
It seems like curl_exec($session) can also be of type true; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

74
        return json_decode(/** @scrutinizer ignore-type */ curl_exec($session));
Loading history...
75
    }
76
77
    /**
78
     * This is a method to get information about the current state of the API. Receive true if its ok and false in otherwise
79
     * @return bool
80
     */
81
    public static function status (): bool {
82
        return self::execute('GET', 'status')->message === 'OK';
83
    }
84
85
    /**
86
     * This is a method for calculating the approximate price in cryptocurrency for a given value in Fiat currency.
87
     * You will need to provide the initial cost in the Fiat currency (amount, currency_from) and the necessary
88
     * cryptocurrency (currency_to) Currently following fiat currencies are available: usd, eur, nzd, brl, gbp.
89
     *
90
     * @param int|float $amount
91
     * @param string    $currency_from
92
     * @param string    $currency_to
93
     *
94
     * @return estimatePriceInterface|errorResponseInterface|mixed
95
     */
96
    public static function getEstimatePrice (int|float $amount, string $currency_from, string $currency_to) {
97
        return self::execute('GET', 'estimate', [
98
            'amount'        => $amount,
99
            'currency_from' => $currency_from,
100
            'currency_to'   => $currency_to
101
        ]);
102
    }
103
104
    /**
105
     * Creates payment. With this method, your customer will be able to complete the payment without leaving your website.
106
     *
107
     * @param int|float      $price_amount        the fiat equivalent of the price to be paid in crypto
108
     * @param string         $price_currency      the fiat currency in which the price_amount is specified (usd, eur, etc).
109
     * @param string         $pay_currency        the cryptocurrency in which the pay_amount is specified (btc, eth, etc).
110
     * @param int|float|null $pay_amount          the amount that users have to pay for the order stated in crypto
111
     * @param string|null    $ipn_callback_url    url to receive callbacks, should contain "http" or "https"
112
     * @param string|null    $order_id            inner store order ID
113
     * @param string|null    $order_description   inner store order description
114
     * @param string|null    $purchase_id         id of purchase for which you want to create aother payment, only used for several payments for one order
115
     * @param string|null    $payout_address      usually the funds will go to the address you specify in your Personal account. In case you want to receive funds on another address, you can specify it in this parameter.
116
     * @param string|null    $payout_currency     currency of your external payout_address, required when payout_adress is specified.
117
     * @param string|null    $payout_extra_id     extra id or memo or tag for external payout_address.
118
     * @param bool|null      $fixed_rate          boolean, can be true or false. Required for fixed-rate exchanges.
119
     * @param bool|null      $is_fee_paid_by_user boolean, can be true or false. Required for fixed-rate exchanges with all fees paid by users.
120
     *
121
     * @return invoicePaymentInterface|errorResponseInterface|mixed
122
     */
123
    public static function createPayment (int|float $price_amount, string $price_currency, string $pay_currency, int|float $pay_amount = null, string $ipn_callback_url = null, string $order_id = null, string $order_description = null, string $purchase_id = null, string $payout_address = null, string $payout_currency = null, string $payout_extra_id = null, bool $fixed_rate = null, bool $is_fee_paid_by_user = null) {
124
        return self::execute('POST', 'payment', [
125
            'price_amount'        => $price_amount,
126
            'price_currency'      => $price_currency,
127
            'pay_currency'        => $pay_currency,
128
            'pay_amount'          => $pay_amount,
129
            'ipn_callback_url'    => $ipn_callback_url,
130
            'order_id'            => $order_id,
131
            'order_description'   => $order_description,
132
            'purchase_id'         => $purchase_id,
133
            'payout_address'      => $payout_address,
134
            'payout_currency'     => $payout_currency,
135
            'payout_extra_id'     => $payout_extra_id,
136
            'fixed_rate'          => $fixed_rate,
137
            'is_fee_paid_by_user' => $is_fee_paid_by_user,
138
        ]);
139
    }
140
141
    /**
142
     * Creates payment by invoice. With this method, your customer will be able to complete the payment without leaving your website.
143
     *
144
     * @param string      $iid invoice id
145
     * @param string      $pay_currency the cryptocurrency in which the pay_amount is specified (btc, eth, etc).
146
     * @param string|null $purchase_id id of purchase for which you want to create aother payment, only used for several payments for one order
147
     * @param string|null $order_description inner store order description
148
     * @param string|null $customer_email user email to which a notification about the successful completion of the payment will be sent
149
     * @param string|null $payout_address usually the funds will go to the address you specify in your Personal account.
150
     * @param string|null $payout_extra_id extra id or memo or tag for external payout_address.
151
     * @param string|null $payout_currency currency of your external payout_address, required when payout_adress is specified.
152
     *
153
     * @return invoicePaymentInterface|errorResponseInterface|mixed
154
     */
155
    public static function createInvoicePayment (string $iid, string $pay_currency, string $purchase_id = null, string $order_description = null, string $customer_email = null, string $payout_address = null, string $payout_extra_id = null, string $payout_currency = null) {
156
        return self::execute('POST', 'invoice-payment', [
157
            'iid'               => $iid,
158
            'pay_currency'      => $pay_currency,
159
            'purchase_id'       => $purchase_id,
160
            'order_description' => $order_description,
161
            'customer_email'    => $customer_email,
162
            'payout_address'    => $payout_address,
163
            'payout_extra_id'   => $payout_extra_id,
164
            'payout_currency'   => $payout_currency
165
        ]);
166
    }
167
168
    /**
169
     * This endpoint is required to get the current estimate on the payment, and update the current estimate.
170
     * Please note! Calling this estimate before expiration_estimate_date will return the current estimate, it won’t be updated.
171
     *
172
     * @param int $paymentID payment ID, for which you want to get the estimate
173
     *
174
     * @return estimateUpdateInterface|errorResponseInterface|mixed
175
     */
176
    public static function updateEstimatePrice (int $paymentID) {
177
        return self::execute('POST', 'payment/' . $paymentID . '/update-merchant-estimate');
178
    }
179
180
    /**
181
     * Get the actual information about the payment.
182
     *
183
     * @param int $paymentID payment ID, for which you want to get the status
184
     *
185
     * @return paymentInterface|errorResponseInterface|mixed
186
     */
187
    public static function getPaymentStatus (int $paymentID) {
188
        return self::execute('GET', 'payment/' . $paymentID);
189
    }
190
191
    /**
192
     * Get the minimum payment amount for a specific pair.
193
     *
194
     * @param string $currency_from
195
     * @param string $currency_to
196
     *
197
     * @return float
198
     */
199
    public static function getMinimumPaymentAmount (string $currency_from, string $currency_to): float {
200
        return self::execute('GET', 'min-amount', [
201
            'currency_from' => $currency_from,
202
            'currency_to'   => $currency_to
203
        ])->min_amount;
204
    }
205
206
    /**
207
     * Creates an invoice. With this method, the customer is required to follow the generated url to complete the payment.
208
     *
209
     * @param int|float   $price_amount
210
     * @param string      $price_currency
211
     * @param string|null $pay_currency
212
     * @param string|null $ipn_callback_url
213
     * @param string|null $order_id
214
     * @param string|null $order_description
215
     * @param string|null $success_url
216
     * @param string|null $cancel_url
217
     *
218
     * @return invoiceResponseInterface|errorResponseInterface|mixed
219
     */
220
    public static function createInvoice (int|float $price_amount, string $price_currency, string $pay_currency = null, string $ipn_callback_url = null, string $order_id = null, string $order_description = null, string $success_url = null, string $cancel_url = null) {
221
        return self::execute('POST', 'invoice', [
222
            'price_amount'      => $price_amount,
223
            'price_currency'    => $price_currency,
224
            'pay_currency'      => $pay_currency,
225
            'ipn_callback_url'  => $ipn_callback_url,
226
            'order_id'          => $order_id,
227
            'order_description' => $order_description,
228
            'success_url'       => $success_url,
229
            'cancel_url'        => $cancel_url
230
        ]);
231
    }
232
233
    /**
234
     * This is a method for obtaining information about all cryptocurrencies available for payments.
235
     *
236
     * @return array
237
     */
238
    public static function getCurrencies (): array {
239
        return self::execute('GET', 'currencies')->currencies;
240
    }
241
242
    /**
243
     * This is a method to obtain detailed information about all cryptocurrencies available for payments.
244
     *
245
     * @return array
246
     */
247
    public static function getFullCurrencies (): array {
248
        return self::execute('GET', 'full-currencies')->currencies;
249
    }
250
251
    /**
252
     * This is a method for obtaining information about the cryptocurrencies available for payments.
253
     * Shows the coins you set as available for payments in the "coins settings" tab on your personal account.
254
     *
255
     * @return array
256
     */
257
    public static function getAvailableCheckedCurrencies (): array {
258
        return self::execute('GET', 'merchant/coins')->currencies;
259
    }
260
261
    /**
262
     * Check remote ip with nowPayments IPN ip
263
     *
264
     * @return bool
265
     */
266
    public static function isNowPayments(): bool {
267
        return in_array(tools::remoteIP(), ['51.89.194.21', '51.75.77.69', '65.21.158.36']);
268
    }
269
270
    /**
271
     * Check is IPN valid or not
272
     *
273
     * @return bool
274
     */
275
    public static function isIPNRequestValid (): bool {
276
        if (empty($_SERVER['HTTP_X_NOWPAYMENTS_SIG'])) {
277
            return false;
278
        }
279
        if (!self::isNowPayments()) {
280
            return false;
281
        }
282
        $request_json = file_get_contents('php://input');
283
        if (empty($request_json)) {
284
            return false;
285
        }
286
        $request_data = json_decode($request_json, true);
287
        ksort($request_data);
288
        $hmac = hash_hmac('sha512', json_encode($request_data, JSON_UNESCAPED_SLASHES), trim(self::$ipn_secret));
289
        return $hmac == $_SERVER['HTTP_X_NOWPAYMENTS_SIG'];
290
    }
291
292
    /**
293
     * First it will check if IPN is valid or not, if its valid , then it will return IPN data
294
     *
295
     * @return ipnDataInterface|mixed
296
     */
297
    public static function getIPN () {
298
        if (!self::isIPNRequestValid()) {
299
            return false;
300
        }
301
        return json_decode(file_get_contents('php://input'));
302
    }
303
304
    protected static function createOrder (float|int $amount, int $user_id, string $description): int|string {
305
        if (!mysql::getMysqli()) {
306
            logger::write("crypto::ezPay function used\ncreating order needed mysql connection in our mysql class", loggerTypes::ERROR);
307
            throw new bptException('MYSQL_CONNECTION_NEEDED');
308
        }
309
310
        mysql::insert('orders', ['user_id', 'type', 'amount', 'description'], [$user_id, callbackTypes::CRYPTO, $amount, $description]);
311
312
        return mysql::insertId();
313
    }
314
315
    protected static function getOrder (int $order_id): bool|object {
316
        if (!mysql::getMysqli()) {
317
            logger::write("crypto::ezPay function used\ncreating order needed mysql connection in our mysql class", loggerTypes::ERROR);
318
            throw new bptException('MYSQL_CONNECTION_NEEDED');
319
        }
320
        $order = mysql::select('orders', '*', ['id' => $order_id], 1);
321
        if ($order->num_rows < 1) {
322
            return false;
323
        }
324
        return $order->fetch_object();
325
    }
326
327
    /**
328
     * An easy way to create invoice
329
     *
330
     * Processing and authorization of ipn callbacks will be done by library
331
     *
332
     * Note : You must activate ipn in your nowPayment account, and you must set ipn_secret in the settings
333
     *
334
     * The related callback will be sent to your cryptoCallback method in handler class
335
     *
336
     * @param float|int $amount
337
     * @param int|null  $user_id
338
     * @param string    $currency
339
     * @param string    $description
340
     * @param bool      $direct_url
341
     * @param bool      $one_time_url
342
     *
343
     * @return bool|string
344
     * @throws bptException
345
     */
346
    public static function ezPay (float|int $amount, int $user_id = null, string $currency = 'usd', string $description = 'Invoice created by BPT library', bool $direct_url = false, bool $one_time_url = true): bool|string {
347
        if (empty(self::$ipn_secret)) {
348
            logger::write("crypto::ezPay function used\nyou must set ipn_secret to use this", loggerTypes::ERROR);
349
            return false;
350
        }
351
        if ($amount < 0) {
352
            logger::write("crypto::ezPay function used\namount must be bigger then 0", loggerTypes::ERROR);
353
            return false;
354
        }
355
        $user_id = $user_id ?? request::catchFields(fields::USER_ID);
356
        $order_id = self::createOrder($amount, $user_id, $description);
357
        $data = ['type' => callbackTypes::CRYPTO, 'action_type' => cryptoCallbackActionTypes::CALLBACK, 'amount' => $amount, 'currency' => $currency, 'user_id' => $user_id, 'order_id' => $order_id];
358
        $callback_url = 'https://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'] . '?data='.urlencode(callback::encodeData($data));
359
360
        $data = ['type' => callbackTypes::CRYPTO, 'action_type' => cryptoCallbackActionTypes::SUCCESS, 'amount' => $amount, 'currency' => $currency, 'user_id' => $user_id, 'order_id' => $order_id];
361
        $success_url = 'https://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'] . '?data='.urlencode(callback::encodeData($data));
362
363
        $invoice_id = self::createInvoice($amount, $currency, null, $callback_url, $order_id, $description, $success_url)->id;
364
365
        $extra_info = [
366
            'invoice_id'       => $invoice_id,
367
            'currency'         => $currency,
368
            'related_payments' => [],
369
            'total_paid'      => 0,
370
        ];
371
        if (!$direct_url && $one_time_url) {
372
            $extra_info['redirected'] = false;
373
        }
374
375
        mysql::update('orders', [
376
            'extra_info' => json_encode($extra_info),
377
        ], ['id' => $order_id], 1);
378
379
        if ($direct_url) {
380
            return 'https://nowpayments.io/payment/?iid='. $invoice_id;
381
        }
382
383
        $data = ['type' => callbackTypes::CRYPTO, 'action_type' => cryptoCallbackActionTypes::REDIRECT, 'amount' => $amount, 'currency' => $currency, 'user_id' => $user_id, 'order_id' => $order_id];
384
        return 'https://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'] . '?data='.urlencode(callback::encodeData($data));
385
    }
386
387
    /**
388
     * @internal Only for BPT self usage , Don't use it in your source!
389
     */
390
    public static function callbackProcess (array $data) {
391
        if (!isset($data['action_type'])) {
392
            return false;
393
        }
394
395
        if (!isset($data['order_id'])) {
396
            return false;
397
        }
398
399
        $action_type = $data['action_type'];
400
        $order_id = $data['order_id'];
401
402
        if ($action_type === cryptoCallbackActionTypes::REDIRECT) {
403
            $order = self::getOrder($order_id);
404
            $extra_info = json_decode($order->extra_info);
405
            if (isset($extra_info->redirected)) {
406
                if ($extra_info->redirected) {
407
                    BPT::exit('This link is one time only, Receive another link');
408
                }
409
            }
410
            $url = 'https://nowpayments.io/payment/?iid='. $extra_info->invoice_id;
411
412
            @header('Location: ' . $url);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for header(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

412
            /** @scrutinizer ignore-unhandled */ @header('Location: ' . $url);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
Are you sure the usage of header('Location: ' . $url) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
413
            die("<meta http-equiv='refresh' content='0; url=$url' /><script>window.location.href = '$url';</script>");
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
414
        }
415
416
        if ($action_type === cryptoCallbackActionTypes::CALLBACK) {
417
            $ipn = self::getIPN();
418
419
            if ($ipn->payment_status !== cryptoStatus::FINISHED && $ipn->payment_status !== cryptoStatus::PARTIALLY_PAID ) {
420
                die();
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
421
            }
422
423
            $payment_id = $ipn->payment_id;
424
425
            $payment = self::getPaymentStatus($payment_id);
426
427
            if (isset($payment->status) && !$payment->status) {
428
                die();
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
429
            }
430
431
            if ($payment->payment_status !== cryptoStatus::FINISHED && $payment->payment_status !== cryptoStatus::PARTIALLY_PAID) {
432
                die();
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
433
            }
434
435
            $order = self::getOrder($order_id);
436
            $extra_info = json_decode($order->extra_info, true);
437
            if (isset($extra_info['related_payments'][$payment_id])) {
438
                die();
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
439
            }
440
441
            $paid = round(isset($payment->actually_paid_at_fiat) && $payment->actually_paid_at_fiat > 0 ? $payment->actually_paid_at_fiat : $payment->actually_paid/$payment->pay_amount*$payment->price_amount, self::$round_decimal);
442
            $extra_info['related_payments'][$payment_id] = $paid;
443
            $extra_info['total_paid'] += $paid;
444
            mysql::update('orders', ['extra_info' => json_encode($extra_info)], ['id' => $order_id], 1);
445
446
            $callback_data = [
447
                'status' => 'unknown',
448
                'order_id' => $order_id,
449
                'user_id' => $order->user_id,
450
                'description' => $order->description,
451
                'real_amount' => $order->amount,
452
                'currency' => $extra_info['currency'],
453
                'paid_amount' => $paid,
454
                'total_paid' => $extra_info['total_paid']
455
            ];
456
457
            if ($payment->payment_status === cryptoStatus::PARTIALLY_PAID) {
458
                $callback_data['status'] = $extra_info['total_paid'] > $order->amount ? cryptoCallbackStatus::EXTRA_PAID : ($extra_info['total_paid'] == $order->amount ? cryptoCallbackStatus::FINISHED : cryptoCallbackStatus::PARTIALLY_PAID);
459
            }
460
461
            if ($payment->payment_status === cryptoStatus::FINISHED) {
462
                $callback_data['status'] = $extra_info['total_paid'] <= $order->amount ? cryptoCallbackStatus::FINISHED : cryptoCallbackStatus::EXTRA_PAID;
463
            }
464
465
            $callback_data = object(...$callback_data);
466
            $callback_data = new cryptoCallback($callback_data);
467
468
            callback::callHandler('cryptoCallback', $callback_data);
469
            return true;
470
        }
471
472
        if ($action_type === cryptoCallbackActionTypes::SUCCESS) {
473
            if (!isset($_GET['NP_id'])) {
474
                return false;
475
            }
476
477
            if (!is_numeric($_GET['NP_id'])) {
478
                return false;
479
            }
480
481
            $payment_id = $_GET['NP_id'];
482
483
            $payment = self::getPaymentStatus($payment_id);
484
485
            if (isset($payment->status) && !$payment->status) {
486
                return false;
487
            }
488
489
            if ($payment->payment_status !== cryptoStatus::FINISHED) {
490
                return false;
491
            }
492
493
            $order = self::getOrder($order_id);
494
            $extra_info = json_decode($order->extra_info);
495
496
            $callback_data = [
497
                'status' => cryptoCallbackStatus::SUCCESS,
498
                'order_id' => $order_id,
499
                'user_id' => $order->user_id,
500
                'description' => $order->description,
501
                'real_amount' => $order->amount,
502
                'currency' => $extra_info->currency,
503
                'paid_amount' => round(isset($payment->actually_paid_at_fiat) && $payment->actually_paid_at_fiat > 0 ? $payment->actually_paid_at_fiat : $payment->actually_paid/$payment->pay_amount*$payment->price_amount, self::$round_decimal),
504
                'total_paid' => $extra_info->total_paid
505
            ];
506
            $callback_data = object(...$callback_data);
507
            $callback_data = new cryptoCallback($callback_data);
508
509
            callback::callHandler('cryptoCallback', $callback_data);
510
            return true;
511
        }
512
513
        return false;
514
    }
515
}