Шаг 1: добавим пользователю баланс
Баланс + крипта + питон - это decimal. В decimal удобно получать значение из string:
from decimal import Decimal
print(Decimal(f'0.001'))
0.001
Добавим в базу данных колонку для хранения баланса пользователя, на примере django ORM, а так же сразу начислим всем пользователям примерно по 4 рубля за регистрацию:
from django.db import models
from django.contrib.auth.models import AbstractUser
from app.models import BaseModel
class User(BaseModel, AbstractUser):
balance = models.DecimalField(
max_digits=8, decimal_places=6, default=Decimal('0.000001')
)
Создадим и выполним миграции (python manage.py makemigrations && python manage.py migrate
). Теперь давайте добавим пользователю баланс в ответ на какое-то его действие на сайте. Например пользователь добавляет на карту отметку, ждет модерации администратором системы, в случае успешной модерации, получает вознаграждение на баланс. Иными словами: обновляем созданную колонку. Ну и принимаем какое-то информационное сообщение об успехе/провале своих действий. Тут напрашивается использование сигнала post_save
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.management import call_command
from decimal import Decimal
from app.models import Marker
@receiver(post_save, sender=Marker)
def save_marker(sender, instance, created, **kwargs):
# is_modertaed - boolean flag means moderation is completed
if instance.is_moderated:
# this is custom add balance django command
call_command("add_user_balance", instance)
Ну и добавим саму команду add_user_balance
для django, для этого создадим package с названием commands, а внутри файл с названием add_user_balance.py:
import json
from django.core.management.base import BaseCommand
from app.models import Marker, UserProfile
class Command(BaseCommand):
help = "Add user balance for new moderated user Marker"
def add_arguments(self, parser):
parser.add_argument("instance", nargs="+", type=Marker)
def handle(self, *args, **options):
instance = options["instance"][0]
parsed_instance = json.loads(instance.id)
profile = UserProfile.objects.get(
id=parsed_instance["profile_id"]
)
profile.user.balance += Decimal('0.00001')
profile.save()
Еще нужно добавить оповещение об обновлении баланса и успешной модерации, о том как это сделать, я писал в прошлой статье.
Аналогичный сигнал нужно повесить на событие delete у данной модели. В этом месте можно будет отправить сообщение о том, что отметка не прошла модерацию.
Шаг 2: добавить форму для оформления выплаты
Нам нужно всего два компонента - один отображает баланс, второй - позволяет заказать выплату. Первый компонент должен уметь открывать второй. Используем для этого React и библиотеку material-ui.
import React, {useState} from "react";
import Tooltip from '@mui/material/Tooltip';
import Button from '@mui/material/Button';
import "./Balance.scss";
const Balance = () => {
const [open, setOpen] = React.useState(false);
const [balance, setBalance] = React.useState(0);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const title = 'Здесь добавим описание условий наших вознаграждений';
return (
<div className="Balance">
<Tooltip title={title}>
<span>{balance} BTC</span>
</Tooltip>
<Button onClick={handleOpen}>Вывод</Button>
<Withdrawal handleClose={handleClose} open={open} />
</div>
)
};
export default Balance;
import React, {useState} from "react";
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Modal from '@mui/material/Modal';
import "./Withdrawal.scss";
const Withdrawal = ({handleClose, open}) => {
const [amount, setAmount] = React.useState('0.000100');
const [wallet, setWallet] = React.useState('');
return (
<div className="Withdrawal">
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box>
<Typography id="modal-modal-title" variant="h6" component="h2">
Вывод от 0.0001 BTC
</Typography>
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
<TextField
required
id="amount"
name="amount"
type="number"
value={amount}
variant="outlined"
inputProps={{
step: 0.00001,
min: 0.0001
}}
label="Сумма"
onChange={(e) => setAmount(
parseFloat(e.target.value).toFixed(6) || '0.0001'
)}
/>
<br/>
<TextField
required
id="wallet"
name="wallet"
value={wallet}
label="Кошелек"
defaultValue=""
onChange={(e) => setWallet(e.target.value)}
/>
<br/>
<Button
size="small"
onClick={submitWithdrawal}
variant="contained"
color="success"
>
Вывести
</Button>
</Typography>
</Box>
</Modal>
</div>
)
};
export default Withdrawal;
Большая часть аттрибутов, которые здесь используются описаны в докумментации material-ui, но на самом деле, мне кажется, что они интуитивно понятны.
Выглядит это примерно так:
Теперь остается только отправить запрос на сервер и выполнить там валидацию, чтобы проверить, что у пользователя хватает баланса, и что он ввел правильный кошелек. Для этого можно использовать разные протоколы передачи данных, удобен Web Socket. Отправляем значения amount и wallet на сервер. Можно написать функцию submitWithdrawal
, которая будет делать socket.emit
или axios.post
на сервер.
Шаг 3: валидируем данные, оформляем выплату
Осталось дело за малым - посмотреть на бэкэнде, что нам прислали и понять, верны ли данные. Для этого нужно выполнить ряд проверок, а именно две:
Ввели валидный BTC кошелек или нет?
Ввели валидную сумму и она не превышает баланс или нет?
Для валидации кошелька используем 58-символьный вариант кодирования base58. Первые 4 байта кодированного адреса кошелька являются контрольной суммой и их нужно сравнить со значением sha256 хеша. Если они совпадают, то кошелек валидный.
def decode_base58(btc_addr: str, length: int) -> bytes:
n = 0
for char in bc:
n = n * 58 + digits58.index(char)
return n.to_bytes(length, "big")
def check_btc_address(btc_addr: str) -> bool:
try:
byte_addr = decode_base58(
btc_addr=btc_addr, length=25
)
return byte_addr[-4:] == sha256(
sha256(byte_addr[:-4]).digest()
).digest()[:4]
except Exception:
return False
Сумму проверить вообще не трудно. Удостоверимся, что она не превышает баланс и не меньше чем размер суммы минимальной выплаты:
from decimal import Decimal
min_amnt = Decimal('0.0001')
def check_balance(amount: Decimal):
return amnt >= min_amnt and balance - amnt >= 0
После того, как мы сделаем вызов этих двух проверрок, мы можем получить результаты валадации и отправить их обратно на клиент, чтобы там отобразить в форме пользователю. В случае же если данные верны и пользователь реально заработал на выплату, мы отправим ему уведомление в телеграм, а администратору системы отправим уведомление в личные сообщения, в котором укажем данные пользователя, кошелек и сумму, которые он указал в заявке.
Результат работы интеграции формы выплаты можно увидеть здесь после авторизации через telegram кнопку.
Саму же выплату на первых порах проще оформлять руками, например, со своего холодного кошелька. Для этого нужно просто сделать платеж по реквизитам, а баланс пользователя списать через админку. В дальнейшем нужно будет автоматизировать этот процесс и делать автоматический платеж после модерации через blockchain.
Заключение
Реклама нынче очень дорогая. Продвижение через контекст, тизерные сети, социальные сети - все это будет требовать серьезных финансовых вливаний, чтобы принести желаемый результат. Иногда бюджет на такие рекламные компании весьма ограничен, но при этом хочется все же получать какой-то полезный трафик, чтобы приложение развивалось и не стояло на месте. Именно для этих целей отлично подходит система вознаграждений для пользователей, которая поможет избежать промежуточное звено, в виде поставщика рекламных услуг, и платить деньги непосредственно своим пользователям, финансово мотивируя их к активности на площадке. Использовать можно не только криптовалюту, но и любой другой финансовый инструмент. При этом можно гибко настроить размеры вознаграждений, что позволит точно спланировать бюджет на продвижение. Тут и волки сыты и овцы целы.