mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
Docker config, compatibility & env var support
This commit is contained in:
parent
71e40b9955
commit
a343ec1bc6
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
/node_modules/
|
/node_modules/
|
||||||
/public/build/
|
/public/build/
|
||||||
|
/releases/
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
|
34
client/Dockerfile
Normal file
34
client/Dockerfile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
FROM node:17-buster-slim as build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY babel.config.js .
|
||||||
|
COPY rollup.config.js .
|
||||||
|
COPY src ./src
|
||||||
|
COPY template ./template
|
||||||
|
COPY public ./public
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:1.17.8@sha256:380eb808e2a3b0dd954f92c1cae2f845e6558a15037efefcabc5b4e03d666d03
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gettext
|
||||||
|
|
||||||
|
COPY docker/docker-entrypoint.sh /
|
||||||
|
COPY docker/setup-env.sh /docker-entrypoint.d/05-setup-env.sh
|
||||||
|
COPY nginx/bitfeed.conf.template /etc/nginx/conf.d/default.conf.template
|
||||||
|
COPY nginx/bitfeed.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/public/build /var/www/bitfeed
|
||||||
|
RUN chmod 766 /var/www/bitfeed/env.js
|
||||||
|
RUN chmod 766 /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
38
client/docker/docker-entrypoint.sh
Executable file
38
client/docker/docker-entrypoint.sh
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# vim:sw=4:ts=4:et
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
|
||||||
|
exec 3>&1
|
||||||
|
else
|
||||||
|
exec 3>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = "nginx" -o "$1" = "nginx-debug" ]; then
|
||||||
|
if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then
|
||||||
|
echo >&3 "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration"
|
||||||
|
|
||||||
|
echo >&3 "$0: Looking for shell scripts in /docker-entrypoint.d/"
|
||||||
|
find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do
|
||||||
|
case "$f" in
|
||||||
|
*.sh)
|
||||||
|
if [ -x "$f" ]; then
|
||||||
|
echo >&3 "$0: Launching $f";
|
||||||
|
"$f"
|
||||||
|
else
|
||||||
|
# warn on shell scripts without exec bit
|
||||||
|
echo >&3 "$0: Ignoring $f, not executable";
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*) echo >&3 "$0: Ignoring $f";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo >&3 "$0: Configuration complete; ready for start up"
|
||||||
|
else
|
||||||
|
echo >&3 "$0: No files found in /docker-entrypoint.d/, skipping configuration"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
3
client/docker/setup-env.sh
Executable file
3
client/docker/setup-env.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
envsubst < "/var/www/bitfeed/env.template.js" > "/var/www/bitfeed/env.js"
|
||||||
|
envsubst '$BACKEND_HOST,$BACKEND_PORT' < "/etc/nginx/conf.d/default.conf.template" > "/etc/nginx/conf.d/default.conf"
|
0
client/nginx/bitfeed.conf
Normal file
0
client/nginx/bitfeed.conf
Normal file
38
client/nginx/bitfeed.conf.template
Normal file
38
client/nginx/bitfeed.conf.template
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
map $sent_http_content_type $expires {
|
||||||
|
default off;
|
||||||
|
text/css max;
|
||||||
|
application/javascript max;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
root /var/www/bitfeed;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
server_name client;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
expires $expires;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://wsmonobackend;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws/txs {
|
||||||
|
proxy_pass http://wsmonobackend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream wsmonobackend {
|
||||||
|
server ${BACKEND_HOST}:${BACKEND_PORT};
|
||||||
|
}
|
4525
client/package-lock.json
generated
4525
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bitfeed-client",
|
"name": "bitfeed-client",
|
||||||
"version": "2.0.4",
|
"version": "2.1.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"dev": "rollup -c -w",
|
"dev": "rollup -c -w",
|
||||||
"start": "sirv public"
|
"start": "sirv public"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.16.5",
|
"@babel/core": "^7.16.5",
|
||||||
"@babel/preset-env": "^7.16.5",
|
"@babel/preset-env": "^7.16.5",
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
"@rollup/plugin-babel": "^5.3.0",
|
||||||
@ -14,24 +14,22 @@
|
|||||||
"@rollup/plugin-html": "^0.2.4",
|
"@rollup/plugin-html": "^0.2.4",
|
||||||
"@rollup/plugin-node-resolve": "^13.1.1",
|
"@rollup/plugin-node-resolve": "^13.1.1",
|
||||||
"@rollup/plugin-replace": "^3.0.0",
|
"@rollup/plugin-replace": "^3.0.0",
|
||||||
|
"d3-color": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"node-sass": "^7.0.0",
|
"locale-currency": "0.0.2",
|
||||||
|
"qrcode": "^1.5.0",
|
||||||
"rollup": "^2.62.0",
|
"rollup": "^2.62.0",
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
"rollup-plugin-css-only": "^3.1.0",
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
"rollup-plugin-glslify": "^1.2.1",
|
"rollup-plugin-glslify": "^1.2.1",
|
||||||
|
"rollup-plugin-inline-svg": "^2.0.0",
|
||||||
"rollup-plugin-livereload": "^2.0.5",
|
"rollup-plugin-livereload": "^2.0.5",
|
||||||
"rollup-plugin-svelte": "^7.1.0",
|
"rollup-plugin-svelte": "^7.1.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"sass": "^1.49.0",
|
||||||
|
"sirv-cli": "^1.0.14",
|
||||||
"svelte": "^3.44.3",
|
"svelte": "^3.44.3",
|
||||||
"svelte-preprocess": "^4.10.1"
|
"svelte-preprocess": "^4.10.1"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"d3-color": "^3.0.1",
|
|
||||||
"d3-interpolate": "^3.0.1",
|
|
||||||
"locale-currency": "0.0.2",
|
|
||||||
"qrcode": "^1.5.0",
|
|
||||||
"rollup-plugin-inline-svg": "^2.0.0",
|
|
||||||
"sirv-cli": "^1.0.14"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
client/public/env.js
Normal file
5
client/public/env.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
window.injected = {
|
||||||
|
TARGET: 'public',
|
||||||
|
OVERRIDE_BACKEND_HOST: 'localhost',
|
||||||
|
OVERRIDE_BACKEND_PORT: 4000
|
||||||
|
}
|
5
client/public/env.template.js
Normal file
5
client/public/env.template.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
window.injected = {
|
||||||
|
TARGET: '${TARGET}',
|
||||||
|
OVERRIDE_BACKEND_HOST: '${OVERRIDE_BACKEND_HOST}',
|
||||||
|
OVERRIDE_BACKEND_PORT: '${OVERRIDE_BACKEND_PORT}'
|
||||||
|
}
|
141
client/public/img/logo.svg
Normal file
141
client/public/img/logo.svg
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="532"
|
||||||
|
height="532"
|
||||||
|
viewBox="0 0 140.75833 140.75834"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:export-filename="/home/rob/Documents/DioxideLabs/Projects/Self/Bitfeed/OpenSource/monorepo/client/assets/icon-200.png"
|
||||||
|
inkscape:export-xdpi="36.090225"
|
||||||
|
inkscape:export-ydpi="36.090225"
|
||||||
|
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||||
|
sodipodi:docname="logo.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="blur">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f7941d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop1048" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f7941d;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop1050" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#blur"
|
||||||
|
id="linearGradient1054"
|
||||||
|
x1="101.60001"
|
||||||
|
y1="183.75832"
|
||||||
|
x2="101.60001"
|
||||||
|
y2="155.18332"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(1,0,0,1.2777777,-3.2404883e-6,-36.756346)" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor=""
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="1"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.9899495"
|
||||||
|
inkscape:cx="253.46947"
|
||||||
|
inkscape:cy="282.01453"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1016"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-156.24165)">
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#102226;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.64583325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:71.81102753;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect1384"
|
||||||
|
width="142.18738"
|
||||||
|
height="143.25647"
|
||||||
|
x="0"
|
||||||
|
y="153.74352" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#00ffcc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.66321445;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:71.81102753;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect817-6-7"
|
||||||
|
width="62.441666"
|
||||||
|
height="62.441666"
|
||||||
|
x="5.2916665"
|
||||||
|
y="229.26665" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#00ffcc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.2187593;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:71.81102753;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect817-6-7-3"
|
||||||
|
width="28.575001"
|
||||||
|
height="28.575001"
|
||||||
|
x="73.025002"
|
||||||
|
y="263.1333" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#00ffcc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.2187593;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:71.81102753;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect817-6-7-3-2"
|
||||||
|
width="28.575003"
|
||||||
|
height="28.575003"
|
||||||
|
x="106.89167"
|
||||||
|
y="263.1333" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#00ffcc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.2187593;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:71.81102753;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect817-6-7-3-2-1"
|
||||||
|
width="28.575003"
|
||||||
|
height="28.575003"
|
||||||
|
x="106.89167"
|
||||||
|
y="229.26665" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#f7941d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.2187593;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:71.81102753;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect817-6-7-3-9-2-7"
|
||||||
|
width="28.575003"
|
||||||
|
height="28.575003"
|
||||||
|
x="73.025002"
|
||||||
|
y="183.75832" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#00ffcc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.21875918;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:71.81102753;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect817-6-7-0"
|
||||||
|
width="28.575001"
|
||||||
|
height="28.575001"
|
||||||
|
x="5.2916665"
|
||||||
|
y="195.39998" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:url(#linearGradient1054);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.37767112;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:71.81102753;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect817-6-7-3-9-2-7-3"
|
||||||
|
width="28.575003"
|
||||||
|
height="36.512501"
|
||||||
|
x="73.025002"
|
||||||
|
y="161.53333" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.5 KiB |
@ -31,8 +31,9 @@
|
|||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
<link rel='stylesheet' href='/global.css?v2.0.4'>
|
<link rel='stylesheet' href='/global.css'>
|
||||||
<link rel='stylesheet' href='/build/bundle.css'>
|
<link rel='stylesheet' href='/build/bundle.css'>
|
||||||
|
<script src="/env.js"></script>
|
||||||
<script defer src="/build/bundle.js"></script>
|
<script defer src="/build/bundle.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import fs from 'fs'
|
|||||||
|
|
||||||
configDotenv();
|
configDotenv();
|
||||||
|
|
||||||
const hash = String(require("child_process").execSync("git rev-parse --short HEAD")).trim();
|
const hash = process.env.npm_package_version;
|
||||||
const htmlOptions = {
|
const htmlOptions = {
|
||||||
template: async ({ attributes, files, meta, publicPath, title }) => {
|
template: async ({ attributes, files, meta, publicPath, title }) => {
|
||||||
const rawTemplate = fs.readFileSync('./template/index.html', { encoding: 'utf8', flag: 'r'})
|
const rawTemplate = fs.readFileSync('./template/index.html', { encoding: 'utf8', flag: 'r'})
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import TxViz from './components/TxViz.svelte'
|
import TxViz from './components/TxViz.svelte'
|
||||||
import analytics from './utils/analytics.js'
|
import analytics from './utils/analytics.js'
|
||||||
|
import config from './config.js'
|
||||||
import { settings } from './stores.js'
|
import { settings } from './stores.js'
|
||||||
|
|
||||||
if (URL) {
|
if (URL) {
|
||||||
@ -10,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$settings.noTrack) analytics.init()
|
if (!$settings.noTrack && config.public) analytics.init()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
@ -3,8 +3,10 @@ export default {
|
|||||||
donationRoot: 'https://donate.monospace.live',
|
donationRoot: 'https://donate.monospace.live',
|
||||||
debug: false,
|
debug: false,
|
||||||
layoutHints: false,
|
layoutHints: false,
|
||||||
websocket_path: '/ws/txs',
|
public: (window.injected.TARGET === "public"),
|
||||||
localSocket: false,
|
backend: window.injected.OVERRIDE_BACKEND_HOST,
|
||||||
|
backendPort: window.injected.OVERRIDE_BACKEND_PORT,
|
||||||
|
secureSocket: (window.isSecureContext && !window.location.host.startsWith('localhost')),
|
||||||
nofeed: false,
|
nofeed: false,
|
||||||
txDelay: 10000,
|
txDelay: 10000,
|
||||||
donationsEnabled: true,
|
donationsEnabled: true,
|
||||||
|
@ -8,7 +8,8 @@ lastBlockId.subscribe(val => { lastBlockSeen = val })
|
|||||||
|
|
||||||
class TxStream {
|
class TxStream {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.websocketUri = config.localSocket ? `ws://localhost:4000${config.websocket_path}` : (config.dev ? `wss://bits.monospace.live${config.websocket_path}` : `wss://${window.location.host}${config.websocket_path}`)
|
this.websocketUri = `${config.secureSocket ? 'wss://' : 'ws://'}${config.backend ? config.backend : window.location.host }${config.backendPort ? ':' + config.backendPort : ''}/ws/txs`
|
||||||
|
console.log('connecting to ', this.websocketUri)
|
||||||
this.reconnectBackoff = 250
|
this.reconnectBackoff = 250
|
||||||
this.websocket = null
|
this.websocket = null
|
||||||
this.setConnected(false)
|
this.setConnected(false)
|
||||||
|
@ -32,8 +32,8 @@
|
|||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
<link rel='stylesheet' href='/global.css'>
|
<link rel='stylesheet' href='/global.css'>
|
||||||
|
|
||||||
{{css}}
|
{{css}}
|
||||||
|
<script src="/env.js"></script>
|
||||||
{{scripts}}
|
{{scripts}}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
30
server/Dockerfile
Normal file
30
server/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
FROM elixir:1.11-slim
|
||||||
|
|
||||||
|
RUN mix local.hex --force \
|
||||||
|
&& mix local.rebar --force
|
||||||
|
|
||||||
|
ENV APP_HOME /app
|
||||||
|
|
||||||
|
WORKDIR $APP_HOME
|
||||||
|
|
||||||
|
EXPOSE 4000
|
||||||
|
|
||||||
|
COPY mix.exs .
|
||||||
|
COPY mix.lock .
|
||||||
|
COPY bitcoinex ./bitcoinex
|
||||||
|
|
||||||
|
RUN mix do deps.get
|
||||||
|
RUN mix do deps.compile
|
||||||
|
|
||||||
|
COPY lib ./lib
|
||||||
|
COPY log ./log
|
||||||
|
COPY config ./config
|
||||||
|
|
||||||
|
ENV MIX_ENV prod
|
||||||
|
ENV RELEASE_NODE bitfeed
|
||||||
|
RUN mix release
|
||||||
|
RUN mkdir /app/data
|
||||||
|
RUN chown -R 1000:1000 /app/
|
||||||
|
RUN chmod -R 755 /app/
|
||||||
|
|
||||||
|
CMD ["/app/_build/prod/rel/prod/bin/prod", "start"]
|
@ -1,30 +0,0 @@
|
|||||||
defmodule Bitcoinex.LightningNetwork.HopHint do
|
|
||||||
@moduledoc """
|
|
||||||
A hop hint is used to help the payer route a payment to the receiver.
|
|
||||||
|
|
||||||
A hint is included in BOLT#11 Invoices.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@enforce_keys [
|
|
||||||
:node_id,
|
|
||||||
:channel_id,
|
|
||||||
:fee_base_m_sat,
|
|
||||||
:fee_proportional_millionths,
|
|
||||||
:cltv_expiry_delta
|
|
||||||
]
|
|
||||||
defstruct [
|
|
||||||
:node_id,
|
|
||||||
:channel_id,
|
|
||||||
:fee_base_m_sat,
|
|
||||||
:fee_proportional_millionths,
|
|
||||||
:cltv_expiry_delta
|
|
||||||
]
|
|
||||||
|
|
||||||
@type t() :: %__MODULE__{
|
|
||||||
node_id: String.t(),
|
|
||||||
channel_id: non_neg_integer,
|
|
||||||
fee_base_m_sat: non_neg_integer,
|
|
||||||
fee_proportional_millionths: non_neg_integer,
|
|
||||||
cltv_expiry_delta: non_neg_integer
|
|
||||||
}
|
|
||||||
end
|
|
@ -1,646 +0,0 @@
|
|||||||
defmodule Bitcoinex.LightningNetwork.Invoice do
|
|
||||||
@moduledoc """
|
|
||||||
Includes BOLT#11 Invoice serialization.
|
|
||||||
|
|
||||||
Reference: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Bitcoinex.{Bech32, Network, Segwit}
|
|
||||||
alias Bitcoinex.LightningNetwork.HopHint
|
|
||||||
alias Decimal, as: D
|
|
||||||
|
|
||||||
use Bitwise
|
|
||||||
# consider using https://github.com/ejpcmac/typed_struct
|
|
||||||
|
|
||||||
@default_min_final_cltv_expiry 18
|
|
||||||
@default_expiry 3600
|
|
||||||
|
|
||||||
@enforce_keys [:network, :destination, :payment_hash, :timestamp]
|
|
||||||
defstruct [
|
|
||||||
:network,
|
|
||||||
:destination,
|
|
||||||
:payment_hash,
|
|
||||||
:amount_msat,
|
|
||||||
:timestamp,
|
|
||||||
:description,
|
|
||||||
:description_hash,
|
|
||||||
:fallback_address,
|
|
||||||
route_hints: [],
|
|
||||||
expiry: @default_expiry,
|
|
||||||
min_final_cltv_expiry: @default_min_final_cltv_expiry
|
|
||||||
]
|
|
||||||
|
|
||||||
@type t() :: %__MODULE__{
|
|
||||||
network: Network.network_name(),
|
|
||||||
destination: String.t(),
|
|
||||||
payment_hash: String.t(),
|
|
||||||
amount_msat: non_neg_integer | nil,
|
|
||||||
timestamp: integer(),
|
|
||||||
expiry: integer() | nil,
|
|
||||||
# description and description_hash are either both non-nil or nil
|
|
||||||
description: String.t() | nil,
|
|
||||||
description_hash: String.t() | nil,
|
|
||||||
fallback_address: String.t() | nil,
|
|
||||||
min_final_cltv_expiry: non_neg_integer,
|
|
||||||
route_hints: list(HopHint.t())
|
|
||||||
}
|
|
||||||
|
|
||||||
@prefix "ln"
|
|
||||||
@valid_multipliers ~w(m u n p)
|
|
||||||
# TODO move it to bitcoin asset?
|
|
||||||
@milli_satoshi_per_bitcoin 100_000_000_000
|
|
||||||
# the 512 bit signature + 8 bit recovery ID.
|
|
||||||
@signature_base32_length 104
|
|
||||||
@timestamp_base32_length 7
|
|
||||||
@sha256_hash_base32_length 52
|
|
||||||
@pubkey_base32_length 53
|
|
||||||
@hop_hint_length 51
|
|
||||||
@type error :: atom
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Decode accepts a Bech32 encoded string invoice and deserializes it.
|
|
||||||
"""
|
|
||||||
@spec decode(String.t()) :: {:ok, t} | {:error, error}
|
|
||||||
def decode(invoice) when is_binary(invoice) do
|
|
||||||
with {:ok, {_encoding_type, hrp, data}} <- Bech32.decode(invoice, :infinity),
|
|
||||||
{:ok, {network, amount_msat}} <- parse_hrp(hrp),
|
|
||||||
{invoice_data, signature_data} = split_at(data, -@signature_base32_length),
|
|
||||||
{:ok, parsed_data} <-
|
|
||||||
parse_invoice_data(invoice_data, network),
|
|
||||||
{:ok, destination} <-
|
|
||||||
validate_and_parse_signature_data(
|
|
||||||
Map.get(parsed_data, :destination),
|
|
||||||
hrp,
|
|
||||||
invoice_data,
|
|
||||||
signature_data
|
|
||||||
) do
|
|
||||||
__MODULE__
|
|
||||||
|> struct(
|
|
||||||
Map.merge(
|
|
||||||
parsed_data,
|
|
||||||
%{
|
|
||||||
network: network,
|
|
||||||
amount_msat: amount_msat,
|
|
||||||
destination: destination
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|> validate_invoice()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def decode(invoice) when is_binary(invoice) do
|
|
||||||
{:error, :no_ln_prefix}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns the expiry of the invoice.
|
|
||||||
"""
|
|
||||||
@spec expires_at(Bitcoinex.LightningNetwork.Invoice.t()) :: DateTime.t()
|
|
||||||
def expires_at(%__MODULE__{} = invoice) do
|
|
||||||
expiry = invoice.expiry
|
|
||||||
Timex.from_unix(invoice.timestamp + expiry, :second)
|
|
||||||
end
|
|
||||||
|
|
||||||
# checking some invariant for invoice
|
|
||||||
# TODO Could we use ecto(without SQL) for this?
|
|
||||||
defp validate_invoice(%__MODULE__{} = invoice) do
|
|
||||||
cond do
|
|
||||||
is_nil(invoice.network) ->
|
|
||||||
{:error, :network_missing}
|
|
||||||
|
|
||||||
!is_nil(invoice.amount_msat) && invoice.amount_msat < 0 ->
|
|
||||||
{:error, :negative_amount_msat}
|
|
||||||
|
|
||||||
is_nil(invoice.payment_hash) ->
|
|
||||||
{:error, :payment_hash_missing}
|
|
||||||
|
|
||||||
is_nil(invoice.description) && is_nil(invoice.description_hash) ->
|
|
||||||
{:error, :both_description_and_description_hash_present}
|
|
||||||
|
|
||||||
!is_nil(invoice.description) && !is_nil(invoice.description_hash) ->
|
|
||||||
{:error, :both_description_and_description_hash_missing}
|
|
||||||
|
|
||||||
# lnd have this but not in Bolt11. do we need to enforce this?
|
|
||||||
Enum.count(invoice.route_hints) > 20 ->
|
|
||||||
{:error, :too_many_private_routes}
|
|
||||||
|
|
||||||
String.length(invoice.payment_hash) != 64 ->
|
|
||||||
{:error, :invalid_payment_hash_length}
|
|
||||||
|
|
||||||
!is_nil(invoice.description_hash) && String.length(invoice.description_hash) != 64 ->
|
|
||||||
{:error, :invalid_payment_hash}
|
|
||||||
|
|
||||||
# String.length(invoice.destination) != 64 ->
|
|
||||||
# {:error, :invalid_destination_length}
|
|
||||||
|
|
||||||
true ->
|
|
||||||
{:ok, invoice}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_and_parse_signature_data(destination, hrp, invoice_data, signature_data)
|
|
||||||
when is_list(invoice_data) and is_list(signature_data) do
|
|
||||||
with {:ok, signature_data_in_byte} <- Bech32.convert_bits(signature_data, 5, 8),
|
|
||||||
{signature, [recoveryId]} = split_at(signature_data_in_byte, -1),
|
|
||||||
{:ok, invoice_data_in_byte} <- Bech32.convert_bits(invoice_data, 5, 8) do
|
|
||||||
to_sign = (hrp |> :erlang.binary_to_list()) ++ invoice_data_in_byte
|
|
||||||
signature = signature |> byte_list_to_binary
|
|
||||||
hash = to_sign |> Bitcoinex.Utils.sha256()
|
|
||||||
|
|
||||||
# TODO if destination exist from tagged field, we dun need to recover but to verify it with signature
|
|
||||||
# but that require convert lg sig before using secp256k1 to verify it
|
|
||||||
# TODO refactor too nested
|
|
||||||
case Bitcoinex.Secp256k1.ecdsa_recover_compact(hash, signature, recoveryId) do
|
|
||||||
{:ok, pubkey} ->
|
|
||||||
if is_nil(destination) or destination == pubkey do
|
|
||||||
{:ok, pubkey}
|
|
||||||
else
|
|
||||||
{:error, :invalid_invoice_signature}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_invoice_data(data, network) when is_list(data) do
|
|
||||||
{timstamp_data, tagged_fields_data} = split_at(data, @timestamp_base32_length)
|
|
||||||
|
|
||||||
with {:ok, timestamp} <- parse_timestamp(timstamp_data),
|
|
||||||
{:ok, parsed_data} <-
|
|
||||||
parse_tagged_fields(tagged_fields_data, network) do
|
|
||||||
{:ok, Map.put(parsed_data, :timestamp, timestamp)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_tagged_fields(data, network) when is_list(data) do
|
|
||||||
do_parse_tagged_fields(data, %{}, network)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_parse_tagged_fields([type, data_length1, data_length2 | rest], acc, network) do
|
|
||||||
data_length = data_length1 <<< 5 ||| data_length2
|
|
||||||
|
|
||||||
if Enum.count(rest) < data_length do
|
|
||||||
{:error, :invalid_field_length}
|
|
||||||
else
|
|
||||||
{data, new_rest} = split_at(rest, data_length)
|
|
||||||
|
|
||||||
case(parse_tagged_field(type, data, acc, network)) do
|
|
||||||
{:ok, acc} ->
|
|
||||||
do_parse_tagged_fields(new_rest, acc, network)
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_parse_tagged_fields(_, acc, _network) do
|
|
||||||
{:ok, acc}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_tagged_field(type, data, acc, network) do
|
|
||||||
case type do
|
|
||||||
1 ->
|
|
||||||
if Map.has_key?(acc, :payment_hash) do
|
|
||||||
{:ok, acc}
|
|
||||||
else
|
|
||||||
case parse_payment_hash(data) do
|
|
||||||
{:ok, payment_hash} ->
|
|
||||||
{:ok, Map.put(acc, :payment_hash, payment_hash)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# r field HopHints
|
|
||||||
3 ->
|
|
||||||
if Map.has_key?(acc, :route_hints) do
|
|
||||||
{:ok, acc}
|
|
||||||
else
|
|
||||||
case parse_hop_hints(data) do
|
|
||||||
{:ok, hop_hints} ->
|
|
||||||
{:ok, Map.put(acc, :route_hints, hop_hints)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# x field
|
|
||||||
6 ->
|
|
||||||
if Map.has_key?(acc, :expiry) do
|
|
||||||
{:ok, acc}
|
|
||||||
else
|
|
||||||
expiry = parse_expiry(data)
|
|
||||||
{:ok, Map.put(acc, :expiry, expiry)}
|
|
||||||
end
|
|
||||||
|
|
||||||
# f field fallback address
|
|
||||||
9 ->
|
|
||||||
if Map.has_key?(acc, :fallback_address) do
|
|
||||||
{:ok, acc}
|
|
||||||
else
|
|
||||||
case parse_fallback_address(data, network) do
|
|
||||||
{:ok, fallback_address} ->
|
|
||||||
{:ok, Map.put(acc, :fallback_address, fallback_address)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# d field
|
|
||||||
13 ->
|
|
||||||
if Map.has_key?(acc, :description) do
|
|
||||||
{:ok, acc}
|
|
||||||
else
|
|
||||||
case parse_description(data) do
|
|
||||||
{:ok, description} ->
|
|
||||||
{:ok, Map.put(acc, :description, description)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# n field destination
|
|
||||||
19 ->
|
|
||||||
case acc do
|
|
||||||
%{destination: destination} when destination != nil ->
|
|
||||||
{:ok, acc}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
case parse_destination(data) do
|
|
||||||
{:ok, destination} ->
|
|
||||||
{:ok, Map.put(acc, :destination, destination)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# h field description hash
|
|
||||||
23 ->
|
|
||||||
if Map.has_key?(acc, :description_hash) do
|
|
||||||
{:ok, acc}
|
|
||||||
else
|
|
||||||
case parse_description_hash(data) do
|
|
||||||
{:ok, description_hash} ->
|
|
||||||
{:ok, Map.put(acc, :description_hash, description_hash)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# c field MINIMUM Fianl CLTV Expiry
|
|
||||||
24 ->
|
|
||||||
if Map.has_key?(acc, :min_final_cltv_expiry) do
|
|
||||||
{:ok, acc}
|
|
||||||
else
|
|
||||||
min_final_cltv_expiry = parse_min_final_cltv_expiry(data)
|
|
||||||
{:ok, Map.put(acc, :min_final_cltv_expiry, min_final_cltv_expiry)}
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:ok, acc}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_timestamp(data) do
|
|
||||||
{:ok, base32_to_integer(data)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_payment_hash(data) when is_list(data) do
|
|
||||||
if Enum.count(data) == @sha256_hash_base32_length do
|
|
||||||
case Bech32.convert_bits(data, 5, 8, false) do
|
|
||||||
{:ok, converted_data} ->
|
|
||||||
{:ok, converted_data |> :binary.list_to_bin() |> Base.encode16(case: :lower)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{:error, :invalid_payment_hash_length}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_description(data) do
|
|
||||||
case Bech32.convert_bits(data, 5, 8, false) do
|
|
||||||
{:ok, description} ->
|
|
||||||
{:ok, :binary.list_to_bin(description)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_expiry(data) do
|
|
||||||
base32_to_integer(data)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec base32_to_integer(maybe_improper_list()) :: any()
|
|
||||||
def base32_to_integer(data) when is_list(data) do
|
|
||||||
Enum.reduce(data, 0, fn val, acc ->
|
|
||||||
acc <<< 5 ||| val
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_destination(data) when is_list(data) do
|
|
||||||
if Enum.count(data) == @pubkey_base32_length do
|
|
||||||
case Bech32.convert_bits(data, 5, 8, false) do
|
|
||||||
{:ok, data_in_bytes} ->
|
|
||||||
{:ok, bytes_to_hex_string(data_in_bytes)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{:ok, nil}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_description_hash(data) when is_list(data) do
|
|
||||||
if Enum.count(data) == @sha256_hash_base32_length do
|
|
||||||
case Bech32.convert_bits(data, 5, 8, false) do
|
|
||||||
{:ok, data_in_bytes} ->
|
|
||||||
{:ok, data_in_bytes |> bytes_to_hex_string}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{:ok, nil}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_fallback_address([], _network) do
|
|
||||||
{:error, :empty_fallback_address}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_fallback_address([version | rest], network) do
|
|
||||||
case version do
|
|
||||||
0 ->
|
|
||||||
case Bech32.convert_bits(rest, 5, 8, false) do
|
|
||||||
{:ok, witness} ->
|
|
||||||
case Enum.count(witness) do
|
|
||||||
witness_program_lenghh when witness_program_lenghh in [20, 32] ->
|
|
||||||
Segwit.encode_address(network, 0, witness)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:error, :invalid_witness_program_length}
|
|
||||||
end
|
|
||||||
|
|
||||||
err ->
|
|
||||||
err
|
|
||||||
end
|
|
||||||
|
|
||||||
17 ->
|
|
||||||
case Bech32.convert_bits(rest, 5, 8, false) do
|
|
||||||
{:ok, pubKeyHash} ->
|
|
||||||
{:ok,
|
|
||||||
Bitcoinex.Address.encode(
|
|
||||||
pubKeyHash |> :binary.list_to_bin(),
|
|
||||||
network,
|
|
||||||
:p2pkh
|
|
||||||
)}
|
|
||||||
|
|
||||||
err ->
|
|
||||||
err
|
|
||||||
end
|
|
||||||
|
|
||||||
18 ->
|
|
||||||
case Bech32.convert_bits(rest, 5, 8, false) do
|
|
||||||
{:ok, scriptHash} ->
|
|
||||||
{:ok,
|
|
||||||
Bitcoinex.Address.encode(
|
|
||||||
scriptHash |> :binary.list_to_bin(),
|
|
||||||
network,
|
|
||||||
:p2sh
|
|
||||||
)}
|
|
||||||
|
|
||||||
err ->
|
|
||||||
err
|
|
||||||
end
|
|
||||||
|
|
||||||
# ignore unknown version
|
|
||||||
_ ->
|
|
||||||
{:ok, nil}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_hop_hints(data) when is_list(data) do
|
|
||||||
with {:ok, data_in_byte} <- Bech32.convert_bits(data, 5, 8, false),
|
|
||||||
{_, true} <-
|
|
||||||
{:validate_hop_hint_data_length, rem(Enum.count(data_in_byte), @hop_hint_length) == 0} do
|
|
||||||
hop_hints =
|
|
||||||
data_in_byte
|
|
||||||
|> Enum.chunk_every(@hop_hint_length)
|
|
||||||
|> Enum.map(&parse_hop_hint/1)
|
|
||||||
|
|
||||||
{:ok, hop_hints}
|
|
||||||
else
|
|
||||||
{:validate_hop_hint_data_length, false} ->
|
|
||||||
{:error, :invalid_hop_hint_data_length}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_integer_from_hex_str!(hex_str) do
|
|
||||||
{hex_str, ""} = Integer.parse(hex_str, 16)
|
|
||||||
hex_str
|
|
||||||
end
|
|
||||||
|
|
||||||
# exoect they are list of integer in byte
|
|
||||||
defp parse_hop_hint(data) when is_list(data) do
|
|
||||||
# 64 bits
|
|
||||||
{node_id_data, rest} = data |> split_at(33)
|
|
||||||
node_id = node_id_data |> bytes_to_hex_string
|
|
||||||
# 64 bits
|
|
||||||
{channel_id_data, rest} = rest |> split_at(8)
|
|
||||||
channel_id = channel_id_data |> bytes_to_hex_string |> parse_integer_from_hex_str!
|
|
||||||
# 32 bits
|
|
||||||
{fee_base_m_sat_data, rest} = rest |> split_at(4)
|
|
||||||
fee_base_m_sat = fee_base_m_sat_data |> bytes_to_hex_string |> parse_integer_from_hex_str!
|
|
||||||
# 32 bits
|
|
||||||
{fee_proportional_millionths_data, rest} = rest |> split_at(4)
|
|
||||||
|
|
||||||
fee_proportional_millionths =
|
|
||||||
fee_proportional_millionths_data |> bytes_to_hex_string |> parse_integer_from_hex_str!
|
|
||||||
|
|
||||||
cltv_expiry_delta = rest |> bytes_to_hex_string |> parse_integer_from_hex_str!
|
|
||||||
|
|
||||||
%HopHint{
|
|
||||||
node_id: node_id,
|
|
||||||
channel_id: channel_id,
|
|
||||||
fee_base_m_sat: fee_base_m_sat,
|
|
||||||
fee_proportional_millionths: fee_proportional_millionths,
|
|
||||||
cltv_expiry_delta: cltv_expiry_delta
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_min_final_cltv_expiry(data) when is_list(data) do
|
|
||||||
base32_to_integer(data)
|
|
||||||
end
|
|
||||||
|
|
||||||
# defp get_pubkey_to_address_magicbyte(network, script_type) do
|
|
||||||
# case {network, script_type} do
|
|
||||||
# {:mainnet, :p2pkh} ->
|
|
||||||
# 0x00
|
|
||||||
|
|
||||||
# {:mainnet, :p2sh} ->
|
|
||||||
# 0x05
|
|
||||||
|
|
||||||
# {network, :p2pkh} when network in [:testnet, :regtest] ->
|
|
||||||
# 0x6F
|
|
||||||
|
|
||||||
# {network, :p2sh} when network in [:testnet, :regtest] ->
|
|
||||||
# 0xC4
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
defp parse_network(@prefix <> rest_hrp) do
|
|
||||||
case Enum.find(Network.supported_networks(), fn %{hrp_segwit_prefix: hrp_segwit_prefix} ->
|
|
||||||
if String.starts_with?(rest_hrp, hrp_segwit_prefix) do
|
|
||||||
size = bit_size(hrp_segwit_prefix)
|
|
||||||
|
|
||||||
case rest_hrp do
|
|
||||||
# without amount
|
|
||||||
^hrp_segwit_prefix ->
|
|
||||||
true
|
|
||||||
|
|
||||||
# with amount. a valid segwit_prefix must be following with base10 digit
|
|
||||||
# ?0..?9 means range of codepoint of 0 - 9
|
|
||||||
# it shoudln't include 0 but that's not responsiblity of passing network function here
|
|
||||||
<<_::size(size), i, _::binary>> when i in ?0..?9 ->
|
|
||||||
true
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end) do
|
|
||||||
nil ->
|
|
||||||
{:error, :invalid_network}
|
|
||||||
|
|
||||||
network ->
|
|
||||||
{:ok, network}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_hrp(hrp) do
|
|
||||||
with {_, @prefix <> rest_hrp} <- {:strip_prefix, hrp},
|
|
||||||
{_, {:ok, %{name: network_name, hrp_segwit_prefix: hrp_segwit_prefix}}} <-
|
|
||||||
{:parse_network, parse_network(hrp)} do
|
|
||||||
hrp_segwit_prefix_size = byte_size(hrp_segwit_prefix)
|
|
||||||
|
|
||||||
case rest_hrp do
|
|
||||||
^hrp_segwit_prefix ->
|
|
||||||
{:ok, {network_name, nil}}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
amount_str = String.slice(rest_hrp, hrp_segwit_prefix_size..-1)
|
|
||||||
|
|
||||||
case calculate_milli_satoshi(amount_str) do
|
|
||||||
{:ok, amount} ->
|
|
||||||
{:ok, {network_name, amount}}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{:strip_prefix, _} ->
|
|
||||||
{:error, :no_ln_prefix}
|
|
||||||
|
|
||||||
{:parse_network, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp calculate_milli_satoshi("0" <> _) do
|
|
||||||
{:error, :amount_with_leading_zero}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp calculate_milli_satoshi(amount_str) do
|
|
||||||
result =
|
|
||||||
case Regex.run(~r/[munp]$/, amount_str) do
|
|
||||||
[multiplier] when multiplier in @valid_multipliers ->
|
|
||||||
case Integer.parse(String.slice(amount_str, 0..-2)) do
|
|
||||||
{amount, ""} ->
|
|
||||||
{:ok, to_bitcoin(amount, multiplier)}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:error, :invalid_amount}
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
case Integer.parse(amount_str) do
|
|
||||||
{amount_in_bitcoin, ""} ->
|
|
||||||
{:ok, amount_in_bitcoin}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:error, :invalid_amount}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
case result do
|
|
||||||
{:ok, amount_in_bitcoin} ->
|
|
||||||
amount_msat_dec = D.mult(amount_in_bitcoin, @milli_satoshi_per_bitcoin)
|
|
||||||
rounded_amount_msat_dec = D.round(amount_msat_dec)
|
|
||||||
|
|
||||||
case D.equal?(rounded_amount_msat_dec, amount_msat_dec) do
|
|
||||||
true ->
|
|
||||||
{:ok, D.to_integer(rounded_amount_msat_dec)}
|
|
||||||
|
|
||||||
false ->
|
|
||||||
{:error, :sub_msat_precision_amount}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_bitcoin(amount, multiplier_str) when is_integer(amount) do
|
|
||||||
multiplier =
|
|
||||||
case multiplier_str do
|
|
||||||
"m" ->
|
|
||||||
0.001
|
|
||||||
|
|
||||||
"u" ->
|
|
||||||
0.000001
|
|
||||||
|
|
||||||
"n" ->
|
|
||||||
0.000000001
|
|
||||||
|
|
||||||
"p" ->
|
|
||||||
0.000000000001
|
|
||||||
end
|
|
||||||
|
|
||||||
D.mult(amount, D.from_float(multiplier))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp bytes_to_hex_string(bytes) when is_list(bytes) do
|
|
||||||
bytes |> :binary.list_to_bin() |> Base.encode16(case: :lower)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp byte_list_to_binary(bytes) when is_list(bytes) do
|
|
||||||
bytes |> :binary.list_to_bin()
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec split_at(Enum.t(), integer()) :: {list(Enum.t()), list(Enum.t())}
|
|
||||||
defp split_at(xs, index) when index >= 0 do
|
|
||||||
{Enum.take(xs, index), Enum.drop(xs, index)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp split_at(xs, index) when index < 0 do
|
|
||||||
{Enum.drop(xs, index), Enum.take(xs, index)}
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,10 +0,0 @@
|
|||||||
defmodule Bitcoinex.LightningNetwork do
|
|
||||||
@moduledoc """
|
|
||||||
Includes serialization and validation for Lightning Network BOLT#11 invoices.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Bitcoinex.LightningNetwork.Invoice
|
|
||||||
|
|
||||||
# defdelegate encode_invoice(invoice), to: Invoice, as: :encode
|
|
||||||
defdelegate decode_invoice(invoice), to: Invoice, as: :decode
|
|
||||||
end
|
|
@ -4,7 +4,7 @@ defmodule Bitcoinex.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :bitcoinex,
|
app: :bitcoinex,
|
||||||
version: "0.1.1",
|
version: "0.2.0",
|
||||||
elixir: "~> 1.8",
|
elixir: "~> 1.8",
|
||||||
package: package(),
|
package: package(),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
@ -31,7 +31,6 @@ defmodule Bitcoinex.MixProject do
|
|||||||
{:excoveralls, "~> 0.10", only: :test},
|
{:excoveralls, "~> 0.10", only: :test},
|
||||||
{:mix_test_watch, "~> 0.8", only: :dev, runtime: false},
|
{:mix_test_watch, "~> 0.8", only: :dev, runtime: false},
|
||||||
{:stream_data, "~> 0.1", only: :test},
|
{:stream_data, "~> 0.1", only: :test},
|
||||||
{:timex, "~> 3.1"},
|
|
||||||
{:decimal, "~> 1.0"},
|
{:decimal, "~> 1.0"},
|
||||||
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
|
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
|
||||||
]
|
]
|
||||||
|
@ -1,503 +0,0 @@
|
|||||||
defmodule Bitcoinex.LightningNetwork.InvoiceTest do
|
|
||||||
use ExUnit.Case
|
|
||||||
doctest Bitcoinex.Segwit
|
|
||||||
|
|
||||||
alias Bitcoinex.LightningNetwork.{Invoice, HopHint}
|
|
||||||
|
|
||||||
setup_all do
|
|
||||||
test_payment_hash = "0001020304050607080900010203040506070809000102030405060708090102"
|
|
||||||
|
|
||||||
test_description_hash_slice =
|
|
||||||
Bitcoinex.Utils.sha256(
|
|
||||||
"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"
|
|
||||||
)
|
|
||||||
|> Base.encode16(case: :lower)
|
|
||||||
|
|
||||||
test_description_coffee = "1 cup coffee"
|
|
||||||
test_description_coffee_japanese = "ナンセンス 1杯"
|
|
||||||
|
|
||||||
test_description_blockstream_ledger =
|
|
||||||
"Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items"
|
|
||||||
|
|
||||||
# testHopHintPubkeyBytes1 = Base.decode64!("")
|
|
||||||
testHopHintPubkey1 = "029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"
|
|
||||||
testHopHintPubkey2 = "039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"
|
|
||||||
testHopHintPubkey3 = "03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"
|
|
||||||
|
|
||||||
testSingleHop = [
|
|
||||||
%HopHint{
|
|
||||||
node_id: testHopHintPubkey1,
|
|
||||||
channel_id: 0x0102030405060708,
|
|
||||||
fee_base_m_sat: 0,
|
|
||||||
fee_proportional_millionths: 20,
|
|
||||||
cltv_expiry_delta: 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
testDoubleHop = [
|
|
||||||
%HopHint{
|
|
||||||
node_id: testHopHintPubkey1,
|
|
||||||
channel_id: 0x0102030405060708,
|
|
||||||
fee_base_m_sat: 1,
|
|
||||||
fee_proportional_millionths: 20,
|
|
||||||
cltv_expiry_delta: 3
|
|
||||||
},
|
|
||||||
%HopHint{
|
|
||||||
node_id: testHopHintPubkey2,
|
|
||||||
channel_id: 0x030405060708090A,
|
|
||||||
fee_base_m_sat: 2,
|
|
||||||
fee_proportional_millionths: 30,
|
|
||||||
cltv_expiry_delta: 4
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
test_address_testnet_P2PKH = "mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP"
|
|
||||||
test_address_mainnet_P2PKH = "1RustyRX2oai4EYYDpQGWvEL62BBGqN9T"
|
|
||||||
test_address_mainnet_P2SH = "3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"
|
|
||||||
test_address_mainnet_P2WPKH = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
|
|
||||||
test_address_mainnet_P2WSH = "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"
|
|
||||||
|
|
||||||
test_pubkey = "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"
|
|
||||||
|
|
||||||
valid_encoded_invoices = [
|
|
||||||
# Please send $3 for a cup of coffee to the same peer, within one minute
|
|
||||||
{
|
|
||||||
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 250_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description: test_description_coffee,
|
|
||||||
expiry: 60,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# pubkey set in 'n' field.
|
|
||||||
{
|
|
||||||
"lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jd3m5klcwhq68vdsmx2rjgxeay5v0tkt2v5sjaky4eqahe4fx3k9sqavvce3capfuwv8rvjng57jrtfajn5dkpqv8yelsewtljwmmycq62k443",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_400_000_000_000,
|
|
||||||
timestamp: 1_503_429_093,
|
|
||||||
description: "",
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad
|
|
||||||
{
|
|
||||||
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
description: "Please consider supporting this project",
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Has a few unknown fields, should just be ignored.
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqtq2v93xxer9vczq8v93xxeqv72xr42ca60022jqu6fu73n453tmnr0ukc0pl0t23w7eavtensjz0j2wcu7nkxhfdgp9y37welajh5kw34mq7m4xuay0a72cwec8qwgqt5vqht",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
description: "Please consider supporting this project",
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute
|
|
||||||
{
|
|
||||||
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 250_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description: test_description_coffee_japanese,
|
|
||||||
expiry: 60,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Now send $24 for an entire list of things (hashed)
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP
|
|
||||||
{
|
|
||||||
"lntb20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un98kmzzhznpurw9sgl2v0nklu2g4d0keph5t7tj9tcqd8rexnd07ux4uv2cjvcqwaxgj7v4uwn5wmypjd5n69z2xm3xgksg28nwht7f6zspwp3f9t",
|
|
||||||
%Invoice{
|
|
||||||
network: :testnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
fallback_address: test_address_testnet_P2PKH,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# 1 sat with chinese description in testnet
|
|
||||||
{
|
|
||||||
"lntb10n1pwt8uswpp5r7j8v60vnevkhxls93x2zp3xyu7z65a4368wh0en8fl70vpypa2sdpzfehjqstdda6kuapqwa5hg6pquju2me5ksucqzys5qzh8dzpqjz7k7pdlal68ew2vx0y9rwaqth758mu0yu0v367kuc8typ08g7tnhh3a7v53svay2efvn7fwah8pesjsgvwrdpjjj795gqp0g4utq",
|
|
||||||
%Invoice{
|
|
||||||
network: :testnet,
|
|
||||||
destination: "0260d9119979caedc570ada883ff614c6efb93f7f7382e25d73ecbeba0b62df2d7",
|
|
||||||
description: "No Amount with 中文",
|
|
||||||
payment_hash: "1fa47669ec9e596b9bf02c4ca10626273c2d53b58e8eebbf333a7fe7b0240f55",
|
|
||||||
amount_msat: 1000,
|
|
||||||
timestamp: 1_555_296_782,
|
|
||||||
min_final_cltv_expiry: 144
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# 1 hophint
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85frzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvncsk57n4v9ehw86wq8fzvjejhv9z3w3q5zh6qkql005x9xl240ch23jk79ujzvr4hsmmafyxghpqe79psktnjl668ntaf4ne7ucs5csqh5mnnk",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
fallback_address: test_address_mainnet_P2PKH,
|
|
||||||
route_hints: testSingleHop,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# two hophint
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
fallback_address: test_address_mainnet_P2PKH,
|
|
||||||
route_hints: testDoubleHop,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9kk822r8plup77n9yq5ep2dfpcydrjwzxs0la84v3tfw43t3vqhek7f05m6uf8lmfkjn7zv7enn76sq65d8u9lxav2pl6x3xnc2ww3lqpagnh0u",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
fallback_address: test_address_mainnet_P2SH,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# # On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
fallback_address: test_address_mainnet_P2WPKH,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qvnjha2auylmwrltv2pkp2t22uy8ura2xsdwhq5nm7s574xva47djmnj2xeycsu7u5v8929mvuux43j0cqhhf32wfyn2th0sv4t9x55sppz5we8",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
fallback_address: test_address_mainnet_P2WSH,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Ignore unknown witness version in fallback address.
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpppw508d6qejxtdg4y5r3zarvary0c5xw7k8txqv6x0a75xuzp0zsdzk5hq6tmfgweltvs6jk5nhtyd9uqksvr48zga9mw08667w8264gkspluu66jhtcmct36nx363km6cquhhv2cpc6q43r",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Ignore fields with unknown lengths.
|
|
||||||
{
|
|
||||||
"lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqpp3qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqshp38yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66np3q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfy8huflvs2zwkymx47cszugvzn5v64ahemzzlmm62rpn9l9rm05h35aceq00tkt296289wepws9jh4499wq2l0vk6xcxffd90dpuqchqqztyayq",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 2_400_000_000_000,
|
|
||||||
timestamp: 1_503_429_093,
|
|
||||||
description_hash: test_description_hash_slice,
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# # Send 2500uBTC for a cup of coffee with a custom CLTV expiry value.
|
|
||||||
{
|
|
||||||
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jscqzysnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66ysxkvnxhcvhz48sn72lp77h4fxcur27z0he48u5qvk3sxse9mr9jhkltt962s8arjnzk8rk59yj5nw4p495747gksj30gza0crhzwjcpgxzy00",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: test_payment_hash,
|
|
||||||
amount_msat: 250_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description: test_description_coffee,
|
|
||||||
min_final_cltv_expiry: 144
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9qn07ytgrxxzad9hc4xt3mawjjt8znfv8xzscs7007v9gh9j569lencxa8xeujzkxs0uamak9aln6ez02uunw6rd2ht2sqe4hz8thcdagpleym0j",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: "462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f",
|
|
||||||
amount_msat: 967_878_534,
|
|
||||||
timestamp: 1_572_468_703,
|
|
||||||
description: test_description_blockstream_ledger,
|
|
||||||
min_final_cltv_expiry: 10,
|
|
||||||
expiry: 604_800,
|
|
||||||
route_hints: [
|
|
||||||
%HopHint{
|
|
||||||
node_id: testHopHintPubkey3,
|
|
||||||
channel_id: 0x08FE4E000CF00001,
|
|
||||||
fee_base_m_sat: 1000,
|
|
||||||
fee_proportional_millionths: 2500,
|
|
||||||
cltv_expiry_delta: 40
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# TODO parsing payment secret
|
|
||||||
{
|
|
||||||
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
|
|
||||||
amount_msat: 2_500_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description: "coffee beans",
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Same, but all upper case
|
|
||||||
{
|
|
||||||
"LNBC25M1PVJLUEZPP5QQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQYPQDQ5VDHKVEN9V5SXYETPDEESSP5ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYGS9Q5SQQQQQQQQQQQQQQQPQSQ67GYE39HFG3ZD8RGC80K32TVY9XK2XUNWM5LZEXNVPX6FD77EN8QAQ424DXGT56CAG2DPT359K3SSYHETKTKPQH24JQNJYW6UQD08SGPTQ44QU",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
|
|
||||||
amount_msat: 2_500_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description: "coffee beans",
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Same, but including fields which must be ignored.
|
|
||||||
{
|
|
||||||
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2jxxfsnucm4jf4zwtznpaxphce606fvhvje5x7d4gw7n73994hgs7nteqvenq8a4ml8aqtchv5d9pf7l558889hp4yyrqv6a7zpq9fgpskqhza",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: test_pubkey,
|
|
||||||
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
|
|
||||||
amount_msat: 2_500_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description: "coffee beans",
|
|
||||||
min_final_cltv_expiry: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lnbcrt320u1pwt8mp3pp57xs8x6cs28zedru0r0hurkz6932e86dvlrzhwvm09azv57qcekxsdqlv9k8gmeqw3jhxarfdenjqumfd4cxcegcqzpgctyyv3qkvr6khzlnd7de95hrxkw8ewfhmyzuu9dh4sgauukpk5mryaex2qs39ksupm8sxj5jsh3hw3fa0gwdjchh7ga8cx7l652g5dgqzp2ddj",
|
|
||||||
%Invoice{
|
|
||||||
network: :regtest,
|
|
||||||
destination: "03f54387039a2932bca652e9fca1d0eb141a7f9c570979a2c469469a8083c73b47",
|
|
||||||
payment_hash: "f1a0736b1051c5968f8f1befc1d85a2c5593e9acf8c577336f2f44ca7818cd8d",
|
|
||||||
amount_msat: 32_000_000,
|
|
||||||
timestamp: 1_555_295_281,
|
|
||||||
description: "alto testing simple",
|
|
||||||
min_final_cltv_expiry: 40,
|
|
||||||
expiry: 3600
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z9kmrgvr7xlaqm47apw3d48zm203kzcq357a4ls9al2ea73r8jcceyjtya6fu5wzzpe50zrge6ulk4nvjcpxlekvmxl6qcs9j3tz0469gq5g658y",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
|
|
||||||
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1",
|
|
||||||
min_final_cltv_expiry: 18,
|
|
||||||
fallback_address: "3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7kepvrhrm9s57hejg0p662ur5j5cr03890fa7k2pypgttmh4897d3raaq85a293e9jpuqwl0rnfuwzam7yr8e690nd2ypcq9hlkdwdvycqa0qza8",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
|
|
||||||
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1",
|
|
||||||
min_final_cltv_expiry: 18,
|
|
||||||
fallback_address: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q28j0v3rwgy9pvjnd48ee2pl8xrpxysd5g44td63g6xcjcu003j3qe8878hluqlvl3km8rm92f5stamd3jw763n3hck0ct7p8wwj463cql26ava",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
|
|
||||||
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
|
|
||||||
amount_msat: 2_000_000_000,
|
|
||||||
timestamp: 1_496_314_658,
|
|
||||||
description_hash: "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1",
|
|
||||||
min_final_cltv_expiry: 18,
|
|
||||||
fallback_address: "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lnbc1230p1pwpw4vhpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq8w3jhxaqxqrrsscqpfmmcvd29nucsnyapspmkpqqf65uedt4zvhqkstelrgyk4nfvwka38c3nlq06agjmazs9nr3uupxp6r0v0gzw4n26redc36urkqwxamqqqu7esys",
|
|
||||||
%Invoice{
|
|
||||||
network: :mainnet,
|
|
||||||
destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad",
|
|
||||||
payment_hash: "0001020304050607080900010203040506070809000102030405060708090102",
|
|
||||||
amount_msat: 123,
|
|
||||||
timestamp: 1_545_033_111,
|
|
||||||
description: "test",
|
|
||||||
min_final_cltv_expiry: 9,
|
|
||||||
expiry: 3600
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lnbcrt1230n1pwt8m44pp56zkej4pmwp273agvad0ljscuq4gsc072xlttlgrzrzpmlwzzhzksdq2wd5k6urvv5cqzpgym4nl5gt8xqy69t4cuwf025xnv968xpgvv30h387whfur5y9hq9h5sd8hkumauluj4dn9kqche8glswdpvc2lu4yua3atkyaefuzuqqp27dfsw",
|
|
||||||
%Invoice{
|
|
||||||
network: :regtest,
|
|
||||||
destination: "03f54387039a2932bca652e9fca1d0eb141a7f9c570979a2c469469a8083c73b47",
|
|
||||||
payment_hash: "d0ad99543b7055e8f50ceb5ff9431c05510c3fca37d6bfa0621883bfb842b8ad",
|
|
||||||
amount_msat: 123_000,
|
|
||||||
timestamp: 1_555_295_925,
|
|
||||||
description: "simple",
|
|
||||||
min_final_cltv_expiry: 40
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
invalid_encoded_invoices = [
|
|
||||||
# no hrp,
|
|
||||||
"asdsaddnasdnas",
|
|
||||||
# too short
|
|
||||||
"lnbc1abcde",
|
|
||||||
# empty hrp
|
|
||||||
"1asdsaddnv4wudz",
|
|
||||||
# hrp too short
|
|
||||||
"ln1asdsaddnv4wudz",
|
|
||||||
# no "ln" prefix
|
|
||||||
"llts1dasdajtkfl6",
|
|
||||||
# invalid segwit prefix
|
|
||||||
"lnts1dasdapukz0w",
|
|
||||||
# invalid amount
|
|
||||||
"lnbcm1aaamcu25m",
|
|
||||||
# invalid amount
|
|
||||||
"lnbc1000000000m1",
|
|
||||||
# empty fallback address field
|
|
||||||
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfqqepvrhrm9s57hejg0p662ur5j5cr03890fa7k2pypgttmh4897d3raaq85a293e9jpuqwl0rnfuwzam7yr8e690nd2ypcq9hlkdwdvycqjhlqg5",
|
|
||||||
# invalid routing info length: not a multiple of 51
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85frqg00000000j9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqsj5cgu",
|
|
||||||
# no payment hash set
|
|
||||||
"lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsjv38luh6p6s2xrv3mzvlmzaya43376h0twal5ax0k6p47498hp3hnaymzhsn424rxqjs0q7apn26yrhaxltq3vzwpqj9nc2r3kzwccsplnq470",
|
|
||||||
# Both Description and DescriptionHash set.
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs03vghs8y0kuj4ulrzls8ln7fnm9dk7sjsnqmghql6hd6jut36clkqpyuq0s5m6fhureyz0szx2qjc8hkgf4xc2hpw8jpu26jfeyvf4cpga36gt",
|
|
||||||
# Neither Description nor DescriptionHash set.
|
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqn2rne0kagfl4e0xag0w6hqeg2dwgc54hrm9m0auw52dhwhwcu559qav309h598pyzn69wh2nqauneyyesnpmaax0g6acr8lh9559jmcquyq5a9",
|
|
||||||
# mixed case
|
|
||||||
"lnbc2500u1PvJlUeZpP5QqQsYqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp",
|
|
||||||
# Lightning Payment Request signature pubkey does not match payee pubkey
|
|
||||||
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durllllll72gy6kphxuhh4a2ffwf9344ytfw98tyhvslsp9y5vt2uxdfhpucph83eqms28dyde9yxgu5ehln4zkwv04nvurxhst77vnng5s0ar9mqpm3cg0l",
|
|
||||||
# Bech32 checksum is invalid
|
|
||||||
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt",
|
|
||||||
# Malformed bech32 string (no 1)
|
|
||||||
"pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
|
|
||||||
# Malformed bech32 string (mixed case)
|
|
||||||
"LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
|
|
||||||
# Signature is not recoverable.
|
|
||||||
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaxtrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspk28uwq",
|
|
||||||
# String is too short.
|
|
||||||
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh",
|
|
||||||
# Invalid multiplier
|
|
||||||
"lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg",
|
|
||||||
# Invalid sub-millisatoshi precision.
|
|
||||||
"lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu7hqtk93pkf7sw55rdv4k9z2vj050rxdr6za9ekfs3nlt5lr89jqpdmxsmlj9urqumg0h9wzpqecw7th56tdms40p2ny9q4ddvjsedzcplva53s"
|
|
||||||
]
|
|
||||||
|
|
||||||
[
|
|
||||||
test_pubkey: test_pubkey,
|
|
||||||
valid_encoded_invoices: valid_encoded_invoices,
|
|
||||||
invalid_encoded_invoices: invalid_encoded_invoices
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "decode/1" do
|
|
||||||
test "successfully decode with valid segwit addresses in mainnet", %{
|
|
||||||
valid_encoded_invoices: valid_encoded_invoices
|
|
||||||
} do
|
|
||||||
for {valid_encoded_invoice, invoice} <- valid_encoded_invoices do
|
|
||||||
assert {:ok, decoded_invoice} = Invoice.decode(valid_encoded_invoice)
|
|
||||||
assert decoded_invoice == invoice
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "fail to decode with invalid segwit addresses in mainnet", %{
|
|
||||||
invalid_encoded_invoices: invalid_encoded_invoices
|
|
||||||
} do
|
|
||||||
for invalid_encoded_invoice <- invalid_encoded_invoices do
|
|
||||||
assert {:error, error} = Invoice.decode(invalid_encoded_invoice)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "expires_at/1" do
|
|
||||||
test "calculates expires at time correctly for diff invoice types", %{
|
|
||||||
valid_encoded_invoices: invoices
|
|
||||||
} do
|
|
||||||
for {_valid_encoded_invoice, invoice} <- invoices do
|
|
||||||
expires_at = Invoice.expires_at(invoice)
|
|
||||||
assert Timex.to_unix(expires_at) - (invoice.expiry || 3600) == invoice.timestamp
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
BIN
server/data/block.dat
Normal file
BIN
server/data/block.dat
Normal file
Binary file not shown.
@ -2,7 +2,7 @@ defmodule BitcoinStream.BlockData do
|
|||||||
@moduledoc """
|
@moduledoc """
|
||||||
Block data module.
|
Block data module.
|
||||||
|
|
||||||
Serves a copy of the latest block
|
Serves a cached copy of the latest block
|
||||||
"""
|
"""
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ defmodule BitcoinStream.BlockData do
|
|||||||
IO.puts("Starting block data link")
|
IO.puts("Starting block data link")
|
||||||
# load block.dat
|
# load block.dat
|
||||||
|
|
||||||
with {:ok, block_data} <- File.read("block.dat"),
|
with {:ok, block_data} <- File.read("data/block.dat"),
|
||||||
{:ok, block} <- BitcoinBlock.decode(block_data),
|
{:ok, block} <- BitcoinBlock.decode(block_data),
|
||||||
{:ok, payload} <- Jason.encode(%{type: "block", block: block}) do
|
{:ok, payload} <- Jason.encode(%{type: "block", block: block}) do
|
||||||
GenServer.start_link(__MODULE__, {block, payload}, opts)
|
GenServer.start_link(__MODULE__, {block, payload}, opts)
|
||||||
|
@ -11,18 +11,19 @@ defmodule BitcoinStream.Bridge do
|
|||||||
alias BitcoinStream.Protocol.Transaction, as: BitcoinTx
|
alias BitcoinStream.Protocol.Transaction, as: BitcoinTx
|
||||||
alias BitcoinStream.Mempool, as: Mempool
|
alias BitcoinStream.Mempool, as: Mempool
|
||||||
|
|
||||||
def child_spec(port: port) do
|
def child_spec(host: host, tx_port: tx_port, block_port: block_port) do
|
||||||
%{
|
%{
|
||||||
id: BitcoinStream.Bridge,
|
id: BitcoinStream.Bridge,
|
||||||
start: {BitcoinStream.Bridge, :start_link, [port]}
|
start: {BitcoinStream.Bridge, :start_link, [host, tx_port, block_port]}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def start_link(port) do
|
def start_link(host, tx_port, block_port) do
|
||||||
IO.puts("Starting Bitcoin bridge on port #{port}")
|
IO.puts("Starting Bitcoin bridge on #{host} ports #{tx_port}, #{block_port}")
|
||||||
connect_to_server(port);
|
connect_to_server(host, tx_port);
|
||||||
txsub(port);
|
connect_to_server(host, block_port);
|
||||||
blocksub(port);
|
txsub(host, tx_port);
|
||||||
|
blocksub(host, block_port);
|
||||||
GenServer.start_link(__MODULE__, %{})
|
GenServer.start_link(__MODULE__, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -33,11 +34,11 @@ defmodule BitcoinStream.Bridge do
|
|||||||
@doc """
|
@doc """
|
||||||
Create zmq client
|
Create zmq client
|
||||||
"""
|
"""
|
||||||
def start_client(port) do
|
def start_client(host, port) do
|
||||||
IO.puts("Starting client on port #{port}");
|
IO.puts("Starting client on #{host} port #{port}");
|
||||||
{:ok, socket} = :chumak.socket(:pair);
|
{:ok, socket} = :chumak.socket(:pair);
|
||||||
IO.puts("Client socket paired");
|
IO.puts("Client socket paired");
|
||||||
{:ok, pid} = :chumak.connect(socket, :tcp, 'localhost', port);
|
{:ok, pid} = :chumak.connect(socket, :tcp, String.to_charlist(host), port);
|
||||||
IO.puts("Client socket connected");
|
IO.puts("Client socket connected");
|
||||||
{socket, pid}
|
{socket, pid}
|
||||||
end
|
end
|
||||||
@ -100,7 +101,7 @@ defmodule BitcoinStream.Bridge do
|
|||||||
IO.puts("client block loop");
|
IO.puts("client block loop");
|
||||||
with {:ok, message} <- :chumak.recv_multipart(socket),
|
with {:ok, message} <- :chumak.recv_multipart(socket),
|
||||||
[_topic, payload, _size] <- message,
|
[_topic, payload, _size] <- message,
|
||||||
:ok <- File.write("block.dat", payload, [:binary]),
|
:ok <- File.write("data/block.dat", payload, [:binary]),
|
||||||
{:ok, block} <- BitcoinBlock.decode(payload) do
|
{:ok, block} <- BitcoinBlock.decode(payload) do
|
||||||
GenServer.cast(:block_data, {:block, block})
|
GenServer.cast(:block_data, {:block, block})
|
||||||
sendBlock(block);
|
sendBlock(block);
|
||||||
@ -117,18 +118,18 @@ defmodule BitcoinStream.Bridge do
|
|||||||
@doc """
|
@doc """
|
||||||
Set up demo zmq client
|
Set up demo zmq client
|
||||||
"""
|
"""
|
||||||
def connect_to_server(port) do
|
def connect_to_server(host, port) do
|
||||||
IO.puts("Starting on #{port}");
|
IO.puts("Starting on #{host}:#{port}");
|
||||||
{client_socket, _client_pid} = start_client(port);
|
{client_socket, _client_pid} = start_client(host, port);
|
||||||
IO.puts("Started client");
|
IO.puts("Started client");
|
||||||
client_socket
|
client_socket
|
||||||
end
|
end
|
||||||
|
|
||||||
def txsub(port) do
|
def txsub(host, port) do
|
||||||
IO.puts("Subscribing to rawtx events")
|
IO.puts("Subscribing to rawtx events")
|
||||||
{:ok, socket} = :chumak.socket(:sub)
|
{:ok, socket} = :chumak.socket(:sub)
|
||||||
:chumak.subscribe(socket, 'rawtx')
|
:chumak.subscribe(socket, 'rawtx')
|
||||||
case :chumak.connect(socket, :tcp, 'localhost', port) do
|
case :chumak.connect(socket, :tcp, String.to_charlist(host), port) do
|
||||||
{:ok, pid} -> IO.puts("Binding ok to pid #{inspect pid}");
|
{:ok, pid} -> IO.puts("Binding ok to pid #{inspect pid}");
|
||||||
{:error, reason} -> IO.puts("Binding failed: #{reason}");
|
{:error, reason} -> IO.puts("Binding failed: #{reason}");
|
||||||
_ -> IO.puts("unhandled response");
|
_ -> IO.puts("unhandled response");
|
||||||
@ -137,11 +138,11 @@ defmodule BitcoinStream.Bridge do
|
|||||||
Task.start(fn -> client_tx_loop(socket) end);
|
Task.start(fn -> client_tx_loop(socket) end);
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocksub(port) do
|
def blocksub(host, port) do
|
||||||
IO.puts("Subscribing to rawblock events")
|
IO.puts("Subscribing to rawblock events")
|
||||||
{:ok, socket} = :chumak.socket(:sub)
|
{:ok, socket} = :chumak.socket(:sub)
|
||||||
:chumak.subscribe(socket, 'rawblock')
|
:chumak.subscribe(socket, 'rawblock')
|
||||||
case :chumak.connect(socket, :tcp, 'localhost', port) do
|
case :chumak.connect(socket, :tcp, String.to_charlist(host), port) do
|
||||||
{:ok, pid} -> IO.puts("Binding ok to pid #{inspect pid}");
|
{:ok, pid} -> IO.puts("Binding ok to pid #{inspect pid}");
|
||||||
{:error, reason} -> IO.puts("Binding failed: #{reason}");
|
{:error, reason} -> IO.puts("Binding failed: #{reason}");
|
||||||
_ -> IO.puts("unhandled response");
|
_ -> IO.puts("unhandled response");
|
||||||
|
@ -8,12 +8,13 @@ defmodule BitcoinStream.Mempool do
|
|||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Start a new mempool tracker,
|
Start a new mempool tracker,
|
||||||
connecting to a bitcoin node at RPC `port` for ground truth data
|
connecting to a bitcoin node at RPC `host:port` for ground truth data
|
||||||
"""
|
"""
|
||||||
def start_link(opts) do
|
def start_link(opts) do
|
||||||
{port, opts} = Keyword.pop(opts, :port);
|
{port, opts} = Keyword.pop(opts, :port);
|
||||||
IO.puts("Starting mempool agent on port #{port}");
|
{host, opts} = Keyword.pop(opts, :host);
|
||||||
case Agent.start_link(fn -> %{count: 0, port: port} end, opts) do
|
IO.puts("Starting mempool agent on #{host} port #{port}");
|
||||||
|
case Agent.start_link(fn -> %{count: 0, host: host, port: port} end, opts) do
|
||||||
{:ok, pid} ->
|
{:ok, pid} ->
|
||||||
sync(pid);
|
sync(pid);
|
||||||
{:ok, pid}
|
{:ok, pid}
|
||||||
@ -22,6 +23,10 @@ defmodule BitcoinStream.Mempool do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def getHost(pid) do
|
||||||
|
Agent.get(pid, &Map.get(&1, :host))
|
||||||
|
end
|
||||||
|
|
||||||
def getPort(pid) do
|
def getPort(pid) do
|
||||||
Agent.get(pid, &Map.get(&1, :port))
|
Agent.get(pid, &Map.get(&1, :port))
|
||||||
end
|
end
|
||||||
@ -51,11 +56,12 @@ defmodule BitcoinStream.Mempool do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sync(pid) do
|
def sync(pid) do
|
||||||
|
host = getHost(pid);
|
||||||
port = getPort(pid);
|
port = getPort(pid);
|
||||||
IO.puts("Syncing mempool with bitcoin node on port #{port}");
|
IO.puts("Syncing mempool with bitcoin node on #{host} port #{port}");
|
||||||
with { user, pw } <- rpc_creds(),
|
with { user, pw } <- rpc_creds(),
|
||||||
{:ok, rpc_request} <- Jason.encode(%{method: "getmempoolinfo", params: [], request_id: 0}),
|
{:ok, rpc_request} <- Jason.encode(%{method: "getmempoolinfo", params: [], request_id: 0}),
|
||||||
{:ok, 200, _headers, body_ref} <- :hackney.request(:post, "http://localhost:#{port}", [{"content-type", "application/json"}], rpc_request, [basic_auth: { user, pw }]),
|
{:ok, 200, _headers, body_ref} <- :hackney.request(:post, "http://#{host}:#{port}", [{"content-type", "application/json"}], rpc_request, [basic_auth: { user, pw }]),
|
||||||
{:ok, body} <- :hackney.body(body_ref),
|
{:ok, body} <- :hackney.body(body_ref),
|
||||||
{:ok, %{"result" => info}} <- Jason.decode(body),
|
{:ok, %{"result" => info}} <- Jason.decode(body),
|
||||||
%{"size" => pool_size} <- info do
|
%{"size" => pool_size} <- info do
|
||||||
|
@ -68,7 +68,7 @@ defp summarise_txns([next | rest], summarised, total) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test() do
|
def test() do
|
||||||
raw_block = File.read!("block.dat")
|
raw_block = File.read!("data/block.dat")
|
||||||
{:ok, block} = Block.decode(raw_block)
|
{:ok, block} = Block.decode(raw_block)
|
||||||
|
|
||||||
block
|
block
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
defmodule BitcoinStream.Router do
|
defmodule BitcoinStream.Router do
|
||||||
use Plug.Router
|
use Plug.Router
|
||||||
|
|
||||||
alias BitcoinStream.Donations.Lightning, as: Lightning
|
|
||||||
|
|
||||||
plug Corsica, origins: "*", allow_headers: :all
|
plug Corsica, origins: "*", allow_headers: :all
|
||||||
plug Plug.Static,
|
plug Plug.Static,
|
||||||
at: "/",
|
at: "/",
|
||||||
|
@ -2,23 +2,29 @@ defmodule BitcoinStream.Server do
|
|||||||
use Application
|
use Application
|
||||||
|
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
|
{ socket_port, "" } = Integer.parse(System.get_env("PORT"));
|
||||||
|
{ zmq_tx_port, "" } = Integer.parse(System.get_env("BITCOIN_ZMQ_RAWTX_PORT"));
|
||||||
|
{ zmq_block_port, "" } = Integer.parse(System.get_env("BITCOIN_ZMQ_RAWBLOCK_PORT"));
|
||||||
|
{ rpc_port, "" } = Integer.parse(System.get_env("BITCOIN_RPC_PORT"));
|
||||||
|
btc_host = System.get_env("BITCOIN_HOST");
|
||||||
|
|
||||||
children = [
|
children = [
|
||||||
{ BitcoinStream.BlockData, [name: :block_data] },
|
{ BitcoinStream.BlockData, [name: :block_data] },
|
||||||
{ BitcoinStream.Mempool, [port: 9959, name: :mempool] },
|
{ BitcoinStream.Mempool, [name: :mempool] },
|
||||||
BitcoinStream.Metrics.Probe,
|
BitcoinStream.Metrics.Probe,
|
||||||
Plug.Cowboy.child_spec(
|
Plug.Cowboy.child_spec(
|
||||||
scheme: :http,
|
scheme: :http,
|
||||||
plug: BitcoinStream.Router,
|
plug: BitcoinStream.Router,
|
||||||
options: [
|
options: [
|
||||||
dispatch: dispatch(),
|
dispatch: dispatch(),
|
||||||
port: 4000
|
port: socket_port
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
Registry.child_spec(
|
Registry.child_spec(
|
||||||
keys: :duplicate,
|
keys: :duplicate,
|
||||||
name: Registry.BitcoinStream
|
name: Registry.BitcoinStream
|
||||||
),
|
),
|
||||||
BitcoinStream.Bridge.child_spec(port: 29000)
|
BitcoinStream.Bridge.child_spec(host: btc_host, tx_port: zmq_tx_port, block_port: zmq_block_port)
|
||||||
]
|
]
|
||||||
|
|
||||||
opts = [strategy: :one_for_one, name: BitcoinStream.Application]
|
opts = [strategy: :one_for_one, name: BitcoinStream.Application]
|
||||||
|
@ -4,7 +4,7 @@ defmodule BitcoinStream.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :bitcoin_stream,
|
app: :bitcoin_stream,
|
||||||
version: "1.4.8",
|
version: "2.1.2",
|
||||||
elixir: "~> 1.10",
|
elixir: "~> 1.10",
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
deps: deps(),
|
deps: deps(),
|
||||||
@ -32,7 +32,7 @@ defmodule BitcoinStream.MixProject do
|
|||||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
|
||||||
# {:mix_systemd, "~> 0.7"},
|
# {:mix_systemd, "~> 0.7"},
|
||||||
# {:mix_systemd, "~> 0.7"},
|
# {:mix_systemd, "~> 0.7"},
|
||||||
{:chumak, github: "zeromq/chumak"},
|
{:chumak, "~> 1.3"},
|
||||||
# {:bitcoinex, "~> 0.1.0"},
|
# {:bitcoinex, "~> 0.1.0"},
|
||||||
# {:bitcoinex, git: "git@github.com:mononaut/bitcoinex.git", tag: "master"},
|
# {:bitcoinex, git: "git@github.com:mononaut/bitcoinex.git", tag: "master"},
|
||||||
{:bitcoinex, path: "./bitcoinex", override: true},
|
{:bitcoinex, path: "./bitcoinex", override: true},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
%{
|
%{
|
||||||
"bear": {:hex, :bear, "0.8.7", "16264309ae5d005d03718a5c82641fcc259c9e8f09adeb6fd79ca4271168656f", [:rebar3], [], "hexpm", "534217dce6a719d59e54fb0eb7a367900dbfc5f85757e8c1f94269df383f6d9b"},
|
"bear": {:hex, :bear, "0.8.7", "16264309ae5d005d03718a5c82641fcc259c9e8f09adeb6fd79ca4271168656f", [:rebar3], [], "hexpm", "534217dce6a719d59e54fb0eb7a367900dbfc5f85757e8c1f94269df383f6d9b"},
|
||||||
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
|
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
|
||||||
"chumak": {:git, "https://github.com/zeromq/chumak.git", "f0de0b609c668370fe6b486d733a137ee53abe9f", []},
|
"chumak": {:hex, :chumak, "1.4.0", "79eb44ba2da1e2a072c06bca1c79016c936423c6b8f826d6a7c2e22e046a3d40", [:rebar3], [], "hexpm", "a3a618a2cae0e3f8e844752e7f6f56c6231c5daef1a8de498a5973baa202cc5c"},
|
||||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||||
"corsica": {:hex, :corsica, "1.1.3", "5f1de40bc9285753aa03afbdd10c364dac79b2ddbf2ba9c5c9c47b397ec06f40", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8156b3a14a114a346262871333a931a1766b2597b56bf994fcfcb65443a348ad"},
|
"corsica": {:hex, :corsica, "1.1.3", "5f1de40bc9285753aa03afbdd10c364dac79b2ddbf2ba9c5c9c47b397ec06f40", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8156b3a14a114a346262871333a931a1766b2597b56bf994fcfcb65443a348ad"},
|
||||||
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
||||||
|
Loading…
Reference in New Issue
Block a user