Skip to content

Good Practices

Withdrawal Example

Controller

php
// FundsAPI is a abstraction for external API

namespace App\Http\Controllers;

use App\Http\Requests\WalletWithdrawRequest;
use FundsAPI\Exceptions\BadRequestException;
use FundsAPI\Payout;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;

class WalletController extends Controller
{
    public function withdraw(WalletWithdrawRequest $request): JsonResponse
    {
        $amount = $request->get('amount');
        $destination = $request->get('destination');

        $fee = $this->getWithdrawFee();
        $commission = num($amount)->mul($fee['percent'] / 100)->add($fee['fixed']);

        $tx = tx($amount)
            ->commission($commission)
            ->processor('withdraw')
            ->from(auth()->user())
            ->status('awaiting_approval')
            ->after(
                /**
                 * Creating payout in after() closure allows you to avoid
                 * the situation when the transaction is created, but the payout is not.
                 */
                function (Transaction $transaction)
                use ($destination, $store, $btcPayConfig) {
                    $payout = $this->createPayout(
                        $transaction->received, // received = amount - commission
                        $destination,
                        $transaction
                    );

                    $transaction->updateMeta([
                        'payout' => [
                            'id'      => $payout->getId(),
                        ],
                        'comment' => [
                            'type'  => 'text',
                            'value' => $destination,
                        ]
                    ]);
                }
            )->commit();

        return response()->json($tx->toApi());
    }

    protected function createPayout(
        string $amount,
        string $destination,
        Transaction $tx
    ): Payout {
        try {
            $payout = FundsAPI::createPayout(
                $amount,
                $destination,
                $tx->getCurrency(),
                meta: [
                    'txid' => $tx->getId(),
                ]
            );
        } catch (BadRequestException $e) {
            $response = @json_decode(
                \Str::extractJson($e->getMessage()),
                true,
                512,
                JSON_THROW_ON_ERROR
            ) ?? [];

            $code = $response['code'] ?? $e->getCode();
            $message = $response['message'] ?? $e->getMessage();

            throw new HttpResponseException(
                response()->json([
                    'errors' => [
                        $code => [
                            $message,
                        ],
                    ],
                ], 422)
            );
        }

        return $payout;
    }
}
php
namespace App\Http\Requests\V1;

use App\Rules\BitcoinAddress;
use App\Rules\MinimumNum;
use Illuminate\Foundation\Http\FormRequest;

class WalletWithdrawRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $user = $this->user();
        return [
            'amount'      => ['required', new MinimumNum, 'lte:'.$user->balance()->value],
            'destination' => ['required', new BitcoinAddress],
        ];
    }
}

Rules

php
namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use O21\LaravelWallet\Numeric;

class MinimumNum implements ValidationRule
{
    protected Numeric $min;

    public function __construct($min = '0.00000001')
    {
        $this->min = num($min);
    }

    /**
     * Run the validation rule.
     *
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (num($value)->lessThan($this->min)) {
            $fail("The {$attribute} must be greater than {$this->min}.");
        }
    }
}
php
namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Kielabokkie\Bitcoin\AddressValidator;

class BitcoinAddress implements ValidationRule
{
    protected AddressValidator $validator;

    public function __construct()
    {
        $this->validator = new AddressValidator();
    }

    /**
     * Run the validation rule.
     *
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || ! $this->validator->isValid($value)) {
            $fail("The $attribute must be a valid bitcoin address.");
        }
    }
}

Transaction Processor

php
namespace App\Transaction\Processors;

use O21\LaravelWallet\Contracts\TransactionProcessor;
use O21\LaravelWallet\Transaction\Processors\Concerns\BaseProcessor;
use O21\LaravelWallet\Transaction\Processors\Contracts\InitialSuccess;

class WithdrawProcessor implements TransactionProcessor, InitialSuccess
{
    use BaseProcessor;
}