[Из песочницы] Делаем приём платежей криптовалютой своими руками
Привет, Хабр!
Время от времени замечаю вопросы о том, как принимать платежи Bitcoin на своём сайте без использования сторонних сервисов. Это достаточно просто, но надо учитывать, что есть подводные камни.
В этой статье я постараюсь максимально подробно, без акцента на каком-либо языке программирования, описать, как сделать приём платежей Bitcoin (а также, при желании — Litecoin, Dash, Bitcoin Cash, Steep, ONION и т.п.), начиная с разворачивания полной ноды и заканчивания проверкой поступления платежа.
Предварительные требования
Подразумевается, что вы имеете сайт, размещенный на VPS, к которой у вас есть root доступ, а также готовы тратить по $15+ на оплату сервера для кошелька.
Установка кошелька
Первым делом надо выделить отдельный сервер для размещения кошелька. Почему именно отдельный сервер? Отдельный сервер позволит снизить риски вывода всех ваших средств злоумышленником в случае взлома основного сайта. Ну и не стоит забывать, что для хранения blockchain требуется много места на диске (~150Gb места на диске и т. п. — подробности по ссылке).
Какие есть варианты дешевых серверов? Их масса, на мой взгляд самый адекватный — сервера от hetzner.de или chipcore.com. На chipcore.com, например, можно взять dedicated с диском на 500Gb (хватит на BTC и еще пару блокчейнов) всего за 990 рублей (примерно 17 баксов). Если знаете что-то дешевле — пишите в комментариях, очень интересно (думаю, не только мне).
После того, как вы осмысленно приняли решение о том, что хотите принимать криптовалюты на своём сайте и купили сервер (либо использовали имеющийся), надо установить bitcoin ноду.
На сервере должна быть установлена любая подходящая операционная система, самый простой вариант — Ubuntu 16.10 (да, на самом деле — это не лучший выбор, лучше установить 16.04 либо дождаться 18.04 и подождать еще пару месяцев для стабилизации). Как правило, заморачиваться с разбивкой диска нет смысла и можно смело использовать 2–4Gb на swap и остальное пускать на корневой раздел (/ или root).
После того, как сервер будет доступен, первое, что надо сделать — отключить авторизацию по паролям и настроить авторизацию по ssh ключам. Сделать это достатчно просто, есть хорошее описание от DigitalOcean.
После того, как сервер будет настроен, достаточно пары команд для того, чтобы запустить полноценную ноду кошелька
Устанавливаем bitcoind
sudo apt-add-repository ppa:bitcoin/bitcoin
sudo apt-get update
sudo apt-get install bitcoind
Это всё, что требуется для установки ноды
Настройка bitcoind
Первым делом надо создать пользователя bitcoin
:
adduser bitcoin
# везде выбираем стандартные значения и указываем какой-нибудь сложный пароль
и создать служебные директории:
mkdir -p /etc/bitcoin
chown bitcoin: /etc/bitcoin
mkdir -p /run/bitcoind
chown bitcoin: /run/bitcoind
mkdir -p /var/lib/bitcoind
chown bitcoin: /var/lib/bitcoind
Теперь осталась самая мелочь — корректно настроить ноду для приёма JSON RPC запросов.
Минимальный конфиг будет выглядеть так:
rpcuser=USERNAME
rpcpassword=PASSWORD
rpcbind=127.0.0.1
rpcallowip=127.0.0.1/32
Его надо положить по адресу /etc/bitcoin/bitcoin.conf
. И не забыть установить корректного владельца:
chown bitcoin: /etc/bitcoin/bitcoin.conf
Важно: использование USERNAME и PASSWORD — deprecated метод и немного не безопасный. Более правильно использовать rpcauth, пример можете найти по ссылке.
Далее, достаточно настроить systemd сервис для запуска ноды (в том числе после перезагрузки).
Для этого можно просто скопировать юнит файл, размещенный по адресу в директорию /etc/systemd/system/
:
wget https://raw.githubusercontent.com/bitcoin/bitcoin/master/contrib/init/bitcoind.service -O /etc/systemd/system/bitcoind.service
После чего запустить его и настроить автозапуск:
systemctl daemon-reload
systemctl start bitcoind
systemctl enable bitcoind
Теперь можно проверить рабостопособность ноды:
curl --data-binary '{"jsonrpc": "1.0", "method": "getinfo", "params": [] }' -H 'Content-Type: application/json' http://USERNAME:PASSWORD@127.0.0.1:8332/
Если всё ок — в ответ придёт примерно такое сообщение:
{"result":{"balance":0.000000000000000,"blocks":59952,"connections":48,"proxy":"","generate":false,
"genproclimit":-1,"difficulty":16.61907875185736},"error":null,"id":"curltest"}
Настройка сервера основного сайта
Осталось только настроить сервер, на котором расположен ваш сайт.
Наиболее безопасный и простой способ сделать доступным на бекенде API кошелька — прокинуть ssh туннель через сервис systemd (ну или любой другой init сервис). В случае использования systemd конфигурация сервиса максимально проста:
[Unit]
Description=SSH Tunnel BTC
After=network.target
[Service]
Restart=always
RestartSec=20
User=tunnel
ExecStart=/usr/bin/ssh -NT -o ServerAliveInterval=60 -L 127.0.0.1:8332:127.0.0.1:8332 tunnel@YOUR_SERVER_IP
[Install]
WantedBy=multi-user.target
Эту конфигурацию нужно разместить по пути /etc/systemd/system/sshtunnel-btc.service
.
После этого ставим сервис в автозапус и запускаем:
systemctl enable sshtunnel-btc.service
systemctl start sshtunnel-btc.service
Для проверки можно постучаться на порт локалхоста и проверить, что всё ок:
curl --data-binary '{"jsonrpc": "1.0", "method": "getinfo", "params": [] }' -H 'Content-Type: application/json' http://USERNAME:PASSWORD@127.0.0.1:8332/
Документация API
Со списком всех методов удобнее всего ознакомиться по ссылке.
Вызывать их очень просто даже через curl, пример запроса мы уже использовали ранее при получении информации о ноде методом getinfo.
Есть два варианта передачи параметров — массивом либо словарём.
Ниже можно увидеть примеры запроса на получение нового адреса с передачей параметров массивом и словарём:
# array
curl --data-binary '{"jsonrpc": "1.0", "method": "getnewaddress", "params": ["test"] }' -H 'Content-Type: application/json' http://USERNAME:PASSWORD@127.0.0.1:8332/
# object
curl --data-binary '{"jsonrpc": "1.0", "method": "getnewaddress", "params": {"account": "test"} }' -H 'Content-Type: application/json' http://USERNAME:PASSWORD@127.0.0.1:8332/
Простой API клиент
Для использования удобно написать простую обертку с нужными нам функциями (либо использовать имеющуюся библиотеку для вашего языка). Пример для ruby:
class Btc
class BtcError < StandardError; end
SUPPORTED_METHODS = %w(get_new_address get_addresses_by_account get_info get_net_totals get_balance get_received_by_address send_to_address list_transactions get_transaction)
class << self
def method_missing(method_name, *args)
if SUPPORTED_METHODS.include?(method_name.to_s)
send_request(method_name.to_s, args.empty? ? nil : args)
else
super
end
end
def respond_to_missing?(method_name, _include_private = false)
SUPPORTED_METHODS.include?(method_name) || super
end
protected
def host
ENV["HOST"] || "http://username:password@127.0.0.1:8332"
end
private
def send_request(method, params = nil)
uri = URI.parse(host)
request = Net::HTTP::Post.new(uri)
request.basic_auth uri.user, uri.password
request.body = JSON.dump(
jsonrpc: "1.0",
method: method.tr("_", ""),
params: params
)
req_options = { use_ssl: uri.scheme == "https" }
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
begin
result = JSON.parse(response.body)
rescue JSON::ParserError
raise BtcError.new(response.body)
end
raise BtcError.new(result["error"].to_json) if result["error"]
result["result"].is_a?(Hash) ? result["result"].deep_symbolize_keys : result["result"]
end
end
end
После этого можно удобно пользоваться им в примерно таком виде:
# run with HOST env variable (HOST=http://username:password@localhost:8332)
Btc.get_info
# => {result: {...}}
Аналогиный пример для node.js:
var http = require('http');
function BtcApi(host, port, username, password) {
this.host = host;
this.port = port;
this.username = username;
this.password = password;
};
BtcApi.methods = [
'getNewAddress',
'getAddressesByAccount',
'getInfo',
'getNetTotals',
'getBalance',
'getReceivedByAddress',
'sendToAddress',
'listTransactions',
'getTransaction',
];
BtcApi.prototype.sendRequest = function(method, params, callback) {
if (BtcApi.methods.indexOf(method) === -1) {
throw new Error('wrong method name ' + method)
};
if (callback == null) {
callback = params;
};
var body = JSON.stringify({
jsonrpc: '1.0',
method: method.toLowerCase(),
params: params,
});
var auth = 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64');
var options = {
host: this.host,
port: this.port,
path: '/',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': auth
},
};
var request = http.request(options, function (response) {
var result = '';
response.setEncoding('utf8');
response.on('data', function (chunk) {
result += chunk;
});
// Listener for intializing callback after receiving complete response
response.on('end', function () {
try {
callback(JSON.parse(result));
} catch (e) {
console.error(e);
callback(result);
}
});
});
request.write(body)
request.end()
};
for (var i = 0; i < BtcApi.methods.length; i++) {
BtcApi.prototype[BtcApi.methods[i]] = function (method) {
return function (params, callback) {
this.sendRequest(method, params, callback)
}
}(BtcApi.methods[i])
}
module.exports = BtcApi
Который можно использовать примерно следующим образом:
var BtcApi = require('./btc');
var client = new BtcApi('127.0.0.1', 8332, 'username', 'password');
client.listTransactions({ count: 1 }, function (response) {
console.log('response: ', JSON.stringify(response));
});
// {"result":[{...}]}
Для Python всё еще проще — официальный способ — использование:
from jsonrpc import ServiceProxy
access = ServiceProxy("http://user:password@127.0.0.1:8332")
access.getinfo()
Собственно, с PHP также нет никаких проблем (рекомендуется использовать http://jsonrpcphp.org/):
require_once 'jsonRPCClient.php';
$bitcoin = new jsonRPCClient('http://user:password@127.0.0.1:8332/');
echo "\n";
print_r($bitcoin->getinfo()); echo "\n";
echo "Received: ".$bitcoin->getreceivedbylabel("Your Address")."\n";
echo "
»;
Хорошая подборка документации находится здесь.
Приведенные выше примеры являются немного доработанными версиями перечисленных по ссылке.
Интеграция с сайтом
Осталась достаточно простая часть — настроить обработку получения платежей и генерации адресов для пополнения.
Сам процесс интеграции приема платежей криптой выглядит примерно так:
- При запросе на оплату от пользователя показываем ему адрес, куда переводить средства
- В фоновом режиме (самый простой вариант — по cron) проверяем список транзакций кошелька и при поступлении новой — начисляем средства / меняем статус оплаты.
Для генерации адресов для приёма можно использовать несколько разных подходов — создание нового адреса для каждого депозита, либо использование постоянного адреса для аккаунта пользователя.
Первый вариант более безопасен (так как сложнее отследить повторные платежи плательщиком) и прост, но может стать проблемой при использовании не очень мощного железа (каждый сгенерированный адрес увеличивает нагрузку на ноду, но заметно это становится только от нескольких миллионов адресов).
Второй вариант — более удобен в случае, если пользователи должны регистрироваться и платят часто, но при этом менее безопасен (например, можно отследить все поступления средств на аккаунт пользователя).
Для генерации адреса пополнения нужно вызвать метод getnewaddress, который в ответе вернёт новый адрес для пополнения. Для удобства можно передать аккаунт в качестве параметра (account), к которому будет привязан созданный адрес. Иногда это может быть удобно для просмотра транзакций по конкретному пользователю.
Для проверки баланса подходят несколько методов. Самый простой способ — на каждый сгенерированный адрес для пополнения создавать запись в базе данных, после чего проверять для каждой из записей через метод getreceivedbyaddress
поступления средств (не самый производительный вариант, но для большинства ситуаций подходит).
Еще хорошим вариантом будет получение информации через listtransactions о последних операциях и для них уже искать пользователя, который получает балансы. Какой вид реализации использовать — выбирать вам.
Важный момент при проверке транзакций — корректно указать кол-во подтверждений для защит от различных атак. Для большинства криптовалют обычно их можно найти в White Paper.
Для bitcoin рекомендуемое значение на данный момент — 6 подтверждений для небольших сумм. Здесь всё хорошо описано.
Примеры кода здесь уже не буду писать, потому что это сильно зависит от используемой технологии.
Заключение
Хотелось написать еще про интеграцию других кошельков, более подробно про требования к серверу и про вывод, как принимать платежи, если сайт размещен не на VPS с root доступом и т. д., но понял, что статья и так получилась большая, поэтому, если будет интересно — лучше опубликую это в одной следующих статей.