bbrtj / btcpaywall

Self-hosted bitcoin payment system
BSD 2-Clause "Simplified" License
7 stars 3 forks source link
bitcoin cryptocurrency hacktoberfest perl

OUTDATED!

This software needs a redesign to work with newest bitcoin core due to introduction of descriptor wallets.

Bitcoin payment service

What is it?

It is a standalone payment server that uses a local Bitcoin node to check the balance of the payment address. It accepts payment requests from one or more sources, shows paywalls to end users and notifies callback addresses of successful payments.

Features

How it works?

Client accounts

Each service that is going to be allowed to request a payment through the server must first be added to the database with a command:

script/btcpaywall add-client <name> <callback address>

Where:

Each service registered as a client must keep track of who bought what. After the payment is complete and the callback resource responded with HTTP 2XX status, the job of the paywall system is done.

Note that payments for each client will still end up in the same Bitcoin HD wallet. This means that all clients should still be the same person or organization, but can be a different service.

Callback address

The callback resource should accept both POST and GET requests:

The POST callback will be used to notify your system that the payment is now complete, while GET callback will be your end users being redirected back from the payment system.

Payment requests

The payment server accepts payment requests through an API call. For a request to be validated, it much identify itself as one of the clients present in the server's database.

A payment request has a lifetime of 14 days and can end up in a couple of states:

            ┌─> pending ──┬────────────────────┐
awaiting ───┤             └─> callback_failed ─┴─> complete
            └─> timeout

The client service only gets notified of awaiting and complete states.

Payment request API

The POST /request/new action can be called by a client in order to create a new payment request. The body of this request should be JSON encoded object with following keys:

{
    "account_id": string,
    "amount": integer,
    "items": array,
    "ts": integer,
    "hash": string
}

Where:

The response of this action will be a JSON encoded object with following keys:

{
    "status": boolean,
    "data": string|array
}

Where data will contain the new request ID if the status is true, or an array of request errors if the status is false.

After making an API call and obtaining requests' identifier, it should be saved locally and the user can be redirected to /paywall/<request_id> page, where he will see the paywall.

Client authentication

Once the client account is created, the secret key must be used to create hash tokens for payment request creation API. The procedure to create a valid hash is as follows:

hash = sha256(account_id ~ amount ~ (items[0] ~ ... ~ items[n]) ~ ts ~ secret)

Where the ~ infix operator joins the strings with two characters // in between them. The output should be encoded as a hexadecimal number.

Server authentication

Once the payment is done, the payment server will run a POST request to an URL provided during account creation. This callback contains a JSON encoded object in its body, with a hash token much like the one in the previous section. The object contains the following keys:

{
    "account_id": string,
    "request_id": string,
    "ts": integer,
    "hash": string
}

Where:

hash = sha256(account_id ~ request_id ~ ts ~ secret)

Where the ~ infix operator joins the strings with two characters // in between them. The output should be encoded as a hexadecimal number.

After checking that all the data is valid, and that the hash created using client's secret matches, the given request_id should be marked as paid for, and proper resources should be granted to the user who is associated with that request. HTTP 2XX status should be returned from the action, to prevent the payment server from querying the callback URL. Returning anything else than HTTP 2XX will cause the payment server to retry the request every minute.

Coins withdrawal

This system is partly compilant with BIP44 / BIP49 / BIP84 standards, which makes the funds visible in HD wallets like Coinomi. However, due to the way HD wallets search for funds (defined in BIP44) coins may not be visible in the wallet in some cases.

Instead, it is recommended to use the withdraw command to move coins to more secure storage. This command imports all the private keys into the Bitcoin node and sends a transaction with all the funds to a given address. The usage is:

script/btcpaywall withdraw <address>

Where <address> is a proper Bitcoin address that you own. Note: there is no confirmation prompt, so make sure to use a valid address.

This command can also be automated to move coins regularly to a cold storage address, for example once a day:

0 0 * * * bash -c 'cd /path/to/project && script/btcpaywall withdraw <address>' >>/path/to/project/logs/cron.log

Installation

This project requires Perl 5.32, PostgreSQL and bitcoind to run properly.

bitcoind setup

  1. Install bitcoind
  2. Configure bitcoind for RPC. Important options are server=1, rpcuser, rpcpassword. You can also add prune=1000
  3. Enable bitcoind service in your system and let it synchronize

Code setup

  1. Clone the repository
  2. Add Carmel to Perl: cpan Carmel
  3. Go into the repository directory and install the dependencies: carmel install && carmel rollout
  4. Copy .env.example to .env. Edit database and bitcoin RPC credentials in this file.
  5. Run configuration tasks:
    • script/btcpaywall migrate --up - will create required database structure.
    • script/btcpaywall generate-master-key - will generate a new bitcoin key, which will be used to store bitcoins. Make sure to back it up and keep secure!
    • script/btcpaywall configure-node - will generate a new bitcoind wallet and tell the node to load it on startup. This wallet does not need to be backed up, it is only necessary to watch addresses.
    • script/btcpaywall add-client <name> <callback address> - will create a new client in the database. Clients are able to request payments, and each time a payment is complete, the callback address (URL) will be queried. The callback address should be a full url, that is containing the schema (usually https://)
  6. For production environments, make sure to set the APP_MODE in .env to deployment, as well as generating new APP_SECRETS (just google "random sha256")

Running the production application

Perl web server

script/btcpaywall prefork -p runs a standalone production web server for the application, ready to be put behind proxy. By default, the server will listen on port 3000.

Additionally, it needs to be set behind a supervisor that will make sure it runs persistently. Use any supervisor of your choice.

You can also use Hypnotoad. See https://docs.mojolicious.org/Mojolicious/Guides/Cookbook#Hypnotoad for more info.

Regular web server (Apache / Nginx)

Set up a web server as a proxy for Mojolicious web server. See https://docs.mojolicious.org/Mojolicious/Guides/Cookbook#Nginx or https://docs.mojolicious.org/Mojolicious/Guides/Cookbook#Apache-mod_proxy, depending on your choice.

Firewall

Set up a firewall of your choice to hide the Perl web server port (default 8080) from outside access.

Cron

Cron needs to be set up to run the request handling action in the background:

* * * * * bash -c 'cd /path/to/project && script/btcpaywall autoresolve' >>/path/to/project/logs/cron.log

FAQ

Can't redeem my coins!

If you can't see your coins in a HD wallet after entering your passphrase, use the withdraw command described above. In case of problems, see the discussion in This issue