$b[0]; } $int = max( strlen( $a[1] ), strlen( $b[1] ) ); $frac = max( strlen( $a[2] ), strlen( $b[2] ) ); $digits_a = str_pad( $a[1], $int, '0', STR_PAD_LEFT ) . str_pad( $a[2], $frac, '0' ); $digits_b = str_pad( $b[1], $int, '0', STR_PAD_LEFT ) . str_pad( $b[2], $frac, '0' ); return $a[0] * ( strcmp( $digits_a, $digits_b ) <=> 0 ); } /** * Sign, integer digits and fraction digits of a plain decimal, without leading or * trailing zeros. * * @return array{0: int, 1: string, 2: string}|null */ public static function split_decimal( string $value ): ?array { if ( ! preg_match( '/^\s*([+-]?)(\d*)(?:\.(\d*))?\s*$/', $value, $m ) || '' === $m[2] . ( $m[3] ?? '' ) ) { return null; } $int = ltrim( $m[2], '0' ); $frac = rtrim( $m[3] ?? '', '0' ); $sign = ( '-' === $m[1] && '' !== $int . $frac ) ? -1 : 1; return [ $sign, $int, $frac ]; } /** A decimal without trailing zeros: "10.000000000000000000" -> "10". */ public static function plain( string $amount ): string { $amount = trim( $amount ); return str_contains( $amount, '.' ) ? rtrim( rtrim( $amount, '0' ), '.' ) : $amount; } /** A plain decimal above zero, trimmed; null for anything else. */ public static function positive( string $value ): ?string { $parts = self::split_decimal( $value ); if ( null === $parts || 1 !== $parts[0] || '' === $parts[1] . $parts[2] ) { return null; } return trim( $value ); } /** A field of an invoice as a string; '' when missing or not a scalar. */ public static function field( ?array $invoice, string $key ): string { $value = is_array( $invoice ) ? ( $invoice[ $key ] ?? '' ) : ''; return is_scalar( $value ) ? (string) $value : ''; } /** * A coin amount for display: "19.90997179" instead of "19.909971790000000000". * Non-numeric input is returned sanitised. */ public static function tidy( string $amount ): string { $amount = sanitize_text_field( $amount ); if ( ! is_numeric( $amount ) ) { return $amount; } return rtrim( rtrim( number_format( (float) $amount, 8, '.', '' ), '0' ), '.' ); } /** A fiat amount with two decimals and its currency: "50.00 USD". */ public static function fiat( string $amount, string $currency ): string { $amount = is_numeric( $amount ) ? number_format( (float) $amount, 2, '.', '' ) : sanitize_text_field( $amount ); return trim( $amount . ' ' . strtoupper( $currency ) ); } /** * The amounts a note and the payment screen show, as the Cryptoway pay page shows them: * invoiced is target_value_symbol rounded up ("To pay"), paid is current_value rounded * down ("Received") — to the cent for USD stablecoins, to 8 decimals for other coins — * and the remainder or overpayment is the difference of those two shown numbers. * Display only: every decision uses the exact values. * * Without current_value the deposit amount from the webhook stands in for paid. It is * net of the Cryptoway fee, so it is never set against the invoiced amount: invoiced, * remaining and overpaid stay empty then. * * A paid amount above zero that would round down to nothing (dust) is shown exactly * instead of as "0.00", and the remainder is taken from that exact figure. * * @param string $symbol Coin and network ('' when unknown). * @param string $target target_value_symbol, or ''. * @param string $paid current_value, or ''. * @param string $deposit Deposit amount from a notification, used when $paid is ''. * @return array{coin: string, base: string, invoiced: string, paid: string, rest: string, over: string} */ public static function shown( string $symbol, string $target, string $paid, string $deposit = '' ): array { $symbol = sanitize_text_field( $symbol ); $target = self::positive( $target ); $paid = self::positive( $paid ); $out = [ 'coin' => '' !== $symbol ? Coins::note_label( $symbol ) : '', 'base' => '' !== $symbol ? Coins::note_coin( $symbol ) : '', 'invoiced' => '', 'paid' => '', 'rest' => '', 'over' => '', ]; if ( '' === $symbol ) { $target = null; } $places = Coins::is_usd_stable( $symbol ) ? 2 : 8; if ( null === $paid ) { $deposit = self::positive( $deposit ); if ( null !== $deposit ) { [ $deposit_digits, $deposit_places ] = self::paid_digits( $deposit, $places ); $out['paid'] = self::format_digits( $deposit_digits, $deposit_places ); return $out; } } $invoiced_digits = null !== $target ? self::scaled_digits( $target, $places, true ) : null; $paid_digits = null; $paid_places = $places; if ( null !== $paid ) { [ $paid_digits, $paid_places ] = self::paid_digits( $paid, $places ); } if ( null !== $invoiced_digits && null !== $paid_digits ) { $invoiced_scaled = $invoiced_digits . str_repeat( '0', $paid_places - $places ); // Enough by the exact amounts, less only after rounding in opposite directions: // shown as paid in full, not as a remainder of one cent. if ( self::compare_digits( $paid_digits, $invoiced_scaled ) < 0 && ( self::compare( $paid, $target ) ?? -1 ) >= 0 ) { $paid_digits = $invoiced_digits; $paid_places = $places; $invoiced_scaled = $invoiced_digits; } $side = self::compare_digits( $paid_digits, $invoiced_scaled ); if ( $side < 0 ) { $out['rest'] = self::format_digits( self::subtract_digits( $invoiced_scaled, $paid_digits ), $paid_places ); } elseif ( $side > 0 ) { $out['over'] = self::format_digits( self::subtract_digits( $paid_digits, $invoiced_scaled ), $paid_places ); } } $out['invoiced'] = null !== $invoiced_digits ? self::format_digits( $invoiced_digits, $places ) : ''; $out['paid'] = null !== $paid_digits ? self::format_digits( $paid_digits, $paid_places ) : ''; return $out; } /** * The amounts as a note phrase: "invoiced 14.39 USDT (BEP20), paid 7.15, remaining 7.24"; * '' when neither amount is known. */ public static function phrase( array $amounts ): string { $invoiced = self::with_coin( $amounts['invoiced'], $amounts['coin'] ); if ( '' !== $amounts['invoiced'] && '' !== $amounts['paid'] ) { if ( '' !== $amounts['rest'] ) { /* translators: 1: invoiced amount with coin, 2: paid amount, 3: remaining amount */ return sprintf( __( 'invoiced %1$s, paid %2$s, remaining %3$s', 'cryptoway-button' ), $invoiced, $amounts['paid'], $amounts['rest'] ); } if ( '' !== $amounts['over'] ) { /* translators: 1: invoiced amount with coin, 2: paid amount, 3: overpaid amount */ return sprintf( __( 'invoiced %1$s, paid %2$s, overpaid %3$s', 'cryptoway-button' ), $invoiced, $amounts['paid'], $amounts['over'] ); } /* translators: 1: invoiced amount with coin, 2: paid amount */ return sprintf( __( 'invoiced %1$s, paid %2$s', 'cryptoway-button' ), $invoiced, $amounts['paid'] ); } if ( '' !== $amounts['invoiced'] ) { /* translators: %s: invoiced amount with coin */ return sprintf( __( 'invoiced %s', 'cryptoway-button' ), $invoiced ); } if ( '' !== $amounts['paid'] ) { /* translators: %s: paid amount with coin */ return sprintf( __( 'paid %s', 'cryptoway-button' ), self::with_coin( $amounts['paid'], $amounts['base'] ) ); } return ''; } public static function with_coin( string $amount, string $coin ): string { return trim( $amount . ' ' . $coin ); } /** The first letter in upper case, for a phrase that starts a sentence. */ public static function sentence_case( string $text ): string { if ( '' === $text ) { return ''; } return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( mb_substr( $text, 0, 1 ) ) . mb_substr( $text, 1 ) : ucfirst( $text ); } /** * A paid amount above zero as shown digits and their places: rounded down to $places, or, * when that would show nothing (dust), exact to its own last decimal. * * @return array{0: string, 1: int} Digits in units of 10^-places, and the places. */ private static function paid_digits( string $amount, int $places ): array { $digits = self::scaled_digits( $amount, $places, false ); if ( '0' !== $digits ) { return [ $digits, $places ]; } $exact = max( $places, strlen( ( self::split_decimal( $amount ) ?? [ 1, '', '' ] )[2] ) ); return [ self::scaled_digits( $amount, $exact, false ), $exact ]; } /** * A plain non-negative decimal as integer digits in units of 10^-$places, rounded up or * down: ("14.38007052", 2, up) -> "1439". Exact digit arithmetic, no float. */ public static function scaled_digits( string $amount, int $places, bool $up ): string { $parts = self::split_decimal( $amount ) ?? [ 1, '', '' ]; $frac = str_pad( $parts[2], $places, '0' ); $digits = ltrim( $parts[1] . substr( $frac, 0, $places ), '0' ); $digits = '' === $digits ? '0' : $digits; if ( $up && '' !== trim( (string) substr( $frac, $places ), '0' ) ) { $digits = self::add_one( $digits ); } return $digits; } /** "1439" + 1 -> "1440", on digit strings. */ private static function add_one( string $digits ): string { $i = strlen( $digits ) - 1; while ( $i >= 0 && '9' === $digits[ $i ] ) { $digits[ $i ] = '0'; --$i; } return $i < 0 ? '1' . $digits : substr_replace( $digits, (string) ( (int) $digits[ $i ] + 1 ), $i, 1 ); } /** Compare digit strings without leading zeros. */ private static function compare_digits( string $a, string $b ): int { return strlen( $a ) <=> strlen( $b ) ?: strcmp( $a, $b ) <=> 0; } /** $a - $b on digit strings, $a not below $b. */ private static function subtract_digits( string $a, string $b ): string { $b = str_pad( $b, strlen( $a ), '0', STR_PAD_LEFT ); $out = ''; $borrow = 0; for ( $i = strlen( $a ) - 1; $i >= 0; $i-- ) { $d = (int) $a[ $i ] - (int) $b[ $i ] - $borrow; $borrow = $d < 0 ? 1 : 0; $out = (string) ( $d + 10 * $borrow ) . $out; } $out = ltrim( $out, '0' ); return '' === $out ? '0' : $out; } /** * Digits in units of 10^-$places as a shown amount: two places are kept ("15.00"), * eight places lose their trailing zeros ("0.0023"). */ public static function format_digits( string $digits, int $places ): string { $digits = str_pad( $digits, $places + 1, '0', STR_PAD_LEFT ); $amount = substr( $digits, 0, -$places ) . '.' . substr( $digits, -$places ); return 2 === $places ? $amount : rtrim( rtrim( $amount, '0' ), '.' ); } /** * The coin amount a payer is asked to send: target_value_symbol rounded up the way the * pay page shows it ("To pay 14.39 USDT"), so a transfer of the shown amount covers the invoice. * * Stablecoins are asked for to the cent whatever precision Cryptoway reports for the * network (USDT on TRON reports 1 and its pay page asks 5.6 for 5.53143838): owner's * decision 2026-10-09 after a live test where 5.54 sent from a Cryptoway wallet arrived as * 5.5 — a sending-side defect on Cryptoway's part, not something the button works around. */ public static function to_pay( string $symbol, string $target ): string { $target = self::positive( $target ); if ( null === $target ) { return ''; } $places = Coins::is_usd_stable( $symbol ) ? 2 : 8; return self::format_digits( self::scaled_digits( $target, $places, true ), $places ); } }