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.
- Dependencies
- Getting started
- Setting up the database
- Setting up a domain name
- Setting up push notifications
- API documentation
- Testing
- Upgrading
- Contributing
- Licensing
- 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
- Clone this repo:
$ git clone https://github.com/blockfirm/pine-payment-server.git $ cd pine-payment-server
- Install dependencies:
$ npm install
- Rename
src/config.template.js
tosrc/config.js
- Install a database by following the steps in Setting up the database
- Install Redis
- Create a log directory:
$ mkdir /var/log/pine
- Start the server in development mode:
$ npm run dev
- Or build it and run in production mode:
$ npm run build $ npm start
You can use any database of PostgreSQL (recommended), MySQL, SQLite, MariaDB, and MS SQL.
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
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>
.
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.
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
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.
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.
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.)
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).
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.
- Open
src/config.js
- Make sure
notifications.webhook
is set to Pine's official Notification Service (default) - Request an API key from Pine (email [email protected]) and enter it in
notifications.apiKey
- Build and restart the server
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.
- Go to https://github.com/blockfirm/pine-notification-service and follow the instructions
- Open
src/config.js
- Set
notifications.webhook
to your own Notification Service - Set
notifications.apiKey
to your obtained API key - Build and restart the server
Method | Endpoint | Description |
---|---|---|
GET | /v1/info | Get information about the server |
GET | /v1/users | Search for users by username |
POST | /v1/users | Create a new user |
GET | /v1/users/:userId | Get a user by ID |
PATCH | /v1/users/:userId | Update a user by ID |
GET | /v1/users/:userId/avatar | Get a user's profile picture |
PUT | /v1/users/:userId/avatar | Change a user's profile picture |
POST | /v1/users/:userId/device-tokens | Add a device token for receiving push notifications |
DELETE | /v1/users/:userId/device-tokens/:deviceTokenId | Remove a device token for a user |
GET | /v1/users/:userId/contact-requests | Get all contact requests for a user |
POST | /v1/users/:userId/contact-requests | Send a contact request to a user |
DELETE | /v1/users/:userId/contact-requests/:contactRequestId | Remove a contact request |
GET | /v1/users/:userId/contacts | Get all contacts for a user |
POST | /v1/users/:userId/contacts | Add a contact to a user |
DELETE | /v1/users/:userId/contacts/:contactId | Remove a contact |
GET | /v1/users/:userId/address | Get a bitcoin address for a user |
POST | /v1/users/:userId/address/used | Flag addresses as used |
GET | /v1/users/:userId/messages | Get all incoming messages for a user |
POST | /v1/users/:userId/messages | Send a message to a user |
DELETE | /v1/users/:userId/messages/:messageId | Remove a message |
POST | /v1/users/:userId/lightning/invoices | Get a new lightning invoice for a user |
GET | /v1/users/:userId/lightning/invoices/unredeemed | Get all unredeemed lightning invoices for a user |
GET | /v1/users/:userId/lightning/invoices/:invoiceId | Get status of an existing lightning invoice |
POST | /v1/users/:userId/lightning/invoices/:invoiceId/redeem | Redeem a paid lightning invoice |
GET | /v1/users/:userId/lightning/capacity | Get inbound lightning capacity for a contact |
Returns information about the server.
{
"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
}
Endpoint to search for users by username.
Name | Type | Description |
---|---|---|
username | string | Required. Comma-separated list of usernames to search for. Maximum 50 usernames per request |
[
{
"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
},
...
]
Endpoint to create a new user. Requires authentication.
As JSON:
Name | Type | Description |
---|---|---|
publicKey | string | A public key encoded as base58check. Used for verifying users and encrypting messages. Must match the authenticated user |
extendedPublicKey | string | An extended public key for a BIP49 account. Encoded as base58check. Used for generating new BIP49 bitcoin addresses on behalf of the user |
username | string | Username of the user. Lowercase a-z , 0-9 , _ , and . . Maximum 20 characters |
displayName | string | Optional display name of the user. Maximum 50 characters |
addressIndex | integer | The index of the current unused address as defined in BIP44. Defaults to 0 |
Returns the created user as JSON.
Endpoint to get a user by ID.
{
"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
}
Endpoint to update a user by ID. Requires authentication.
As JSON:
Name | Type | Description |
---|---|---|
displayName | string | Display name of the user. Maximum 50 characters |
addressIndex | integer | The index of the current unused address as defined in BIP44. Starts at 0 |
Returns the updated user as JSON.
Endpoint to get a user's profile picture.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to get the profile picture for |
Name | Type | Description |
---|---|---|
byUsername | boolean | Set to 1 to get the profile picture by username instead of user ID |
A 250x250px JPEG if the user has set a profile picture.
Endpoint to change a user's avatar. Requires authentication.
As JSON:
Name | Type | Description |
---|---|---|
image | string | Base64-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 |
{
"checksum": "" (string) A hash of the image used for caching purposes
}
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.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to add a device token for |
As JSON:
Name | Type | Description |
---|---|---|
ios | string | An iOS device token |
{
"id": "" (string) The ID of the added device token
}
Endpoint to remove a device token for a user. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to remove a device token for |
deviceTokenId | string | ID of the device token to remove |
Endpoint to get all contact requests for a user. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to get contact requests for |
[
{
"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)
},
...
]
Endpoint to send a contact request to a user. Requires external authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to send the contact request to |
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.
Endpoint to remove a contact request. Requires authentication. Both the receiver and the sender can remove the contact request.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to remove a contact request for |
contactRequestId | string | ID of the contact request to remove |
Endpoint to get all contacts for a user. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to get contacts for |
[
{
"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)
},
...
]
Endpoint to add a contact to a user. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to add the contact to |
As JSON:
Name | Type | Description |
---|---|---|
address | string | Pine address of the contact to add |
waitingForContactRequest | boolean | Whether or not the user is waiting for the contact to accept a contact request (optional) |
{
"id": "" (string) The ID of the created contact (not user ID)
}
Endpoint to remove a contact. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to remove a contact for |
contactId | string | ID of the contact to remove (not user ID) |
Endpoint to get a bitcoin address for a user. Requires external authentication and that the user has the authenticated user as a contact.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to get a bitcoin address for |
{
"address": "" (string) A bitcoin address (`P2SH(P2WSH)`)
}
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.
Name | Type | Description |
---|---|---|
userId | string | ID of the user that the addresses belong to |
As JSON:
Name | Type | Description |
---|---|---|
addresses | array | An array of bitcoin addresses (strings) |
200 OK
Endpoint to get all incoming messages (payments) for a user. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to get messages for |
[
{
"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)
},
...
]
Endpoint to send a message (a payment) to a user. Requires external authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to send the message to |
As JSON:
Name | Type | Description |
---|---|---|
encryptedMessage | string | Base64-encoded JSON string of an ECIES object/structure (see below) encrypted with the recipient's public key |
signature | string | A 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
).
Name | Type | Description |
---|---|---|
iv | string | Initialization vector serialized as hex (16 bytes) |
ephemPublicKey | string | Ephemeral public key serialized as hex (65 bytes) |
ciphertext | string | Encrypted JSON string of a Message object/structure (see below) serialized as hex |
mac | string | Message authentication code serialized as hex (32 bytes) |
Message Structure
Name | Type | Description |
---|---|---|
version | integer | 1 |
type | string | Only 'payment' at the moment |
data | object | Additional data attached to the message |
data.transaction | string | A signed bitcoin transaction serialized in raw format (https://bitcoin.org/en/developer-reference#raw-transaction-format) |
data.network | string | 'bitcoin_mainnet' or 'bitcoin_testnet' |
201 Created
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).
Name | Type | Description |
---|---|---|
userId | string | ID of the user to remove a message for |
messageId | string | ID of the message to remove |
Endpoint to get a new lightning invoice for a user. Requires external authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to get an invoice for |
As JSON:
Name | Type | Description |
---|---|---|
amount | string | The invoice amount in satoshis |
paymentMessage | string | Encrypted payment message to send to the payee (recipient) when the invoice has been paid. See POST /v1/users/:userId/messages for more information |
paymentMessageSignature | string | Signature of the payment message signed by the payer (sender). See POST /v1/users/:userId/messages for more information |
As JSON:
Name | Type | Description |
---|---|---|
id | string | The ID of the created invoice |
amount | string | The specified invoice amount in satoshis |
paymentRequest | string | Lightning payment request |
Endpoint to get all unredeemed lightning invoices for a user. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to get unredeemed invoices for |
A JSON array of:
Name | Type | Description |
---|---|---|
id | string | The ID of the invoice |
messageId | string | ID of the payment message that was triggered by this invoice |
payer | string | Pine address of the user who paid the invoice |
paid | boolean | Whether the invoice has been paid (true ) |
paidAmount | string | The amount that was paid in satoshis |
paidAt | number | When the invoice was paid (unix timestamp) |
redeemed | boolean | Whether the invoice has been redeemed (false ) |
redeemedAt | number | When the invoice was redeemed by its payee (unix timestamp) |
Endpoint to get the status of an existing invoice for a user. Only payer and payee are authorized to access an invoice. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user that owns the invoice (payee) |
invoiceId | string | ID of the invoice |
As JSON:
Name | Type | Description |
---|---|---|
id | string | The ID of the invoice |
payer | string | Pine address of the user who should pay the invoice |
paid | boolean | Whether the invoice has been paid or not |
paidAmount | string | The amount that was paid in satoshis |
paidAt | number | When the invoice was paid (unix timestamp) |
redeemed | boolean | Whether the invoice has been redeemed by its payee or not |
redeemedAt | number | When the invoice was redeemed by its payee (unix timestamp) |
Endpoint to redeem a paid lightning invoice to user's node. Only payee is authorized to redeem an invoice. Requires authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user that owns the invoice (payee) |
invoiceId | string | ID of the invoice to redeem |
As JSON:
Name | Type | Description |
---|---|---|
paymentRequest | string | Payment request to redeem the invoice amount to |
Returns 200 OK if the invoice was successfully redeemed.
Endpoint to get inbound lightning capacity for a contact. Requires external authentication.
Name | Type | Description |
---|---|---|
userId | string | ID of the user to get inbound lightning capacity for |
As JSON:
Name | Type | Description |
---|---|---|
inbound | string | Inbound lightning capacity in satoshis |
Errors are returned as JSON in the following format:
{
"code": "<error code>",
"message": "<error message>"
}
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).
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
...
}
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
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
To upgrade an existing server instance, run the following commands. Please note that you should back up your database before performing the upgrade.
cd pine-payment-server/
git pull
npm install
npx sequelize-cli db:migrate
- Restart the server
Want to help us making Pine better? Great, but first read the CONTRIBUTING.md file for instructions.
Pine Payment Server is licensed under the Apache License, Version 2.0. See LICENSE for full license text.