Привет, Хабр!
Время от времени замечаю вопросы о том, как принимать платежи 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 "<pre>\n";
print_r($bitcoin->getinfo()); echo "\n";
echo "Received: ".$bitcoin->getreceivedbylabel("Your Address")."\n";
echo "</pre>";
Хорошая подборка документации находится здесь.
Приведенные выше примеры являются немного доработанными версиями перечисленных по ссылке.
Интеграция с сайтом
Осталась достаточно простая часть — настроить обработку получения платежей и генерации адресов для пополнения.
Сам процесс интеграции приема платежей криптой выглядит примерно так:
- При запросе на оплату от пользователя показываем ему адрес, куда переводить средства
- В фоновом режиме (самый простой вариант — по cron) проверяем список транзакций кошелька и при поступлении новой — начисляем средства / меняем статус оплаты.
Для генерации адресов для приёма можно использовать несколько разных подходов – создание нового адреса для каждого депозита, либо использование постоянного адреса для аккаунта пользователя.
Первый вариант более безопасен (так как сложнее отследить повторные платежи плательщиком) и прост, но может стать проблемой при использовании не очень мощного железа (каждый сгенерированный адрес увеличивает нагрузку на ноду, но заметно это становится только от нескольких миллионов адресов).
Второй вариант — более удобен в случае, если пользователи должны регистрироваться и платят часто, но при этом менее безопасен (например, можно отследить все поступления средств на аккаунт пользователя).
Для генерации адреса пополнения нужно вызвать метод getnewaddress, который в ответе вернёт новый адрес для пополнения. Для удобства можно передать аккаунт в качестве параметра (account), к которому будет привязан созданный адрес. Иногда это может быть удобно для просмотра транзакций по конкретному пользователю.
Для проверки баланса подходят несколько методов. Самый простой способ — на каждый сгенерированный адрес для пополнения создавать запись в базе данных, после чего проверять для каждой из записей через метод getreceivedbyaddress
поступления средств (не самый производительный вариант, но для большинства ситуаций подходит).
Еще хорошим вариантом будет получение информации через listtransactions о последних операциях и для них уже искать пользователя, который получает балансы. Какой вид реализации использовать — выбирать вам.
Важный момент при проверке транзакций — корректно указать кол-во подтверждений для защит от различных атак. Для большинства криптовалют обычно их можно найти в White Paper.
Для bitcoin рекомендуемое значение на данный момент — 6 подтверждений для небольших сумм. Здесь всё хорошо описано.
Примеры кода здесь уже не буду писать, потому что это сильно зависит от используемой технологии.
Заключение
Хотелось написать еще про интеграцию других кошельков, более подробно про требования к серверу и про вывод, как принимать платежи, если сайт размещен не на VPS с root доступом и т. д., но понял, что статья и так получилась большая, поэтому, если будет интересно — лучше опубликую это в одной следующих статей.
nikitasius
В биткоин клиенте нету давно аккаунтов, но архаизмы остались. Весь инет рекомендует оперировать raw транзакциями:
Всего 2 команды :)
Отправка биткоинов (если дойдет до этого) делается через rawtransactions: createrawtransaction/signrawtransaction/sendrawtransaction, в первой указываются архивом транзакции на валете, которые надо использовать (через айди и vout). Следом там указыватся архив получателей. Комиссия — это недоданные биткоины. То бишь если там в сумме 0.9, а отправлено юзерам 0.8, то 0.1 уйдет как miner fee.
обычная тоже проходит:
curl -u login:passwd -X POST -H "Content-Type: application/json" --data 'data_here' http://ip:port
у меня своя либа для нескольких валлетов (btc/eth/xmr) и 3 трейд сайтов. Самый кривой апи — это у ETH.
joochooz Автор
Да, действительно, на аккаунты лучше не опираться, как оказалось. Если кому-то интересно — то вот issue на github с доп. информацией.
А вот raw транзакции показались излишне замороченными для быстрого старта. Сам с ними много не работал, но, как понимаю, это может сократить комиссию, если есть необходимость отправки пачкой платежи на несколько адресов?
А по curl — я вообще обычно для краткости пишу -d 'data_here' ).
А можно в личку скинуть линк на либу? Или она проприетарная?
nikitasius
Все платежи надо делать через них, иначе логику (выбор транзакций для создания новой) контролирует сам клиент (bitcoin core client). Комиссия — как я писал выше, это просто недоздача. Через core клиент получаем размер комиссию через
estimatesmartfee 2
, что даст стоимость для 1кб данных чтобы максимум через через 2 блока оказаться в blockchain. Следом расчитать размер комиссии по формуле IN146+OUT34+11 bytes, где IN — колво входых адресов, а OUT — выходных. Вот по этой формуле расчитывается комиссия, чтобы за 7-8 минут получить первый конфирм. Далее — многие используют усредненные комиссии, там 30 минут — час, когда несрочно.Транзакция, сделанная руками (тестнет, там комиссия от балды, но обычно 0.01): https://live.blockcypher.com/btc-testnet/tx/ffa1b85c05f52205d432f290680b2ccb09c93d5049186cdcfbc33953deb224c2/
{"jsonrpc":"2.0","id":"23767114995403","method":"createrawtransaction","params":[[{"txid":"9f0ff312df1c879594c89f5c25c634ef11e95a61591daf295e7b931ac32b12ec","vout":0}],{"n3wm3yNqXURGzAHHjEMUzrtHtH2KKVgmLx":0.08}]}
— 0.01 ушло на комиссию сети.затем вывод подписывается через
signrawtransaction
и ее вывод отправляется черезsendrawtransaction
.Кстати, про комиссию, если например надо отправить 3 BTC, комиссия 0.05BTC, и на входе мы имеем 5 BTC, то надо сделать raw и отправить 3 получателю, 1.95 себе на новый адресс. Это, например, биткоин клиент генерит сам (когда на автомате посылает). Так же в случае с raw можно делать транзакции от транзакций с 0 уровнем подтверждения, то бишь там пришли бабки, и надо с ним сделать неспешных платежей. То можно спокойно генерить новые tx от tx, которые были отправлены недавно.
Не могу расшарить, это ядро проекта, с пулом и прочими пряниками. Кстати пул очень хорошая вещь. И через raw надо группировать платежи юзерам. Это уменьшает комиссию им + можно очень много плюшек вводить (как создана логика пула). Даже далеко ходить не надо — просто группировать все на 1 адрес это уже пул. И потраченная комиссия потом просто взымается с юзеров. А если биткоины юзеров "простаивают", то они и так уже в системе. Но это к теме статьи не относится :)
antonverov
Это довольно сложно, если учесть, что среднее время генерации блока 10 минут :)
А что у вас за проект, если не секрет?
nikitasius
Да и либ в сети полно, как нормальных, так и не очень.
Для узкоспециализорованных проектов надо как правио 5-7 вызовов реализовать из всего перечня. Далее — выходной формат унифицируется как возможно. Хотя для разных вылют разная точность.
antonverov
Для приема платежей можно запускать bitcoind с опцией -prune=551. В этом случае он будет занимать около 4Gb. И это все равно будет полная нода — с полной валидацией всех блоков локально.
joochooz Автор
Спасибо за способ, действительно может сильно снизить требования к серверу!
nikitasius
-prune=колво_мегабайт
c 8192 он занимает 9.9
Так что 4 с 551 мегабайтами — что-то там не так.
antonverov
sudo du -h
77M ./blocks/index
516M ./blocks
424K ./database
2.9G ./chainstate
3.4G .
551M — это размер под блоки.
2.9G — chainstate — база непотраченных выходов (балансов).
Всего — 3.4G
nikitasius
на тест лаптопе
В любом случае 500 метров под блоки какая-то экономия на спичках.
antonverov
Еще под кошелек (у меня его нет) мегабайт 500.
vagran
Для простого приёма оплаты по-моему проще подключить сторонний платёжный сервис типа BitPay. Некоторые из них позволяют принимать платежи в сотнях криптовалют, что самому точно не имеет смысла делать.
joochooz Автор
Согласен, если объём платежей планируется небольшой и маржа позволяет, то сторонний сервис — самый лучший вариант. Но, например, для криптовалютной биржи или обменника такой вариант не самый лучший
aml
Вообще для приёма платежей не нужно хранить секретные ключи на сервере — достаточно публичных. BIP-32 позволяет генерировать индивидуальные адреса для приёма платежей хоть под каждую транзакцию, имея только публичный (расширенный публичный, если точнее) ключ. Тогда можно и отдельный сервер не заводить для битков. Остаётся риск подмены ключа злоумышленником, но от этого и отдельный сервер не поможет.
joochooz Автор
Да, действительно, тоже хороший вариант, правда, я в своё время не смог найти быстро простого и удобного способа использовать BIP-32, плюс была необходимость в совершении автоматизированных платежей.