Skip to content

Repository files navigation

Pine Payment Server

GitHub ReleaseBuild StatusCoverage Status

This is an implementation of the Pine Payment Protocol - a second layer protocol on top of bitcoin for sending payments using human-readable payment addresses similar to email addresses. It doesn't change the way transactions are made or stored on the bitcoin network and blockchain. The server only handles anonymous user profiles and the exchange of bitcoin addresses and signed end-to-end encrypted transactions in a decentralized, secure and private manner.

Table of Contents

Dependencies

  • Node.js (v10) and Restify for creating the REST API
  • PostgreSQL or another SQL database for persistent storage
  • Redis for queuing notifications and persisting some state

Getting started

  1. Clone this repo:
    $ git clone https://github.com/blockfirm/pine-payment-server.git
    $ cd pine-payment-server
    
  2. Install dependencies:
    $ npm install
    
  3. Rename src/config.template.js to src/config.js
  4. Install a database by following the steps in Setting up the database
  5. Install Redis
  6. Create a log directory:
    $ mkdir /var/log/pine
    
  7. Start the server in development mode:
    $ npm run dev
    
  8. Or build it and run in production mode:
    $ npm run build
    $ npm start
    

Setting up the database

You can use any database of PostgreSQL (recommended), MySQL, SQLite, MariaDB, and MS SQL.

Create a new database

Once you have installed the database server you need to create a new database. This is how you can do it with Postgres:

$ sudo su - postgres
$ createdb pine_payment_server

Create a new user

Then create a new user to be used by the Pine Payment Server:

$ psql pine_payment_server
> CREATE USER pine WITH ENCRYPTED PASSWORD '<password>';
> GRANT ALL PRIVILEGES ON DATABASE pine_payment_server TO pine;
> \q

Don't forget to enter a strong password instead of <password>.

Configure Pine Payment Server

Open src/config.js and in the database section, set dialect to 'postgres' and then set the username and password for the database you created in the previous steps.

Install a client library

If you're using a database other than PostgreSQL you'll need to install a client library for that database:

  • MySQL: npm install mysql2
  • MariaDB: npm install mariadb
  • SQLite: npm install sqlite3
  • MS SQL: npm install tedious

Setting up a domain name

Configure your DNS

Create a new subdomain on the domain that you want to use for your Pine Payment Server. The new subdomain should be either an A or CNAME record with the value pine-payment-server and point to your Pine server.

Configure SSL

The Pine Payment Protocol requires all payment servers to be configured with SSL (HTTPS). The server doesn't directly support that so instead you need to setup a reverse proxy such as nginx and terminate there before forwarding to the Pine Payment Server. The easiest way to obtain an SSL certificate is by using Let's Encrypt.

Test your setup

Browse to https://pine-payment-server.example.org/v1/info and verify that you get a JSON object as a response.

(Replace example.org with your own domain name.)

Setting up push notifications

There are two ways to set up push notifications; set up your own notification service with your own app, or send them to the official Pine app through Pine's official notification service (requires API key).

Send notifications to the official Pine app

Note: This method will only work once Pine is released and has an official service to send notifications through.

This is the only way if you just want to host your own Pine Payment Server but still want to use the Pine app from the App Store and continue to get push notifications.

  1. Open src/config.js
  2. Make sure notifications.webhook is set to Pine's official Notification Service (default)
  3. Request an API key from Pine (email hi@pine.pm) and enter it in notifications.apiKey
  4. Build and restart the server

Send notifications to your own app

If you are running the app from source you will need to configure and host your own Notification Service in order to be able to receive push notifications.

  1. Go to https://github.com/blockfirm/pine-notification-service and follow the instructions
  2. Open src/config.js
  3. Set notifications.webhook to your own Notification Service
  4. Set notifications.apiKey to your obtained API key
  5. Build and restart the server

API documentation

Endpoints

MethodEndpointDescription
GET/v1/infoGet information about the server
GET/v1/usersSearch for users by username
POST/v1/usersCreate a new user
GET/v1/users/:userIdGet a user by ID
PATCH/v1/users/:userIdUpdate a user by ID
GET/v1/users/:userId/avatarGet a user's profile picture
PUT/v1/users/:userId/avatarChange a user's profile picture
POST/v1/users/:userId/device-tokensAdd a device token for receiving push notifications
DELETE/v1/users/:userId/device-tokens/:deviceTokenIdRemove a device token for a user
GET/v1/users/:userId/contact-requestsGet all contact requests for a user
POST/v1/users/:userId/contact-requestsSend a contact request to a user
DELETE/v1/users/:userId/contact-requests/:contactRequestIdRemove a contact request
GET/v1/users/:userId/contactsGet all contacts for a user
POST/v1/users/:userId/contactsAdd a contact to a user
DELETE/v1/users/:userId/contacts/:contactIdRemove a contact
GET/v1/users/:userId/addressGet a bitcoin address for a user
POST/v1/users/:userId/address/usedFlag addresses as used
GET/v1/users/:userId/messagesGet all incoming messages for a user
POST/v1/users/:userId/messagesSend a message to a user
DELETE/v1/users/:userId/messages/:messageIdRemove a message
POST/v1/users/:userId/lightning/invoicesGet a new lightning invoice for a user
GET/v1/users/:userId/lightning/invoices/unredeemedGet all unredeemed lightning invoices for a user
GET/v1/users/:userId/lightning/invoices/:invoiceIdGet status of an existing lightning invoice
POST/v1/users/:userId/lightning/invoices/:invoiceId/redeemRedeem a paid lightning invoice
GET/v1/users/:userId/lightning/capacityGet inbound lightning capacity for a contact

GET /v1/info

Returns information about the server.

Returns

{
"isOpenForRegistrations": true, (bool) Whether or not the server is open for registrations
"network": "mainnet", (string) Bitcoin network the server has been configured to be used with
"lightning": false (bool) Whether or not the server supports lightning payments
}

GET /v1/users

Endpoint to search for users by username.

Query String Parameters

NameTypeDescription
usernamestringRequired. Comma-separated list of usernames to search for. Maximum 50 usernames per request

Returns

[
{
"id": "", (string) User ID - a hash 160 of the user's public key
"publicKey": "", (string) A public key encoded as base58check
"username": "", (string) Username of the user
"displayName": "", (string) Display name of the user
"avatar": { (object) Metadata about the user's avatar. Null if the user doesn't have any
"checksum": "" (string) A checksum of the image
},
"hasLightningCapacity": true (boolean) Whether the user has any kind of lightning capacity
},
...
]

POST /v1/users

Endpoint to create a new user. Requires authentication.

Body

As JSON:

NameTypeDescription
publicKeystringA public key encoded as base58check. Used for verifying users and encrypting messages. Must match the authenticated user
extendedPublicKeystringAn extended public key for a BIP49 account. Encoded as base58check. Used for generating new BIP49 bitcoin addresses on behalf of the user
usernamestringUsername of the user. Lowercase a-z, 0-9, _, and .. Maximum 20 characters
displayNamestringOptional display name of the user. Maximum 50 characters
addressIndexintegerThe index of the current unused address as defined in BIP44. Defaults to 0

Returns

Returns the created user as JSON.

GET /v1/users/:userId

Endpoint to get a user by ID.

Returns

{
"id": "", (string) User ID - a hash 160 of the user's public key
"publicKey": "", (string) A public key encoded as base58check
"username": "", (string) Username of the user
"displayName": "", (string) Display name of the user
"avatar": { (object) Metadata about the user's avatar. Null if the user doesn't have any
"checksum": "" (string) A checksum of the image
},
"hasLightningCapacity": true (boolean) Whether the user has any kind of lightning capacity
}

PATCH /v1/users/:userId

Endpoint to update a user by ID. Requires authentication.

Body

As JSON:

NameTypeDescription
displayNamestringDisplay name of the user. Maximum 50 characters
addressIndexintegerThe index of the current unused address as defined in BIP44. Starts at 0

Returns

Returns the updated user as JSON.

GET /v1/users/:userId/avatar

Endpoint to get a user's profile picture.

Parameters

NameTypeDescription
userIdstringID of the user to get the profile picture for

Query String Parameters

NameTypeDescription
byUsernamebooleanSet to 1 to get the profile picture by username instead of user ID

Returns

A 250x250px JPEG if the user has set a profile picture.

PUT /v1/users/:userId/avatar

Endpoint to change a user's avatar. Requires authentication.

Body

As JSON:

NameTypeDescription
imagestringBase64-encoded image (bmp, gif, jpeg, png, or tiff). Image will be scaled to 250x250px and cropped if the aspect ratio is not 1:1. Maximum 50 KB

Returns

{
"checksum": "" (string) A hash of the image used for caching purposes
}

POST /v1/users/:userId/device-tokens

Endpoint to add a device token for a user to receive push notifications. A user can have multiple device tokens for multiple devices. Old device tokens will automatically be removed. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user to add a device token for

Body

As JSON:

NameTypeDescription
iosstringAn iOS device token

Returns

{
"id": "" (string) The ID of the added device token
}

DELETE /v1/users/:userId/device-tokens/:deviceTokenId

Endpoint to remove a device token for a user. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user to remove a device token for
deviceTokenIdstringID of the device token to remove

GET /v1/users/:userId/contact-requests

Endpoint to get all contact requests for a user. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user to get contact requests for

Returns

[
{
"id": "", (string) The ID of the contact request
"from": "" (string) Pine address of the user who sent the contact request
"createdAt": 1550706061 (integer) When the contact request was created (unix timestamp)
},
...
]

POST /v1/users/:userId/contact-requests

Endpoint to send a contact request to a user. Requires external authentication.

Parameters

NameTypeDescription
userIdstringID of the user to send the contact request to

Returns

If the contact request was created:

201 Created

{
"id": "", (string) The ID of the created contact request
"from": "", (string) Pine address of the user who sent the contact request
"createdAt": 1550706061 (integer) When the contact request was created (unix timestamp)
}

If the contact request was accepted immediately:

202 Accepted

Contact requests can be accepted immediately if the receiving user is waiting for an incoming contact request due to a sent contact request to the other party.

DELETE /v1/users/:userId/contact-requests/:contactRequestId

Endpoint to remove a contact request. Requires authentication. Both the receiver and the sender can remove the contact request.

Parameters

NameTypeDescription
userIdstringID of the user to remove a contact request for
contactRequestIdstringID of the contact request to remove

GET /v1/users/:userId/contacts

Endpoint to get all contacts for a user. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user to get contacts for

Returns

[
{
"id": "", (string) The ID of the contact (not user ID)
"address": "", (string) Pine address of the contact
"waitingForContactRequest": false, (boolean) Whether or not the user is waiting for the contact to accept a contact request
"createdAt": 1550706061 (integer) When the contact was added (unix timestamp)
},
...
]

POST /v1/users/:userId/contacts

Endpoint to add a contact to a user. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user to add the contact to

Body

As JSON:

NameTypeDescription
addressstringPine address of the contact to add
waitingForContactRequestbooleanWhether or not the user is waiting for the contact to accept a contact request (optional)

Returns

{
"id": "" (string) The ID of the created contact (not user ID)
}

DELETE /v1/users/:userId/contacts/:contactId

Endpoint to remove a contact. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user to remove a contact for
contactIdstringID of the contact to remove (not user ID)

GET /v1/users/:userId/address

Endpoint to get a bitcoin address for a user. Requires external authentication and that the user has the authenticated user as a contact.

Parameters

NameTypeDescription
userIdstringID of the user to get a bitcoin address for

Returns

{
"address": "" (string) A bitcoin address (`P2SH(P2WSH)`)
}

POST /v1/users/:userId/address/used

Endpoint to flag bitcoin addresses as used. Used to tell the server that an address has been used in a transaction so that it can release it for contacts who has allocated it. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user that the addresses belong to

Body

As JSON:

NameTypeDescription
addressesarrayAn array of bitcoin addresses (strings)

Returns

200 OK

GET /v1/users/:userId/messages

Endpoint to get all incoming messages (payments) for a user. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user to get messages for

Returns

[
{
"id": "", (string) The ID of the message
"from": "", (string) Pine address of the sender
"encryptedMessage": "", (string) The encrypted message (see below for details)
"signature": "", (string) A signature of the encrypted message signed by the sender (see below for details)
"invoiceId": "" (string) An invoice ID if the message was sent because of a paid lightning invoice
"createdAt": 1550706061 (integer) When the message was sent (unix timestamp)
},
...
]

POST /v1/users/:userId/messages

Endpoint to send a message (a payment) to a user. Requires external authentication.

Parameters

NameTypeDescription
userIdstringID of the user to send the message to

Body

As JSON:

NameTypeDescription
encryptedMessagestringBase64-encoded JSON string of an ECIES object/structure (see below) encrypted with the recipient's public key
signaturestringA signature of the encryptedMessage field signed by the sender (secp256k1.sign(sha256(sha256(encryptedMessage)), senderPrivateKey).toBase64() with recovery)

ECIES Structure

The message is end-to-end encrypted using Elliptic Curve Integrated Encryption (ECIES) with the same curve as bitcoin uses (secp256k1).

NameTypeDescription
ivstringInitialization vector serialized as hex (16 bytes)
ephemPublicKeystringEphemeral public key serialized as hex (65 bytes)
ciphertextstringEncrypted JSON string of a Message object/structure (see below) serialized as hex
macstringMessage authentication code serialized as hex (32 bytes)

Message Structure

NameTypeDescription
versioninteger1
typestringOnly 'payment' at the moment
dataobjectAdditional data attached to the message
data.transactionstringA signed bitcoin transaction serialized in raw format (https://bitcoin.org/en/developer-reference#raw-transaction-format)
data.networkstring'bitcoin_mainnet' or 'bitcoin_testnet'

Returns

201 Created

DELETE /v1/users/:userId/messages/:messageId

Endpoint to remove a message. Requires authentication.

Note: Deleted messages will only be flagged as deleted in the database. This is so that they could later be restored by the user if needed (WIP).

Parameters

NameTypeDescription
userIdstringID of the user to remove a message for
messageIdstringID of the message to remove

POST /v1/users/:userId/lightning/invoices

Endpoint to get a new lightning invoice for a user. Requires external authentication.

Parameters

NameTypeDescription
userIdstringID of the user to get an invoice for

Body

As JSON:

NameTypeDescription
amountstringThe invoice amount in satoshis
paymentMessagestringEncrypted payment message to send to the payee (recipient) when the invoice has been paid. See POST /v1/users/:userId/messages for more information
paymentMessageSignaturestringSignature of the payment message signed by the payer (sender). See POST /v1/users/:userId/messages for more information

Returns

As JSON:

NameTypeDescription
idstringThe ID of the created invoice
amountstringThe specified invoice amount in satoshis
paymentRequeststringLightning payment request

GET /v1/users/:userId/lightning/invoices/unredeemed

Endpoint to get all unredeemed lightning invoices for a user. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user to get unredeemed invoices for

Returns

A JSON array of:

NameTypeDescription
idstringThe ID of the invoice
messageIdstringID of the payment message that was triggered by this invoice
payerstringPine address of the user who paid the invoice
paidbooleanWhether the invoice has been paid (true)
paidAmountstringThe amount that was paid in satoshis
paidAtnumberWhen the invoice was paid (unix timestamp)
redeemedbooleanWhether the invoice has been redeemed (false)
redeemedAtnumberWhen the invoice was redeemed by its payee (unix timestamp)

GET /v1/users/:userId/lightning/invoices/:invoiceId

Endpoint to get the status of an existing invoice for a user. Only payer and payee are authorized to access an invoice. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user that owns the invoice (payee)
invoiceIdstringID of the invoice

Returns

As JSON:

NameTypeDescription
idstringThe ID of the invoice
payerstringPine address of the user who should pay the invoice
paidbooleanWhether the invoice has been paid or not
paidAmountstringThe amount that was paid in satoshis
paidAtnumberWhen the invoice was paid (unix timestamp)
redeemedbooleanWhether the invoice has been redeemed by its payee or not
redeemedAtnumberWhen the invoice was redeemed by its payee (unix timestamp)

POST /v1/users/:userId/lightning/invoices/:invoiceId/redeem

Endpoint to redeem a paid lightning invoice to user's node. Only payee is authorized to redeem an invoice. Requires authentication.

Parameters

NameTypeDescription
userIdstringID of the user that owns the invoice (payee)
invoiceIdstringID of the invoice to redeem

Body

As JSON:

NameTypeDescription
paymentRequeststringPayment request to redeem the invoice amount to

Returns

Returns 200 OK if the invoice was successfully redeemed.

GET /v1/users/:userId/lightning/capacity

Endpoint to get inbound lightning capacity for a contact. Requires external authentication.

Parameters

NameTypeDescription
userIdstringID of the user to get inbound lightning capacity for

Returns

As JSON:

NameTypeDescription
inboundstringInbound lightning capacity in satoshis

Error handling

Errors are returned as JSON in the following format:

{
"code": "<error code>",
"message": "<error message>"
}

Authentication

Some endpoints require authentication using HTTP Basic Authorization. That can be done by setting the Authorization header to the following:

Basic <credentials>

<credentials> must be replaced with a base64-encoded string of the user ID or address and a signature of the raw request body:

base64('<userId/address>:<signature>')

The User ID is a base58check-encoded hash 160 (ripemd160(sha256(publicKey))) of the user's public key. If authenticating as an external user from another server, this should be the address of the user instead.

The signature is a signature of the request path and the raw request body using the user's private key (secp256k1.sign(sha256(sha256(path + body)), privateKey).toBase64() with recovery).

Rate limiting

The API is rate limited to 1 request per second with bursts up to 10 requests. The rate limiting is based on the Token Bucket algorithm and can be configured in src/config.js at api.rateLimit.

The limit is per IP number, so if your server is behind a reverse proxy or similar you must change the config to rate limit by the X-Forwarded-For header instead of the actual IP:

rateLimit: {
...
ip: false,xff: true...}

Testing

Unit tests

The unit tests mostly test the cryptographic functions and other things related to the server setup. The API endpoints are easier to test using integration tests.

To run the unit tests, run the following command:

$ npm test

Integration tests

The integration tests test that the API endpoints are working as expected. They require that you have a testing environment set up and configured locally.

To run the integration tests, run the following command:

$ npm run test-it

Upgrading

To upgrade an existing server instance, run the following commands. Please note that you should back up your database before performing the upgrade.

  1. cd pine-payment-server/
  2. git pull
  3. npm install
  4. npx sequelize-cli db:migrate
  5. Restart the server

Contributing

Want to help us making Pine better? Great, but first read the CONTRIBUTING.md file for instructions.

Licensing

Pine Payment Server is licensed under the Apache License, Version 2.0. See LICENSE for full license text.

About

👤 Implementation of the Pine Payment Protocol.

Resources

Contributing

Stars

10 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages