Compare commits

...

45 Commits
dev ... master

Author SHA1 Message Date
dependabot[bot]
48ac61f1e7
Bump minimist from 1.2.5 to 1.2.6 (#70)
Some checks failed
Run Tests / run-test (push) Has been cancelled
Run Tests / coverage-check (push) Has been cancelled
Run Tests / lint-check (push) Has been cancelled
Run Tests / format-check (push) Has been cancelled
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-03 11:45:24 +00:00
Nicolas Dorier
7ee35f911e
Update README.md 2022-02-08 12:42:06 +09:00
Tim Akinbo
92cfe4342c
bump version to v0.2.5 (#63)
Some checks failed
Run Tests / run-test (push) Has been cancelled
Run Tests / coverage-check (push) Has been cancelled
Run Tests / lint-check (push) Has been cancelled
Run Tests / format-check (push) Has been cancelled
2021-05-11 15:34:54 +00:00
Andrew Camilleri
18f99d0958
Bump packages and run formatter (#62) 2021-05-11 15:03:09 +00:00
dependabot[bot]
1a505e4bd1
Bump hosted-git-info from 2.8.8 to 2.8.9 (#61) 2021-05-11 14:32:57 +00:00
dependabot[bot]
8e31ca5376
Bump lodash from 4.17.20 to 4.17.21 (#60) 2021-05-11 14:32:20 +00:00
dependabot[bot]
b586ac5c1e
Bump underscore from 1.12.0 to 1.12.1 (#59) 2021-05-11 14:29:12 +00:00
dependabot[bot]
8c88098652
Bump elliptic from 6.5.3 to 6.5.4 (#57) 2021-03-08 23:19:57 +00:00
Jonathan Underwood
60ef5b938a
100% Coverage (#56) 2021-02-08 01:40:17 +00:00
Aryan J
f8abf8d9a1
Fix Invoice.exceptionStatus type (#50) 2021-02-07 08:33:53 +00:00
Aryan J
67ea5e6236
Narrow set for Invoice.status type (#41) 2021-02-06 22:09:34 +00:00
Jonathan Underwood
d09292a7e9
Add Github Actions CI (#55) 2021-02-04 09:53:34 +00:00
Tim Akinbo
65618dc27e
update dependencies to resolve vulnerable packages (#54) 2021-02-03 11:03:47 +00:00
Jonathan Underwood
b99ba5409b
Docker for testing (#42) 2021-02-02 16:20:50 +00:00
pixelcookie11
76eeaff72a
Fixed typo (#53) 2021-02-02 07:02:39 +00:00
Tim Akinbo
53b02890a6
updated documentation on private key generation (#46) 2020-06-18 20:59:18 +00:00
dependabot[bot]
99d5fd4b35
Bump https-proxy-agent from 2.2.1 to 2.2.4 (#40)
Bumps [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) from 2.2.1 to 2.2.4.
- [Release notes](https://github.com/TooTallNate/node-https-proxy-agent/releases)
- [Commits](https://github.com/TooTallNate/node-https-proxy-agent/compare/2.2.1...2.2.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-05-01 04:57:45 +00:00
dependabot[bot]
0a0038c040
Bump acorn from 5.7.3 to 5.7.4 (#39)
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-05-01 04:57:31 +00:00
Tim Akinbo
67cb31d832
use correct type assertions for invoice price (#31)
* do not dedupe packages

* assert price as any as it could realisitically be either a float or string
2019-09-27 22:37:49 +00:00
Tim Akinbo
7f18735ee1
dedupe and bump version to 0.2.4 (#30) 2019-09-27 22:05:00 +00:00
Jonathan Underwood
0170ccc774 Match Create Invoice args to BTCPayServer source code (#29) 2019-09-27 11:48:46 +00:00
Nicolas Dorier
65d503a3b5
Merge pull request #26 from junderw/fixPackages
Fix package.json
2019-08-31 00:01:05 +09:00
junderw
5f1b9357d5
Fix package.json 2019-08-30 23:39:10 +09:00
Nicolas Dorier
db7c5fe825
Merge pull request #25 from btcpayserver/dependabot/npm_and_yarn/mixin-deep-1.3.2
Bump mixin-deep from 1.3.1 to 1.3.2
2019-08-30 23:18:42 +09:00
dependabot[bot]
d94d8ce89f
Bump mixin-deep from 1.3.1 to 1.3.2
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-30 14:18:27 +00:00
nicolas.dorier
d2020e83b8
Update ESLint 2019-08-30 23:12:18 +09:00
Nicolas Dorier
9df008114d
Merge pull request #24 from PatrickLemke/bugfix-documentation
get_rates fix error in documentation
2019-08-12 13:29:23 +09:00
Patrick Lemke
f7f552f663 fix error documentation 2019-08-10 22:22:34 +02:00
Tim Akinbo
564385d1ba
bump to v0.2.3 after fixing CVE-2019-10744 (#22) 2019-07-14 10:39:10 +00:00
dependabot[bot]
d6d04e660f Bump lodash from 4.17.11 to 4.17.14 (#21)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-14 10:33:06 +00:00
Nicolas Dorier
f3ea5077a7
Merge pull request #19 from junderw/fixSyntax
Add tests, fix bugs (broken), fix syntax
2019-05-21 17:41:16 +09:00
junderw
85cf06876d
Allow for ignoring sandbox 2019-05-21 10:19:19 +09:00
junderw
f7b7e9795b
remove sleep 2019-05-21 09:57:26 +09:00
junderw
8accbffb4f
Make sure it doesn't automatically delete our key :-P 2019-05-21 09:57:26 +09:00
junderw
6c370218c2
Add browser scraping to get pairing code 2019-05-21 09:57:25 +09:00
junderw
c506084a41
Add tests (Need pairingcode fix) 2019-05-21 09:57:25 +09:00
junderw
b9429cb1ee
Fix some syntax 2019-05-21 09:57:15 +09:00
Jonathan Underwood
095114ede8 Add git repo (#18)
* Add Github link to package.json

* 0.2.2
2019-05-20 15:01:50 +00:00
Tim Akinbo
7b7d81f7e9
fix documentation (#17)
* fix installation instructions

* bump version to 0.2.1
2019-05-17 03:45:42 +00:00
Jonathan Underwood
69dc50dfdc Fix TS (#15)
* Fix TS stuff

* single quote + trailing commas for tests folder

* Specific for includes... allow for ts testing later

* Export Invoice and Rate interfaces to library user

* Use prepare

* test checks format and lint
2019-05-17 02:59:33 +00:00
Christoph Ott
b102037cf8 Typescript (#13)
* Keymanagment: new Buffer() is deprecated, use new Buffer.from() instead

Also, you only generate your initial keypair once, so every time you use the key in your app, it is not derived, from the previously keypair var, but e.g. from environment variables

* Docs: Make easier to understand, use ES6 Javascript

* added invoices api

* added semicolon and replaced lets to const

* updated only slightly for still using javascript but linted with eslint

* reworked code to typescript with async await support

* going on with ts refactoring

* going on with ts refactoring

* going on with ts refactoring

* adde sample code

* fix for typescript

* using request-promise again

* Fix: removed not needed interface
2019-05-16 00:13:24 +00:00
Christoph Ott
0124a0c059 added invoices api (#10) 2019-04-18 19:39:30 +00:00
Nadav Ivgi
100d28dfbe Update README (#7)
- Update installation instructions to use `btcpayserver/node-btcpay`

- Provide simpler copy-and-paste commands for generating private key and pairing it
2019-01-01 01:31:28 +04:00
Daniel Lutz
84832111cb Fix duplicated code in docs, fix get_rates, update .gitignore, use native Promises, remove unused bn.js node module (#6)
* Docs: Fix duplicated code

Make use of keypair variable

* Update .gitignore to include packag-lock.json

This file should be commited:
https://docs.npmjs.com/files/package-lock.json

Also added some general gitignores

* Docs: Log the keypair after generating

* Remove unused bn.js node module

* Use native Javascript Promises instead of bluebird

* Fix get_rates method by applying store_id to the requests params

This basically reverts commit 0aae9b69bd

* Add newline char to gitignore
2018-10-31 13:05:07 +00:00
Daniel Lutz
9a9f108cd2 Make documenation more easier to understand (#5)
* Keymanagment: new Buffer() is deprecated, use new Buffer.from() instead

Also, you only generate your initial keypair once, so every time you use the key in your app, it is not derived, from the previously keypair var, but e.g. from environment variables

* Docs: Make easier to understand, use ES6 Javascript
2018-10-18 14:09:36 +00:00
27 changed files with 16084 additions and 263 deletions

55
.github/workflows/main_ci.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Run Tests
on: [push]
jobs:
run-test:
runs-on: ubuntu-latest
services:
btcpay:
image: junderw/btcpay-client-test-server@sha256:2ab65051329e0250cd7d5ff3f3182a95d81228e469b518beb570a288f313e5c1
ports:
- 49392:49392
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm test
coverage-check:
runs-on: ubuntu-latest
services:
btcpay:
image: junderw/btcpay-client-test-server@sha256:2ab65051329e0250cd7d5ff3f3182a95d81228e469b518beb570a288f313e5c1
ports:
- 49392:49392
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm run coverage
lint-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm run lint
format-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm run format:ci

64
.gitignore vendored
View File

@ -1,2 +1,62 @@
node_modules/
package-lock.json
# See https://help.github.com/ignore-files/ for more about ignoring files.
# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Environment variables #
#########################
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Node.js #
################
/node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# IDE #
#######
.vscode
.idea
dist/
coverage/

View File

@ -1,62 +1,71 @@
> :warning: This package is deprecated, BTCPay Server is exposing a new, more complete and easy to use API called `Greenfield`. [Check the doc](https://docs.btcpayserver.org/Development/GreenFieldExample/)
# node-btcpay
## Install
```shell
npm install https://github.com/tanjalo/node-btcpay
npm i btcpay
```
## Private key generation
* Generate and save private key:
```bash
$ node -p "require('btcpay').crypto.generate_keypair().getPrivate('hex')"
XXXXXXXXXXXXXXXXXXXXX
```
Store the printed value in a safe place, e.g. environment variables
## Pairing
* Generate and save private key:
```js
let btcpay = require('btcpay')
var keypair = btcpay.crypto.generate_keypair()
```
* Create client:
```js
var client = new btcpay.BTCPayClient('https://btcpayserverhostname', keypair)
```
After generating your private key, you have to pair your client with your BTCPay store:
* On BTCPay Server > Stores > Settings > Access Tokens > Create a new token, (leave PublicKey blank) > Request pairing
* Copy pairing code:
* Pair client to server and save returned token:
```js
client.pair_client(<pairing-code>).then(res => console.log(res))
>>> { merchant: '6gi59fB1LKxHuyY29m8tR6tRysWppk9TnuoM7wT77Las' }
```
* Recreate client:
```js
var client = new btcpay.BTCPayClient('https://btcpayserverhostname', keypair, {merchant: '6gi59fB1LKxHuyY29m8tR6tRysWppk9TnuoM7wT77Las'})
```bash
# Replace the BTCPAY_XXX envirnoment variables with your values and run:
$ [space] BTCPAY_URL=https://mydomain.com/ BTCPAY_KEY=... BTCPAY_PAIRCODE=... node -e "const btcpay=require('btcpay'); new btcpay.BTCPayClient(process.env.BTCPAY_URL, btcpay.crypto.load_keypair(Buffer.from(process.env.BTCPAY_KEY, 'hex'))).pair_client(process.env.BTCPAY_PAIRCODE).then(console.log).catch(console.error)"
# (prepend the line with a space to prevent BTCPAY_KEY from being saved to your bash history)
>>> { merchant: 'XXXXXX' }
```
Store the value of "merchant" in a safe place, e.g. environment variables
## Creating a client
## Recreating a client
After pairing your client to the store, you can recreate the client as needed and use it in your code
```js
var client = new btcpay.BTCPayClient('https://btcpayserverhostname', keypair, {merchant: '6gi59fB1LKxHuyY29m8tR6tRysWppk9TnuoM7wT77Las'})
const btcpay = require('btcpay')
const keypair = btcpay.crypto.load_keypair(new Buffer.from(<PRIVATEKEY>, 'hex'))
// Recreate client
const client = new btcpay.BTCPayClient(<BTCPAYURL>, keypair, {merchant: <MERCHANT>})
```
## Get rates
### Get rates
Fetches current rates from BitcoinAverage (using your BTCPayServer)
```js
client.get_rates('BTC_USD').then(rates => console.log(rates))
client.get_rates(['BTC_USD'], <STOREID>)
.then(rates => console.log(rates))
.catch(err => console.log(err))
```
The first argument accepts a comma-separated list of currency pair.
### Create invoice
See [BitPay Invoice API documentation](https://bitpay.com/api#resource-Invoices)
```js
client.create_invoice({price: 20, currency: 'USD'})
.then(invoice => console.log(invoice.url))
.catch(err => console.log(err))
```
The first argument accept comma-separated list of currency pair.
## Create invoice
See BitPay API documentation: https://bitpay.com/api#resource-Invoices
### Get invoice
```js
client.create_invoice({"price": 20, "currency": "USD"}).then(invoice => console.log(invoice.url))
client.get_invoice(<invoice-id>)
.then(invoice => console.log(invoice.status))
.catch(err => console.log(err))
```
## Get invoice
```js
client.get_invoice(<invoice-id>).then(invoice => console.log(invoice.status))
```
## Key Management
```js
var privateKey = keypair.getPrivate().toString('hex')
var keypair = btcpay.crypto.load_keypair(new Buffer(privateKey, "hex"))
```

161
client.js
View File

@ -1,161 +0,0 @@
let _ = require('underscore')
let Promise = require('bluebird')
let crypto = require('./cryptography')
let qs = require('querystring')
let rp = require('request-promise')
// TODO: peer certificate verification
function BTCPayClient(host, keypair, tokens) {
this.host = host
this.kp = keypair
this.tokens = tokens == undefined ? {} : tokens
this.client_id = crypto.get_sin_from_key(keypair)
this.user_agent = 'node-btcpay'
this.options = {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Accept-Version': '2.0.0'
},
json: true
}
}
BTCPayClient.prototype._create_signed_headers = function (uri, payload) {
return {
'X-Identity': Buffer.from(this.kp.getPublic().encodeCompressed()).toString('hex'),
'X-Signature': crypto.sign(uri + payload, this.kp).toString('hex')
}
}
BTCPayClient.prototype._signed_get_request = function (path, params, token) {
let _token = token ? token : _.values(this.tokens)[0]
let _params = params ? params : {}
_params['token'] = _token
let _uri = this.host + path
let _payload = '?' + qs.stringify(_params)
let _options = JSON.parse(JSON.stringify(this.options))
_.extend(_options.headers, this._create_signed_headers(_uri, _payload))
_options['uri'] = _uri
_options['qs'] = _params
return rp.get(_options).then(resp => {
return new Promise(function(resolve, reject) {
resolve(resp['data'])
})
}).catch(err => {
return new Promise(function(resolve, reject) {
reject(err)
})
})
}
BTCPayClient.prototype._signed_post_request = function (path, payload, token) {
let _token = token ? token : _.values(this.tokens)[0]
payload['token'] = _token
let _uri = this.host + path
let _payload = JSON.stringify(payload)
let _options = JSON.parse(JSON.stringify(this.options))
_.extend(_options.headers, this._create_signed_headers(_uri, _payload))
_options['uri'] = _uri
_options['body'] = payload
return rp.post(_options).then(resp => {
return new Promise(function (resolve, reject) {
resolve(resp['data'])
})
}).catch(err => {
return new Promise(function (resolve, reject) {
reject(err)
})
})
}
BTCPayClient.prototype._unsigned_request = function (path, payload) {
let _uri = this.host + path
let _options = JSON.parse(JSON.stringify(this.options))
if (payload) {
_options['uri'] = _uri
_options['body'] = payload
return rp.post(_options).then(resp => {
return new Promise(function (resolve, reject) {
resolve(resp['data'])
})
}).catch(err => {
return new Promise(function (resolve, reject) {
reject(err)
})
})
} else {
_options['uri'] = _uri
return rp.get(_options).then(resp => {
return new Promise(function (resolve, reject) {
resolve(resp['data'])
})
}).catch(err => {
return new Promise(function (resolve, reject) {
reject(err)
})
})
}
}
BTCPayClient.prototype.get_rates = function (currencyPairs) {
let _params = {currencyPairs: currencyPairs}
return this._signed_get_request('/rates', _params)
}
BTCPayClient.prototype.create_invoice = function (payload, token) {
let re = new RegExp('^[A-Z]{3,3}$')
if (!re.test(payload['currency'])) {
throw 'Currency is invalid'
}
if (isNaN(parseFloat(payload['price']))) {
throw 'Price must be a float'
}
return this._signed_post_request('/invoices', payload, token)
}
BTCPayClient.prototype.get_invoice = function (invoice_id, token) {
return this._signed_get_request('/invoices/' + invoice_id, token)
}
BTCPayClient.prototype.pair_client = function (code) {
let re = new RegExp('^\\w{7,7}$')
if (!re.test(code)) {
throw 'pairing code is not valid'
}
let payload = {id: this.client_id, pairingCode: code}
return this._unsigned_request('/tokens', payload).then(data => {
let _data = data[0]
return new Promise(function (resolve, reject) {
let _res = {}
_res[_data.facade] = _data.token
resolve(_res)
})
})
}
module.exports = BTCPayClient

View File

@ -1,45 +0,0 @@
let crypto = require('crypto')
let EC = require('elliptic').ec,
ec = new EC('secp256k1');
let bs58 = require('bs58')
const BN = require('bn.js')
let generate_keypair = function () {
let kp = ec.genKeyPair()
return kp
}
let load_keypair = function (buf) {
return ec.keyFromPrivate(buf)
}
let get_sin_from_key = function (kp) {
let pk = Buffer.from(kp.getPublic().encodeCompressed())
let version = get_version_from_compressed_key(pk)
let checksum = get_checksum_from_version(version)
return bs58.encode(Buffer.concat([version, checksum]))
}
let sign = function (message, kp) {
let digest = crypto.createHash('sha256').update(message).digest()
return Buffer.from(kp.sign(digest).toDER())
}
let get_version_from_compressed_key = function (pk) {
let sh2 = crypto.createHash('sha256').update(pk).digest()
let rp = crypto.createHash('ripemd160').update(sh2).digest()
return Buffer.concat([Buffer.from('0F', 'hex'), Buffer.from('02', 'hex'), rp])
}
let get_checksum_from_version = function (version) {
let h1 = crypto.createHash('sha256').update(version).digest()
let h2 = crypto.createHash('sha256').update(h1).digest()
return h2.slice(0, 4)
}
exports.generate_keypair = generate_keypair
exports.load_keypair = load_keypair
exports.get_sin_from_key = get_sin_from_key
exports.sign = sign

80
docker/Dockerfile Normal file
View File

@ -0,0 +1,80 @@
FROM junderw/nbxplorer-client-test-server
MAINTAINER Jonathan Underwood
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /root
# Install postgres
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main" >> /etc/apt/sources.list.d/pgdg.list
RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN apt update && \
apt install -y \
postgresql-11 \
dotnet-sdk-3.1
RUN mkdir -p /pgsql/data
RUN chmod 700 /pgsql/data && \
chown postgres:postgres /pgsql/data
RUN su postgres -c "\
cd /pgsql/data; \
/usr/lib/postgresql/11/bin/pg_ctl initdb --pgdata=/pgsql/data; \
/usr/lib/postgresql/11/bin/postgres -D /pgsql/data -h 0.0.0.0 -i & \
sleep 5; \
psql -h 127.0.0.1 -c \"CREATE DATABASE btcpayserverregtest;\"; \
sleep 3; \
/usr/lib/postgresql/11/bin/pg_ctl stop --pgdata=/pgsql/data -m f"
# Install BTCPayServer
RUN git clone https://github.com/btcpayserver/btcpayserver.git && \
cd btcpayserver/ && \
git checkout 80e46db && \
DOTNET_CLI_TELEMETRY_OPTOUT=1 dotnet build -c Release BTCPayServer/BTCPayServer.csproj
# Re-install NBXplorer because I'm an idiot
RUN cd NBXplorer/ && \
git fetch origin && \
git checkout ce2f21f && \
DOTNET_CLI_TELEMETRY_OPTOUT=1 dotnet build -c Release NBXplorer/NBXplorer.csproj
RUN sed -i 's/generate 432/generatetoaddress 432 \$\(bitcoin-cli -regtest getnewaddress\)/g' /root/run_bitcoind_service.sh
RUN apt install -y \
libx11-6 libx11-xcb1 libxcb1 libxcb-dri3-0 libxcomposite1 \
libxcursor1 libxdamage1 libxi6 libxtst6 libnss3 libcups2 \
libxss1 libdrm2 libxrandr2 libgbm1 libasound2 libatk1.0-0 \
libatk-bridge2.0-0 libpangocairo-1.0-0 libgtk-3-0
COPY \
start_everything.sh \
start_btcpay.sh \
./
RUN chmod +x start_everything.sh && \
chmod +x start_btcpay.sh
RUN mkdir -p /root/registerAdmin && \
cd /root/registerAdmin && \
npm init -y && \
npm install puppeteer@3.0.1 btcpay@0.2.4
COPY \
registerAdmin.js \
./registerAdmin
RUN /root/start_everything.sh >/dev/null 2>&1 & \
sleep 20 && \
BTCPAY_IGNORE_SANDBOX_ERROR=1 node /root/registerAdmin/registerAdmin.js
RUN apt-get purge -y \
libx11-6 libx11-xcb1 libxcb1 libxcb-dri3-0 libxcomposite1 \
libxcursor1 libxdamage1 libxi6 libxtst6 libnss3 libcups2 \
libxss1 libdrm2 libxrandr2 libgbm1 libasound2 libatk1.0-0 \
libatk-bridge2.0-0 libpangocairo-1.0-0 libgtk-3-0 && \
apt-get autoremove -y && \
rm -rf /root/registerAdmin
ENTRYPOINT ["/root/start_everything.sh"]
EXPOSE 18271
EXPOSE 23828
EXPOSE 49392
EXPOSE 8080

30
docker/README.md Normal file
View File

@ -0,0 +1,30 @@
# Docker Image
## Build
```bash
cd ./docker
./build.sh btcpay-client-test-server
```
## OR Pull from docker hub
```bash
docker pull junderw/btcpay-client-test-server
```
## run on localhost then run tests
```bash
# If you built it
docker run -d -p 49392:49392 btcpay-client-test-server
# OR, if you pulled from docker hub
docker run -d -p 49392:49392 junderw/btcpay-client-test-server
# Wait 6 seconds before running the tests
# Install deps
npm install
# Run tests
npm test
```

25
docker/build.sh Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
TEMP=$1-temp
CONT=tempcont$(date +%s)
docker build $DIR -t $TEMP
docker run --name $CONT $TEMP &
sleep 120
docker exec $CONT bash -c "bitcoin-cli -regtest sendtoaddress \$(cat /root/btcpay.address.p2wpkh) 0.1337"
docker exec $CONT bash -c "bitcoin-cli -regtest sendtoaddress \$(cat /root/btcpay.address.p2shp2wpkh) 0.1337"
docker exec $CONT bash -c "bitcoin-cli -regtest generatetoaddress 6 mwhw4fySPWHZ4CUT1QHXWqAdE2wj9pJsa4"
sleep 20
docker exec $CONT bash -c "kill \$(pgrep BTCPayServer)"
docker exec $CONT su postgres -c "/usr/lib/postgresql/11/bin/pg_ctl stop --pgdata=/pgsql/data -m f"
docker exec $CONT bash -c "kill \$(pgrep NBXplorer)"
docker exec $CONT bash -c "kill \$(pgrep node)"
docker exec $CONT bitcoin-cli -regtest stop
docker exec $CONT bash -c "sed -i 's/\/usr\/bin\/bitcoin-cli -regtest generatetoaddress 432 mwhw4fySPWHZ4CUT1QHXWqAdE2wj9pJsa4//g' /root/run_bitcoind_service.sh"
docker exec $CONT bash -c "sed -i 's/sleep 2//g' /root/run_bitcoind_service.sh"
docker exec $CONT bash -c "sed -i 's/\/cookie/\/tokens/g' /root/stopAndCookie/stopAndCookie.js"
docker exec $CONT bash -c "sed -i 's/\/datadir\/RegTest\/.cookie/\/root\/btcpaytokens/g' /root/stopAndCookie/stopAndCookie.js"
docker exec $CONT bash -c "sed -i 's/sleep infinity/node \/root\/stopAndCookie\/stopAndCookie.js \&\nsleep infinity/g' /root/start_everything.sh"
docker stop $CONT
docker commit $CONT $TEMP
$DIR/build_stage2.sh $1

27
docker/build_stage2.sh Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
TEMP=$1-temp
CONT=tempcont$(date +%s)
# Start up the temp image and wait for BTCPayServer to calm down, then shutdown
docker run --name $CONT $TEMP &
sleep 15
docker exec $CONT bash -c "kill \$(pgrep BTCPayServer)"
docker exec $CONT su postgres -c "/usr/lib/postgresql/11/bin/pg_ctl stop --pgdata=/pgsql/data -m f"
docker exec $CONT bash -c "kill \$(pgrep NBXplorer)"
docker exec $CONT bash -c "kill \$(pgrep node)"
docker exec $CONT bitcoin-cli -regtest stop
# New checkout (for testing)
# docker exec $CONT bash -c "cd /root/btcpayserver; \
# git fetch origin; \
# git checkout 79c70b3;"
# Replace login files
docker exec $CONT bash -c "sed -i 's/input asp-for=\"Email\"/input asp-for=\"Email\" value=\"test\@example.com\"/g' /root/btcpayserver/BTCPayServer/Views/Account/Login.cshtml"
docker exec $CONT bash -c "sed -i 's/input asp-for=\"Password\"/input asp-for=\"Password\" value=\"satoshinakamoto\"/g' /root/btcpayserver/BTCPayServer/Views/Account/Login.cshtml"
# Rebuild BTCPayServer
docker exec $CONT bash -c "cd /root/btcpayserver; DOTNET_CLI_TELEMETRY_OPTOUT=1 dotnet build -c Release BTCPayServer/BTCPayServer.csproj"
sleep 2
docker stop $CONT
docker commit $CONT $1

237
docker/registerAdmin.js Normal file
View File

@ -0,0 +1,237 @@
const puppeteer = require('puppeteer')
const fs = require('fs')
const btcpay = require('btcpay')
const IGNORE_SANDBOX_ERROR = process.env['BTCPAY_IGNORE_SANDBOX_ERROR'];
const URL = 'http://127.0.0.1:49392';
const STORENAME = 'Test Store for testing';
const USER_NAME = 'test@example.com';
const PASSWORD = 'satoshinakamoto';
const WINDOW_WIDTH = 1920
const WINDOW_HEIGHT = 1080
const HEADLESS = true
// const sleep = ms => new Promise(r => setTimeout(r,ms))
function writeAddress(address, type) {
fs.writeFileSync('/root/btcpay.address.' + type, address)
}
function writeTokens(tokens) {
fs.writeFileSync('/root/btcpaytokens', tokens)
}
async function getElValue(page, qs) {
const idElement = await page.$$(qs);
return idElement[0]
.getProperty('value')
.then(v => v.jsonValue());
}
async function main() {
const browser = await puppeteer.launch({
headless: HEADLESS,
args: ['--window-size=' + WINDOW_WIDTH + ',' + WINDOW_HEIGHT],
}).then(
v => v, // if success, passthrough
// if error, check for env and ignore sandbox and warn.
err => {
if (IGNORE_SANDBOX_ERROR === '1') {
console.warn(
'WARNING!!! Error occurred, Chromium will be started ' +
"without sandbox. This won't guarantee success.",
);
return puppeteer.launch({
headless: HEADLESS,
ignoreDefaultArgs: ['--disable-extensions'],
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--window-size=' + WINDOW_WIDTH + ',' + WINDOW_HEIGHT,
],
});
} else {
console.warn(
'If "No usable sandbox!" error, retry test with ' +
'BTCPAY_IGNORE_SANDBOX_ERROR=1',
);
throw err;
}
},
);
const page = (await browser.pages())[0];
await page.setViewport({ width: WINDOW_WIDTH, height: WINDOW_HEIGHT });
await page.goto(URL + '/Account/Register');
// Set up admin account
await page.type('#Email', USER_NAME);
await page.type('#Password', PASSWORD);
await page.type('#ConfirmPassword', PASSWORD);
await page.click('#RegisterButton');
await page.waitForSelector('#Stores');
// On main screen, setup first store
await page.click('#Stores');
await page.waitForSelector('#CreateStore');
await page.click('#CreateStore');
await page.type('#Name', STORENAME);
await page.click('#Create');
await page.waitForSelector('#Id');
const STORE_ID = await getElValue(page, '#Id');
// Add new wallet
await page.click('#ModifyBTC');
await page.waitForSelector('#import-from-btn');
await page.click('#import-from-btn');
await page.waitForSelector('#nbxplorergeneratewalletbtn');
await page.click('#nbxplorergeneratewalletbtn');
await page.waitForSelector('#btn-generate');
await page.evaluate(() => {
document.querySelector('#SavePrivateKeys').click();
document.querySelector('#btn-generate').click();
});
await page.waitForSelector('#confirm');
await page.evaluate(() => {
document.querySelector('#confirm').click();
document.querySelector('#submit').click();
});
await page.waitForSelector('#PayJoinEnabled');
// Enable PayJoin
await page.evaluate(() => {
document.querySelector('#PayJoinEnabled').click();
document.querySelector('#Save').click();
});
const alertQS = 'div.alert.alert-success.alert-dismissible'
await page.waitForSelector(alertQS);
// Get first address and write to disk
await page.click('#Wallets');
const firstManageLinkQS = 'table.table.table-sm.table-responsive-md > tbody > ' +
'tr:nth-of-type(1) > td:nth-of-type(4) > a:nth-of-type(1)'
await page.waitForSelector(firstManageLinkQS);
await page.click(firstManageLinkQS);
await page.waitForSelector('#WalletReceive');
await page.click('#WalletReceive');
await page.waitForSelector('#generateButton');
await page.click('#generateButton');
await page.waitForSelector('#vue-address');
const address = await getElValue(page, '#vue-address');
writeAddress(address, 'p2wpkh');
// On main screen, setup second store
await page.goto(URL + '/stores');
await page.waitForSelector('#CreateStore');
await page.click('#CreateStore');
await page.type('#Name', STORENAME + ' 2');
await page.click('#Create');
await page.waitForSelector('#Id');
const STORE_ID2 = await getElValue(page, '#Id');
// Add new wallet
await page.click('#ModifyBTC');
await page.waitForSelector('#import-from-btn');
await page.click('#import-from-btn');
await page.waitForSelector('#nbxplorergeneratewalletbtn');
await page.click('#nbxplorergeneratewalletbtn');
await page.waitForSelector('#btn-generate');
await page.evaluate(() => {
document.querySelector('#ScriptPubKeyType').value = 'SegwitP2SH';
document.querySelector('#SavePrivateKeys').click();
document.querySelector('#btn-generate').click();
});
await page.waitForSelector('#confirm');
await page.evaluate(() => {
document.querySelector('#confirm').click();
document.querySelector('#submit').click();
});
await page.waitForSelector('#PayJoinEnabled');
// Enable PayJoin
await page.evaluate(() => {
document.querySelector('#PayJoinEnabled').click();
document.querySelector('#Save').click();
});
await page.waitForSelector(alertQS);
// Get first address and write to disk
await page.click('#Wallets');
const secondManageLinkQS = 'table.table.table-sm.table-responsive-md > tbody > ' +
'tr:nth-of-type(2) > td:nth-of-type(4) > a:nth-of-type(1)'
await page.waitForSelector(secondManageLinkQS);
await page.click(secondManageLinkQS);
await page.waitForSelector('#WalletReceive');
await page.click('#WalletReceive');
await page.waitForSelector('#generateButton');
await page.click('#generateButton');
await page.waitForSelector('#vue-address');
const address2 = await getElValue(page, '#vue-address');
writeAddress(address2, 'p2shp2wpkh');
const tokens = {};
// Get token for store 1
await page.goto(URL + '/stores/' + STORE_ID + '/Tokens/Create');
await page.waitForSelector('input#Label');
await page.waitForSelector('[type="submit"]');
await page.type('#Label', 'token1');
await page.click('[type="submit"]');
await page.waitForSelector('button[type="submit"]');
await page.click('[type="submit"]');
await page.waitForSelector('div.alert.alert-success.alert-dismissible');
const contents1 = await page.evaluate(() => {
const el = document.querySelector(
'div.alert.alert-success.alert-dismissible',
);
if (el === null) return '';
return el.innerHTML;
});
const pairingCode1 = (contents1.match(
/Server initiated pairing code: (\S{7})/,
) || [])[1];
const kp1 = btcpay.crypto.generate_keypair()
const client1 = new btcpay.BTCPayClient(URL, kp1)
const token1 = await client1.pair_client(pairingCode1)
tokens.p2wpkh = token1;
// Get token for store 2
await page.goto(URL + '/stores/' + STORE_ID2 + '/Tokens/Create');
await page.waitForSelector('input#Label');
await page.waitForSelector('[type="submit"]');
await page.type('#Label', 'token2');
await page.click('[type="submit"]');
await page.waitForSelector('button[type="submit"]');
await page.click('[type="submit"]');
await page.waitForSelector('div.alert.alert-success.alert-dismissible');
const contents2 = await page.evaluate(() => {
const el = document.querySelector(
'div.alert.alert-success.alert-dismissible',
);
if (el === null) return '';
return el.innerHTML;
});
const pairingCode2 = (contents2.match(
/Server initiated pairing code: (\S{7})/,
) || [])[1];
const kp2 = btcpay.crypto.generate_keypair()
const client2 = new btcpay.BTCPayClient(URL, kp2)
const token2 = await client2.pair_client(pairingCode2)
tokens.p2shp2wpkh = token2;
tokens.privateKeys = {
p2wpkh: kp1.getPrivate('hex'),
p2shp2wpkh: kp2.getPrivate('hex'),
}
writeTokens(JSON.stringify(tokens))
}
main()
.then(() => {
process.exit(0);
})
.catch(err => {
console.error(err)
process.exit(1);
});

14
docker/start_btcpay.sh Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
cd /root/btcpayserver
export BTCPAY_POSTGRES="User ID=postgres;Host=127.0.0.1;Port=5432;Database=btcpayserverregtest"
export BTCPAY_NETWORK=regtest
export BTCPAY_BIND=0.0.0.0:49392
export BTCPAY_ROOTPATH=/
export BTCPAY_CHAINS=btc
export BTCPAY_BTCEXPLORERURL=http://127.0.0.1:23828
export BTCPAY_BTCEXPLORERCOOKIEFILE=/datadir/RegTest/.cookie
# Run NBXplorer
dotnet run --no-launch-profile --no-build -c Release -p "BTCPayServer/BTCPayServer.csproj" -- $@

30
docker/start_everything.sh Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# trap ctrl-c and call ctrl_c()
trap ctrl_c INT
function ctrl_c() {
echo "** Trapped CTRL-C"
exit 1
}
# Run regtest app for using regtest-client
/root/run_regtest_app.sh &
disown
# Run NBXplorer
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
$DIR/start_nbx.sh &
disown
# Run postgres
chmod 777 /root
su postgres -c "/usr/lib/postgresql/11/bin/postgres -D /pgsql/data -h 0.0.0.0 -i" &
disown
sleep 5
# Run BTCPayServer
$DIR/start_btcpay.sh &
disown
sleep infinity

View File

@ -1,5 +0,0 @@
let crypto = require('./cryptography')
let BTCPayClient = require('./client')
exports.crypto = crypto
exports.BTCPayClient = BTCPayClient

31
jest.json Normal file
View File

@ -0,0 +1,31 @@
{
"moduleFileExtensions": [
"ts",
"js",
"json"
],
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testEnvironment" : "node",
"testRegex": "/tests/.*\\.(test|spec)\\.(ts)$",
"testURL": "http://localhost/",
"coverageThreshold": {
"global": {
"statements": 90,
"branches": 90,
"functions": 90,
"lines": 90
}
},
"collectCoverageFrom": [
"src/**/*.ts",
"!**/node_modules/**",
"!**/vendor/**"
],
"coverageReporters": [
"lcov",
"text"
],
"verbose": true
}

14729
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,55 @@
{
"name": "btcpay",
"version": "0.1.0",
"version": "0.2.5",
"description": "A nodejs client implementation for BTCPay",
"main": "index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "rimraf dist && tsc -p ./tsconfig.json",
"coverage": "npm run unit -- --coverage",
"format": "npm run prettier -- --write",
"format:ci": "npm run prettier -- --check",
"lint": "tslint -p tsconfig.json -c tslint.json",
"prepare": "npm run build",
"prettier": "prettier src/**/*.ts tests/**/*.spec.ts --single-quote --trailing-comma all",
"test": "npm run format:ci && npm run lint && npm run coverage",
"unit": "jest --config=jest.json --runInBand"
},
"author": "Tim Akinbo <tim@tanjalo.com>",
"contributors": [
"Tim Akinbo <tim@tanjalo.com>",
"Christoph Ott <christoph.ott@lean-coders.at>",
"Jonathan Underwood <junderwood@bitcoinbank.co.jp>"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/btcpayserver/node-btcpay.git"
},
"dependencies": {
"bluebird": "^3.5.1",
"bn.js": "^4.11.8",
"bs58": "^4.0.1",
"elliptic": "^6.4.0",
"request": "^2.85.0",
"request-promise": "^4.2.2",
"underscore": "^1.8.3"
"elliptic": "^6.5.4",
"request": "^2.88.2",
"request-promise": "^4.2.6",
"underscore": "^1.13.1"
},
"devDependencies": {
"@types/bs58": "^4.0.1",
"@types/elliptic": "^6.4.12",
"@types/jest": "^26.0.23",
"@types/node": "^15.0.2",
"@types/puppeteer": "^5.4.3",
"@types/request": "^2.48.5",
"@types/request-promise": "^4.1.47",
"@types/underscore": "^1.11.2",
"jest": "^26.6.3",
"prettier": "^2.3.0",
"puppeteer": "^9.1.1",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.6",
"tslint": "^5.20.1",
"typescript": "^4.2.4"
}
}

153
src/core/client.ts Normal file
View File

@ -0,0 +1,153 @@
import * as elliptic from 'elliptic';
import * as qs from 'querystring';
import * as rp from 'request-promise';
import * as _ from 'underscore';
import {
CreateInvoiceArgs,
GetInvoicesArgs,
PairClientResponse,
} from '../models/client';
import { Cryptography as crypto } from './cryptography';
import { Invoice } from '../models/invoice';
import { Rate } from '../models/rate';
export class BTCPayClient {
private clientId: string;
private userAgent: string;
private options: any;
constructor(
private host: string,
private kp: elliptic.ec.KeyPair,
private tokens: any = {},
) {
this.host = this.host.replace(/\/+$/, '');
this.clientId = crypto.get_sin_from_key(this.kp);
this.userAgent = 'node-btcpay';
this.options = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'User-Agent': this.userAgent,
'X-Accept-Version': '2.0.0',
},
json: true,
};
}
public async pair_client(code: string): Promise<PairClientResponse> {
const re = new RegExp('^\\w{7}$');
if (!re.test(code)) {
throw new Error('pairing code is not valid');
}
const payload = {
id: this.clientId,
pairingCode: code,
};
return this.unsigned_request('/tokens', payload).then((data: any) => {
const _data = data[0];
const _res: any = {};
_res[_data.facade] = _data.token;
return _res;
});
}
public async get_rates(
currencyPairs: string[],
storeID: string,
): Promise<Rate[]> {
return this.signed_get_request('/rates', {
currencyPairs: currencyPairs.join(','),
storeID,
});
}
public async create_invoice(
payload: CreateInvoiceArgs,
token?: any,
): Promise<Invoice> {
const re = new RegExp('^[A-Z]{3}$');
if (!re.test(payload.currency)) {
throw new Error('Currency is invalid');
}
if (isNaN(parseFloat(payload.price as any))) {
throw new Error('Price must be a float');
}
return this.signed_post_request('/invoices', payload, token);
}
public async get_invoice(invoiceId: string, token?: any): Promise<Invoice> {
return this.signed_get_request('/invoices/' + invoiceId, token);
}
public async get_invoices(
params?: GetInvoicesArgs,
token?: any,
): Promise<Invoice[]> {
return this.signed_get_request('/invoices', params, token);
}
private create_signed_headers(uri: string, payload: string) {
return {
'X-Identity': Buffer.from(
this.kp.getPublic().encodeCompressed(),
).toString('hex'),
'X-Signature': crypto.sign(uri + payload, this.kp).toString('hex'),
};
}
private async signed_get_request(
path: string,
params: any = {},
token: any = _.values(this.tokens)[0],
): Promise<any> {
params.token = token;
const _options = JSON.parse(JSON.stringify(this.options));
const _uri = this.host + path;
const _payload = '?' + qs.stringify(params);
_.extend(_options.headers, this.create_signed_headers(_uri, _payload));
_options.uri = _uri;
_options.qs = params;
return rp.get(_options).then((resp: any) => resp.data);
}
private async signed_post_request(
path: string,
payload: any,
token: any = _.values(this.tokens)[0],
): Promise<any> {
payload.token = token;
const _uri = this.host + path;
const _payload = JSON.stringify(payload);
const _options = JSON.parse(JSON.stringify(this.options));
_.extend(_options.headers, this.create_signed_headers(_uri, _payload));
_options.uri = _uri;
_options.body = payload;
return rp.post(_options).then((resp: any) => resp.data);
}
private async unsigned_request(path: string, payload: any): Promise<any> {
const _mixin: any = {
method: 'POST',
uri: this.host + path,
body: payload,
};
const _options = { ...JSON.parse(JSON.stringify(this.options)), ..._mixin };
return rp(_options).then((resp: any) => resp.data);
}
}

55
src/core/cryptography.ts Normal file
View File

@ -0,0 +1,55 @@
import * as elliptic from 'elliptic';
import * as bs58 from 'bs58';
import * as crypto from 'crypto';
const ec = new elliptic.ec('secp256k1');
export class Cryptography {
public static generate_keypair(): elliptic.ec.KeyPair {
const kp = ec.genKeyPair();
return kp;
}
public static load_keypair(
buf: Buffer | string | elliptic.ec.KeyPair,
): elliptic.ec.KeyPair {
return ec.keyFromPrivate(buf);
}
public static get_sin_from_key(kp: elliptic.ec.KeyPair): string {
const pk: crypto.BinaryLike = Buffer.from(
kp.getPublic().encodeCompressed(),
);
const version: Buffer = Cryptography.get_version_from_compressed_key(pk);
const checksum: Buffer = Cryptography.get_checksum_from_version(version);
return bs58.encode(Buffer.concat([version, checksum]));
}
public static sign(
message: crypto.BinaryLike,
kp: elliptic.ec.KeyPair,
): Buffer {
const digest = crypto.createHash('sha256').update(message).digest();
return Buffer.from(kp.sign(digest).toDER());
}
private static get_version_from_compressed_key(
pk: crypto.BinaryLike,
): Buffer {
const sh2 = crypto.createHash('sha256').update(pk).digest();
const rp = crypto.createHash('ripemd160').update(sh2).digest();
return Buffer.concat([
Buffer.from('0F', 'hex'),
Buffer.from('02', 'hex'),
rp,
]);
}
private static get_checksum_from_version(version: crypto.BinaryLike): Buffer {
const h1 = crypto.createHash('sha256').update(version).digest();
const h2 = crypto.createHash('sha256').update(h1).digest();
return h2.slice(0, 4);
}
}

6
src/index.ts Normal file
View File

@ -0,0 +1,6 @@
import { Cryptography } from './core/cryptography';
import { BTCPayClient } from './core/client';
export { Invoice } from './models/invoice';
export { Rate } from './models/rate';
export { Cryptography as crypto, BTCPayClient };

49
src/models/client.ts Normal file
View File

@ -0,0 +1,49 @@
export interface PairClientResponse {
merchant: string;
}
export interface GetInvoicesArgs {
status?: string;
orderId?: string;
itemCode?: string;
dateStart?: string;
dateEnd?: string;
limit?: number;
offset?: number;
}
export interface CreateInvoiceArgs {
currency: string;
price: number;
orderId?: string | number;
expirationTime?: string;
itemDesc?: string;
itemCode?: string;
posData?: string;
status?: string;
redirectUrl?: string;
transactionSpeed?: 'low' | 'low-medium' | 'medium' | 'high';
physical?: boolean;
supportedTransactionCurrencies?: {
[index: string]: {
enabled: boolean;
};
};
refundable?: boolean;
taxIncluded?: number;
token?: string;
redirectAutomatically?: boolean;
notificationEmail?: string;
notificationURL?: string;
extendedNotifications?: boolean;
fullNotifications?: boolean;
buyerEmail?: string;
buyerPhone?: string;
buyerCountry?: string;
buyerZip?: string;
buyerState?: string;
buyerCity?: string;
buyerAddress2?: string;
buyerAddress1?: string;
buyerName?: string;
}

73
src/models/invoice.ts Normal file
View File

@ -0,0 +1,73 @@
export interface Invoice {
url: string;
posData: string | null;
status: 'new' | 'paid' | 'confirmed' | 'complete' | 'expired' | 'invalid';
btcPrice: string;
btcDue: string;
cryptoInfo: Array<{
cryptoCode: string;
paymentType: string;
rate: number;
exRates: any[];
paid: string;
price: string;
due: string;
paymentUrls: any[];
address: string;
url: string;
totalDue: string;
networkFee: string;
txCount: number;
cryptoPaid: string;
payments: any[];
}>;
price: number;
currency: string;
exRates: any;
buyerTotalBtcAmount: string | null;
itemDesc: string | null;
itemCode: string | null;
orderId: string | null;
guid: string;
id: string;
invoiceTime: number;
expirationTime: number;
currentTime: number;
lowFeeDetected: boolean;
btcPaid: string;
rate: number;
exceptionStatus: false | 'paidOver' | 'paidLate' | 'paidPartial' | 'marked';
paymentUrls: {
BIP21: string | null;
BIP72: string | null;
BIP72b: string | null;
BIP73: string | null;
BOLT11: string | null;
};
refundAddressRequestPending: boolean;
buyerPaidBtcMinerFee: string | null;
bitcoinAddress: string;
token: string;
flags: {
refundable: boolean;
};
paymentSubtotals: any;
paymentTotals: any;
amountPaid: number;
minerFees: any;
exchangeRates: any;
supportedTransactionCurrencies: any;
addresses: any;
paymentCodes: any;
buyer: {
name: string | null;
address1: string | null;
address2: string | null;
locality: string | null;
region: string | null;
postalCode: string | null;
country: string | null;
phone: string | null;
email: string | null;
};
}

7
src/models/rate.ts Normal file
View File

@ -0,0 +1,7 @@
export interface Rate {
name: string;
cryptoCode: string;
currencyPair: string;
code: string;
rate: number;
}

182
tests/core/client.spec.ts Normal file
View File

@ -0,0 +1,182 @@
import * as elliptic from 'elliptic';
import * as puppeteer from 'puppeteer';
import { Browser, Page } from 'puppeteer';
import { BTCPayClient } from '../../src/core/client';
import { Cryptography as myCrypto } from '../../src/core/cryptography';
const IGNORE_SANDBOX_ERROR = process.env['BTCPAY_IGNORE_SANDBOX_ERROR'];
const USER_NAME = 'test@example.com';
const PASSWORD = 'satoshinakamoto';
const URL = 'http://127.0.0.1:49392';
const MY_PRIVATE_KEY = Buffer.from(
'31eb31ecf1a640c9d1e0a1105501f36235f8c7d51d67dcf74ccc968d74cb6b25',
'hex',
);
let STORE_ID = '';
const WINDOW_WIDTH = 1920;
const WINDOW_HEIGHT = 1080;
let INVOICE_ID = '';
const HEADLESS = true;
const loginAndGetPairingCode = async (): Promise<{
browser: Browser;
page: Page;
pairingCode: string;
}> => {
const newTokenName = 'autotest ' + new Date().getTime();
const browser = await puppeteer
.launch({
headless: HEADLESS,
args: ['--window-size=' + WINDOW_WIDTH + ',' + WINDOW_HEIGHT],
})
.then(
(v) => v, // if success, passthrough
// if error, check for env and ignore sandbox and warn.
(err) => {
if (IGNORE_SANDBOX_ERROR === '1') {
console.warn(
'WARNING!!! Error occurred, Chromium will be started ' +
"without sandbox. This won't guarantee success.",
);
return puppeteer.launch({
headless: HEADLESS,
ignoreDefaultArgs: ['--disable-extensions'],
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--window-size=' + WINDOW_WIDTH + ',' + WINDOW_HEIGHT,
],
});
} else {
console.warn(
'If "No usable sandbox!" error, retry test with ' +
'BTCPAY_IGNORE_SANDBOX_ERROR=1',
);
throw err;
}
},
);
const page = (await browser.pages())[0];
await page.setViewport({ width: WINDOW_WIDTH, height: WINDOW_HEIGHT });
try {
await page.goto(URL + '/Account/Login');
} catch (e) {
if (e.message === `net::ERR_CONNECTION_REFUSED at ${URL}/Account/Login`) {
browser.close();
console.log(
'Please start docker container locally:\n' +
'docker run -p 127.0.0.1:49392:49392 junderw/btcpay-client-test-server',
);
return {
page,
browser,
pairingCode: '',
};
}
throw e;
}
await page.click('#LoginButton');
await page.goto(URL + '/stores');
await page.waitForSelector('#CreateStore');
await page.click(
'table.table.table-sm.table-responsive-md > tbody > ' +
'tr:nth-of-type(1) > td:nth-of-type(3) > a:nth-of-type(2)',
);
await page.waitForSelector('#Id');
const idElement = await page.$$('#Id');
STORE_ID = (await idElement[0]
.getProperty('value')
.then((v) => v?.jsonValue())) as string;
await page.goto(URL + '/stores/' + STORE_ID + '/Tokens/Create');
await page.waitForSelector('input#Label');
await page.waitForSelector('[type="submit"]');
await page.type('#Label', newTokenName);
await page.click('[type="submit"]');
await page.waitForSelector('button[type="submit"]');
await page.click('[type="submit"]');
await page.waitForSelector('div.alert.alert-success.alert-dismissible');
const contents = await page.evaluate(() => {
const el = document.querySelector(
'div.alert.alert-success.alert-dismissible',
);
if (el === null) {
return '';
}
return el.innerHTML;
});
const pairingCode = (contents.match(
/Server initiated pairing code: (\S{7})/,
) || [])[1];
if (!pairingCode) {
throw new Error('Could not get pairing code');
}
return {
browser,
page,
pairingCode,
};
};
let MY_KEYPAIR: elliptic.ec.KeyPair;
let client: BTCPayClient;
describe('btcpay.core.client', () => {
beforeAll(async () => {
jest.setTimeout(20000); // browser takes a while
MY_KEYPAIR = myCrypto.load_keypair(MY_PRIVATE_KEY);
client = new BTCPayClient(URL, MY_KEYPAIR);
});
it('should pair with server', async () => {
const pairingData = await loginAndGetPairingCode();
const myClient = new BTCPayClient(URL, MY_KEYPAIR);
const result = await myClient.pair_client(pairingData.pairingCode);
client = new BTCPayClient(URL, MY_KEYPAIR, result);
expect(result.merchant).toBeDefined();
pairingData.browser.close();
await expect(myClient.pair_client('hduheufhfuf')).rejects.toThrow(
/^pairing code is not valid$/,
);
});
it('should get rates', async () => {
const results = await client.get_rates(['LTC_USD', 'BTC_USD'], STORE_ID);
expect(results[0].rate).toBeDefined();
});
it('should create an invoice', async () => {
const results = await client.create_invoice({
currency: 'USD',
price: 1.12,
});
INVOICE_ID = results.id;
expect(results.bitcoinAddress).toBeDefined();
await expect(
client.create_invoice({
currency: 'KDFAHKJFKJ',
price: 1.12,
}),
).rejects.toThrow(/^Currency is invalid$/);
await expect(
client.create_invoice({
currency: 'USD',
// @ts-ignore
price: 'xkhdfhu',
}),
).rejects.toThrow(/^Price must be a float$/);
});
it('should get invoice', async () => {
const results = await client.get_invoice(INVOICE_ID);
expect(results.id).toBe(INVOICE_ID);
});
it('should get multiple invoices', async () => {
const results = await client.get_invoices();
expect(results[0].bitcoinAddress).toBeDefined();
});
});

View File

@ -0,0 +1,33 @@
import { Cryptography as myCrypto } from '../../src/core/cryptography';
const MY_PRIVATE_KEY = Buffer.from(
'31eb31ecf1a640cd91e0a1105501f36235f8c7d51d67dcf74ccc968d74cb6b25',
'hex',
);
describe('btcpay.core.cryptography', () => {
it('should generate a keypair', () => {
const kp = myCrypto.generate_keypair();
const priv = kp.getPrivate();
expect(priv).toBeDefined();
});
it('should load a keypair from Buffer', () => {
const kp = myCrypto.load_keypair(MY_PRIVATE_KEY);
const priv = kp.getPrivate();
expect(priv).toBeDefined();
});
it('should get sin from key', () => {
const kp = myCrypto.load_keypair(MY_PRIVATE_KEY);
const sin = myCrypto.get_sin_from_key(kp);
expect(sin).toBe('TfDnXWvj6bBhkduYiZnohg5qhtDu5VWohhw');
});
it('should sign a message', () => {
const kp = myCrypto.load_keypair(MY_PRIVATE_KEY);
const message = Buffer.from('Satoshi', 'utf8');
const sig = myCrypto.sign(message, kp);
expect(sig.toString('hex')).toBe(
'304402205b0a505c180bddbd4a8836de0f2ac10b52b327d0e932352d28d170fb81517a' +
'770220307ac2ec2134d81fd04df6a1662b0962ad1322209c2e45ff8af63d3f12e0d089',
);
});
});

9
tests/index.spec.ts Normal file
View File

@ -0,0 +1,9 @@
import * as btcpay from '../src/index';
describe('btcpay.index', () => {
it('should import', () => {
expect(btcpay).toBeDefined();
expect(btcpay.crypto).toBeDefined();
expect(btcpay.BTCPayClient).toBeDefined();
});
});

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"strict": true,
"declaration": true,
"outDir": "dist",
"moduleResolution": "node",
"esModuleInterop": false
},
"lib": [
"es2017",
"node"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts",
"node_modules",
"tests"
]
}

80
tslint.json Normal file
View File

@ -0,0 +1,80 @@
{
"rules": {
"align": [false, "parameters", "arguments", "statements"],
"ban": [true, ["angular", "forEach"]],
"class-name": true,
"comment-format": [false, "check-space", "check-lowercase"],
"curly": true,
"eofline": false,
"forin": true,
"indent": [true, 4],
"interface-name": false,
"jsdoc-format": true,
"label-position": true,
"max-line-length": [false, 140],
"member-ordering": [
false,
"public-before-private",
"static-before-instance",
"variables-before-functions"
],
"no-any": false,
"no-arg": true,
"no-bitwise": true,
"no-console": [false, "debug", "info", "time", "timeEnd", "trace"],
"no-construct": true,
"no-constructor-vars": false,
"no-debugger": false,
"no-shadowed-variable": true,
"no-duplicate-variable": true,
"no-empty": false,
"no-eval": true,
"no-require-imports": true,
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"no-var-requires": true,
"one-line": [
true,
"check-catch",
"check-else",
"check-open-brace",
"check-whitespace"
],
"quotemark": [true, "single"],
"radix": false,
"semicolon": true,
"triple-equals": [true, "allow-null-check"],
"typedef": [
true,
"callSignature",
"catchClause",
"indexSignature",
"parameter",
"propertySignature",
"variableDeclarator"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [true, "allow-leading-underscore"],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}