Docker config, compatibility & env var support

This commit is contained in:
Mononaut 2022-01-23 10:31:49 -06:00
parent 71e40b9955
commit a343ec1bc6
32 changed files with 592 additions and 5538 deletions

1
client/.gitignore vendored
View File

@ -1,5 +1,6 @@
/node_modules/
/public/build/
/releases/
.DS_Store
.env

34
client/Dockerfile Normal file
View 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;"]

View 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
View 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"

View File

View 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

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
{
"name": "bitfeed-client",
"version": "2.0.4",
"version": "2.1.2",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public"
},
"devDependencies": {
"dependencies": {
"@babel/core": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"@rollup/plugin-babel": "^5.3.0",
@ -14,24 +14,22 @@
"@rollup/plugin-html": "^0.2.4",
"@rollup/plugin-node-resolve": "^13.1.1",
"@rollup/plugin-replace": "^3.0.0",
"d3-color": "^3.0.1",
"d3-interpolate": "^3.0.1",
"dotenv": "^10.0.0",
"node-sass": "^7.0.0",
"locale-currency": "0.0.2",
"qrcode": "^1.5.0",
"rollup": "^2.62.0",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-glslify": "^1.2.1",
"rollup-plugin-inline-svg": "^2.0.0",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.49.0",
"sirv-cli": "^1.0.14",
"svelte": "^3.44.3",
"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
View File

@ -0,0 +1,5 @@
window.injected = {
TARGET: 'public',
OVERRIDE_BACKEND_HOST: 'localhost',
OVERRIDE_BACKEND_PORT: 4000
}

View 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
View 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

View File

@ -31,8 +31,9 @@
<meta name="msapplication-TileColor" content="#da532c">
<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'>
<script src="/env.js"></script>
<script defer src="/build/bundle.js"></script>
</head>

View File

@ -16,7 +16,7 @@ import fs from 'fs'
configDotenv();
const hash = String(require("child_process").execSync("git rev-parse --short HEAD")).trim();
const hash = process.env.npm_package_version;
const htmlOptions = {
template: async ({ attributes, files, meta, publicPath, title }) => {
const rawTemplate = fs.readFileSync('./template/index.html', { encoding: 'utf8', flag: 'r'})

View File

@ -1,6 +1,7 @@
<script>
import TxViz from './components/TxViz.svelte'
import analytics from './utils/analytics.js'
import config from './config.js'
import { settings } from './stores.js'
if (URL) {
@ -10,7 +11,7 @@
}
}
if (!$settings.noTrack) analytics.init()
if (!$settings.noTrack && config.public) analytics.init()
</script>
<main>

View File

@ -3,8 +3,10 @@ export default {
donationRoot: 'https://donate.monospace.live',
debug: false,
layoutHints: false,
websocket_path: '/ws/txs',
localSocket: false,
public: (window.injected.TARGET === "public"),
backend: window.injected.OVERRIDE_BACKEND_HOST,
backendPort: window.injected.OVERRIDE_BACKEND_PORT,
secureSocket: (window.isSecureContext && !window.location.host.startsWith('localhost')),
nofeed: false,
txDelay: 10000,
donationsEnabled: true,

View File

@ -8,7 +8,8 @@ lastBlockId.subscribe(val => { lastBlockSeen = val })
class TxStream {
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.websocket = null
this.setConnected(false)

View File

@ -32,8 +32,8 @@
<meta name="theme-color" content="#ffffff">
<link rel='stylesheet' href='/global.css'>
{{css}}
<script src="/env.js"></script>
{{scripts}}
</head>

30
server/Dockerfile Normal file
View 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"]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -4,7 +4,7 @@ defmodule Bitcoinex.MixProject do
def project do
[
app: :bitcoinex,
version: "0.1.1",
version: "0.2.0",
elixir: "~> 1.8",
package: package(),
start_permanent: Mix.env() == :prod,
@ -31,7 +31,6 @@ defmodule Bitcoinex.MixProject do
{:excoveralls, "~> 0.10", only: :test},
{:mix_test_watch, "~> 0.8", only: :dev, runtime: false},
{:stream_data, "~> 0.1", only: :test},
{:timex, "~> 3.1"},
{:decimal, "~> 1.0"},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
]

View File

@ -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

Binary file not shown.

View File

@ -2,7 +2,7 @@ defmodule BitcoinStream.BlockData do
@moduledoc """
Block data module.
Serves a copy of the latest block
Serves a cached copy of the latest block
"""
use GenServer
@ -12,7 +12,7 @@ defmodule BitcoinStream.BlockData do
IO.puts("Starting block data link")
# 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, payload} <- Jason.encode(%{type: "block", block: block}) do
GenServer.start_link(__MODULE__, {block, payload}, opts)

View File

@ -11,18 +11,19 @@ defmodule BitcoinStream.Bridge do
alias BitcoinStream.Protocol.Transaction, as: BitcoinTx
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,
start: {BitcoinStream.Bridge, :start_link, [port]}
start: {BitcoinStream.Bridge, :start_link, [host, tx_port, block_port]}
}
end
def start_link(port) do
IO.puts("Starting Bitcoin bridge on port #{port}")
connect_to_server(port);
txsub(port);
blocksub(port);
def start_link(host, tx_port, block_port) do
IO.puts("Starting Bitcoin bridge on #{host} ports #{tx_port}, #{block_port}")
connect_to_server(host, tx_port);
connect_to_server(host, block_port);
txsub(host, tx_port);
blocksub(host, block_port);
GenServer.start_link(__MODULE__, %{})
end
@ -33,11 +34,11 @@ defmodule BitcoinStream.Bridge do
@doc """
Create zmq client
"""
def start_client(port) do
IO.puts("Starting client on port #{port}");
def start_client(host, port) do
IO.puts("Starting client on #{host} port #{port}");
{:ok, socket} = :chumak.socket(:pair);
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");
{socket, pid}
end
@ -100,7 +101,7 @@ defmodule BitcoinStream.Bridge do
IO.puts("client block loop");
with {:ok, message} <- :chumak.recv_multipart(socket),
[_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
GenServer.cast(:block_data, {:block, block})
sendBlock(block);
@ -117,18 +118,18 @@ defmodule BitcoinStream.Bridge do
@doc """
Set up demo zmq client
"""
def connect_to_server(port) do
IO.puts("Starting on #{port}");
{client_socket, _client_pid} = start_client(port);
def connect_to_server(host, port) do
IO.puts("Starting on #{host}:#{port}");
{client_socket, _client_pid} = start_client(host, port);
IO.puts("Started client");
client_socket
end
def txsub(port) do
def txsub(host, port) do
IO.puts("Subscribing to rawtx events")
{:ok, socket} = :chumak.socket(:sub)
: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}");
{:error, reason} -> IO.puts("Binding failed: #{reason}");
_ -> IO.puts("unhandled response");
@ -137,11 +138,11 @@ defmodule BitcoinStream.Bridge do
Task.start(fn -> client_tx_loop(socket) end);
end
def blocksub(port) do
def blocksub(host, port) do
IO.puts("Subscribing to rawblock events")
{:ok, socket} = :chumak.socket(:sub)
: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}");
{:error, reason} -> IO.puts("Binding failed: #{reason}");
_ -> IO.puts("unhandled response");

View File

@ -8,12 +8,13 @@ defmodule BitcoinStream.Mempool do
@doc """
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
{port, opts} = Keyword.pop(opts, :port);
IO.puts("Starting mempool agent on port #{port}");
case Agent.start_link(fn -> %{count: 0, port: port} end, opts) do
{host, opts} = Keyword.pop(opts, :host);
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} ->
sync(pid);
{:ok, pid}
@ -22,6 +23,10 @@ defmodule BitcoinStream.Mempool do
end
end
def getHost(pid) do
Agent.get(pid, &Map.get(&1, :host))
end
def getPort(pid) do
Agent.get(pid, &Map.get(&1, :port))
end
@ -51,11 +56,12 @@ defmodule BitcoinStream.Mempool do
end
def sync(pid) do
host = getHost(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(),
{: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, %{"result" => info}} <- Jason.decode(body),
%{"size" => pool_size} <- info do

View File

@ -68,7 +68,7 @@ defp summarise_txns([next | rest], summarised, total) do
end
def test() do
raw_block = File.read!("block.dat")
raw_block = File.read!("data/block.dat")
{:ok, block} = Block.decode(raw_block)
block

View File

@ -1,8 +1,6 @@
defmodule BitcoinStream.Router do
use Plug.Router
alias BitcoinStream.Donations.Lightning, as: Lightning
plug Corsica, origins: "*", allow_headers: :all
plug Plug.Static,
at: "/",

View File

@ -2,23 +2,29 @@ defmodule BitcoinStream.Server do
use Application
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 = [
{ BitcoinStream.BlockData, [name: :block_data] },
{ BitcoinStream.Mempool, [port: 9959, name: :mempool] },
{ BitcoinStream.Mempool, [name: :mempool] },
BitcoinStream.Metrics.Probe,
Plug.Cowboy.child_spec(
scheme: :http,
plug: BitcoinStream.Router,
options: [
dispatch: dispatch(),
port: 4000
port: socket_port
]
),
Registry.child_spec(
keys: :duplicate,
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]

View File

@ -4,7 +4,7 @@ defmodule BitcoinStream.MixProject do
def project do
[
app: :bitcoin_stream,
version: "1.4.8",
version: "2.1.2",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
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"},
# {:mix_systemd, "~> 0.7"},
# {:mix_systemd, "~> 0.7"},
{:chumak, github: "zeromq/chumak"},
{:chumak, "~> 1.3"},
# {:bitcoinex, "~> 0.1.0"},
# {:bitcoinex, git: "git@github.com:mononaut/bitcoinex.git", tag: "master"},
{:bitcoinex, path: "./bitcoinex", override: true},

View File

@ -1,7 +1,7 @@
%{
"bear": {:hex, :bear, "0.8.7", "16264309ae5d005d03718a5c82641fcc259c9e8f09adeb6fd79ca4271168656f", [:rebar3], [], "hexpm", "534217dce6a719d59e54fb0eb7a367900dbfc5f85757e8c1f94269df383f6d9b"},
"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"},
"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"},