move to cryptoadvance repo
61
README.md
|
|
@ -1,43 +1,46 @@
|
|||
# Specter Desktop
|
||||
|
||||
## DISCLAIMER
|
||||
A flask web GUI for Bitcoin Core to use with airgapped hardware wallets
|
||||
|
||||
This software is **WORK IN PROGRESS and NOT READY TO USE YET**. The master branch is empty **until the first release** of the software with core functionality implemented. At the moment all the code is in the [0.0.0-alpha branch](https://github.com/cryptoadvance/specter-desktop/tree/0.0.0-alpha).
|
||||
This is the working alpha branch. Random comments, weird pieces of code and comments, not documented, breaking changes etc.
|
||||
|
||||
## Why?
|
||||
Wait until we merge it to master, then you can use it.
|
||||
|
||||
Bitcoin Core has a very powerful command line interface and a wonderful daemon. Using PSBT and HWI it can also work with hardware wallets, but at the moment it is too linux-way. The same applies to multisignature setups.
|
||||
## TODO
|
||||
|
||||
The goal of this project is to make a convenient and user-friendly GUI around Bitcoin Core with a focus on multisignature setup with airgapped hardware wallets.
|
||||
- Send transaction:
|
||||
- save psbt that is not finished
|
||||
- upload file and combine psbt
|
||||
- determine who else should sign (just look up pubkeys)
|
||||
- show as qr
|
||||
- work with coldcard to fix their multisig
|
||||
- Display cosigners xpubs
|
||||
- Migration between app versions?
|
||||
|
||||
At the moment we are working on integration of our [Specter-DIY hardware wallet](https://github.com/cryptoadvance/specter-diy) that uses QR codes as a main communication channel, and ColdCard that uses SD cards. Later on we plan to integrate "hot" hardware wallets using [HWI tool](https://github.com/bitcoin-core/HWI) and [Junction](https://github.com/justinmoon/junction).
|
||||
### Later:
|
||||
|
||||
## Current status
|
||||
- Import wallet from coldcard
|
||||
- Show addresses QR codes in the PSBT
|
||||
- Delete wallets (requires access to the wallet folder - can only unload remotely)
|
||||
|
||||
Most of the code is there, app should be ready for alpha testing by September 8th.
|
||||
### A bit later:
|
||||
|
||||
## A few screenshots
|
||||
- proper error handling
|
||||
- run flask server from distribution
|
||||
- run bitcoind and bitcoin-cli from the distribution
|
||||
- bitcoin configuration gui
|
||||
|
||||
### Adding a new device
|
||||
### Nice to have
|
||||
|
||||

|
||||
- control fee
|
||||
- Tx batching (sendtomany)
|
||||
- coin control
|
||||
- rbf
|
||||
- privacy mode - hide balances and/or menu bar (can be in settings)
|
||||
- mobile version
|
||||
- use vue js?
|
||||
- specter-pi - airgapped bitcoin core for signing on pi
|
||||
|
||||

|
||||
### Far in the future
|
||||
|
||||
### Creating a new wallet
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Wallet interface
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Configuration
|
||||
|
||||

|
||||
- remote node over tor - camera requires https, how to solve? (let's encrypt doesn't work)
|
||||
|
|
|
|||
140
descriptor.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import re
|
||||
|
||||
# From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
|
||||
|
||||
def PolyMod(c, val):
|
||||
c0 = c >> 35
|
||||
c = ((c & 0x7ffffffff) << 5) ^ val
|
||||
if (c0 & 1):
|
||||
c ^= 0xf5dee51989
|
||||
if (c0 & 2):
|
||||
c ^= 0xa9fdca3312
|
||||
if (c0 & 4):
|
||||
c ^= 0x1bab10e32d
|
||||
if (c0 & 8):
|
||||
c ^= 0x3706b1677a
|
||||
if (c0 & 16):
|
||||
c ^= 0x644d626ffd
|
||||
return c
|
||||
|
||||
def DescriptorChecksum(desc):
|
||||
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
c = 1
|
||||
cls = 0
|
||||
clscount = 0
|
||||
for ch in desc:
|
||||
pos = INPUT_CHARSET.find(ch)
|
||||
if pos == -1:
|
||||
return ""
|
||||
c = PolyMod(c, pos & 31)
|
||||
cls = cls * 3 + (pos >> 5)
|
||||
clscount += 1
|
||||
if clscount == 3:
|
||||
c = PolyMod(c, cls)
|
||||
cls = 0
|
||||
clscount = 0
|
||||
if clscount > 0:
|
||||
c = PolyMod(c, cls)
|
||||
for j in range (0, 8):
|
||||
c = PolyMod(c, 0)
|
||||
c ^= 1
|
||||
|
||||
ret = [None] * 8
|
||||
for j in range(0, 8):
|
||||
ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
|
||||
return ''.join(ret)
|
||||
|
||||
def AddChecksum(desc):
|
||||
return desc + "#" + DescriptorChecksum(desc)
|
||||
|
||||
class Descriptor:
|
||||
def __init__(self, origin_fingerprint, origin_path, base_key, path_suffix, testnet, sh_wpkh, wpkh):
|
||||
self.origin_fingerprint = origin_fingerprint
|
||||
self.origin_path = origin_path
|
||||
self.path_suffix = path_suffix
|
||||
self.base_key = base_key
|
||||
self.testnet = testnet
|
||||
self.sh_wpkh = sh_wpkh
|
||||
self.wpkh = wpkh
|
||||
self.m_path = None
|
||||
|
||||
if origin_path:
|
||||
self.m_path_base = "m" + origin_path
|
||||
self.m_path = "m" + origin_path + (path_suffix or "")
|
||||
|
||||
@classmethod
|
||||
def parse(cls, desc, testnet = False):
|
||||
sh_wpkh = None
|
||||
wpkh = None
|
||||
origin_fingerprint = None
|
||||
origin_path = None
|
||||
base_key_and_path_match = None
|
||||
base_key = None
|
||||
path_suffix = None
|
||||
|
||||
# Check the checksum
|
||||
check_split = desc.split('#')
|
||||
if len(check_split) > 2:
|
||||
return None
|
||||
if len(check_split) == 2:
|
||||
if len(check_split[1]) != 8:
|
||||
return None
|
||||
checksum = DescriptorChecksum(check_split[0])
|
||||
if not checksum.strip():
|
||||
return None
|
||||
if checksum != check_split[1]:
|
||||
return None
|
||||
desc = check_split[0]
|
||||
|
||||
if desc.startswith("sh(wpkh("):
|
||||
sh_wpkh = True
|
||||
elif desc.startswith("wpkh("):
|
||||
wpkh = True
|
||||
|
||||
origin_match = re.search(r"\[(.*)\]", desc)
|
||||
if origin_match:
|
||||
origin = origin_match.group(1)
|
||||
match = re.search(r"^([0-9a-fA-F]{8})(\/.*)", origin)
|
||||
if match:
|
||||
origin_fingerprint = match.group(1)
|
||||
origin_path = match.group(2)
|
||||
# Replace h with '
|
||||
origin_path = origin_path.replace('h', '\'')
|
||||
|
||||
base_key_and_path_match = re.search(r"\[.*\](\w+)([\/\)][\d'\/\*]*)", desc)
|
||||
else:
|
||||
base_key_and_path_match = re.search(r"\((\w+)([\/\)][\d'\/\*]*)", desc)
|
||||
|
||||
if base_key_and_path_match:
|
||||
base_key = base_key_and_path_match.group(1)
|
||||
path_suffix = base_key_and_path_match.group(2)
|
||||
if path_suffix == ")":
|
||||
path_suffix = None
|
||||
else:
|
||||
if origin_match == None:
|
||||
return None
|
||||
|
||||
return cls(origin_fingerprint, origin_path, base_key, path_suffix, testnet, sh_wpkh, wpkh)
|
||||
|
||||
|
||||
def serialize(self):
|
||||
descriptor_open = 'pkh('
|
||||
descriptor_close = ')'
|
||||
origin = ''
|
||||
path_suffix = ''
|
||||
|
||||
if self.wpkh == True:
|
||||
descriptor_open = 'wpkh('
|
||||
elif self.sh_wpkh == True:
|
||||
descriptor_open = 'sh(wpkh('
|
||||
descriptor_close = '))'
|
||||
|
||||
if self.origin_fingerprint and self.origin_path:
|
||||
origin = '[' + self.origin_fingerprint + self.origin_path + ']'
|
||||
|
||||
if self.path_suffix:
|
||||
path_suffix = self.path_suffix
|
||||
|
||||
return AddChecksum(descriptor_open + origin + self.base_key + path_suffix + descriptor_close)
|
||||
197
helpers.py
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import hashlib
|
||||
import subprocess
|
||||
|
||||
import collections
|
||||
import six
|
||||
from collections import OrderedDict
|
||||
import os, json
|
||||
|
||||
try:
|
||||
collectionsAbc = collections.abc
|
||||
except:
|
||||
collectionsAbc = collections
|
||||
|
||||
def deep_update(d, u):
|
||||
for k, v in six.iteritems(u):
|
||||
dv = d.get(k, {})
|
||||
if not isinstance(dv, collectionsAbc.Mapping):
|
||||
d[k] = v
|
||||
elif isinstance(v, collectionsAbc.Mapping):
|
||||
d[k] = deep_update(dv, v)
|
||||
else:
|
||||
d[k] = v
|
||||
return d
|
||||
|
||||
def load_jsons(folder, key=None):
|
||||
files = [f for f in os.listdir(folder) if f.endswith(".json")]
|
||||
files.sort(key=lambda x: os.path.getmtime(os.path.join(folder, x)))
|
||||
dd = OrderedDict()
|
||||
for fname in files:
|
||||
with open(os.path.join(folder, fname)) as f:
|
||||
d = json.loads(f.read())
|
||||
if key is None:
|
||||
dd[fname[:-5]] = d
|
||||
else:
|
||||
d["fullpath"] = os.path.join(folder, fname)
|
||||
d["alias"] = fname[:-5]
|
||||
dd[d[key]] = d
|
||||
return dd
|
||||
|
||||
BASE58_ALPHABET = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
|
||||
VALID_PREFIXES = {
|
||||
b"\x04\x35\x87\xcf": { # testnet
|
||||
b"\x04\x35\x87\xcf": None, # unknown, maybe pkh
|
||||
b"\x04\x4a\x52\x62": "sh-wpkh",
|
||||
b"\x04\x5f\x1c\xf6": "wpkh",
|
||||
b"\x02\x42\x89\xef": "sh-wsh",
|
||||
b"\x02\x57\x54\x83": "wsh",
|
||||
},
|
||||
b"\x04\x88\xb2\x1e": { # mainnet
|
||||
b"\x04\x88\xb2\x1e": None, #unknown, maybe pkh
|
||||
b"\x04\x9d\x7c\xb2": "sh-wpkh",
|
||||
b"\x04\xb2\x47\x46": "wpkh",
|
||||
b"\x02\x95\xb4\x3f": "sh-wsh",
|
||||
b"\x02\xaa\x7e\xd3": "wsh",
|
||||
}
|
||||
}
|
||||
|
||||
def double_sha256(s):
|
||||
return hashlib.sha256(hashlib.sha256(s).digest()).digest()
|
||||
|
||||
def encode_base58(s):
|
||||
# determine how many 0 bytes (b'\x00') s starts with
|
||||
count = 0
|
||||
for c in s:
|
||||
if c == 0:
|
||||
count += 1
|
||||
else:
|
||||
break
|
||||
prefix = b'1' * count
|
||||
# convert from binary to hex, then hex to integer
|
||||
num = int.from_bytes(s, 'big')
|
||||
result = bytearray()
|
||||
while num > 0:
|
||||
num, mod = divmod(num, 58)
|
||||
result.insert(0, BASE58_ALPHABET[mod])
|
||||
|
||||
return prefix + bytes(result)
|
||||
|
||||
def encode_base58_checksum(s):
|
||||
return encode_base58(s + double_sha256(s)[:4]).decode('ascii')
|
||||
|
||||
|
||||
def decode_base58(s, num_bytes=82, strip_leading_zeros=False):
|
||||
num = 0
|
||||
for c in s.encode('ascii'):
|
||||
num *= 58
|
||||
num += BASE58_ALPHABET.index(c)
|
||||
combined = num.to_bytes(num_bytes, byteorder='big')
|
||||
if strip_leading_zeros:
|
||||
while combined[0] == 0:
|
||||
combined = combined[1:]
|
||||
checksum = combined[-4:]
|
||||
if double_sha256(combined[:-4])[:4] != checksum:
|
||||
raise ValueError('bad address: {} {}'.format(
|
||||
checksum, double_sha256(combined)[:4]))
|
||||
return combined[:-4]
|
||||
|
||||
def parse_xpub(xpub):
|
||||
r = {"derivation": None}
|
||||
derivation = None
|
||||
arr = xpub.strip().split("]")
|
||||
r["original"] = arr[-1]
|
||||
if len(arr) > 1:
|
||||
derivation = arr[0].replace("'","h").lower()
|
||||
xpub = arr[1]
|
||||
if derivation is not None:
|
||||
if derivation[0]!="[":
|
||||
raise "Missing leading ["
|
||||
arr = derivation[1:].split("/")
|
||||
try:
|
||||
fng = bytes.fromhex(arr[0])
|
||||
except:
|
||||
raise Exception("Fingerprint is not hex")
|
||||
if len(fng) != 4:
|
||||
raise Exception("Incorrect fingerprint length")
|
||||
r["fingerprint"] = arr[0]
|
||||
for der in arr[1:]:
|
||||
if der[-1] == "h":
|
||||
der = der[:-1]
|
||||
try:
|
||||
i = int(der)
|
||||
except:
|
||||
print("index")
|
||||
raise Exception("Incorrect index")
|
||||
arr[0] = "m"
|
||||
r["derivation"] = "/".join(arr)
|
||||
# checking xpub prefix and defining key type
|
||||
b = decode_base58(xpub, num_bytes=82)
|
||||
prefix = b[:4]
|
||||
is_valid = False
|
||||
key_type = None
|
||||
for k in VALID_PREFIXES:
|
||||
if prefix in VALID_PREFIXES[k].keys():
|
||||
key_type = VALID_PREFIXES[k][prefix]
|
||||
prefix = k
|
||||
is_valid = True
|
||||
break
|
||||
if not is_valid:
|
||||
raise Exception("Invalid xpub prefix: %s", prefix.hex())
|
||||
|
||||
# defining key type from derivation
|
||||
if r["derivation"] is not None and key_type is None:
|
||||
arr = r["derivation"].split("/")
|
||||
purpose = arr[1]
|
||||
if purpose == "44h":
|
||||
key_type = "pkh"
|
||||
elif purpose == "49h":
|
||||
key_type = "sh-wpkh"
|
||||
elif purpose == "84h":
|
||||
key_type = "wpkh"
|
||||
elif purpose == "45h":
|
||||
key_type = "sh"
|
||||
elif purpose == "48h":
|
||||
if len(arr)>=5:
|
||||
if arr[4] == "1h":
|
||||
key_type = "sh-wsh"
|
||||
elif arr[4] == "2h":
|
||||
key_type = "wsh"
|
||||
r["type"] = key_type
|
||||
|
||||
b = prefix + b[4:]
|
||||
r["xpub"] = encode_base58_checksum(b)
|
||||
return r
|
||||
|
||||
def normalize_xpubs(xpubs):
|
||||
xpubs = xpubs
|
||||
lines = [l.strip() for l in xpubs.split("\n") if len(l) > 0]
|
||||
parsed = []
|
||||
failed = []
|
||||
normalized = []
|
||||
for line in lines:
|
||||
try:
|
||||
x = parse_xpub(line)
|
||||
normalized.append(x)
|
||||
parsed.append(line)
|
||||
except:
|
||||
failed.append(line)
|
||||
return (normalized, parsed, failed)
|
||||
|
||||
# should work in all python versions
|
||||
def run_shell(cmd):
|
||||
"""
|
||||
Runs a shell command.
|
||||
Example: run(["ls", "-a"])
|
||||
Returns: dict({"code": returncode, "out": stdout, "err": stderr})
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(cmd,
|
||||
stdout = subprocess.PIPE,
|
||||
stderr = subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = proc.communicate()
|
||||
return { "code": proc.returncode, "out": stdout, "err": stderr }
|
||||
except:
|
||||
return { "code": 0xf00dbabe, "out": b"", "err": b"Can't run subprocess" }
|
||||
|
||||
BIN
nwjs_app/app.icns
Normal file
BIN
nwjs_app/ca.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
nwjs_app/icon.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
76
nwjs_app/index.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Specter Desktop</title>
|
||||
<style type="text/css">
|
||||
html, body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #192432;
|
||||
/*color: #7f8fa4;*/
|
||||
color: #fff;
|
||||
font-family: "Source Sans Pro", sans-serif;
|
||||
font-weight: lighter;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
img{
|
||||
width: 300px;
|
||||
}
|
||||
div{
|
||||
margin: 20px;
|
||||
}
|
||||
.top{
|
||||
-webkit-app-region: drag;
|
||||
height: 70px;
|
||||
width: 100%;
|
||||
/*border: 1px solid red;*/
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top"> </div>
|
||||
<div>
|
||||
<img src="ca.png">
|
||||
</div>
|
||||
<div id="lbl">
|
||||
Specter Desktop is loading...
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
var err;
|
||||
let url = "http://localhost:25441/";
|
||||
setInterval( () => {
|
||||
var xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.onreadystatechange = function() {
|
||||
if(xmlHttp.status == 200 || xmlHttp.status == 302){
|
||||
window.location.href = url;
|
||||
}else{
|
||||
document.getElementById("lbl").innerHTML = "Connection failed... Trying again...";
|
||||
}
|
||||
}
|
||||
xmlHttp.onerror = function(e){
|
||||
document.getElementById("lbl").innerHTML = "Connection failed... Trying again...";
|
||||
console.log(xmlHttp);
|
||||
err = xmlHttp;
|
||||
}
|
||||
xmlHttp.open("GET", url, true); // true for asynchronous
|
||||
xmlHttp.send(null);
|
||||
},5000);
|
||||
</script>
|
||||
</html>
|
||||
10
nwjs_app/package.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "Specter Desktop",
|
||||
"main": "index.html",
|
||||
"window": {
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"icon": "icon.png",
|
||||
"frame": true
|
||||
}
|
||||
}
|
||||
82
rpc.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import requests, json, os
|
||||
|
||||
# TODO: redefine __dir__ and help
|
||||
|
||||
RPC_PORTS = { "test": 18332, "regtest": 18443, "main": 8332 }
|
||||
|
||||
class BitcoinCLI:
|
||||
def __init__(self, user, passwd, host="127.0.0.1", port=18332, protocol="http", path="", timeout=3):
|
||||
path = path.replace("//","/") # just in case
|
||||
self.user = user
|
||||
self.passwd = passwd
|
||||
self.port = port
|
||||
self.protocol = protocol
|
||||
self.host = host
|
||||
self.path = path
|
||||
self.timeout = timeout
|
||||
self.r = None
|
||||
def wallet(name=""):
|
||||
return BitcoinCLI(user=self.user,
|
||||
passwd=self.passwd,
|
||||
port=self.port,
|
||||
protocol=self.protocol,
|
||||
host=self.host,
|
||||
path="{}/wallet/{}".format(self.path, name),
|
||||
timeout=self.timeout
|
||||
)
|
||||
self.wallet = wallet
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return "{s.protocol}://{s.user}:{s.passwd}@{s.host}:{s.port}{s.path}".format(s=self)
|
||||
|
||||
|
||||
def __getattr__(self, method):
|
||||
# if hasattr(self, method):
|
||||
# self.
|
||||
headers = {'content-type': 'application/json'}
|
||||
def fn(*args, **kwargs):
|
||||
payload = {
|
||||
"method": method,
|
||||
"params": args,
|
||||
"jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
}
|
||||
timeout = self.timeout
|
||||
if "timeout" in kwargs:
|
||||
timeout = kwargs["timeout"]
|
||||
url = self.url
|
||||
if "wallet" in kwargs:
|
||||
url = url+"/wallet/{}".format(kwargs["wallet"])
|
||||
r = requests.post(
|
||||
url, data=json.dumps(payload), headers=headers, timeout=timeout)
|
||||
self.r = r
|
||||
if r.status_code != 200:
|
||||
raise Exception("Server responded with error code %d: %s" % (r.status_code, r.text))
|
||||
r = r.json()
|
||||
if r["error"] is not None:
|
||||
raise Exception(r["error"])
|
||||
return r["result"]
|
||||
return fn
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
cli = BitcoinCLI("bitcoinrpc", "foi3uf092ury97iufhjf30982hf928uew9jd209j", port=18443)
|
||||
|
||||
print(cli.url)
|
||||
|
||||
print(cli.getmininginfo())
|
||||
|
||||
print(cli.listwallets())
|
||||
|
||||
##### WORKING WITH WALLETS #########
|
||||
|
||||
print(cli.getbalance(wallet=""))
|
||||
|
||||
# or
|
||||
|
||||
w = cli.wallet("") # will load default wallet.dat
|
||||
|
||||
print(w.url)
|
||||
|
||||
print(w.getbalance()) # now you can run -rpcwallet commands
|
||||
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 189 KiB |
487
server.py
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
import sys, json, os, time, base64
|
||||
import requests
|
||||
import random, copy
|
||||
from collections import OrderedDict
|
||||
from threading import Thread
|
||||
|
||||
from flask import Flask, render_template, request, redirect
|
||||
from flask_qrcode import QRcode
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
from helpers import normalize_xpubs, run_shell
|
||||
from descriptor import AddChecksum
|
||||
from rpc import BitcoinCLI, RPC_PORTS
|
||||
|
||||
from specter import Specter, purposes, addrtypes
|
||||
from datetime import datetime
|
||||
import urllib
|
||||
|
||||
app = Flask(__name__)
|
||||
QRcode(app) # enable qr codes generation
|
||||
|
||||
DATA_FOLDER = "~/.specter"
|
||||
|
||||
rand = random.randint(0, 1e32) # to force style refresh
|
||||
|
||||
MSIG_TYPES = {
|
||||
"legacy": "P2SH",
|
||||
"p2sh-segwit": "P2SH_P2WSH",
|
||||
"bech32": "P2WSH"
|
||||
}
|
||||
SINGLE_TYPES = {
|
||||
"legacy": "P2PKH",
|
||||
"p2sh-segwit": "P2SH_P2WPKH",
|
||||
"bech32": "P2WPKH"
|
||||
}
|
||||
|
||||
|
||||
################ routes ####################
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
specter.check()
|
||||
if len(specter.wallets) > 0:
|
||||
return redirect("/wallets/%s" % specter.wallets[specter.wallets.names()[0]]["alias"])
|
||||
# TODO: add onboarding process
|
||||
return render_template("base.html", specter=specter, rand=rand)
|
||||
|
||||
@app.route('/settings/', methods=['GET', 'POST'])
|
||||
def settings():
|
||||
specter.check()
|
||||
rpc = specter.config["rpc"]
|
||||
user = rpc["user"]
|
||||
passwd = rpc["password"]
|
||||
port = rpc["port"]
|
||||
host = rpc["host"]
|
||||
test = None
|
||||
if request.method == 'POST':
|
||||
user = request.form['username']
|
||||
passwd = request.form['password']
|
||||
port = request.form['port']
|
||||
host = request.form["host"]
|
||||
action = request.form['action']
|
||||
if action == "test":
|
||||
test = specter.test_rpc(user=user, password=passwd, port=port, host=host)
|
||||
if action == "save":
|
||||
specter.update_rpc(user=user, password=passwd, port=port, host=host)
|
||||
specter.check()
|
||||
return redirect("/")
|
||||
else:
|
||||
pass
|
||||
return render_template("settings.html", username=user, password=passwd, port=port, host=host, specter=specter, rand=rand)
|
||||
|
||||
################# wallet management #####################
|
||||
|
||||
@app.route('/new_wallet/')
|
||||
def new_wallet():
|
||||
specter.check()
|
||||
err = None
|
||||
if specter.chain is None:
|
||||
err = "Configure Bitcoin Core to create wallets"
|
||||
return render_template("base.html", error=err, specter=specter, rand=rand)
|
||||
return render_template("new_wallet.html", specter=specter, rand=rand)
|
||||
|
||||
@app.route('/new_wallet/simple/', methods=['GET', 'POST'])
|
||||
def new_wallet_simple():
|
||||
specter.check()
|
||||
name = "Simple"
|
||||
wallet_name = name
|
||||
i = 2
|
||||
err = None
|
||||
while wallet_name in specter.wallets.names():
|
||||
wallet_name = "%s %d" % (name, i)
|
||||
i+=1
|
||||
device = None
|
||||
if request.method == 'POST':
|
||||
action = request.form['action']
|
||||
wallet_name = request.form['wallet_name']
|
||||
if wallet_name in specter.wallets.names():
|
||||
err = "Wallet already exists"
|
||||
if "device" not in request.form:
|
||||
err = "Select the device"
|
||||
else:
|
||||
device_name = request.form['device']
|
||||
wallet_type = request.form['type']
|
||||
if action == 'device' and err is None:
|
||||
dev = copy.deepcopy(specter.devices[device_name])
|
||||
prefix = "tpub"
|
||||
if specter.chain == "main":
|
||||
prefix = "xpub"
|
||||
allowed_types = [None, wallet_type]
|
||||
dev["keys"] = [k for k in dev["keys"] if k["xpub"].startswith(prefix) and k["type"] in allowed_types]
|
||||
pur = {
|
||||
None: "General",
|
||||
"wpkh": "Segwit (bech32)",
|
||||
"sh-wpkh": "Nested Segwit",
|
||||
"pkh": "Legacy",
|
||||
}
|
||||
return render_template("new_simple_keys.html", purposes=pur, wallet_type=wallet_type, wallet_name=wallet_name, device=dev, error=err, specter=specter, rand=rand)
|
||||
if action == 'key' and err is None:
|
||||
original_xpub = request.form['key']
|
||||
device = specter.devices[device_name]
|
||||
key = None
|
||||
for k in device["keys"]:
|
||||
if k["original"] == original_xpub:
|
||||
key = k
|
||||
break
|
||||
if key is None:
|
||||
return render_template("base.html", error="Key not found", specter=specter, rand=rand)
|
||||
# create a wallet here
|
||||
wallet = specter.wallets.create_simple(wallet_name, wallet_type, key, device)
|
||||
return redirect("/wallets/%s/" % wallet["alias"])
|
||||
return render_template("new_simple.html", wallet_name=wallet_name, device=device, error=err, specter=specter, rand=rand)
|
||||
|
||||
@app.route('/new_wallet/multisig/', methods=['GET', 'POST'])
|
||||
def new_wallet_multi():
|
||||
specter.check()
|
||||
name = "Multisig"
|
||||
wallet_type = "wsh"
|
||||
wallet_name = name
|
||||
i = 2
|
||||
err = None
|
||||
while wallet_name in specter.wallets.names():
|
||||
wallet_name = "%s %d" % (name, i)
|
||||
i+=1
|
||||
device = None
|
||||
|
||||
sigs_total = len(specter.devices)
|
||||
if sigs_total < 2:
|
||||
err = "You need more devices to do multisig"
|
||||
return render_template("base.html", specter=specter, rand=rand)
|
||||
sigs_required = sigs_total*2//3
|
||||
if sigs_required < 2:
|
||||
sigs_required = 2
|
||||
cosigner_index = 0
|
||||
cosigners = []
|
||||
keys = []
|
||||
|
||||
if request.method == 'POST':
|
||||
print(request.form)
|
||||
action = request.form['action']
|
||||
wallet_name = request.form['wallet_name']
|
||||
cosigner_index = int(request.form['cosigner_index'])
|
||||
sigs_required = int(request.form['sigs_required'])
|
||||
sigs_total = int(request.form['sigs_total'])
|
||||
if wallet_name in specter.wallets.names():
|
||||
err = "Wallet already exists"
|
||||
wallet_type = request.form['type']
|
||||
for i in range(0, cosigner_index):
|
||||
cosigners.append(request.form['cosigner%d' % i])
|
||||
pur = {
|
||||
None: "General",
|
||||
"wsh": "Segwit (bech32)",
|
||||
"sh-wsh": "Nested Segwit",
|
||||
"sh": "Legacy",
|
||||
}
|
||||
if action == 'device' and err is None:
|
||||
if "device" not in request.form:
|
||||
err = "Select the device"
|
||||
else:
|
||||
device_name = request.form['device']
|
||||
if err is None:
|
||||
cosigner_index += 1
|
||||
cosigners.append(request.form["device"])
|
||||
print(cosigners, len(cosigners), sigs_required)
|
||||
if len(cosigners) == sigs_total:
|
||||
devs = []
|
||||
prefix = "tpub"
|
||||
if specter.chain == "main":
|
||||
prefix = "xpub"
|
||||
for k in cosigners:
|
||||
dev = copy.deepcopy(specter.devices[k])
|
||||
dev["keys"] = [k for k in dev["keys"] if k["xpub"].startswith(prefix) and (k["type"] is None or k["type"] == wallet_type)]
|
||||
if len(dev["keys"]) == 0:
|
||||
err = "Device %s doesn't have keys matching this wallet type" % dev["name"]
|
||||
devs.append(dev)
|
||||
return render_template("new_simple_keys.html", purposes=pur,
|
||||
wallet_type=wallet_type, wallet_name=wallet_name,
|
||||
cosigners=devs, keys=keys, sigs_required=sigs_required,
|
||||
sigs_total=sigs_total, cosigner_index=cosigner_index,
|
||||
error=err, specter=specter, rand=rand)
|
||||
if action == 'key' and err is None:
|
||||
cosigners = [specter.devices[k] for k in cosigners]
|
||||
for i in range(0, cosigner_index):
|
||||
try:
|
||||
key = request.form['key%d' % i]
|
||||
for k in cosigners[i]["keys"]:
|
||||
if k["original"] == key:
|
||||
keys.append(k)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
devs = []
|
||||
if len(keys) != sigs_total or len(cosigners) != sigs_total:
|
||||
prefix = "tpub"
|
||||
if specter.chain == "main":
|
||||
prefix = "xpub"
|
||||
for k in cosigners:
|
||||
dev = copy.deepcopy(k)
|
||||
dev["keys"] = [k for k in dev["keys"] if k["xpub"].startswith(prefix) and (k["type"] is None or k["type"] == wallet_type)]
|
||||
devs.append(dev)
|
||||
err="Did you select all the keys?"
|
||||
return render_template("new_simple_keys.html", purposes=pur,
|
||||
wallet_type=wallet_type, wallet_name=wallet_name,
|
||||
cosigners=devs, keys=keys, sigs_required=sigs_required,
|
||||
sigs_total=sigs_total, cosigner_index=cosigner_index,
|
||||
error=err, specter=specter, rand=rand)
|
||||
# create a wallet here
|
||||
wallet = specter.wallets.create_multi(wallet_name, sigs_required, wallet_type, keys, cosigners)
|
||||
return redirect("/wallets/%s/" % wallet["alias"])
|
||||
return render_template("new_simple.html", cosigners=cosigners, wallet_type=wallet_type, wallet_name=wallet_name, device=device, error=err, sigs_required=sigs_required, sigs_total=sigs_total, cosigner_index=cosigner_index, specter=specter, rand=rand)
|
||||
|
||||
@app.route('/wallets/<wallet_alias>/')
|
||||
def wallet(wallet_alias):
|
||||
specter.check()
|
||||
try:
|
||||
wallet = specter.wallets.get_by_alias(wallet_alias)
|
||||
except:
|
||||
return render_template("base.html", error="Wallet not found", specter=specter, rand=rand)
|
||||
print(wallet.balance["untrusted_pending"] + wallet.balance["trusted"])
|
||||
if wallet.balance["untrusted_pending"] + wallet.balance["trusted"] == 0:
|
||||
return redirect("/wallets/%s/receive/" % wallet_alias)
|
||||
else:
|
||||
return redirect("/wallets/%s/tx/" % wallet_alias)
|
||||
|
||||
@app.route('/wallets/<wallet_alias>/tx/')
|
||||
def wallet_tx(wallet_alias):
|
||||
specter.check()
|
||||
try:
|
||||
wallet = specter.wallets.get_by_alias(wallet_alias)
|
||||
except:
|
||||
return render_template("base.html", error="Wallet not found", specter=specter, rand=rand)
|
||||
return render_template("wallet_tx.html", wallet_alias=wallet_alias, wallet=wallet, specter=specter, rand=rand)
|
||||
|
||||
@app.route('/wallets/<wallet_alias>/receive/', methods=['GET', 'POST'])
|
||||
def wallet_receive(wallet_alias):
|
||||
specter.check()
|
||||
try:
|
||||
wallet = specter.wallets.get_by_alias(wallet_alias)
|
||||
except:
|
||||
return render_template("base.html", error="Wallet not found", specter=specter, rand=rand)
|
||||
if request.method == "POST":
|
||||
action = request.form['action']
|
||||
if action == "newaddress":
|
||||
wallet.getnewaddress()
|
||||
return render_template("wallet_receive.html", wallet_alias=wallet_alias, wallet=wallet, specter=specter, rand=rand)
|
||||
|
||||
@app.route('/wallets/<wallet_alias>/send/', methods=['GET', 'POST'])
|
||||
def wallet_send(wallet_alias):
|
||||
specter.check()
|
||||
try:
|
||||
wallet = specter.wallets.get_by_alias(wallet_alias)
|
||||
except:
|
||||
return render_template("base.html", error="Wallet not found", specter=specter, rand=rand)
|
||||
psbt = None
|
||||
address = ""
|
||||
amount = 0
|
||||
err = None
|
||||
if request.method == "POST":
|
||||
action = request.form['action']
|
||||
if action == "createpsbt":
|
||||
address = request.form['address']
|
||||
amount = float(request.form['amount'])
|
||||
# try:
|
||||
psbt = wallet.createpsbt(address, amount)
|
||||
if psbt is None:
|
||||
err = "Probably you don't have enough funds, or something else..."
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
# err = wallet.geterror()
|
||||
# if err is None:
|
||||
# err = "Unknown error"
|
||||
# print(err, e)
|
||||
return render_template("wallet_send.html", psbt=psbt, address=address, amount=amount, wallet_alias=wallet_alias, wallet=wallet, specter=specter, rand=rand)
|
||||
|
||||
@app.route('/wallets/<wallet_alias>/settings/')
|
||||
def wallet_settings(wallet_alias):
|
||||
specter.check()
|
||||
try:
|
||||
wallet = specter.wallets.get_by_alias(wallet_alias)
|
||||
except:
|
||||
return render_template("base.html", error="Wallet not found", specter=specter, rand=rand)
|
||||
cc_file = None
|
||||
if wallet.is_multisig():
|
||||
CC_TYPES = {
|
||||
'legacy': 'BIP45',
|
||||
'p2sh-segwit': 'P2WSH-P2SH',
|
||||
'bech32': 'P2WSH'
|
||||
}
|
||||
cc_file = """# Coldcard Multisig setup file (created on Specter Desktop)
|
||||
#
|
||||
Name: {}
|
||||
Policy: {} of {}
|
||||
Derivation: {}
|
||||
Format: {}
|
||||
""".format(wallet['name'], wallet['sigs_required'],
|
||||
len(wallet['keys']), wallet['keys'][0]["derivation"].replace("h","'"),
|
||||
CC_TYPES[wallet['address_type']]
|
||||
)
|
||||
for k in wallet['keys']:
|
||||
cc_file += "{}: {}\n".format(k['fingerprint'].upper(), k['xpub'])
|
||||
qr_text = "name={}\ntype={}\nm={}\nn={}".format(wallet["name"], MSIG_TYPES[wallet['address_type']], wallet['sigs_required'], len(wallet['keys']))
|
||||
for k in wallet['keys']:
|
||||
qr_text += "\n[{}{}]{}".format(k['fingerprint'], k['derivation'][1:], k['xpub'])
|
||||
return render_template("wallet_settings.html",
|
||||
cc_file=urllib.parse.quote(cc_file),
|
||||
wallet_alias=wallet_alias, wallet=wallet,
|
||||
specter=specter, rand=rand,
|
||||
qr_text=qr_text)
|
||||
else:
|
||||
qr_text = "name={}\ntype={}".format(wallet["name"], SINGLE_TYPES[wallet['address_type']])
|
||||
k = wallet["key"]
|
||||
qr_text += "\n[{}{}]{}".format(k['fingerprint'], k['derivation'][1:], k['xpub'])
|
||||
return render_template("wallet_settings.html",
|
||||
wallet_alias=wallet_alias, wallet=wallet,
|
||||
specter=specter, rand=rand,
|
||||
qr_text=qr_text)
|
||||
|
||||
################# devices management #####################
|
||||
|
||||
@app.route('/new_device/')
|
||||
def new_device():
|
||||
specter.check()
|
||||
return render_template("new_device.html", specter=specter, rand=rand)
|
||||
|
||||
@app.route('/new_device/<device_type>/', methods=['GET', 'POST'])
|
||||
def new_device_xpubs(device_type):
|
||||
err = None
|
||||
specter.check()
|
||||
# get default new name
|
||||
name = device_type.capitalize()
|
||||
device_name = name
|
||||
i = 2
|
||||
while device_name in specter.devices.names():
|
||||
device_name = "%s %d" % (name, i)
|
||||
i+=1
|
||||
|
||||
xpubs = ""
|
||||
if request.method == 'POST':
|
||||
device_name = request.form['device_name']
|
||||
if device_name in specter.devices.names():
|
||||
err = "Device with this name already exists"
|
||||
xpubs = request.form['xpubs']
|
||||
normalized, parsed, failed = normalize_xpubs(xpubs)
|
||||
if len(failed) > 0:
|
||||
err = "Failed to parse these xpubs:\n" + "\n".join(failed)
|
||||
if err is None:
|
||||
dev = specter.devices.add(name=device_name, device_type=device_type, keys=normalized)
|
||||
return redirect("/devices/%s/" % dev["alias"])
|
||||
return render_template("new_device_xpubs.html", device_type=device_type, device_name=device_name, xpubs=xpubs, error=err, specter=specter, rand=rand)
|
||||
|
||||
def get_key_meta(key):
|
||||
k = copy.deepcopy(key)
|
||||
k["chain"] = "Mainnet" if k["xpub"].startswith("xpub") else "Testnet"
|
||||
k["purpose"] = purposes[k["type"]]
|
||||
if k["derivation"] is not None:
|
||||
k["combined"] = "[%s%s]%s" % (k["fingerprint"], k["derivation"][1:], k["xpub"])
|
||||
else:
|
||||
k["combined"] = k["xpub"]
|
||||
return k
|
||||
|
||||
@app.route('/devices/<device_alias>/', methods=['GET', 'POST'])
|
||||
def device(device_alias):
|
||||
specter.check()
|
||||
try:
|
||||
device = specter.devices.get_by_alias(device_alias)
|
||||
except:
|
||||
return render_template("base.html", error="Device not found", specter=specter, rand=rand)
|
||||
if request.method == 'POST':
|
||||
action = request.form['action']
|
||||
if action == "forget":
|
||||
specter.devices.remove(device)
|
||||
return redirect("/")
|
||||
if action == "delete_key":
|
||||
key = request.form['key']
|
||||
device.remove_key(key)
|
||||
if action == "add_keys":
|
||||
return render_template("new_device_xpubs.html", device_alias=device_alias, device=device, device_type=device["type"], specter=specter, rand=rand)
|
||||
if action == "morekeys":
|
||||
# refactor to fn
|
||||
xpubs = request.form['xpubs']
|
||||
normalized, parsed, failed = normalize_xpubs(xpubs)
|
||||
err = None
|
||||
if len(failed) > 0:
|
||||
err = "Failed to parse these xpubs:\n" + "\n".join(failed)
|
||||
return render_template("new_device_xpubs.html", device_alias=device_alias, device=device, xpubs=xpubs, device_type=device["type"], error=err, specter=specter, rand=rand)
|
||||
if err is None:
|
||||
device.add_keys(normalized)
|
||||
device = copy.deepcopy(device)
|
||||
device["keys"] = [get_key_meta(key) for key in device["keys"]]
|
||||
device["keys"].sort(key=lambda x: x["chain"]+x["purpose"], reverse=True)
|
||||
return render_template("device.html", device_alias=device_alias, device=device, purposes=purposes, specter=specter, rand=rand)
|
||||
|
||||
############### filters ##################
|
||||
|
||||
@app.template_filter('datetime')
|
||||
def timedatetime(s):
|
||||
return format(datetime.fromtimestamp(s), "%d.%m.%Y %H:%M")
|
||||
|
||||
@app.template_filter('derivation')
|
||||
def derivation(wallet):
|
||||
s = "address=m/0/{}\n".format(wallet['address_index'])
|
||||
if wallet.is_multisig():
|
||||
s += "type={}".format(MSIG_TYPES[wallet['address_type']])
|
||||
for k in wallet['keys']:
|
||||
s += "\n{}{}".format(k['fingerprint'], k['derivation'][1:])
|
||||
else:
|
||||
s += "type={}".format(SINGLE_TYPES[wallet['address_type']])
|
||||
k = wallet['key']
|
||||
s += "\n{}{}".format(k['fingerprint'], k['derivation'][1:])
|
||||
return s
|
||||
# d = wallet["recv_descriptor"].split("#")[0].replace("*",str(wallet["address_index"]))
|
||||
# if wallet.is_multisig():
|
||||
# for k in wallet["keys"]:
|
||||
# d = d.replace(k["xpub"],"m")
|
||||
# else:
|
||||
# d = d.replace(wallet["key"]["xpub"],"m")
|
||||
# pass
|
||||
# return d
|
||||
|
||||
@app.template_filter('txonaddr')
|
||||
def txonaddr(wallet):
|
||||
addr = wallet["address"]
|
||||
txlist = [tx for tx in wallet.transactions if tx["address"] == addr]
|
||||
return len(txlist)
|
||||
|
||||
@app.template_filter('prettyjson')
|
||||
def txonaddr(obj):
|
||||
return json.dumps(obj, indent=4)
|
||||
|
||||
|
||||
############### startup ##################
|
||||
|
||||
def run_chrome(timeout):
|
||||
t0 = time.time()
|
||||
t = t0
|
||||
r = None
|
||||
url = "http://localhost:25441/"
|
||||
while t < t0+10 or r is None:
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except:
|
||||
time.sleep(0.2)
|
||||
run_shell(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","--app=%s" % url])
|
||||
os._exit(0) # stop server when chrome app is closed
|
||||
|
||||
if __name__ == '__main__':
|
||||
debug = True
|
||||
specter = Specter(DATA_FOLDER)
|
||||
specter.check()
|
||||
# also run chrome at start
|
||||
if len(sys.argv) > 1 and sys.argv[-1]=="--app":
|
||||
debug = False # to prevent new windows on reload
|
||||
th = Thread(target=run_chrome, args=(10,))
|
||||
th.start()
|
||||
|
||||
|
||||
# watch templates folder to reload when something changes
|
||||
extra_dirs = ['templates']
|
||||
extra_files = extra_dirs[:]
|
||||
for extra_dir in extra_dirs:
|
||||
for dirname, dirs, files in os.walk(extra_dir):
|
||||
for filename in files:
|
||||
filename = os.path.join(dirname, filename)
|
||||
if os.path.isfile(filename):
|
||||
extra_files.append(filename)
|
||||
|
||||
app.run(port=25441, debug=debug, extra_files=extra_files)
|
||||
636
specter.py
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
from rpc import BitcoinCLI, RPC_PORTS
|
||||
import os, json, copy
|
||||
from helpers import deep_update, load_jsons
|
||||
from collections import OrderedDict
|
||||
from descriptor import AddChecksum
|
||||
import base64
|
||||
|
||||
WALLET_CHUNK = 20
|
||||
|
||||
purposes = OrderedDict({
|
||||
None: "General",
|
||||
"wpkh": "Single (Segwit)",
|
||||
"sh-wpkh": "Single (Nested)",
|
||||
"pkh": "Single (Legacy)",
|
||||
"wsh": "Multisig (Segwit)",
|
||||
"sh-wsh": "Multisig (Nested)",
|
||||
"sh": "Multisig (Legacy)",
|
||||
})
|
||||
|
||||
addrtypes = {
|
||||
"pkh": "legacy",
|
||||
"sh-wpkh": "p2sh-segwit",
|
||||
"wpkh": "bech32",
|
||||
"sh": "legacy",
|
||||
"sh-wsh": "p2sh-segwit",
|
||||
"wsh": "bech32"
|
||||
}
|
||||
|
||||
def load_bitcoin_conf(path=None):
|
||||
# trying to find bitcoin.conf in default location
|
||||
if path is None:
|
||||
# add other platforms later
|
||||
paths = ["~/Library/Application Support/Bitcoin/", "~/.bitcoin/"]
|
||||
for p in paths:
|
||||
p = os.path.expanduser(p)
|
||||
if os.path.isfile(os.path.join(p, "bitcoin.conf")):
|
||||
path = os.path.join(p, "bitcoin.conf")
|
||||
break
|
||||
if path is None:
|
||||
raise Exception("Didn't find bitcoin.conf in standard folders")
|
||||
with open(path, "r") as f:
|
||||
content = f.read()
|
||||
lines = content.split("\n")
|
||||
conf = {}
|
||||
for line in lines:
|
||||
arr = line.split("#")[0].split("=") # get rid of comments and get values
|
||||
if len(arr) == 2:
|
||||
k = arr[0].strip()
|
||||
v = arr[1].strip()
|
||||
if k.startswith("rpc"): #rpcuser, rpcpassword, rpcport
|
||||
conf[k[3:]] = v
|
||||
if k == "testnet" and v=="1" and "port" not in conf:
|
||||
conf["port"] = RPC_PORTS["test"]
|
||||
if k == "regtest" and v=="1" and "port" not in conf:
|
||||
conf["port"] = RPC_PORTS["regtest"]
|
||||
return conf
|
||||
|
||||
def alias(name):
|
||||
name = name.replace(" ", "_")
|
||||
return "".join(x for x in name if x.isalnum() or x=="_").lower()
|
||||
|
||||
class Specter:
|
||||
def __init__(self, data_folder="./data", config={}):
|
||||
if data_folder.startswith("~"):
|
||||
data_folder = os.path.expanduser(data_folder)
|
||||
self.data_folder = data_folder
|
||||
self.cli = None
|
||||
self.devices = None
|
||||
self.wallets = None
|
||||
|
||||
self.file_config = None # what comes from config file
|
||||
self.arg_config = config # what comes from arguments
|
||||
|
||||
# default config
|
||||
self.config = {
|
||||
"rpc": {
|
||||
"user": None,
|
||||
"password": None,
|
||||
"port": RPC_PORTS["main"],
|
||||
"host": "localhost", # localhost
|
||||
"protocol": "http" # https for the future
|
||||
},
|
||||
# add hwi later?
|
||||
}
|
||||
|
||||
# creating folders if they don't exist
|
||||
if not os.path.isdir(data_folder):
|
||||
os.mkdir(data_folder)
|
||||
|
||||
self._info = { "chain": None, "last_chain": None }
|
||||
# health check: loads config and tests rpc
|
||||
self.check()
|
||||
|
||||
def check(self):
|
||||
# bitcoin.conf
|
||||
try:
|
||||
self.config["rpc"].update(load_bitcoin_conf()) # should rize an error if fails
|
||||
except:
|
||||
pass # not a big deal
|
||||
|
||||
# config.json file
|
||||
if os.path.isfile(os.path.join(self.data_folder, "config.json")):
|
||||
with open(os.path.join(self.data_folder, "config.json"), "r") as f:
|
||||
self.file_config = json.loads(f.read())
|
||||
deep_update(self.config, self.file_config)
|
||||
|
||||
# init arguments
|
||||
deep_update(self.config, self.arg_config) # override loaded config
|
||||
|
||||
# check if we have user, password and can connect
|
||||
self._is_configured = bool(self.config["rpc"]["user"] and self.config["rpc"]["password"])
|
||||
self._is_running = False
|
||||
if self._is_configured:
|
||||
self.cli = BitcoinCLI(self.config["rpc"]["user"], self.config["rpc"]["password"],
|
||||
host=self.config["rpc"]["host"], port=self.config["rpc"]["port"], protocol=self.config["rpc"]["protocol"])
|
||||
try:
|
||||
self._info = self.cli.getmininginfo()
|
||||
self._is_running = True
|
||||
except:
|
||||
# last_chain is used to manage wallets when can't reach bitcoin-cli
|
||||
last_chain = self._info["chain"]
|
||||
if "last_chain" in self._info and last_chain is None:
|
||||
last_chain = self.info["last_chain"]
|
||||
self._info = { "chain": None, "last_chain": last_chain }
|
||||
|
||||
chain = self._info["chain"]
|
||||
if chain is None:
|
||||
chain = self._info["last_chain"]
|
||||
if self.wallets is None:
|
||||
self.wallets = WalletManager(os.path.join(self.data_folder, "wallets"), self.cli, chain=chain)
|
||||
else:
|
||||
self.wallets.update(os.path.join(self.data_folder, "wallets"), self.cli, chain=chain)
|
||||
if self.devices is None:
|
||||
self.devices = DeviceManager(os.path.join(self.data_folder, "devices"))
|
||||
else:
|
||||
self.devices.update(os.path.join(self.data_folder, "devices"))
|
||||
|
||||
try:
|
||||
self.wallets.load_all()
|
||||
except Exception as e:
|
||||
print("can't load wallets...", e)
|
||||
|
||||
|
||||
def test_rpc(self, **kwargs):
|
||||
conf = copy.deepcopy(self.config["rpc"])
|
||||
conf.update(kwargs)
|
||||
cli = BitcoinCLI(conf["user"], conf["password"],
|
||||
host=conf["host"], port=conf["port"], protocol=conf["protocol"])
|
||||
r = {}
|
||||
try:
|
||||
r["out"] = json.dumps(cli.getmininginfo(),indent=4)
|
||||
r["err"] = ""
|
||||
r["code"] = 0
|
||||
except:
|
||||
r["out"] = ""
|
||||
if cli.r is not None and "error" in cli.r:
|
||||
r["err"] = cli.r["error"]
|
||||
r["code"] = cli.r.status_code
|
||||
else:
|
||||
r["err"] = "Failed to connect"
|
||||
r["code"] = -1
|
||||
return r
|
||||
|
||||
def update_rpc(self, **kwargs):
|
||||
need_update = False
|
||||
for k in kwargs:
|
||||
if self.config["rpc"][k] != kwargs[k]:
|
||||
self.config["rpc"][k] = kwargs[k]
|
||||
need_update = True
|
||||
if need_update:
|
||||
with open(os.path.join(self.data_folder, "config.json"), "w") as f:
|
||||
f.write(json.dumps(self.config, indent=4))
|
||||
self.check()
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return self._info
|
||||
|
||||
@property
|
||||
def chain(self):
|
||||
return self._info["chain"]
|
||||
|
||||
|
||||
class DeviceManager:
|
||||
def __init__(self, data_folder):
|
||||
self.update(data_folder)
|
||||
|
||||
def update(self, data_folder=None):
|
||||
if data_folder is not None:
|
||||
self.data_folder = data_folder
|
||||
if data_folder.startswith("~"):
|
||||
data_folder = os.path.expanduser(data_folder)
|
||||
# creating folders if they don't exist
|
||||
if not os.path.isdir(data_folder):
|
||||
os.mkdir(data_folder)
|
||||
self._devices = load_jsons(self.data_folder, key="name")
|
||||
|
||||
def names(self):
|
||||
return list(self._devices.keys())
|
||||
|
||||
def add(self, name, device_type, keys):
|
||||
dev = {
|
||||
"name": name,
|
||||
"type": device_type,
|
||||
"keys": []
|
||||
}
|
||||
fname = alias(name)
|
||||
i = 2
|
||||
while os.path.isfile(os.path.join(self.data_folder, "%s.json" % fname)):
|
||||
fname = alias("%s %d" % (name, i))
|
||||
i+=1
|
||||
# removing duplicates
|
||||
key_arr = [k["original"] for k in dev["keys"]]
|
||||
for k in keys:
|
||||
if k["original"] not in key_arr:
|
||||
dev["keys"].append(k)
|
||||
key_arr.append(k["original"])
|
||||
with open(os.path.join(self.data_folder, "%s.json" % fname), "w") as f:
|
||||
f.write(json.dumps(dev,indent=4))
|
||||
self.update() # reload files
|
||||
return self[name]
|
||||
|
||||
def get_by_alias(self, fname):
|
||||
for dev in self:
|
||||
if dev["alias"] == fname:
|
||||
return dev
|
||||
|
||||
def remove(self, device):
|
||||
os.remove(device["fullpath"])
|
||||
self.update()
|
||||
|
||||
def __getitem__(self, name):
|
||||
return Device(self._devices[name], manager=self)
|
||||
|
||||
def __iter__(self):
|
||||
self._n = 0
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
arr = list(self._devices.keys())
|
||||
if self._n < len(arr):
|
||||
v = self._devices[arr[self._n]]
|
||||
self._n += 1
|
||||
return Device(v, manager=self)
|
||||
else:
|
||||
raise StopIteration
|
||||
|
||||
def __len__(self):
|
||||
return len(self._devices.keys())
|
||||
|
||||
class Device(dict):
|
||||
def __init__(self, d, manager=None):
|
||||
self.manager = manager
|
||||
self.update(d)
|
||||
self._dict = d
|
||||
|
||||
def update_keys(self, keys):
|
||||
self["keys"] = keys
|
||||
with open(self["fullpath"], "r") as f:
|
||||
content = json.loads(f.read())
|
||||
content["keys"] = self["keys"]
|
||||
with open(self["fullpath"], "w") as f:
|
||||
f.write(json.dumps(content,indent=4))
|
||||
self.manager.update()
|
||||
|
||||
def remove_key(self, key):
|
||||
keys = [k for k in self["keys"] if k["original"]!=key]
|
||||
self.update_keys(keys)
|
||||
|
||||
def add_keys(self, normalized):
|
||||
key_arr = [k["original"] for k in self["keys"]]
|
||||
keys = self["keys"]
|
||||
for k in normalized:
|
||||
if k["original"] not in key_arr:
|
||||
keys.append(k)
|
||||
key_arr.append(k["original"])
|
||||
self.update_keys(keys)
|
||||
|
||||
class WalletManager:
|
||||
# chain is required to manage wallets when bitcoin-cli is not running
|
||||
def __init__(self, data_folder, cli, chain, path="specter/"):
|
||||
self.data_folder = data_folder
|
||||
self.chain = chain
|
||||
self.cli = cli
|
||||
self.cli_path = path
|
||||
self.update(data_folder, cli, chain)
|
||||
|
||||
def update(self, data_folder=None, cli=None, chain=None):
|
||||
if chain is not None:
|
||||
self.chain = chain
|
||||
if data_folder is not None:
|
||||
self.data_folder = data_folder
|
||||
if data_folder.startswith("~"):
|
||||
data_folder = os.path.expanduser(data_folder)
|
||||
# creating folders if they don't exist
|
||||
if not os.path.isdir(data_folder):
|
||||
os.mkdir(data_folder)
|
||||
self.working_folder = None
|
||||
if self.chain is not None and self.data_folder is not None:
|
||||
self.working_folder = os.path.join(self.data_folder, self.chain)
|
||||
if self.working_folder is not None and not os.path.isdir(self.working_folder):
|
||||
os.mkdir(self.working_folder)
|
||||
if cli is not None:
|
||||
self.cli = cli
|
||||
if self.working_folder is not None:
|
||||
self._wallets = load_jsons(self.working_folder, key="name")
|
||||
else:
|
||||
self._wallets = {}
|
||||
|
||||
def load_all(self):
|
||||
loaded_wallets = self.cli.listwallets()
|
||||
loadable_wallets = [w["name"] for w in self.cli.listwalletdir()["wallets"]]
|
||||
not_loaded_wallets = [w for w in loadable_wallets if w not in loaded_wallets]
|
||||
print(not_loaded_wallets)
|
||||
for w in self:
|
||||
if self.cli_path+w["alias"] in not_loaded_wallets:
|
||||
print("loading", w["alias"])
|
||||
self.cli.loadwallet(self.cli_path+w["alias"])
|
||||
|
||||
def get_by_alias(self, fname):
|
||||
for dev in self:
|
||||
if dev["alias"] == fname:
|
||||
return dev
|
||||
|
||||
def names(self):
|
||||
return list(self._wallets.keys())
|
||||
|
||||
def create_simple(self, name, key_type, key, device):
|
||||
al = alias(name)
|
||||
i = 2
|
||||
while os.path.isfile(os.path.join(self.working_folder, "%s.json" % al)):
|
||||
al = alias("%s %d" % (name, i))
|
||||
i+=1
|
||||
arr = key_type.split("-")
|
||||
desc = key["xpub"]
|
||||
if key["derivation"] is not None:
|
||||
desc = "[%s%s]%s" % (key["fingerprint"], key["derivation"][1:], key["xpub"])
|
||||
recv_desc = "%s/0/*" % desc
|
||||
change_desc = "%s/1/*" % desc
|
||||
for el in arr[::-1]:
|
||||
recv_desc = "%s(%s)" % (el, recv_desc)
|
||||
change_desc = "%s(%s)" % (el, change_desc)
|
||||
recv_desc = AddChecksum(recv_desc)
|
||||
change_desc = AddChecksum(change_desc)
|
||||
o = {
|
||||
"type": "simple",
|
||||
"name": name,
|
||||
"description": purposes[key_type],
|
||||
"key": key,
|
||||
"recv_descriptor": recv_desc,
|
||||
"change_descriptor": change_desc,
|
||||
"device": device["name"],
|
||||
"address_type": addrtypes[key_type],
|
||||
"address_index": 0,
|
||||
"keypool": WALLET_CHUNK,
|
||||
"address": None,
|
||||
"change_index": 0,
|
||||
"change_address": None,
|
||||
"change_keypool": WALLET_CHUNK,
|
||||
}
|
||||
self._wallets[al] = o
|
||||
args = [
|
||||
{
|
||||
"desc": recv_desc,
|
||||
"internal": False,
|
||||
"range": [0, WALLET_CHUNK],
|
||||
"timestamp": "now",
|
||||
"keypool": True,
|
||||
"watchonly": True
|
||||
},
|
||||
{
|
||||
"desc": change_desc,
|
||||
"internal": True,
|
||||
"range": [0, WALLET_CHUNK],
|
||||
"timestamp": "now",
|
||||
"keypool": True,
|
||||
"watchonly": True
|
||||
}
|
||||
]
|
||||
r = self.cli.createwallet(self.cli_path+al, True)
|
||||
r = self.cli.importmulti(args, wallet=self.cli_path+al, timeout=120)
|
||||
addr = self.cli.deriveaddresses(recv_desc, [0, 1])[0]
|
||||
change_addr = self.cli.deriveaddresses(change_desc, [0, 1])[0]
|
||||
o["address"] = addr
|
||||
o["change_address"] = change_addr
|
||||
if self.working_folder is not None:
|
||||
fullpath = os.path.join(self.working_folder, "%s.json" % al)
|
||||
with open(fullpath, "w+") as f:
|
||||
f.write(json.dumps(o, indent=4))
|
||||
o["alias"] = al
|
||||
o["fullpath"] = fullpath
|
||||
self.update()
|
||||
return Wallet(o, self)
|
||||
|
||||
def create_multi(self, name, sigs_required, key_type, keys, devices):
|
||||
al = alias(name)
|
||||
i = 2
|
||||
while os.path.isfile(os.path.join(self.working_folder, "%s.json" % al)):
|
||||
al = alias("%s %d" % (name, i))
|
||||
i+=1
|
||||
# TODO: refactor, ugly
|
||||
arr = key_type.split("-")
|
||||
descs = [key["xpub"] for key in keys]
|
||||
for i, desc in enumerate(descs):
|
||||
key = keys[i]
|
||||
if key["derivation"] is not None:
|
||||
descs[i] = "[%s%s]%s" % (key["fingerprint"], key["derivation"][1:], key["xpub"])
|
||||
recv_descs = ["%s/0/*" % desc for desc in descs]
|
||||
change_descs = ["%s/1/*" % desc for desc in descs]
|
||||
recv_desc = "multi({},{})".format(sigs_required, ",".join(recv_descs))
|
||||
change_desc = "multi({},{})".format(sigs_required, ",".join(change_descs))
|
||||
for el in arr[::-1]:
|
||||
recv_desc = "%s(%s)" % (el, recv_desc)
|
||||
change_desc = "%s(%s)" % (el, change_desc)
|
||||
recv_desc = AddChecksum(recv_desc)
|
||||
change_desc = AddChecksum(change_desc)
|
||||
o = {
|
||||
"type": "multisig",
|
||||
"name": name,
|
||||
"description": "{} of {} {}".format(sigs_required, len(keys), purposes[key_type]),
|
||||
"sigs_required": sigs_required,
|
||||
"keys": keys,
|
||||
"recv_descriptor": recv_desc,
|
||||
"change_descriptor": change_desc,
|
||||
"devices": [device["name"] for device in devices],
|
||||
"address_type": addrtypes[key_type],
|
||||
"address_index": 0,
|
||||
"keypool": WALLET_CHUNK,
|
||||
"address": None,
|
||||
"change_address": None,
|
||||
"change_keypool": WALLET_CHUNK,
|
||||
}
|
||||
self._wallets[al] = o
|
||||
args = [
|
||||
{
|
||||
"desc": recv_desc,
|
||||
"internal": False,
|
||||
"range": [0, o["keypool"]],
|
||||
"timestamp": "now",
|
||||
"keypool": True,
|
||||
"watchonly": True
|
||||
},
|
||||
{
|
||||
"desc": change_desc,
|
||||
"internal": True,
|
||||
"range": [0, o["keypool"]],
|
||||
"timestamp": "now",
|
||||
"keypool": True,
|
||||
"watchonly": True
|
||||
}
|
||||
]
|
||||
r = self.cli.createwallet(self.cli_path+al, True)
|
||||
r = self.cli.importmulti(args, wallet=self.cli_path+al, timeout=120)
|
||||
print(args)
|
||||
addr = self.cli.deriveaddresses(recv_desc, [0, 1])[0]
|
||||
o["address"] = addr
|
||||
if self.working_folder is not None:
|
||||
with open(os.path.join(self.working_folder, "%s.json" % al), "w+") as f:
|
||||
f.write(json.dumps(o, indent=4))
|
||||
self.update()
|
||||
return self[name]
|
||||
|
||||
def __getitem__(self, name):
|
||||
return Wallet(self._wallets[name], manager=self)
|
||||
|
||||
def __iter__(self):
|
||||
self._n = 0
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
arr = list(self._wallets.keys())
|
||||
if self._n < len(arr):
|
||||
v = self._wallets[arr[self._n]]
|
||||
self._n += 1
|
||||
return Wallet(v, manager=self)
|
||||
else:
|
||||
raise StopIteration
|
||||
|
||||
def __len__(self):
|
||||
return len(self._wallets.keys())
|
||||
|
||||
class Wallet(dict):
|
||||
def __init__(self, d, manager=None):
|
||||
self.update(d)
|
||||
self.manager = manager
|
||||
self.cli_path = manager.cli_path
|
||||
self.cli = manager.cli.wallet(self.cli_path+self["alias"])
|
||||
self._dict = d
|
||||
self.getdata()
|
||||
|
||||
def _commit(self):
|
||||
with open(self["fullpath"], "w") as f:
|
||||
f.write(json.dumps(self._dict, indent=4))
|
||||
self.update(self._dict)
|
||||
self.manager.update()
|
||||
|
||||
def is_multisig(self):
|
||||
return "sigs_required" in self
|
||||
|
||||
def _check_change(self):
|
||||
addr = self["change_address"]
|
||||
if addr is not None:
|
||||
v = self.cli.getreceivedbyaddress(addr, 0)
|
||||
if v > 0:
|
||||
self._dict["change_index"] += 1
|
||||
index = self._dict["change_index"]
|
||||
self._dict["change_address"] = self.cli.deriveaddresses(self._dict["change_descriptor"], [index, index+1])[0]
|
||||
if index == self._dict["change_keypool"]-1:
|
||||
self._dict["change_keypool"] = self.keypoolrefill(self._dict["change_keypool"])
|
||||
|
||||
def getdata(self):
|
||||
try:
|
||||
self.balance = self.getbalances()
|
||||
except:
|
||||
self.balance = None
|
||||
try:
|
||||
self.transactions = self.cli.listtransactions("*", 20, 0, True)[::-1]
|
||||
except:
|
||||
self.transactions = None
|
||||
self._check_change()
|
||||
return {
|
||||
"balance": self.balance,
|
||||
"transactions": self.transactions
|
||||
}
|
||||
|
||||
def getnewaddress(self):
|
||||
self._dict["address_index"] += 1
|
||||
index = self._dict["address_index"]
|
||||
addr = self.cli.deriveaddresses(self._dict["recv_descriptor"], [index, index+1])[0]
|
||||
if index == self._dict["keypool"]-1:
|
||||
self._dict["keypool"] = self.keypoolrefill(self._dict["keypool"])
|
||||
self._dict["address"] = addr
|
||||
self._commit()
|
||||
return addr
|
||||
|
||||
def geterror(self):
|
||||
if self.cli.r is not None:
|
||||
try:
|
||||
err = self.cli.r.json()
|
||||
except:
|
||||
return self.cli.r.text
|
||||
if "error" in err:
|
||||
if "message" in err["error"]:
|
||||
return err["error"]["message"]
|
||||
return err
|
||||
return self.cli.r
|
||||
return None
|
||||
|
||||
def getbalance(self, *args, **kwargs):
|
||||
default_args = ["*", 0, True]
|
||||
args = list(args) + default_args[len(args):]
|
||||
try:
|
||||
return self.cli.getbalance(*args, **kwargs)
|
||||
except:
|
||||
return None
|
||||
|
||||
def getbalances(self, *args, **kwargs):
|
||||
""" 18.1 doesn't support it, so we need to build it ourselves... """
|
||||
r = {
|
||||
"trusted": 0,
|
||||
"untrusted_pending": 0,
|
||||
}
|
||||
try:
|
||||
r["trusted"] = self.getbalance()
|
||||
unspent = self.cli.listunspent(0, 0)
|
||||
for t in unspent:
|
||||
r["untrusted_pending"] += t["amount"]
|
||||
except:
|
||||
r = { "trusted": None, "untrusted_pending": None }
|
||||
self.balance = r
|
||||
return r
|
||||
|
||||
def getfullbalance(self):
|
||||
r = self.getbalances()
|
||||
if r["trusted"] is None:
|
||||
return None
|
||||
return r["trusted"]+r["untrusted_pending"]
|
||||
|
||||
def keypoolrefill(self, start, end=None, change=False):
|
||||
if end is None:
|
||||
end = start + WALLET_CHUNK
|
||||
desc = self["recv_descriptor"] if not change else self["change_descriptor"]
|
||||
args = [
|
||||
{
|
||||
"desc": self["recv_descriptor"],
|
||||
"internal": change,
|
||||
"range": [start, end],
|
||||
"timestamp": "now",
|
||||
"keypool": True,
|
||||
"watchonly": True
|
||||
}
|
||||
]
|
||||
r = self.cli.importmulti(args, timeout=120)
|
||||
return end
|
||||
|
||||
@property
|
||||
def fullbalance(self):
|
||||
if self.balance is None:
|
||||
return None
|
||||
if self.balance["trusted"] is None or self.balance["untrusted_pending"] is None:
|
||||
return None
|
||||
return self.balance["trusted"]+self.balance["untrusted_pending"]
|
||||
|
||||
def createpsbt(self, address:str, amount:float):
|
||||
if self.fullbalance < amount:
|
||||
return None
|
||||
extra_inputs = []
|
||||
if self.balance["trusted"] < amount:
|
||||
txlist = self.cli.listunspent(0,0)
|
||||
b = amount-self.balance["trusted"]
|
||||
for tx in txlist:
|
||||
extra_inputs.append({"txid": tx["txid"], "vout": tx["vout"]})
|
||||
b -= tx["amount"]
|
||||
if b < 0:
|
||||
break;
|
||||
r = self.cli.walletcreatefundedpsbt(extra_inputs, [{address: amount}], 0,
|
||||
{
|
||||
"includeWatching": True,
|
||||
"changeAddress": self["change_address"]
|
||||
}, True)
|
||||
b64psbt = r["psbt"]
|
||||
psbt = self.cli.decodepsbt(b64psbt)
|
||||
psbt['base64'] = b64psbt
|
||||
return psbt
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# specter = Specter("~/_specter", config={"rpc":{"port":18332}})
|
||||
# specter = Specter(config={"rpc":{"port":18332}})
|
||||
specter = Specter("~/.specter")
|
||||
w = specter.wallets['Stupid']
|
||||
print(w.getbalances())
|
||||
# print(w.getbalance("*", 0, True))
|
||||
# for v in specter.devices:
|
||||
# print(v)
|
||||
# print(v.is_multisig())
|
||||
BIN
static/img/ca.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
36
static/img/coldcard.svg
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="209px" height="359px" viewBox="0 0 209 359" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="devices" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group">
|
||||
<rect id="Rectangle" stroke="#FFFFFF" x="0.5" y="0.5" width="208" height="358" rx="19"></rect>
|
||||
<rect id="Rectangle" stroke="#FFFFFF" x="28.5" y="49.5" width="96" height="49" rx="6"></rect>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="54.5" cy="166.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="104.5" cy="166.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="154.5" cy="166.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="54.5" cy="216.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="104.5" cy="216.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="154.5" cy="216.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="54.5" cy="266.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="104.5" cy="266.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="154.5" cy="266.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="54.5" cy="316.5" r="21.5"></circle>
|
||||
<rect id="Rectangle" stroke-opacity="0.385869565" stroke="#FFFFFF" x="0.5" y="0.5" width="208" height="124" rx="19"></rect>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="104.5" cy="316.5" r="21.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="154.5" cy="316.5" r="21.5"></circle>
|
||||
<text id="COLDCARD" font-family="Helvetica" font-size="12" font-weight="normal" fill="#F8FEFF" fill-opacity="0.39">
|
||||
<tspan x="20" y="26">COLDCARD</tspan>
|
||||
</text>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="139.5" cy="60.5" r="5.5"></circle>
|
||||
<circle id="Oval" stroke="#FFFFFF" cx="139.5" cy="90.5" r="5.5"></circle>
|
||||
<path d="M12.3457341,122.625737 L12.3457341,338.305246 C12.3457341,342.723524 15.9274561,346.305246 20.3457341,346.305246 L189.96993,346.305246 C194.388208,346.305246 197.96993,342.723524 197.96993,338.305246 L197.96993,122.625737" id="Path" stroke-opacity="0.39" stroke="#FFFFFF"></path>
|
||||
<path d="M132,106.698453 L195.96993,106.698453" id="Path-2" stroke-opacity="0.39" stroke="#FFFFFF"></path>
|
||||
<path d="M132,101.698453 L195.96993,101.698453" id="Path-2" stroke-opacity="0.39" stroke="#FFFFFF"></path>
|
||||
<path d="M132,46.6984533 L195.96993,46.6984533" id="Path-2" stroke-opacity="0.39" stroke="#FFFFFF"></path>
|
||||
<path d="M132,41.6984533 L195.96993,41.6984533" id="Path-2" stroke-opacity="0.39" stroke="#FFFFFF"></path>
|
||||
<path d="M14.6369556,113.805945 L14.6369556,15.1560431 C14.6369556,12.9469041 16.4278166,11.1560431 18.6369556,11.1560431 L192.182085,11.1560431 C194.391224,11.1560431 196.182085,12.9469041 196.182085,15.1560431 L196.182085,113.805945" id="Path-3" stroke-opacity="0.39" stroke="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
26
static/img/coldcard_icon.svg
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="24px" viewBox="0 0 14 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 5</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="devices" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-5">
|
||||
<g id="Group-4">
|
||||
<rect id="Rectangle" stroke="#FFFFFF" stroke-width="0.7" x="0.35" y="0.35" width="13.3" height="23.3" rx="1"></rect>
|
||||
<path d="M2,0 L12,0 C13.1045695,-2.02906125e-16 14,0.8954305 14,2 L14,9 L0,9 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z M2,4 L2,7 L9,7 L9,4 L2,4 Z" id="Combined-Shape" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="3" cy="11" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="7" cy="11" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="11" cy="11" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="3" cy="14" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="7" cy="14" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="11" cy="14" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="3" cy="17" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="7" cy="17" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="11" cy="17" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="3" cy="20" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="7" cy="20" r="1"></circle>
|
||||
<circle id="Oval" fill="#FFFFFF" cx="11" cy="20" r="1"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
static/img/icon.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
static/img/loader.gif
Normal file
|
After Width: | Height: | Size: 729 KiB |
13
static/img/multisig_icon.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="136px" height="183px" viewBox="0 0 136 183" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 2</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-2" transform="translate(56.301537, 101.210101) rotate(70.000000) translate(-56.301537, -101.210101) translate(-37.698463, 21.710101)">
|
||||
<path d="M119.379895,40.6029336 C114.608986,25.846267 101.692623,15.2696003 86.5071681,15.2696003 C67.2489863,15.2696003 51.5980772,32.306267 51.5980772,53.2696003 C51.5980772,74.2329336 67.2489863,91.2696003 86.5071681,91.2696003 C101.692623,91.2696003 114.608986,80.6929336 119.379895,65.936267 L144.688986,65.936267 L144.688986,91.2696003 L167.961714,91.2696003 L167.961714,65.936267 L179.598077,65.936267 L179.598077,40.6029336 L119.379895,40.6029336 Z M86.5071681,65.936267 C80.1071681,65.936267 74.8708045,60.236267 74.8708045,53.2696003 C74.8708045,46.3029336 80.1071681,40.6029336 86.5071681,40.6029336 C92.9071681,40.6029336 98.1435318,46.3029336 98.1435318,53.2696003 C98.1435318,60.236267 92.9071681,65.936267 86.5071681,65.936267 Z" id="Shape" fill-opacity="0.53" fill="#FFFFFF" fill-rule="nonzero" transform="translate(115.598077, 53.269600) rotate(-15.000000) translate(-115.598077, -53.269600) "></path>
|
||||
<path d="M109.318138,82.5452222 C104.547229,67.7885555 91.6308655,57.2118888 76.445411,57.2118888 C57.1872292,57.2118888 41.5363201,74.2485555 41.5363201,95.2118888 C41.5363201,116.175222 57.1872292,133.211889 76.445411,133.211889 C91.6308655,133.211889 104.547229,122.635222 109.318138,107.878555 L134.627229,107.878555 L134.627229,133.211889 L157.899956,133.211889 L157.899956,107.878555 L169.53632,107.878555 L169.53632,82.5452222 L109.318138,82.5452222 Z M76.445411,107.878555 C70.045411,107.878555 64.8090473,102.178555 64.8090473,95.2118888 C64.8090473,88.2452222 70.045411,82.5452222 76.445411,82.5452222 C82.845411,82.5452222 88.0817746,88.2452222 88.0817746,95.2118888 C88.0817746,102.178555 82.845411,107.878555 76.445411,107.878555 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero" transform="translate(105.536320, 95.211889) rotate(28.000000) translate(-105.536320, -95.211889) "></path>
|
||||
<circle id="Oval" stroke="#FFFFFF" stroke-width="10" cx="42" cy="64" r="42"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
15
static/img/other.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="171px" height="303px" viewBox="0 0 171 303" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 2</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="devices" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-2">
|
||||
<rect id="Rectangle" stroke="#FFFFFF" x="0.5" y="0.5" width="170" height="302" rx="11"></rect>
|
||||
<g id="unknown_device" transform="translate(33.000000, 99.000000)">
|
||||
<polygon id="Path" points="0 0 106 0 106 106 0 106"></polygon>
|
||||
<path d="M66.3333333,39.6666667 L39.6666667,39.6666667 L39.6666667,66.3333333 L66.3333333,66.3333333 L66.3333333,39.6666667 Z M57.4444444,57.4444444 L48.5555556,57.4444444 L48.5555556,48.5555556 L57.4444444,48.5555556 L57.4444444,57.4444444 Z M93,48.5555556 L93,39.6666667 L84.1111111,39.6666667 L84.1111111,30.7777778 C84.1111111,25.8888889 80.1111111,21.8888889 75.2222222,21.8888889 L66.3333333,21.8888889 L66.3333333,13 L57.4444444,13 L57.4444444,21.8888889 L48.5555556,21.8888889 L48.5555556,13 L39.6666667,13 L39.6666667,21.8888889 L30.7777778,21.8888889 C25.8888889,21.8888889 21.8888889,25.8888889 21.8888889,30.7777778 L21.8888889,39.6666667 L13,39.6666667 L13,48.5555556 L21.8888889,48.5555556 L21.8888889,57.4444444 L13,57.4444444 L13,66.3333333 L21.8888889,66.3333333 L21.8888889,75.2222222 C21.8888889,80.1111111 25.8888889,84.1111111 30.7777778,84.1111111 L39.6666667,84.1111111 L39.6666667,93 L48.5555556,93 L48.5555556,84.1111111 L57.4444444,84.1111111 L57.4444444,93 L66.3333333,93 L66.3333333,84.1111111 L75.2222222,84.1111111 C80.1111111,84.1111111 84.1111111,80.1111111 84.1111111,75.2222222 L84.1111111,66.3333333 L93,66.3333333 L93,57.4444444 L84.1111111,57.4444444 L84.1111111,48.5555556 L93,48.5555556 Z M75.2222222,75.2222222 L30.7777778,75.2222222 L30.7777778,30.7777778 L75.2222222,30.7777778 L75.2222222,75.2222222 Z" id="Shape" fill-opacity="0.389999986" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
15
static/img/other_icon.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="24px" viewBox="0 0 14 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 6</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="devices" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-6">
|
||||
<rect id="Rectangle" stroke="#FFFFFF" x="0.5" y="0.5" width="13" height="23" rx="2"></rect>
|
||||
<g id="unknown_device" transform="translate(1.000000, 6.000000)">
|
||||
<polygon id="Path" points="0 0 12 0 12 12 0 12"></polygon>
|
||||
<path d="M7.5,4.5 L4.5,4.5 L4.5,7.5 L7.5,7.5 L7.5,4.5 Z M6.5,6.5 L5.5,6.5 L5.5,5.5 L6.5,5.5 L6.5,6.5 Z M10.5,5.5 L10.5,4.5 L9.5,4.5 L9.5,3.5 C9.5,2.95 9.05,2.5 8.5,2.5 L7.5,2.5 L7.5,1.5 L6.5,1.5 L6.5,2.5 L5.5,2.5 L5.5,1.5 L4.5,1.5 L4.5,2.5 L3.5,2.5 C2.95,2.5 2.5,2.95 2.5,3.5 L2.5,4.5 L1.5,4.5 L1.5,5.5 L2.5,5.5 L2.5,6.5 L1.5,6.5 L1.5,7.5 L2.5,7.5 L2.5,8.5 C2.5,9.05 2.95,9.5 3.5,9.5 L4.5,9.5 L4.5,10.5 L5.5,10.5 L5.5,9.5 L6.5,9.5 L6.5,10.5 L7.5,10.5 L7.5,9.5 L8.5,9.5 C9.05,9.5 9.5,9.05 9.5,8.5 L9.5,7.5 L10.5,7.5 L10.5,6.5 L9.5,6.5 L9.5,5.5 L10.5,5.5 Z M8.5,8.5 L3.5,8.5 L3.5,3.5 L8.5,3.5 L8.5,8.5 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
static/img/qr_icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="qrcode" class="svg-inline--fa fa-qrcode fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#fff" d="M0 224h192V32H0v192zM64 96h64v64H64V96zm192-64v192h192V32H256zm128 128h-64V96h64v64zM0 480h192V288H0v192zm64-128h64v64H64v-64zm352-64h32v128h-96v-32h-32v96h-64V288h96v32h64v-32zm0 160h32v32h-32v-32zm-64 0h32v32h-32v-32z"></path></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
11
static/img/qr_tiny.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="9px" height="9px" viewBox="0 0 9 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>qr_icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="devices" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="qr_icon" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<path d="M0,3.85714286 L3.85714286,3.85714286 L3.85714286,0 L0,0 L0,3.85714286 Z M1.28571429,1.28571429 L2.57142857,1.28571429 L2.57142857,2.57142857 L1.28571429,2.57142857 L1.28571429,1.28571429 Z M5.14285714,0 L5.14285714,3.85714286 L9,3.85714286 L9,0 L5.14285714,0 Z M7.71428571,2.57142857 L6.42857143,2.57142857 L6.42857143,1.28571429 L7.71428571,1.28571429 L7.71428571,2.57142857 Z M0,9 L3.85714286,9 L3.85714286,5.14285714 L0,5.14285714 L0,9 Z M1.28571429,6.42857143 L2.57142857,6.42857143 L2.57142857,7.71428571 L1.28571429,7.71428571 L1.28571429,6.42857143 Z M8.35714286,5.14285714 L9,5.14285714 L9,7.71428571 L7.07142857,7.71428571 L7.07142857,7.07142857 L6.42857143,7.07142857 L6.42857143,9 L5.14285714,9 L5.14285714,5.14285714 L7.07142857,5.14285714 L7.07142857,5.78571429 L8.35714286,5.78571429 L8.35714286,5.14285714 Z M8.35714286,8.35714286 L9,8.35714286 L9,9 L8.35714286,9 L8.35714286,8.35714286 Z M7.07142857,8.35714286 L7.71428571,8.35714286 L7.71428571,9 L7.07142857,9 L7.07142857,8.35714286 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
12
static/img/receive_icon.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="16px" viewBox="0 0 14 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>baseline-publish-24px</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="screenshot" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="baseline-publish-24px" transform="translate(7.000000, 8.000000) scale(1, -1) translate(-7.000000, -8.000000) translate(-5.000000, -4.000000)">
|
||||
<polygon id="Path" points="0 0 24 0 24 24 0 24"></polygon>
|
||||
<path d="M5,4 L5,6 L19,6 L19,4 L5,4 Z M5,14 L9,14 L9,20 L15,20 L15,14 L19,14 L12,7 L5,14 Z" id="Shape" fill="#4A90E2" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 848 B |
12
static/img/send_icon.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="16px" viewBox="0 0 14 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>baseline-publish-24px</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="screenshot" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="baseline-publish-24px" transform="translate(7.000000, 8.000000) scale(1, -1) translate(-7.000000, -8.000000) translate(-5.000000, -4.000000)">
|
||||
<polygon id="Path" points="0 0 24 0 24 24 0 24"></polygon>
|
||||
<path d="M5,4 L5,6 L19,6 L19,4 L5,4 Z M5,13 L9,13 L9,7 L15,7 L15,13 L19,13 L12,20 L5,13 Z" id="Shape" fill="#F5A623" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 847 B |
9
static/img/simple_icon.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="124px" height="110px" viewBox="0 0 124 110" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M65.1303835,38.9315219 C60.3594744,24.1748552 47.4431108,13.5981885 32.2576562,13.5981885 C12.9994744,13.5981885 -2.65143467,30.6348552 -2.65143467,51.5981885 C-2.65143467,72.5615219 12.9994744,89.5981885 32.2576562,89.5981885 C47.4431108,89.5981885 60.3594744,79.0215219 65.1303835,64.2648552 L90.4394744,64.2648552 L90.4394744,89.5981885 L113.712202,89.5981885 L113.712202,64.2648552 L125.348565,64.2648552 L125.348565,38.9315219 L65.1303835,38.9315219 Z M32.2576562,64.2648552 C25.8576562,64.2648552 20.6212926,58.5648552 20.6212926,51.5981885 C20.6212926,44.6315219 25.8576562,38.9315219 32.2576562,38.9315219 C38.6576562,38.9315219 43.8940199,44.6315219 43.8940199,51.5981885 C43.8940199,58.5648552 38.6576562,64.2648552 32.2576562,64.2648552 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero" transform="translate(61.348565, 51.598189) rotate(28.000000) translate(-61.348565, -51.598189) "></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
247
static/img/specter.svg
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="171px" height="303px" viewBox="0 0 171 303" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 2</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="devices" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-2">
|
||||
<rect id="Rectangle" stroke="#FFFFFF" x="0.5" y="0.5" width="170" height="302" rx="11"></rect>
|
||||
<rect id="Rectangle" stroke="#FFFFFF" x="12.5" y="29.5" width="147" height="247" rx="2"></rect>
|
||||
<g id="Group" opacity="0.39" transform="translate(44.000000, 87.000000)" fill="#FFFFFF">
|
||||
<rect id="p" x="0" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="4" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="20" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="60" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="76" width="4" height="4"></rect>
|
||||
<rect id="p" x="0" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="4" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="4" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="4" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="4" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="4" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="4" y="48" width="4" height="4"></rect>
|
||||
<rect id="p" x="4" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="4" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="44" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="8" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="48" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="12" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="44" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="48" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="16" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="20" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="20" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="20" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="20" y="44" width="4" height="4"></rect>
|
||||
<rect id="p" x="20" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="20" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="4" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="20" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="48" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="60" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="76" width="4" height="4"></rect>
|
||||
<rect id="p" x="24" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="28" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="28" y="44" width="4" height="4"></rect>
|
||||
<rect id="p" x="28" y="48" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="76" width="4" height="4"></rect>
|
||||
<rect id="p" x="32" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="20" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="44" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="76" width="4" height="4"></rect>
|
||||
<rect id="p" x="36" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="4" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="20" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="60" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="76" width="4" height="4"></rect>
|
||||
<rect id="p" x="40" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="4" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="20" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="28" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="44" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="44" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="20" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="28" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="48" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="48" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="52" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="52" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="52" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="52" y="60" width="4" height="4"></rect>
|
||||
<rect id="p" x="52" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="52" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="52" y="76" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="4" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="20" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="56" y="76" width="4" height="4"></rect>
|
||||
<rect id="p" x="60" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="60" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="60" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="60" y="48" width="4" height="4"></rect>
|
||||
<rect id="p" x="60" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="60" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="60" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="60" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="64" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="44" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="56" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="60" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="72" width="4" height="4"></rect>
|
||||
<rect id="p" x="68" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="60" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="64" width="4" height="4"></rect>
|
||||
<rect id="p" x="72" y="80" width="4" height="4"></rect>
|
||||
<rect id="p" x="76" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="76" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="76" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="76" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="76" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="76" y="60" width="4" height="4"></rect>
|
||||
<rect id="p" x="76" y="68" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="0" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="4" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="8" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="12" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="16" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="20" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="24" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="32" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="36" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="40" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="44" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="52" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="60" width="4" height="4"></rect>
|
||||
<rect id="p" x="80" y="76" width="4" height="4"></rect>
|
||||
</g>
|
||||
<text id="Specter-DIY" font-family="Helvetica" font-size="12" font-weight="normal" fill="#F8FEFF" fill-opacity="0.39">
|
||||
<tspan x="53" y="198">Specter DIY</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
16
static/img/specter_icon.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="15px" height="24px" viewBox="0 0 15 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 3</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="devices" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-3" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<g id="specter_icon">
|
||||
<path d="M12.8783652,0.0200166806 L2.14013649,0 C0.957429483,0 -3.55271368e-15,1.02085071 -3.55271368e-15,2.28190158 L-3.55271368e-15,21.7180984 C-3.55271368e-15,22.9791493 0.957429483,24 2.14013649,24 L12.8595921,24 C14.0422991,24 14.9997286,22.9791493 14.9997286,21.7180984 L14.9997286,2.28190158 C15.0185017,1.02085071 14.0610722,0.0200166806 12.8783652,0.0200166806 Z M13.9390469,21.7180984 L1.07945481,21.7180984 L1.07945481,4.00333611 L13.9390469,4.00333611 L13.9390469,21.7180984 Z" id="Shape"></path>
|
||||
</g>
|
||||
<g id="qr_icon" transform="translate(3.000000, 8.000000)">
|
||||
<path d="M0,3.85714286 L3.85714286,3.85714286 L3.85714286,0 L0,0 L0,3.85714286 Z M1.28571429,1.28571429 L2.57142857,1.28571429 L2.57142857,2.57142857 L1.28571429,2.57142857 L1.28571429,1.28571429 Z M5.14285714,0 L5.14285714,3.85714286 L9,3.85714286 L9,0 L5.14285714,0 Z M7.71428571,2.57142857 L6.42857143,2.57142857 L6.42857143,1.28571429 L7.71428571,1.28571429 L7.71428571,2.57142857 Z M0,9 L3.85714286,9 L3.85714286,5.14285714 L0,5.14285714 L0,9 Z M1.28571429,6.42857143 L2.57142857,6.42857143 L2.57142857,7.71428571 L1.28571429,7.71428571 L1.28571429,6.42857143 Z M8.35714286,5.14285714 L9,5.14285714 L9,7.71428571 L7.07142857,7.71428571 L7.07142857,7.07142857 L6.42857143,7.07142857 L6.42857143,9 L5.14285714,9 L5.14285714,5.14285714 L7.07142857,5.14285714 L7.07142857,5.78571429 L8.35714286,5.78571429 L8.35714286,5.14285714 Z M8.35714286,8.35714286 L9,8.35714286 L9,9 L8.35714286,9 L8.35714286,8.35714286 Z M7.07142857,8.35714286 L7.71428571,8.35714286 L7.71428571,9 L7.07142857,9 L7.07142857,8.35714286 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
9
static/img/unconfirmed_receive_icon.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Combined Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="screenshot" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M7.992,0 C12.416,0 16,3.584 16,8 C16,12.416 12.416,16 7.992,16 C3.576,16 0,12.416 0,8 C0,3.584 3.576,0 7.992,0 Z M8,14.4 C11.536,14.4 14.4,11.536 14.4,8 C14.4,4.464 11.536,1.6 8,1.6 C4.464,1.6 1.6,4.464 1.6,8 C1.6,11.536 4.464,14.4 8,14.4 Z M8.5,4 L8.5,8.01639344 L13,10.0590164 L12.25,11 L7,8.59016393 L7,4 L8.5,4 Z" id="Combined-Shape" fill="#4B8CD8" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 839 B |
9
static/img/unconfirmed_send_icon.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Combined Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="screenshot" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M7.992,0 C12.416,0 16,3.584 16,8 C16,12.416 12.416,16 7.992,16 C3.576,16 0,12.416 0,8 C0,3.584 3.576,0 7.992,0 Z M8,14.4 C11.536,14.4 14.4,11.536 14.4,8 C14.4,4.464 11.536,1.6 8,1.6 C4.464,1.6 1.6,4.464 1.6,8 C1.6,11.536 4.464,14.4 8,14.4 Z M8.5,4 L8.5,8.01639344 L13,10.0590164 L12.25,11 L7,8.59016393 L7,4 L8.5,4 Z" id="Combined-Shape" fill="#F5A623" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 839 B |
86
static/qr/qr-scanner-worker.min.js
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
'use strict';(function(){function S(a,b){let c=[],d="";b=a.readBits([8,16,16][b]);for(let d=0;d<b;d++){let b=a.readBits(8);c.push(b)}try{d+=decodeURIComponent(c.map((a)=>`%${("0"+a.toString(16)).substr(-2)}`).join(""))}catch(e){}return{bytes:c,text:d}}function T(a,b){a=new U(a);b=9>=b?0:26>=b?1:2;let c={text:"",bytes:[],chunks:[]};for(;4<=a.available();){var d=a.readBits(4);if(d===y.Terminator)return c;if(d===y.ECI)0===a.readBits(1)?c.chunks.push({type:u.ECI,assignmentNumber:a.readBits(7)}):0===a.readBits(1)?
|
||||
c.chunks.push({type:u.ECI,assignmentNumber:a.readBits(14)}):0===a.readBits(1)?c.chunks.push({type:u.ECI,assignmentNumber:a.readBits(21)}):c.chunks.push({type:u.ECI,assignmentNumber:-1});else if(d===y.Numeric){var e=a;d=[];for(var g="",f=e.readBits([10,12,14][b]);3<=f;){var h=e.readBits(10);if(1E3<=h)throw Error("Invalid numeric value above 999");var k=Math.floor(h/100),m=Math.floor(h/10)%10;h%=10;d.push(48+k,48+m,48+h);g+=k.toString()+m.toString()+h.toString();f-=3}if(2===f){f=e.readBits(7);if(100<=
|
||||
f)throw Error("Invalid numeric value above 99");e=Math.floor(f/10);f%=10;d.push(48+e,48+f);g+=e.toString()+f.toString()}else if(1===f){e=e.readBits(4);if(10<=e)throw Error("Invalid numeric value above 9");d.push(48+e);g+=e.toString()}d={bytes:d,text:g};c.text+=d.text;c.bytes.push(...d.bytes);c.chunks.push({type:u.Numeric,text:d.text})}else if(d===y.Alphanumeric){e=a;d=[];g="";for(f=e.readBits([9,11,13][b]);2<=f;)m=e.readBits(11),k=Math.floor(m/45),m%=45,d.push(B[k].charCodeAt(0),B[m].charCodeAt(0)),
|
||||
g+=B[k]+B[m],f-=2;1===f&&(e=e.readBits(6),d.push(B[e].charCodeAt(0)),g+=B[e]);d={bytes:d,text:g};c.text+=d.text;c.bytes.push(...d.bytes);c.chunks.push({type:u.Alphanumeric,text:d.text})}else if(d===y.Byte)d=S(a,b),c.text+=d.text,c.bytes.push(...d.bytes),c.chunks.push({type:u.Byte,bytes:d.bytes,text:d.text});else if(d===y.Kanji){g=a;d=[];e=g.readBits([8,10,12][b]);for(f=0;f<e;f++)k=g.readBits(13),k=Math.floor(k/192)<<8|k%192,k=7936>k?k+33088:k+49472,d.push(k>>8,k&255);g=(new TextDecoder("shift-jis")).decode(Uint8Array.from(d));
|
||||
d={bytes:d,text:g};c.text+=d.text;c.bytes.push(...d.bytes);c.chunks.push({type:u.Kanji,bytes:d.bytes,text:d.text})}}if(0===a.available()||0===a.readBits(a.available()))return c}function J(a,b){return a^b}function V(a,b,c,d){b.degree()<c.degree()&&([b,c]=[c,b]);let e=a.zero;for(var g=a.one;c.degree()>=d/2;){var f=b;let d=e;b=c;e=g;if(b.isZero())return null;c=f;g=a.zero;f=b.getCoefficient(b.degree());for(f=a.inverse(f);c.degree()>=b.degree()&&!c.isZero();){let d=c.degree()-b.degree(),e=a.multiply(c.getCoefficient(c.degree()),
|
||||
f);g=g.addOrSubtract(a.buildMonomial(d,e));c=c.addOrSubtract(b.multiplyByMonomial(d,e))}g=g.multiplyPoly(e).addOrSubtract(d);if(c.degree()>=b.degree())return null}d=g.getCoefficient(0);if(0===d)return null;a=a.inverse(d);return[g.multiply(a),c.multiply(a)]}function W(a,b){let c=new Uint8ClampedArray(a.length);c.set(a);a=new X(285,256,0);var d=new w(a,c),e=new Uint8ClampedArray(b),g=!1;for(var f=0;f<b;f++){var h=d.evaluateAt(a.exp(f+a.generatorBase));e[e.length-1-f]=h;0!==h&&(g=!0)}if(!g)return c;
|
||||
d=new w(a,e);d=V(a,a.buildMonomial(b,1),d,b);if(null===d)return null;b=d[0];f=b.degree();if(1===f)b=[b.getCoefficient(1)];else{e=Array(f);g=0;for(h=1;h<a.size&&g<f;h++)0===b.evaluateAt(h)&&(e[g]=a.inverse(h),g++);b=g!==f?null:e}if(null==b)return null;d=d[1];e=b.length;g=Array(e);for(f=0;f<e;f++){h=a.inverse(b[f]);let c=1;for(let d=0;d<e;d++)f!==d&&(c=a.multiply(c,J(1,a.multiply(b[d],h))));g[f]=a.multiply(d.evaluateAt(h),a.inverse(c));0!==a.generatorBase&&(g[f]=a.multiply(g[f],h))}d=g;for(e=0;e<b.length;e++){g=
|
||||
c.length-1-a.log(b[e]);if(0>g)return null;c[g]^=d[e]}return c}function E(a,b){a^=b;for(b=0;a;)b++,a&=a-1;return b}function C(a,b){return b<<1|a}function Y(a,b,c){c=Z[c.dataMask];let d=a.height;var e=17+4*b.versionNumber,g=A.createEmpty(e,e);g.setRegion(0,0,9,9,!0);g.setRegion(e-8,0,8,9,!0);g.setRegion(0,e-8,9,8,!0);for(var f of b.alignmentPatternCenters)for(var h of b.alignmentPatternCenters)6===f&&6===h||6===f&&h===e-7||f===e-7&&6===h||g.setRegion(f-2,h-2,5,5,!0);g.setRegion(6,9,1,e-17,!0);g.setRegion(9,
|
||||
6,e-17,1,!0);6<b.versionNumber&&(g.setRegion(e-11,0,3,6,!0),g.setRegion(0,e-11,6,3,!0));b=g;f=[];e=h=0;g=!0;for(let k=d-1;0<k;k-=2){6===k&&k--;for(let m=0;m<d;m++){let p=g?d-1-m:m;for(let d=0;2>d;d++){let g=k-d;if(!b.get(g,p)){e++;let b=a.get(g,p);c({y:p,x:g})&&(b=!b);h=h<<1|b;8===e&&(f.push(h),h=e=0)}}}g=!g}return f}function aa(a){var b=a.height,c=Math.floor((b-17)/4);if(6>=c)return K[c-1];c=0;for(var d=5;0<=d;d--)for(var e=b-9;e>=b-11;e--)c=C(a.get(e,d),c);d=0;for(e=5;0<=e;e--)for(let c=b-9;c>=
|
||||
b-11;c--)d=C(a.get(e,c),d);a=Infinity;let g;for(let e of K){if(e.infoBits===c||e.infoBits===d)return e;b=E(c,e.infoBits);b<a&&(g=e,a=b);b=E(d,e.infoBits);b<a&&(g=e,a=b)}if(3>=a)return g}function ba(a){let b=0;for(var c=0;8>=c;c++)6!==c&&(b=C(a.get(c,8),b));for(c=7;0<=c;c--)6!==c&&(b=C(a.get(8,c),b));var d=a.height;c=0;for(var e=d-1;e>=d-7;e--)c=C(a.get(8,e),c);for(e=d-8;e<d;e++)c=C(a.get(e,8),c);a=Infinity;d=null;for(let {bits:g,formatInfo:f}of ca){if(g===b||g===c)return f;e=E(b,g);e<a&&(d=f,a=e);
|
||||
b!==c&&(e=E(c,g),e<a&&(d=f,a=e))}return 3>=a?d:null}function da(a,b,c){let d=b.errorCorrectionLevels[c],e=[],g=0;d.ecBlocks.forEach((a)=>{for(let b=0;b<a.numBlocks;b++)e.push({numDataCodewords:a.dataCodewordsPerBlock,codewords:[]}),g+=a.dataCodewordsPerBlock+d.ecCodewordsPerBlock});if(a.length<g)return null;a=a.slice(0,g);b=d.ecBlocks[0].dataCodewordsPerBlock;for(c=0;c<b;c++)for(var f of e)f.codewords.push(a.shift());if(1<d.ecBlocks.length)for(f=d.ecBlocks[0].numBlocks,b=d.ecBlocks[1].numBlocks,c=
|
||||
0;c<b;c++)e[f+c].codewords.push(a.shift());for(;0<a.length;)for(let b of e)b.codewords.push(a.shift());return e}function L(a){let b=aa(a);if(!b)return null;var c=ba(a);if(!c)return null;a=Y(a,b,c);var d=da(a,b,c.errorCorrectionLevel);if(!d)return null;c=d.reduce((a,b)=>a+b.numDataCodewords,0);c=new Uint8ClampedArray(c);a=0;for(let b of d){d=W(b.codewords,b.codewords.length-b.numDataCodewords);if(!d)return null;for(let e=0;e<b.numDataCodewords;e++)c[a++]=d[e]}try{return T(c,b.versionNumber)}catch(e){return null}}
|
||||
function M(a,b,c,d){var e=a.x-b.x+c.x-d.x;let g=a.y-b.y+c.y-d.y;if(0===e&&0===g)return{a11:b.x-a.x,a12:b.y-a.y,a13:0,a21:c.x-b.x,a22:c.y-b.y,a23:0,a31:a.x,a32:a.y,a33:1};{let h=b.x-c.x;var f=d.x-c.x;let k=b.y-c.y,m=d.y-c.y;c=h*m-f*k;f=(e*m-f*g)/c;e=(h*g-e*k)/c;return{a11:b.x-a.x+f*b.x,a12:b.y-a.y+f*b.y,a13:f,a21:d.x-a.x+e*d.x,a22:d.y-a.y+e*d.y,a23:e,a31:a.x,a32:a.y,a33:1}}}function ea(a,b,c,d){a=M(a,b,c,d);return{a11:a.a22*a.a33-a.a23*a.a32,a12:a.a13*a.a32-a.a12*a.a33,a13:a.a12*a.a23-a.a13*a.a22,
|
||||
a21:a.a23*a.a31-a.a21*a.a33,a22:a.a11*a.a33-a.a13*a.a31,a23:a.a13*a.a21-a.a11*a.a23,a31:a.a21*a.a32-a.a22*a.a31,a32:a.a12*a.a31-a.a11*a.a32,a33:a.a11*a.a22-a.a12*a.a21}}function fa(a,b){var c=ea({x:3.5,y:3.5},{x:b.dimension-3.5,y:3.5},{x:b.dimension-6.5,y:b.dimension-6.5},{x:3.5,y:b.dimension-3.5}),d=M(b.topLeft,b.topRight,b.alignmentPattern,b.bottomLeft),e=d.a11*c.a11+d.a21*c.a12+d.a31*c.a13,g=d.a12*c.a11+d.a22*c.a12+d.a32*c.a13,f=d.a13*c.a11+d.a23*c.a12+d.a33*c.a13,h=d.a11*c.a21+d.a21*c.a22+d.a31*
|
||||
c.a23,k=d.a12*c.a21+d.a22*c.a22+d.a32*c.a23,m=d.a13*c.a21+d.a23*c.a22+d.a33*c.a23,p=d.a11*c.a31+d.a21*c.a32+d.a31*c.a33,n=d.a12*c.a31+d.a22*c.a32+d.a32*c.a33,l=d.a13*c.a31+d.a23*c.a32+d.a33*c.a33;c=A.createEmpty(b.dimension,b.dimension);d=(a,b)=>{const c=f*a+m*b+l;return{x:(e*a+h*b+p)/c,y:(g*a+k*b+n)/c}};for(let e=0;e<b.dimension;e++)for(let f=0;f<b.dimension;f++){let b=d(f+.5,e+.5);c.set(f,e,a.get(Math.floor(b.x),Math.floor(b.y)))}return{matrix:c,mappingFunction:d}}function t(a){return a.reduce((a,
|
||||
c)=>a+c)}function ha(a,b,c){let d=x(a,b),e=x(b,c),g=x(a,c),f,h,k;e>=d&&e>=g?[f,h,k]=[b,a,c]:g>=e&&g>=d?[f,h,k]=[a,b,c]:[f,h,k]=[a,c,b];0>(k.x-h.x)*(f.y-h.y)-(k.y-h.y)*(f.x-h.x)&&([f,k]=[k,f]);return{bottomLeft:f,topLeft:h,topRight:k}}function ia(a,b,c,d){d=(t(z(a,c,d,5))/7+t(z(a,b,d,5))/7+t(z(c,a,d,5))/7+t(z(b,a,d,5))/7)/4;if(1>d)throw Error("Invalid module size");b=Math.round(x(a,b)/d);a=Math.round(x(a,c)/d);a=Math.floor((b+a)/2)+7;switch(a%4){case 0:a++;break;case 2:a--}return{dimension:a,moduleSize:d}}
|
||||
function N(a,b,c,d){let e=[{x:Math.floor(a.x),y:Math.floor(a.y)}];var g=Math.abs(b.y-a.y)>Math.abs(b.x-a.x);if(g){var f=Math.floor(a.y);var h=Math.floor(a.x);a=Math.floor(b.y);b=Math.floor(b.x)}else f=Math.floor(a.x),h=Math.floor(a.y),a=Math.floor(b.x),b=Math.floor(b.y);let k=Math.abs(a-f),m=Math.abs(b-h),p=Math.floor(-k/2),n=f<a?1:-1,l=h<b?1:-1,q=!0;for(let r=f,F=h;r!==a+n;r+=n){f=g?F:r;h=g?r:F;if(c.get(f,h)!==q&&(q=!q,e.push({x:f,y:h}),e.length===d+1))break;p+=m;if(0<p){if(F===b)break;F+=l;p-=k}}c=
|
||||
[];for(g=0;g<d;g++)e[g]&&e[g+1]?c.push(x(e[g],e[g+1])):c.push(0);return c}function z(a,b,c,d){let e=b.y-a.y,g=b.x-a.x;b=N(a,b,c,Math.ceil(d/2));a=N(a,{x:a.x-g,y:a.y-e},c,Math.ceil(d/2));c=b.shift()+a.shift()-1;return a.concat(c).concat(...b)}function G(a,b){let c=t(a)/t(b),d=0;b.forEach((b,g)=>{d+=Math.pow(a[g]-b*c,2)});return{averageSize:c,error:d}}function O(a,b,c){try{let d=z(a,{x:-1,y:a.y},c,b.length),e=z(a,{x:a.x,y:-1},c,b.length),g=z(a,{x:Math.max(0,a.x-a.y)-1,y:Math.max(0,a.y-a.x)-1},c,b.length),
|
||||
f=z(a,{x:Math.min(c.width,a.x+a.y)+1,y:Math.min(c.height,a.y+a.x)+1},c,b.length),h=G(d,b),k=G(e,b),m=G(g,b),p=G(f,b),n=(h.averageSize+k.averageSize+m.averageSize+p.averageSize)/4;return Math.sqrt(h.error*h.error+k.error*k.error+m.error*m.error+p.error*p.error)+(Math.pow(h.averageSize-n,2)+Math.pow(k.averageSize-n,2)+Math.pow(m.averageSize-n,2)+Math.pow(p.averageSize-n,2))/n}catch(d){return Infinity}}function ja(a){var b=[],c=[],d=[],e=[];for(let r=0;r<=a.height;r++){let k=0,m=!1,l=[0,0,0,0,0];for(let b=
|
||||
-1;b<=a.width;b++){var g=a.get(b,r);if(g===m)k++;else{l=[l[1],l[2],l[3],l[4],k];k=1;m=g;var f=t(l)/7;f=Math.abs(l[0]-f)<f&&Math.abs(l[1]-f)<f&&Math.abs(l[2]-3*f)<3*f&&Math.abs(l[3]-f)<f&&Math.abs(l[4]-f)<f&&!g;var h=t(l.slice(-3))/3;g=Math.abs(l[2]-h)<h&&Math.abs(l[3]-h)<h&&Math.abs(l[4]-h)<h&&g;if(f){let a=b-l[3]-l[4],d=a-l[2];f={startX:d,endX:a,y:r};h=c.filter((b)=>d>=b.bottom.startX&&d<=b.bottom.endX||a>=b.bottom.startX&&d<=b.bottom.endX||d<=b.bottom.startX&&a>=b.bottom.endX&&1.5>l[2]/(b.bottom.endX-
|
||||
b.bottom.startX)&&.5<l[2]/(b.bottom.endX-b.bottom.startX));0<h.length?h[0].bottom=f:c.push({top:f,bottom:f})}if(g){let a=b-l[4],c=a-l[3];g={startX:c,y:r,endX:a};f=e.filter((b)=>c>=b.bottom.startX&&c<=b.bottom.endX||a>=b.bottom.startX&&c<=b.bottom.endX||c<=b.bottom.startX&&a>=b.bottom.endX&&1.5>l[2]/(b.bottom.endX-b.bottom.startX)&&.5<l[2]/(b.bottom.endX-b.bottom.startX));0<f.length?f[0].bottom=g:e.push({top:g,bottom:g})}}}b.push(...c.filter((a)=>a.bottom.y!==r&&2<=a.bottom.y-a.top.y));c=c.filter((a)=>
|
||||
a.bottom.y===r);d.push(...e.filter((a)=>a.bottom.y!==r));e=e.filter((a)=>a.bottom.y===r)}b.push(...c.filter((a)=>2<=a.bottom.y-a.top.y));d.push(...e);b=b.filter((a)=>2<=a.bottom.y-a.top.y).map((b)=>{const c=(b.top.startX+b.top.endX+b.bottom.startX+b.bottom.endX)/4,d=(b.top.y+b.bottom.y+1)/2;if(a.get(Math.round(c),Math.round(d)))return b=[b.top.endX-b.top.startX,b.bottom.endX-b.bottom.startX,b.bottom.y-b.top.y+1],b=t(b)/b.length,{score:O({x:Math.round(c),y:Math.round(d)},[1,1,3,1,1],a),x:c,y:d,size:b}}).filter((a)=>
|
||||
!!a).sort((a,b)=>a.score-b.score).map((a,b,c)=>{if(4<b)return null;c=c.filter((a,c)=>b!==c).map((b)=>({x:b.x,y:b.y,score:b.score+Math.pow(b.size-a.size,2)/a.size,size:b.size})).sort((a,b)=>a.score-b.score);if(2>c.length)return null;const d=a.score+c[0].score+c[1].score;return{points:[a].concat(c.slice(0,2)),score:d}}).filter((a)=>!!a).sort((a,b)=>a.score-b.score);if(0===b.length)return null;var {topRight:k,topLeft:m,bottomLeft:p}=ha(b[0].points[0],b[0].points[1],b[0].points[2]);let n;try{({dimension:n,
|
||||
moduleSize:l}=ia(m,k,p,a))}catch(r){return null}b=k.x-m.x+p.x;c=k.y-m.y+p.y;var l=(x(m,p)+x(m,k))/2/l;e=1-3/l;let q={x:m.x+e*(b-m.x),y:m.y+e*(c-m.y)};d=d.map((b)=>{const c=(b.top.startX+b.top.endX+b.bottom.startX+b.bottom.endX)/4,d=(b.top.y+b.bottom.y+1)/2;if(a.get(Math.floor(c),Math.floor(d)))return t([b.top.endX-b.top.startX,b.bottom.endX-b.bottom.startX,b.bottom.y-b.top.y+1]),b=O({x:Math.floor(c),y:Math.floor(d)},[1,1,1],a)+x({x:c,y:d},q),{x:c,y:d,score:b}}).filter((a)=>!!a).sort((a,b)=>a.score-
|
||||
b.score);d=15<=l&&d.length?d[0]:q;return{alignmentPattern:{x:d.x,y:d.y},bottomLeft:{x:p.x,y:p.y},dimension:n,topLeft:{x:m.x,y:m.y},topRight:{x:k.x,y:k.y}}}function P(a){let b=ja(a);if(!b)return null;a=fa(a,b);var c=a.matrix;if(null==c)c=null;else{var d=L(c);if(d)c=d;else{for(d=0;d<c.width;d++)for(let a=d+1;a<c.height;a++)c.get(d,a)!==c.get(a,d)&&(c.set(d,a,!c.get(d,a)),c.set(a,d,!c.get(a,d)));c=L(c)}}return c?{binaryData:c.bytes,data:c.text,chunks:c.chunks,location:{topRightCorner:a.mappingFunction(b.dimension,
|
||||
0),topLeftCorner:a.mappingFunction(0,0),bottomRightCorner:a.mappingFunction(b.dimension,b.dimension),bottomLeftCorner:a.mappingFunction(0,b.dimension),topRightFinderPattern:b.topRight,topLeftFinderPattern:b.topLeft,bottomLeftFinderPattern:b.bottomLeft,bottomRightAlignmentPattern:b.alignmentPattern}}:null}function Q(a,b){Object.keys(b).forEach((c)=>{a[c]=b[c]})}function I(a,b,c,d={}){let e=Object.create(null);Q(e,ka);Q(e,d);d="onlyInvert"===e.inversionAttempts||"invertFirst"===e.inversionAttempts;
|
||||
var g="attemptBoth"===e.inversionAttempts||"invertFirst"===e.inversionAttempts;var f=e.greyScaleWeights,h=e.canOverwriteImage,k=b*c;if(a.length!==4*k)throw Error("Malformed data passed to binarizer.");var m=0;if(h){var p=new Uint8ClampedArray(a.buffer,m,k);m+=k}p=new R(b,c,p);if(f.useIntegerApproximation)for(var n=0;n<c;n++)for(var l=0;l<b;l++){var q=4*(n*b+l);p.set(l,n,f.red*a[q]+f.green*a[q+1]+f.blue*a[q+2]+128>>8)}else for(n=0;n<c;n++)for(l=0;l<b;l++)q=4*(n*b+l),p.set(l,n,f.red*a[q]+f.green*a[q+
|
||||
1]+f.blue*a[q+2]);f=Math.ceil(b/8);n=Math.ceil(c/8);l=f*n;if(h){var r=new Uint8ClampedArray(a.buffer,m,l);m+=l}r=new R(f,n,r);for(l=0;l<n;l++)for(q=0;q<f;q++){var u=0,v=Infinity,t=0;for(let a=0;8>a;a++)for(let b=0;8>b;b++){let c=p.get(8*q+b,8*l+a);u+=c;v=Math.min(v,c);t=Math.max(t,c)}u/=Math.pow(8,2);24>=t-v&&(u=v/2,0<l&&0<q&&(t=(r.get(q,l-1)+2*r.get(q-1,l)+r.get(q-1,l-1))/4,v<t&&(u=t)));r.set(q,l,u)}h?(l=new Uint8ClampedArray(a.buffer,m,k),m+=k,l=new A(l,b)):l=A.createEmpty(b,c);q=null;g&&(h?(a=
|
||||
new Uint8ClampedArray(a.buffer,m,k),q=new A(a,b)):q=A.createEmpty(b,c));for(b=0;b<n;b++)for(a=0;a<f;a++){c=f-3;c=2>a?2:a>c?c:a;h=n-3;h=2>b?2:b>h?h:b;k=0;for(m=-2;2>=m;m++)for(v=-2;2>=v;v++)k+=r.get(c+m,h+v);c=k/25;for(h=0;8>h;h++)for(k=0;8>k;k++)m=8*a+h,v=8*b+k,t=p.get(m,v),l.set(m,v,t<=c),g&&q.set(m,v,!(t<=c))}g=g?{binarized:l,inverted:q}:{binarized:l};let {binarized:w,inverted:x}=g;(g=P(d?x:w))||"attemptBoth"!==e.inversionAttempts&&"invertFirst"!==e.inversionAttempts||(g=P(d?w:x));return g}class A{static createEmpty(a,
|
||||
b){return new A(new Uint8ClampedArray(a*b),a)}constructor(a,b){this.width=b;this.height=a.length/b;this.data=a}get(a,b){return 0>a||a>=this.width||0>b||b>=this.height?!1:!!this.data[b*this.width+a]}set(a,b,c){this.data[b*this.width+a]=c?1:0}setRegion(a,b,c,d,e){for(let g=b;g<b+d;g++)for(let b=a;b<a+c;b++)this.set(b,g,!!e)}}class R{constructor(a,b,c){this.width=a;a*=b;if(c&&c.length!==a)throw Error("Wrong buffer size");this.data=c||new Uint8ClampedArray(a)}get(a,b){return this.data[b*this.width+a]}set(a,
|
||||
b,c){this.data[b*this.width+a]=c}}class U{constructor(a){this.bitOffset=this.byteOffset=0;this.bytes=a}readBits(a){if(1>a||32<a||a>this.available())throw Error("Cannot read "+a.toString()+" bits");var b=0;if(0<this.bitOffset){b=8-this.bitOffset;var c=a<b?a:b;b-=c;b=(this.bytes[this.byteOffset]&255>>8-c<<b)>>b;a-=c;this.bitOffset+=c;8===this.bitOffset&&(this.bitOffset=0,this.byteOffset++)}if(0<a){for(;8<=a;)b=b<<8|this.bytes[this.byteOffset]&255,this.byteOffset++,a-=8;0<a&&(c=8-a,b=b<<a|(this.bytes[this.byteOffset]&
|
||||
255>>c<<c)>>c,this.bitOffset+=a)}return b}available(){return 8*(this.bytes.length-this.byteOffset)-this.bitOffset}}var u;(function(a){a.Numeric="numeric";a.Alphanumeric="alphanumeric";a.Byte="byte";a.Kanji="kanji";a.ECI="eci"})(u||(u={}));var y;(function(a){a[a.Terminator=0]="Terminator";a[a.Numeric=1]="Numeric";a[a.Alphanumeric=2]="Alphanumeric";a[a.Byte=4]="Byte";a[a.Kanji=8]="Kanji";a[a.ECI=7]="ECI"})(y||(y={}));let B="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");class w{constructor(a,
|
||||
b){if(0===b.length)throw Error("No coefficients.");this.field=a;let c=b.length;if(1<c&&0===b[0]){let d=1;for(;d<c&&0===b[d];)d++;if(d===c)this.coefficients=a.zero.coefficients;else for(this.coefficients=new Uint8ClampedArray(c-d),a=0;a<this.coefficients.length;a++)this.coefficients[a]=b[d+a]}else this.coefficients=b}degree(){return this.coefficients.length-1}isZero(){return 0===this.coefficients[0]}getCoefficient(a){return this.coefficients[this.coefficients.length-1-a]}addOrSubtract(a){if(this.isZero())return a;
|
||||
if(a.isZero())return this;let b=this.coefficients;a=a.coefficients;b.length>a.length&&([b,a]=[a,b]);let c=new Uint8ClampedArray(a.length),d=a.length-b.length;for(var e=0;e<d;e++)c[e]=a[e];for(e=d;e<a.length;e++)c[e]=b[e-d]^a[e];return new w(this.field,c)}multiply(a){if(0===a)return this.field.zero;if(1===a)return this;let b=this.coefficients.length,c=new Uint8ClampedArray(b);for(let d=0;d<b;d++)c[d]=this.field.multiply(this.coefficients[d],a);return new w(this.field,c)}multiplyPoly(a){if(this.isZero()||
|
||||
a.isZero())return this.field.zero;let b=this.coefficients,c=b.length;a=a.coefficients;let d=a.length,e=new Uint8ClampedArray(c+d-1);for(let h=0;h<c;h++){let c=b[h];for(let b=0;b<d;b++){var g=h+b,f=this.field.multiply(c,a[b]);e[g]=e[h+b]^f}}return new w(this.field,e)}multiplyByMonomial(a,b){if(0>a)throw Error("Invalid degree less than 0");if(0===b)return this.field.zero;let c=this.coefficients.length;a=new Uint8ClampedArray(c+a);for(let d=0;d<c;d++)a[d]=this.field.multiply(this.coefficients[d],b);
|
||||
return new w(this.field,a)}evaluateAt(a){let b=0;if(0===a)return this.getCoefficient(0);let c=this.coefficients.length;if(1===a)return this.coefficients.forEach((a)=>{b^=a}),b;b=this.coefficients[0];for(let d=1;d<c;d++)b=J(this.field.multiply(a,b),this.coefficients[d]);return b}}class X{constructor(a,b,c){this.primitive=a;this.size=b;this.generatorBase=c;this.expTable=Array(this.size);this.logTable=Array(this.size);a=1;for(b=0;b<this.size;b++)this.expTable[b]=a,a*=2,a>=this.size&&(a=(a^this.primitive)&
|
||||
this.size-1);for(a=0;a<this.size-1;a++)this.logTable[this.expTable[a]]=a;this.zero=new w(this,Uint8ClampedArray.from([0]));this.one=new w(this,Uint8ClampedArray.from([1]))}multiply(a,b){return 0===a||0===b?0:this.expTable[(this.logTable[a]+this.logTable[b])%(this.size-1)]}inverse(a){if(0===a)throw Error("Can't invert 0");return this.expTable[this.size-this.logTable[a]-1]}buildMonomial(a,b){if(0>a)throw Error("Invalid monomial degree less than 0");if(0===b)return this.zero;a=new Uint8ClampedArray(a+
|
||||
1);a[0]=b;return new w(this,a)}log(a){if(0===a)throw Error("Can't take log(0)");return this.logTable[a]}exp(a){return this.expTable[a]}}let K=[{infoBits:null,versionNumber:1,alignmentPatternCenters:[],errorCorrectionLevels:[{ecCodewordsPerBlock:7,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:19}]},{ecCodewordsPerBlock:10,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:16}]},{ecCodewordsPerBlock:13,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:13}]},{ecCodewordsPerBlock:17,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:9}]}]},
|
||||
{infoBits:null,versionNumber:2,alignmentPatternCenters:[6,18],errorCorrectionLevels:[{ecCodewordsPerBlock:10,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:34}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:28}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:22}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:16}]}]},{infoBits:null,versionNumber:3,alignmentPatternCenters:[6,22],errorCorrectionLevels:[{ecCodewordsPerBlock:15,ecBlocks:[{numBlocks:1,
|
||||
dataCodewordsPerBlock:55}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:13}]}]},{infoBits:null,versionNumber:4,alignmentPatternCenters:[6,26],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:80}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:32}]},{ecCodewordsPerBlock:26,
|
||||
ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:9}]}]},{infoBits:null,versionNumber:5,alignmentPatternCenters:[6,30],errorCorrectionLevels:[{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:108}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:43}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:15},{numBlocks:2,dataCodewordsPerBlock:16}]},{ecCodewordsPerBlock:22,
|
||||
ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:11},{numBlocks:2,dataCodewordsPerBlock:12}]}]},{infoBits:null,versionNumber:6,alignmentPatternCenters:[6,34],errorCorrectionLevels:[{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:68}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:27}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:19}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:15}]}]},{infoBits:31892,versionNumber:7,
|
||||
alignmentPatternCenters:[6,22,38],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:78}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:31}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:14},{numBlocks:4,dataCodewordsPerBlock:15}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:13},{numBlocks:1,dataCodewordsPerBlock:14}]}]},{infoBits:34236,versionNumber:8,alignmentPatternCenters:[6,24,42],
|
||||
errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:97}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:38},{numBlocks:2,dataCodewordsPerBlock:39}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:18},{numBlocks:2,dataCodewordsPerBlock:19}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:14},{numBlocks:2,dataCodewordsPerBlock:15}]}]},{infoBits:39577,versionNumber:9,alignmentPatternCenters:[6,
|
||||
26,46],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:36},{numBlocks:2,dataCodewordsPerBlock:37}]},{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:16},{numBlocks:4,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:12},{numBlocks:4,dataCodewordsPerBlock:13}]}]},{infoBits:42195,versionNumber:10,alignmentPatternCenters:[6,
|
||||
28,50],errorCorrectionLevels:[{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:68},{numBlocks:2,dataCodewordsPerBlock:69}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:43},{numBlocks:1,dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:19},{numBlocks:2,dataCodewordsPerBlock:20}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:15},{numBlocks:2,dataCodewordsPerBlock:16}]}]},{infoBits:48118,
|
||||
versionNumber:11,alignmentPatternCenters:[6,30,54],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:81}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:50},{numBlocks:4,dataCodewordsPerBlock:51}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:22},{numBlocks:4,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:12},{numBlocks:8,dataCodewordsPerBlock:13}]}]},{infoBits:51042,
|
||||
versionNumber:12,alignmentPatternCenters:[6,32,58],errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:92},{numBlocks:2,dataCodewordsPerBlock:93}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:36},{numBlocks:2,dataCodewordsPerBlock:37}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:20},{numBlocks:6,dataCodewordsPerBlock:21}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:14},{numBlocks:4,
|
||||
dataCodewordsPerBlock:15}]}]},{infoBits:55367,versionNumber:13,alignmentPatternCenters:[6,34,62],errorCorrectionLevels:[{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:107}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:37},{numBlocks:1,dataCodewordsPerBlock:38}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:20},{numBlocks:4,dataCodewordsPerBlock:21}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:11},{numBlocks:4,
|
||||
dataCodewordsPerBlock:12}]}]},{infoBits:58893,versionNumber:14,alignmentPatternCenters:[6,26,46,66],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:115},{numBlocks:1,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:40},{numBlocks:5,dataCodewordsPerBlock:41}]},{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:16},{numBlocks:5,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:11,
|
||||
dataCodewordsPerBlock:12},{numBlocks:5,dataCodewordsPerBlock:13}]}]},{infoBits:63784,versionNumber:15,alignmentPatternCenters:[6,26,48,70],errorCorrectionLevels:[{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:87},{numBlocks:1,dataCodewordsPerBlock:88}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:41},{numBlocks:5,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:24},{numBlocks:7,dataCodewordsPerBlock:25}]},
|
||||
{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:12},{numBlocks:7,dataCodewordsPerBlock:13}]}]},{infoBits:68472,versionNumber:16,alignmentPatternCenters:[6,26,50,74],errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:98},{numBlocks:1,dataCodewordsPerBlock:99}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:45},{numBlocks:3,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:19},
|
||||
{numBlocks:2,dataCodewordsPerBlock:20}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:15},{numBlocks:13,dataCodewordsPerBlock:16}]}]},{infoBits:70749,versionNumber:17,alignmentPatternCenters:[6,30,54,78],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:107},{numBlocks:5,dataCodewordsPerBlock:108}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:46},{numBlocks:1,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:28,
|
||||
ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:22},{numBlocks:15,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:14},{numBlocks:17,dataCodewordsPerBlock:15}]}]},{infoBits:76311,versionNumber:18,alignmentPatternCenters:[6,30,56,82],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:120},{numBlocks:1,dataCodewordsPerBlock:121}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:9,dataCodewordsPerBlock:43},{numBlocks:4,
|
||||
dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:22},{numBlocks:1,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:14},{numBlocks:19,dataCodewordsPerBlock:15}]}]},{infoBits:79154,versionNumber:19,alignmentPatternCenters:[6,30,58,86],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:113},{numBlocks:4,dataCodewordsPerBlock:114}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:3,
|
||||
dataCodewordsPerBlock:44},{numBlocks:11,dataCodewordsPerBlock:45}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:21},{numBlocks:4,dataCodewordsPerBlock:22}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:9,dataCodewordsPerBlock:13},{numBlocks:16,dataCodewordsPerBlock:14}]}]},{infoBits:84390,versionNumber:20,alignmentPatternCenters:[6,34,62,90],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:107},{numBlocks:5,dataCodewordsPerBlock:108}]},
|
||||
{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:41},{numBlocks:13,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:24},{numBlocks:5,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:15},{numBlocks:10,dataCodewordsPerBlock:16}]}]},{infoBits:87683,versionNumber:21,alignmentPatternCenters:[6,28,50,72,94],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:116},
|
||||
{numBlocks:4,dataCodewordsPerBlock:117}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:22},{numBlocks:6,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:16},{numBlocks:6,dataCodewordsPerBlock:17}]}]},{infoBits:92361,versionNumber:22,alignmentPatternCenters:[6,26,50,74,98],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:111},
|
||||
{numBlocks:7,dataCodewordsPerBlock:112}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:24},{numBlocks:16,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:34,dataCodewordsPerBlock:13}]}]},{infoBits:96236,versionNumber:23,alignmentPatternCenters:[6,30,54,74,102],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:121},{numBlocks:5,dataCodewordsPerBlock:122}]},
|
||||
{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:47},{numBlocks:14,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:24},{numBlocks:14,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:16,dataCodewordsPerBlock:15},{numBlocks:14,dataCodewordsPerBlock:16}]}]},{infoBits:102084,versionNumber:24,alignmentPatternCenters:[6,28,54,80,106],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:117},
|
||||
{numBlocks:4,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:45},{numBlocks:14,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:24},{numBlocks:16,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:30,dataCodewordsPerBlock:16},{numBlocks:2,dataCodewordsPerBlock:17}]}]},{infoBits:102881,versionNumber:25,alignmentPatternCenters:[6,32,58,84,110],errorCorrectionLevels:[{ecCodewordsPerBlock:26,
|
||||
ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:106},{numBlocks:4,dataCodewordsPerBlock:107}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:47},{numBlocks:13,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:24},{numBlocks:22,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:15},{numBlocks:13,dataCodewordsPerBlock:16}]}]},{infoBits:110507,versionNumber:26,alignmentPatternCenters:[6,
|
||||
30,58,86,114],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:114},{numBlocks:2,dataCodewordsPerBlock:115}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:46},{numBlocks:4,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:28,dataCodewordsPerBlock:22},{numBlocks:6,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:33,dataCodewordsPerBlock:16},{numBlocks:4,dataCodewordsPerBlock:17}]}]},
|
||||
{infoBits:110734,versionNumber:27,alignmentPatternCenters:[6,34,62,90,118],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:122},{numBlocks:4,dataCodewordsPerBlock:123}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:45},{numBlocks:3,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:23},{numBlocks:26,dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:15},
|
||||
{numBlocks:28,dataCodewordsPerBlock:16}]}]},{infoBits:117786,versionNumber:28,alignmentPatternCenters:[6,26,50,74,98,122],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:117},{numBlocks:10,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:45},{numBlocks:23,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:24},{numBlocks:31,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,
|
||||
ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:15},{numBlocks:31,dataCodewordsPerBlock:16}]}]},{infoBits:119615,versionNumber:29,alignmentPatternCenters:[6,30,54,78,102,126],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:116},{numBlocks:7,dataCodewordsPerBlock:117}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:21,dataCodewordsPerBlock:45},{numBlocks:7,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:23},{numBlocks:37,
|
||||
dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:15},{numBlocks:26,dataCodewordsPerBlock:16}]}]},{infoBits:126325,versionNumber:30,alignmentPatternCenters:[6,26,52,78,104,130],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:115},{numBlocks:10,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:47},{numBlocks:10,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,
|
||||
ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:24},{numBlocks:25,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:23,dataCodewordsPerBlock:15},{numBlocks:25,dataCodewordsPerBlock:16}]}]},{infoBits:127568,versionNumber:31,alignmentPatternCenters:[6,30,56,82,108,134],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:115},{numBlocks:3,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:46},
|
||||
{numBlocks:29,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:42,dataCodewordsPerBlock:24},{numBlocks:1,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:23,dataCodewordsPerBlock:15},{numBlocks:28,dataCodewordsPerBlock:16}]}]},{infoBits:133589,versionNumber:32,alignmentPatternCenters:[6,34,60,86,112,138],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:115}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,
|
||||
dataCodewordsPerBlock:46},{numBlocks:23,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:24},{numBlocks:35,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:15},{numBlocks:35,dataCodewordsPerBlock:16}]}]},{infoBits:136944,versionNumber:33,alignmentPatternCenters:[6,30,58,86,114,142],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:115},{numBlocks:1,dataCodewordsPerBlock:116}]},
|
||||
{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:14,dataCodewordsPerBlock:46},{numBlocks:21,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:29,dataCodewordsPerBlock:24},{numBlocks:19,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:15},{numBlocks:46,dataCodewordsPerBlock:16}]}]},{infoBits:141498,versionNumber:34,alignmentPatternCenters:[6,34,62,90,118,146],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:115},
|
||||
{numBlocks:6,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:14,dataCodewordsPerBlock:46},{numBlocks:23,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:44,dataCodewordsPerBlock:24},{numBlocks:7,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:59,dataCodewordsPerBlock:16},{numBlocks:1,dataCodewordsPerBlock:17}]}]},{infoBits:145311,versionNumber:35,alignmentPatternCenters:[6,30,54,78,102,126,150],errorCorrectionLevels:[{ecCodewordsPerBlock:30,
|
||||
ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:121},{numBlocks:7,dataCodewordsPerBlock:122}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:47},{numBlocks:26,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:39,dataCodewordsPerBlock:24},{numBlocks:14,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:15},{numBlocks:41,dataCodewordsPerBlock:16}]}]},{infoBits:150283,versionNumber:36,alignmentPatternCenters:[6,
|
||||
24,50,76,102,128,154],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:121},{numBlocks:14,dataCodewordsPerBlock:122}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:47},{numBlocks:34,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:46,dataCodewordsPerBlock:24},{numBlocks:10,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:15},{numBlocks:64,dataCodewordsPerBlock:16}]}]},
|
||||
{infoBits:152622,versionNumber:37,alignmentPatternCenters:[6,28,54,80,106,132,158],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:122},{numBlocks:4,dataCodewordsPerBlock:123}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:29,dataCodewordsPerBlock:46},{numBlocks:14,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:49,dataCodewordsPerBlock:24},{numBlocks:10,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:24,
|
||||
dataCodewordsPerBlock:15},{numBlocks:46,dataCodewordsPerBlock:16}]}]},{infoBits:158308,versionNumber:38,alignmentPatternCenters:[6,32,58,84,110,136,162],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:122},{numBlocks:18,dataCodewordsPerBlock:123}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:46},{numBlocks:32,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:48,dataCodewordsPerBlock:24},{numBlocks:14,dataCodewordsPerBlock:25}]},
|
||||
{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:42,dataCodewordsPerBlock:15},{numBlocks:32,dataCodewordsPerBlock:16}]}]},{infoBits:161089,versionNumber:39,alignmentPatternCenters:[6,26,54,82,110,138,166],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:20,dataCodewordsPerBlock:117},{numBlocks:4,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:40,dataCodewordsPerBlock:47},{numBlocks:7,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:43,
|
||||
dataCodewordsPerBlock:24},{numBlocks:22,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:15},{numBlocks:67,dataCodewordsPerBlock:16}]}]},{infoBits:167017,versionNumber:40,alignmentPatternCenters:[6,30,58,86,114,142,170],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:118},{numBlocks:6,dataCodewordsPerBlock:119}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:18,dataCodewordsPerBlock:47},{numBlocks:31,dataCodewordsPerBlock:48}]},
|
||||
{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:34,dataCodewordsPerBlock:24},{numBlocks:34,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:20,dataCodewordsPerBlock:15},{numBlocks:61,dataCodewordsPerBlock:16}]}]}],ca=[{bits:21522,formatInfo:{errorCorrectionLevel:1,dataMask:0}},{bits:20773,formatInfo:{errorCorrectionLevel:1,dataMask:1}},{bits:24188,formatInfo:{errorCorrectionLevel:1,dataMask:2}},{bits:23371,formatInfo:{errorCorrectionLevel:1,dataMask:3}},{bits:17913,formatInfo:{errorCorrectionLevel:1,
|
||||
dataMask:4}},{bits:16590,formatInfo:{errorCorrectionLevel:1,dataMask:5}},{bits:20375,formatInfo:{errorCorrectionLevel:1,dataMask:6}},{bits:19104,formatInfo:{errorCorrectionLevel:1,dataMask:7}},{bits:30660,formatInfo:{errorCorrectionLevel:0,dataMask:0}},{bits:29427,formatInfo:{errorCorrectionLevel:0,dataMask:1}},{bits:32170,formatInfo:{errorCorrectionLevel:0,dataMask:2}},{bits:30877,formatInfo:{errorCorrectionLevel:0,dataMask:3}},{bits:26159,formatInfo:{errorCorrectionLevel:0,dataMask:4}},{bits:25368,
|
||||
formatInfo:{errorCorrectionLevel:0,dataMask:5}},{bits:27713,formatInfo:{errorCorrectionLevel:0,dataMask:6}},{bits:26998,formatInfo:{errorCorrectionLevel:0,dataMask:7}},{bits:5769,formatInfo:{errorCorrectionLevel:3,dataMask:0}},{bits:5054,formatInfo:{errorCorrectionLevel:3,dataMask:1}},{bits:7399,formatInfo:{errorCorrectionLevel:3,dataMask:2}},{bits:6608,formatInfo:{errorCorrectionLevel:3,dataMask:3}},{bits:1890,formatInfo:{errorCorrectionLevel:3,dataMask:4}},{bits:597,formatInfo:{errorCorrectionLevel:3,
|
||||
dataMask:5}},{bits:3340,formatInfo:{errorCorrectionLevel:3,dataMask:6}},{bits:2107,formatInfo:{errorCorrectionLevel:3,dataMask:7}},{bits:13663,formatInfo:{errorCorrectionLevel:2,dataMask:0}},{bits:12392,formatInfo:{errorCorrectionLevel:2,dataMask:1}},{bits:16177,formatInfo:{errorCorrectionLevel:2,dataMask:2}},{bits:14854,formatInfo:{errorCorrectionLevel:2,dataMask:3}},{bits:9396,formatInfo:{errorCorrectionLevel:2,dataMask:4}},{bits:8579,formatInfo:{errorCorrectionLevel:2,dataMask:5}},{bits:11994,
|
||||
formatInfo:{errorCorrectionLevel:2,dataMask:6}},{bits:11245,formatInfo:{errorCorrectionLevel:2,dataMask:7}}],Z=[(a)=>0===(a.y+a.x)%2,(a)=>0===a.y%2,(a)=>0===a.x%3,(a)=>0===(a.y+a.x)%3,(a)=>0===(Math.floor(a.y/2)+Math.floor(a.x/3))%2,(a)=>0===a.x*a.y%2+a.x*a.y%3,(a)=>0===(a.y*a.x%2+a.y*a.x%3)%2,(a)=>0===((a.y+a.x)%2+a.y*a.x%3)%2],x=(a,b)=>Math.sqrt(Math.pow(b.x-a.x,2)+Math.pow(b.y-a.y,2)),ka={inversionAttempts:"attemptBoth",greyScaleWeights:{red:.2126,green:.7152,blue:.0722,useIntegerApproximation:!1},
|
||||
canOverwriteImage:!0};I.default=I;let H="dontInvert",D={red:77,green:150,blue:29,useIntegerApproximation:!0};self.onmessage=(a)=>{let b=a.data.data;switch(a.data.type){case "decode":a=I(b.data,b.width,b.height,{inversionAttempts:H,greyScaleWeights:D});self.postMessage({type:"qrResult",data:a?a.data:null});break;case "grayscaleWeights":D.red=b.red;D.green=b.green;D.blue=b.blue;D.useIntegerApproximation=b.useIntegerApproximation;break;case "inversionMode":switch(b){case "original":H="dontInvert";break;
|
||||
case "invert":H="attemptBoth";break;case "both":H="attemptBoth";break;default:throw Error("Invalid inversion mode");}break;case "close":self.close()}}})();
|
||||
//# sourceMappingURL=qr-scanner-worker.min.js.map
|
||||
12
static/qr/qr-scanner.min.js
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class e{static hasCamera(){return navigator.mediaDevices.enumerateDevices().then((a)=>a.some((a)=>"videoinput"===a.kind)).catch(()=>!1)}constructor(a,c,b=e.DEFAULT_CANVAS_SIZE){this.$video=a;this.$canvas=document.createElement("canvas");this._onDecode=c;this._paused=this._active=!1;this.$canvas.width=b;this.$canvas.height=b;this._sourceRect={x:0,y:0,width:b,height:b};this._onCanPlay=this._onCanPlay.bind(this);this._onPlay=this._onPlay.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);
|
||||
this.$video.addEventListener("canplay",this._onCanPlay);this.$video.addEventListener("play",this._onPlay);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrWorker=new Worker(e.WORKER_PATH)}destroy(){this.$video.removeEventListener("canplay",this._onCanPlay);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);this.stop();this._qrWorker.postMessage({type:"close"})}start(){if(this._active&&!this._paused)return Promise.resolve();
|
||||
"https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https.");this._active=!0;this._paused=!1;if(document.hidden)return Promise.resolve();clearTimeout(this._offTimeout);this._offTimeout=null;if(this.$video.srcObject)return this.$video.play(),Promise.resolve();let a="environment";return this._getCameraStream("environment",!0).catch(()=>{a="user";return this._getCameraStream()}).then((c)=>{this.$video.srcObject=c;this._setVideoMirror(a)}).catch((a)=>
|
||||
{this._active=!1;throw a;})}stop(){this.pause();this._active=!1}pause(){this._paused=!0;this._active&&(this.$video.pause(),this._offTimeout||(this._offTimeout=setTimeout(()=>{let a=this.$video.srcObject&&this.$video.srcObject.getTracks()[0];a&&(a.stop(),this._offTimeout=this.$video.srcObject=null)},300)))}static scanImage(a,c=null,b=null,d=null,f=!1,g=!1){let h=!1,l=new Promise((l,g)=>{b||(b=new Worker(e.WORKER_PATH),h=!0,b.postMessage({type:"inversionMode",data:"both"}));let n,m,k;m=(a)=>{"qrResult"===
|
||||
a.data.type&&(b.removeEventListener("message",m),b.removeEventListener("error",k),clearTimeout(n),null!==a.data.data?l(a.data.data):g("QR code not found."))};k=(a)=>{b.removeEventListener("message",m);b.removeEventListener("error",k);clearTimeout(n);g("Scanner error: "+(a?a.message||a:"Unknown Error"))};b.addEventListener("message",m);b.addEventListener("error",k);n=setTimeout(()=>k("timeout"),3E3);e._loadImage(a).then((a)=>{a=e._getImageData(a,c,d,f);b.postMessage({type:"decode",data:a},[a.data.buffer])}).catch(k)});
|
||||
c&&g&&(l=l.catch(()=>e.scanImage(a,null,b,d,f)));return l=l.finally(()=>{h&&b.postMessage({type:"close"})})}setGrayscaleWeights(a,c,b,d=!0){this._qrWorker.postMessage({type:"grayscaleWeights",data:{red:a,green:c,blue:b,useIntegerApproximation:d}})}setInversionMode(a){this._qrWorker.postMessage({type:"inversionMode",data:a})}_onCanPlay(){this._updateSourceRect();this.$video.play()}_onPlay(){this._updateSourceRect();this._scanFrame()}_onVisibilityChange(){document.hidden?this.pause():this._active&&
|
||||
this.start()}_updateSourceRect(){let a=Math.round(2/3*Math.min(this.$video.videoWidth,this.$video.videoHeight));this._sourceRect.width=this._sourceRect.height=a;this._sourceRect.x=(this.$video.videoWidth-a)/2;this._sourceRect.y=(this.$video.videoHeight-a)/2}_scanFrame(){if(!this._active||this.$video.paused||this.$video.ended)return!1;requestAnimationFrame(()=>{e.scanImage(this.$video,this._sourceRect,this._qrWorker,this.$canvas,!0).then(this._onDecode,(a)=>{this._active&&"QR code not found."!==a&&
|
||||
console.error(a)}).then(()=>this._scanFrame())})}_getCameraStream(a,c=!1){let b=[{width:{min:1024}},{width:{min:768}},{}];a&&(c&&(a={exact:a}),b.forEach((b)=>b.facingMode=a));return this._getMatchingCameraStream(b)}_getMatchingCameraStream(a){return 0===a.length?Promise.reject("Camera not found."):navigator.mediaDevices.getUserMedia({video:a.shift()}).catch(()=>this._getMatchingCameraStream(a))}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}static _getImageData(a,c=
|
||||
null,b=null,d=!1){b=b||document.createElement("canvas");let f=c&&c.x?c.x:0,g=c&&c.y?c.y:0,h=c&&c.width?c.width:a.width||a.videoWidth;c=c&&c.height?c.height:a.height||a.videoHeight;d||b.width===h&&b.height===c||(b.width=h,b.height=c);d=b.getContext("2d",{alpha:!1});d.imageSmoothingEnabled=!1;d.drawImage(a,f,g,h,c,0,0,b.width,b.height);return d.getImageData(0,0,b.width,b.height)}static _loadImage(a){if(a instanceof HTMLCanvasElement||a instanceof HTMLVideoElement||window.ImageBitmap&&a instanceof window.ImageBitmap||
|
||||
window.OffscreenCanvas&&a instanceof window.OffscreenCanvas)return Promise.resolve(a);if(a instanceof Image)return e._awaitImageLoad(a).then(()=>a);if(a instanceof File||a instanceof URL||"string"===typeof a){let c=new Image;c.src=a instanceof File?URL.createObjectURL(a):a;return e._awaitImageLoad(c).then(()=>{a instanceof File&&URL.revokeObjectURL(c.src);return c})}return Promise.reject("Unsupported image type.")}static _awaitImageLoad(a){return new Promise((c,b)=>{if(a.complete&&0!==a.naturalWidth)c();
|
||||
else{let d,f;d=()=>{a.removeEventListener("load",d);a.removeEventListener("error",f);c()};f=()=>{a.removeEventListener("load",d);a.removeEventListener("error",f);b("Image load error")};a.addEventListener("load",d);a.addEventListener("error",f)}})}}e.DEFAULT_CANVAS_SIZE=400;e.WORKER_PATH="qr-scanner-worker.min.js";export default e;
|
||||
//# sourceMappingURL=qr-scanner.min.js.map
|
||||
1
static/qr/qr-scanner.min.js.map
Normal file
514
static/styles.css
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
html, body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #192432;
|
||||
color: #7f8fa4;
|
||||
font-family: "Source Sans Pro", sans-serif;
|
||||
font-weight: lighter;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.loader{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.9);
|
||||
flex-direction: column;
|
||||
}
|
||||
.loader img{
|
||||
width: 390px;
|
||||
height: 428px;
|
||||
margin: 20px;
|
||||
}
|
||||
.overflow{
|
||||
flex-wrap: wrap;
|
||||
/*max-width: 100%;*/
|
||||
justify-content: center;
|
||||
}
|
||||
tr.unconfirmed{
|
||||
background: #192432;
|
||||
/*font-style: italic;*/
|
||||
}
|
||||
.unconfirmed td img{
|
||||
opacity: 0.7;
|
||||
}
|
||||
.unconfirmed:hover td img{
|
||||
opacity: 1;
|
||||
}
|
||||
.row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/*flex-grow: 1;*/
|
||||
}
|
||||
.row.center{
|
||||
justify-content: center;
|
||||
}
|
||||
.holder{
|
||||
height: 100%;
|
||||
}
|
||||
.qr{
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
}
|
||||
.qr.broad{
|
||||
padding: 40px;
|
||||
}
|
||||
.side .balance{
|
||||
margin-left: auto;
|
||||
margin-right: 20px;
|
||||
font-size: 0.7em;
|
||||
align-self: center;
|
||||
}
|
||||
.grow{
|
||||
flex-grow: 1;
|
||||
}
|
||||
nav.top{
|
||||
height: 70px;
|
||||
border-bottom: 1px solid #313E50;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
nav.side{
|
||||
background: #263044;
|
||||
width: 250px;
|
||||
max-width: 250px;
|
||||
min-width: 250px;
|
||||
border-right: 1px solid #313E50;
|
||||
flex-grow: 0;
|
||||
/*padding-top: 30px;*/
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
main{
|
||||
flex-grow: 1;
|
||||
background: #192432;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/*justify-content: center;*/
|
||||
padding: 50px 0 20px 0;
|
||||
/*padding: 30px 0 20px 0;*/
|
||||
overflow-y: scroll;
|
||||
}
|
||||
nav.side > .item{
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-left: 3px solid transparent;
|
||||
/*font-size: 0.85em;*/
|
||||
}
|
||||
nav.side > a.item.active{
|
||||
border-left: 3px solid #4A90E2;
|
||||
background: rgba(0,0,0,0.1);
|
||||
/*color: #fff;*/
|
||||
}
|
||||
a.item{
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
a.btn, a.small-card{
|
||||
text-decoration: none;
|
||||
}
|
||||
a.item:hover{
|
||||
color: #F8FEFF;
|
||||
}
|
||||
.item.core > svg, .item.core > img{
|
||||
margin: 5px 12px 7px 23px;
|
||||
}
|
||||
.item:hover > svg, .item:hover > img{
|
||||
opacity: 1;
|
||||
}
|
||||
.item > svg, .item > img{
|
||||
opacity: 0.7;
|
||||
margin: 0px 10px 0px 30px;
|
||||
}
|
||||
.item:hover > svg{
|
||||
opacity: 1;
|
||||
}
|
||||
svg.core .main{
|
||||
fill: #79869B;
|
||||
}
|
||||
svg.core.test .main{
|
||||
fill: #00F100;
|
||||
}
|
||||
svg.core.main .main{
|
||||
fill: #FF9A00;
|
||||
}
|
||||
svg.core.regtest .main{
|
||||
fill: #00CAF1;
|
||||
}
|
||||
small{
|
||||
font-size: 0.7em;
|
||||
}
|
||||
nav .separator{
|
||||
text-transform: uppercase;
|
||||
/*text-align: center;*/
|
||||
font-size: 0.85em;
|
||||
margin: 20px 0px 5px 26px;
|
||||
/*font-weight: bold;*/
|
||||
/*opacity: 0.8;*/
|
||||
}
|
||||
.item .btn{
|
||||
margin: 5px 20px 5px 20px;
|
||||
min-width: 170px;
|
||||
}
|
||||
.btn{
|
||||
background: #506072;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 8px 15px 8px 10px;
|
||||
width: 170px;
|
||||
color: #fff;
|
||||
font-size: 0.85em;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.btn:hover{
|
||||
background: #405062;
|
||||
}
|
||||
.btn svg{
|
||||
margin-right: 5px;
|
||||
}
|
||||
.btn svg path{
|
||||
stroke: #fff;
|
||||
fill: #fff;
|
||||
}
|
||||
.footer{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
font-size: 0.7em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.footer img{
|
||||
width: 85px;
|
||||
margin: 10px 0 20px 0;
|
||||
}
|
||||
.log{
|
||||
/*display: block;*/
|
||||
max-width: 500px;
|
||||
/*max-width: 70%;*/
|
||||
}
|
||||
pre{
|
||||
overflow-x: scroll;
|
||||
background: rgba(0,0,0,0.1);
|
||||
padding: 10px;
|
||||
margin: 10px -10px;
|
||||
}
|
||||
h1, h2{
|
||||
font-weight: lighter;
|
||||
/*font-size: 2em;*/
|
||||
}
|
||||
h1{
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
h2{
|
||||
font-size: 1.25em;
|
||||
margin: 0 0 0px 0;
|
||||
}
|
||||
input, textarea, .input{
|
||||
border: 1px solid #506072;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
padding: 8px 15px 8px 10px;
|
||||
width: 100%;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
min-width: 170px;
|
||||
color: #fff;
|
||||
}
|
||||
.input{
|
||||
text-align: left;
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
.input.inline{
|
||||
display: inline-block;
|
||||
}
|
||||
input.inline, .input.inline{
|
||||
width: auto;
|
||||
min-width: 300px;
|
||||
}
|
||||
input[type="number"].inline, .input.inline{
|
||||
min-width: 30px;
|
||||
width: 80px;
|
||||
margin: 0 30px;
|
||||
}
|
||||
textarea{
|
||||
font-size: 0.7em;
|
||||
}
|
||||
input:hover{
|
||||
border: 1px solid #607082;
|
||||
}
|
||||
.card{
|
||||
width: 580px;
|
||||
border: 1px solid #506072;
|
||||
border-radius: 4px;
|
||||
padding: 40px;
|
||||
}
|
||||
.spacer{
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
}
|
||||
.center{
|
||||
text-align: center;
|
||||
}
|
||||
.small-card{
|
||||
min-width: 220px;
|
||||
/*height: 250px;*/
|
||||
padding: 40px;
|
||||
background: transparent;
|
||||
border: 1px solid #506072;
|
||||
border-radius: 4px;
|
||||
/*padding: 8px 15px 8px 10px;*/
|
||||
color: #fff;
|
||||
font-size: 0.85em;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin: 20px;
|
||||
}
|
||||
.small-card:hover{
|
||||
background: #203042;
|
||||
}
|
||||
.small-card.highlighted{
|
||||
border: 1px solid #4B8CD8 !important;
|
||||
cursor: default;
|
||||
}
|
||||
.small-card.highlighted:hover{
|
||||
background: transparent !important;
|
||||
}
|
||||
.small-card img{
|
||||
height: 130px;
|
||||
width: 70px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.note{
|
||||
font-size: 0.7em;
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
textarea{
|
||||
margin-bottom: 5px;
|
||||
height: 100px;
|
||||
}
|
||||
.inputfile {
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
}
|
||||
.inputfile + label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.card .note{
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
/*.btn.btn-inline{
|
||||
display: inline;
|
||||
padding: 5px 15px;
|
||||
align-items: center;
|
||||
margin: 0 5px;
|
||||
font-size: 15px;
|
||||
}*/
|
||||
.btn > img{
|
||||
width: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.btn.centered{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 300px;
|
||||
}
|
||||
.video{
|
||||
width: 90%;
|
||||
}
|
||||
.popup{
|
||||
background: rgba(0,0,0,0.7);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.notification{
|
||||
min-width: 200px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
font-size: 0.7em;
|
||||
color: #fff;
|
||||
overflow-x: scroll;
|
||||
background: #313E50;
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 7px 30px;
|
||||
box-shadow: 0 3px 3px rgba(0,0,0,0.7);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.notification.error, .btn.danger{
|
||||
background: #951E2D;
|
||||
}
|
||||
.btn.danger:hover{
|
||||
background: #A12737;
|
||||
}
|
||||
.btn.action{
|
||||
background: #3575C0;
|
||||
}
|
||||
.btn.action:hover{
|
||||
background: #4B8CD8;
|
||||
}
|
||||
.btn.radio{
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px solid #405062;
|
||||
}
|
||||
.btn.radio:hover{
|
||||
background: #304052;
|
||||
}
|
||||
input[type="radio"].hidden{
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
}
|
||||
input[type="radio"]:checked + .btn.radio, .btn.radio.checked{
|
||||
background: #405062;
|
||||
}
|
||||
input[type="radio"]:checked + .btn{
|
||||
background: #3575C0;
|
||||
}
|
||||
input[type="radio"]:checked + .btn.hovering{
|
||||
visibility: visible;
|
||||
}
|
||||
.btn.radio.left{
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
.btn.radio.right{
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
input[type="radio"]:checked + .small-card{
|
||||
background: #304052;
|
||||
}
|
||||
table{
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85em;
|
||||
width: 100%;
|
||||
border: 1px solid #313E50;
|
||||
}
|
||||
.table-holder{
|
||||
padding: 0 30px;
|
||||
width: 100%;
|
||||
}
|
||||
.full-width{
|
||||
all: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
td, th{
|
||||
padding: 20px 10px;
|
||||
border-bottom: 1px solid #313E50;
|
||||
}
|
||||
tr, thead{
|
||||
background: #263044;
|
||||
}
|
||||
th{
|
||||
text-align: left;
|
||||
font-weight: inherit;
|
||||
border-bottom: 3px solid #313E50;
|
||||
}
|
||||
tbody tr:hover{
|
||||
background: rgba(255,255,255,0.03);
|
||||
color: #fff;
|
||||
}
|
||||
td.xpub{
|
||||
max-width: 300px;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
td.txid{
|
||||
max-width: 200px;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
table a{
|
||||
display: inline;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
table a:hover{
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
table svg.core.test{
|
||||
margin: 0 3px -3px 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.qr-popup{
|
||||
/*display: none;*/
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
/*margin-left: -100px;*/
|
||||
z-index: 100;
|
||||
}
|
||||
.qr-trigger:hover .qr-popup{
|
||||
/*display: block;*/
|
||||
visibility: visible;
|
||||
}
|
||||
.qr-trigger:hover ~ .qr.alt-qr{
|
||||
opacity: 0.02;
|
||||
}
|
||||
.qr-popup img{
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
}
|
||||
table .btn{
|
||||
margin: -10px 0;
|
||||
padding: 5px 10px;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
table .btn.hovering{
|
||||
visibility: hidden;
|
||||
}
|
||||
table tr:hover .btn.hovering{
|
||||
visibility: visible;
|
||||
}
|
||||
135
templates/base.html
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="shortcut icon" type="image/png" href="/static/img/icon.png"/>
|
||||
<title>Specter Desktop</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles.css?{{rand}}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="row holder">
|
||||
<nav class="side">
|
||||
<a href="/settings/" class="item core">
|
||||
<svg class="core {{specter.info.chain}}" height="32" width="32" viewBox="0 0 64 64">
|
||||
<g>
|
||||
<path class="main" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||
<path fill="" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<div>
|
||||
Bitcoin Core<br>
|
||||
<small>
|
||||
{{specter.chain}}:
|
||||
{% if specter.info["blocks"] %}
|
||||
{{ specter.info.blocks }} blocks
|
||||
{% else %}
|
||||
{{ specter.info.chain }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</a>
|
||||
<div class="separator">
|
||||
Wallets
|
||||
</div>
|
||||
{% for wallet in specter.wallets %}
|
||||
{% if wallet_alias == wallet.alias %}
|
||||
<a href="/wallets/{{wallet.alias}}/" class="item active">
|
||||
{% else %}
|
||||
<a href="/wallets/{{wallet.alias}}/" class="item">
|
||||
{% endif %}
|
||||
{% if wallet.is_multisig() %}
|
||||
<svg width="18px" height="25px" viewBox="0 0 140 170" version="1.1">
|
||||
{% else %}
|
||||
<svg width="18px" height="25px" viewBox="0 50 100 150" version="1.1">
|
||||
{% endif %}
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-2" transform="translate(57.000000, 99.500000) rotate(75.000000) translate(-57.000000, -99.500000) translate(-37.000000, 25.000000)">
|
||||
<path d="M109.318138,82.5452222 C104.547229,67.7885555 91.6308655,57.2118888 76.445411,57.2118888 C57.1872292,57.2118888 41.5363201,74.2485555 41.5363201,95.2118888 C41.5363201,116.175222 57.1872292,133.211889 76.445411,133.211889 C91.6308655,133.211889 104.547229,122.635222 109.318138,107.878555 L134.627229,107.878555 L134.627229,133.211889 L157.899956,133.211889 L157.899956,107.878555 L169.53632,107.878555 L169.53632,82.5452222 L109.318138,82.5452222 Z M76.445411,107.878555 C70.045411,107.878555 64.8090473,102.178555 64.8090473,95.2118888 C64.8090473,88.2452222 70.045411,82.5452222 76.445411,82.5452222 C82.845411,82.5452222 88.0817746,88.2452222 88.0817746,95.2118888 C88.0817746,102.178555 82.845411,107.878555 76.445411,107.878555 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero" transform="translate(105.536320, 95.211889) rotate(15.000000) translate(-105.536320, -95.211889) "></path>
|
||||
{% if wallet.is_multisig() %}
|
||||
<path d="M119.379895,40.6029336 C114.608986,25.846267 101.692623,15.2696003 86.5071681,15.2696003 C67.2489863,15.2696003 51.5980772,32.306267 51.5980772,53.2696003 C51.5980772,74.2329336 67.2489863,91.2696003 86.5071681,91.2696003 C101.692623,91.2696003 114.608986,80.6929336 119.379895,65.936267 L144.688986,65.936267 L144.688986,91.2696003 L167.961714,91.2696003 L167.961714,65.936267 L179.598077,65.936267 L179.598077,40.6029336 L119.379895,40.6029336 Z M86.5071681,65.936267 C80.1071681,65.936267 74.8708045,60.236267 74.8708045,53.2696003 C74.8708045,46.3029336 80.1071681,40.6029336 86.5071681,40.6029336 C92.9071681,40.6029336 98.1435318,46.3029336 98.1435318,53.2696003 C98.1435318,60.236267 92.9071681,65.936267 86.5071681,65.936267 Z" id="Shape" fill-opacity="0.53" fill="#FFFFFF" fill-rule="nonzero" transform="translate(115.598077, 53.269600) rotate(-15.000000) translate(-115.598077, -53.269600) "></path>
|
||||
<circle id="Oval" stroke="#FFFFFF" stroke-width="10" cx="42" cy="64" r="42"></circle>
|
||||
{% endif %}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="grow">
|
||||
<div class="row">
|
||||
{{wallet.name}}
|
||||
{% if specter.chain %}
|
||||
{% if wallet.fullbalance is not none %}
|
||||
<span class="balance">{{ "%0.8f" % (wallet.fullbalance ) | float}}
|
||||
<!-- {% if specter.chain != "main" %} t{%endif%}BTC -->
|
||||
</span>
|
||||
{%endif%}
|
||||
{%endif%}</div>
|
||||
<small>{{ wallet.description }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
<div class="item">
|
||||
<a href="/new_wallet/" class="btn">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>Create new wallet
|
||||
</a>
|
||||
</div>
|
||||
<div class="separator">
|
||||
Devices
|
||||
</div>
|
||||
{% for device in specter.devices %}
|
||||
{% if device_alias == device.alias %}
|
||||
<a href="/devices/{{device.alias}}/" class="item active">
|
||||
{% else %}
|
||||
<a href="/devices/{{device.alias}}/" class="item">
|
||||
{% endif %}
|
||||
<img src="/static/img/{{device.type}}_icon.svg" width="18px">
|
||||
<div>{{device.name}}<br><small>{{ device["keys"] | length}} keys</small></div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
<div class="item">
|
||||
<a href="/new_device/" class="btn">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>Add new device
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Brought to you by<br>
|
||||
<a href="https://cryptoadvance.io" target="_blank">
|
||||
<img src="/static/img/ca.png">
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
{% if error %}
|
||||
<div class="notification error">
|
||||
ERROR: {{error}}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block main %}
|
||||
{% if error %}
|
||||
<br><br>Something went wrong :(<br><br>{{error}}
|
||||
{% else %}
|
||||
<br><br>Nothing here
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
<div class="loader" id="loader">
|
||||
<img src="/static/img/loader.gif"/>
|
||||
<h1>It may take a while...</h1>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
function showLoader(){
|
||||
console.log("Loading");
|
||||
window.setTimeout(()=>{
|
||||
document.getElementById("loader").style.display = "flex";
|
||||
}, 1000);
|
||||
}
|
||||
function hideLoader(){
|
||||
document.getElementById("loader").style.display = "none";
|
||||
}
|
||||
// document.querySelectorAll("button").forEach((e)=>{
|
||||
// e.addEventListener("click", showLoader);
|
||||
// });
|
||||
</script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
62
templates/device.html
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1>Public keys of {{device.name}}</h1>
|
||||
<div class="table-holder">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Network</th><th>Purpose</th><th>Derivation</th><th></th><th>Key</th><th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for key in device["keys"] %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if key["xpub"].startswith("xpub") %}
|
||||
<svg class="core main" height="15" width="15" viewBox="0 0 64 64">
|
||||
{%else%}
|
||||
<svg class="core test" height="15" width="15" viewBox="0 0 64 64">
|
||||
{%endif%}
|
||||
<g>
|
||||
<path class="main" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||
<path fill="" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</g>
|
||||
</svg>
|
||||
{% if key["xpub"].startswith("xpub") %}
|
||||
Main
|
||||
{%else%}
|
||||
Test
|
||||
{%endif%}
|
||||
</td>
|
||||
<td>{{ purposes[key["type"]] }}</td>
|
||||
<td>{{key["derivation"]}}</td>
|
||||
<td width="10" class="qr-trigger">
|
||||
<img src="/static/img/qr_tiny.svg"/>
|
||||
<div class="qr-popup">
|
||||
<img src="{{qrcode('name='+device['name']+'\n'+key['combined'])}}">
|
||||
</div>
|
||||
</td>
|
||||
<td class="xpub">{{key["original"]}}</td>
|
||||
<td width="80px">
|
||||
<form action="./" method="POST">
|
||||
<form action="./" method="POST">
|
||||
<input type="hidden" name="key" value="{{key['original']}}">
|
||||
<button type="submit" name="action" value="delete_key" class="btn danger hovering">Delete</button>
|
||||
</form>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="row">
|
||||
<form action="./" method="POST">
|
||||
<button type="submit" name="action" value="add_keys" class="btn centered">Add more keys</button>
|
||||
</form>
|
||||
|
||||
<form action="./" method="POST">
|
||||
<button type="submit" name="action" value="forget" class="btn danger centered">Forget the device</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
templates/new_device.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div class="spacer"></div>
|
||||
<h1>Select the hardware</h1>
|
||||
<div class="row">
|
||||
<a href="./coldcard/" class="small-card">
|
||||
<img src="/static/img/coldcard_icon.svg?{{rand}}">
|
||||
ColdCard
|
||||
</a>
|
||||
<a href="./specter/" class="small-card">
|
||||
<img src="/static/img/specter_icon.svg?{{rand}}">
|
||||
DIY Specter
|
||||
</a>
|
||||
<a href="./other/" class="small-card">
|
||||
<img src="/static/img/other_icon.svg?{{rand}}">
|
||||
Other device
|
||||
</a>
|
||||
</div>
|
||||
<div><br><br></div>
|
||||
<div class="note">
|
||||
<p>In order to use Bitcoin Core in watch only mode with hardware devices we need to export extended public keys from it.</p>
|
||||
<p>Currently we support only airgapped devices. Do not connect the hardware device to your online computer. Airgap is the best way to improve your security.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
127
templates/new_device_xpubs.html
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<form action="./" method="POST">
|
||||
<!-- <div class="spacer"></div> -->
|
||||
{% if device %}
|
||||
<h1>Adding keys to {{device["name"]}}</h1>
|
||||
{% else %}
|
||||
<h1>Setting up a new device</h1>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<form action="./" method="POST">
|
||||
{% if not device %}
|
||||
<div>Name it <input type="text" name="device_name" class="inline" value="{{ device_name }}" placeholder="Name your device"></div><br>
|
||||
{% endif %}
|
||||
<h2>Scan, paste or load xpubs:</h2>
|
||||
<div class="note">
|
||||
{% if not device %}
|
||||
You will be able add more keys later, from the device menu.
|
||||
{% endif %}
|
||||
</div>
|
||||
<textarea id="txt" name="xpubs" placeholder="Enter your xpubs here">{{xpubs}}</textarea>
|
||||
<div class="row">
|
||||
<a href="#" class="btn" id="scanme"><img src="/static/img/qr_icon.svg" height="12px"> Scan</a>
|
||||
<input type="file" id="file" class="inputfile" multiple/>
|
||||
<label for="file" class="btn">Choose files</label>
|
||||
</div>
|
||||
<br><div class="note">
|
||||
One line per xpub. Ideally with derivation path in the form "<b>[fingerprint/derivation/path]xpub</b>".<br>
|
||||
Examples:<br>
|
||||
<pre style="font-size: 1.2em">[f79ec910/84'/1'/0']tpubDA5h1hw24fzoBi9...pR1DiM4EBu
|
||||
[F79EC910/48h/1h/0h/2h]Vpub5DRk6RvRAjZf9rmZV1Q8...TZSDQL67e5i
|
||||
upub5En4f7k8gaG2KDHvBeEYox...rFpJRHpiZ4DE</pre>
|
||||
Loading from file supports ColdCard and Electrum wallet format.<br>
|
||||
</div>
|
||||
<button type="submit" class="btn centered" name="action" value="morekeys">Continue</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="note">
|
||||
{%if device_type == "coldcard" %}
|
||||
<p>Export extended public keys for single-key wallets to the SD card from<br>
|
||||
<b>Advanced → MicroSD Card → Electrum Wallet</b></p>
|
||||
<p>To export multisignature pubkeys go to <br>
|
||||
<b>Settings → Multisig Wallets Export XPUB</b></p>
|
||||
<p>We recommend using Native Segwit.</p>
|
||||
{% endif %}
|
||||
{%if device_type == "specter" %}
|
||||
<p>Scan QR codes with extended public keys one by one.<br>
|
||||
<p>We recommend importing <b>Native Segwit</b> and <b>Segwit Multisig</b> keys.</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<div class="popup" id="popup">
|
||||
<video muted playsinline id="qr-video" class="video"></video>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var el = document.getElementById("file");
|
||||
var txt = document.getElementById("txt");
|
||||
|
||||
el.addEventListener("change", (e) => {
|
||||
files = el.files;
|
||||
console.log(files);
|
||||
for(let i=0; i<files.length; i++){
|
||||
console.log(files[i].name);
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
let str = reader.result
|
||||
if(str.indexOf("{") >= 0){
|
||||
let json = JSON.parse(str);
|
||||
console.log(str);
|
||||
if("keystore" in json){ // ColdCard electrum file
|
||||
let prefix = "";
|
||||
if( ("ckcc_xfp" in json.keystore) && ("derivation" in json.keystore)){
|
||||
prefix = "[";
|
||||
let num = json.keystore.ckcc_xfp;
|
||||
for(let i=0;i<4;i++){
|
||||
prefix += (num % 256).toString(16);
|
||||
num = num >> 8;
|
||||
}
|
||||
prefix += json.keystore.derivation.substring(1);
|
||||
prefix += "]";
|
||||
}
|
||||
let s = prefix + json.keystore.xpub + "\n";
|
||||
txt.value += s;
|
||||
}else if("xfp" in json){ // probably ColdCard multisig file
|
||||
let s = "";
|
||||
for(let k in json){
|
||||
if(k+"_deriv" in json){
|
||||
s += "["+json.xfp+json[k+"_deriv"].substring(1)+"]"+json[k]+"\n";
|
||||
}
|
||||
}
|
||||
txt.value += s;
|
||||
}
|
||||
}else{
|
||||
txt.value += str+"\n";
|
||||
}
|
||||
}
|
||||
reader.readAsText(files[i]);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
<script type="module">
|
||||
import QrScanner from "/static/qr/qr-scanner.min.js";
|
||||
QrScanner.WORKER_PATH = '/static/qr/qr-scanner-worker.min.js';
|
||||
|
||||
const video = document.getElementById('qr-video');
|
||||
|
||||
const scanner = new QrScanner(video, result => {
|
||||
scanner.stop();
|
||||
document.getElementById("popup").style.display = 'none';
|
||||
document.getElementById("txt").value += result + "\n";
|
||||
});
|
||||
document.getElementById("scanme").addEventListener("click", function(){
|
||||
document.getElementById("popup").style.display = 'flex';
|
||||
scanner.start();
|
||||
});
|
||||
document.getElementById("popup").addEventListener("click", function(){
|
||||
document.getElementById("popup").style.display = 'none';
|
||||
scanner.stop();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
109
templates/new_simple.html
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<form action="./" method="POST">
|
||||
|
||||
<div class="center">Name it <input type="text" name="wallet_name" class="inline" value="{{ wallet_name }}" placeholder="Name your wallet"></div><br>
|
||||
|
||||
|
||||
<h1>Type of the wallet</h1>
|
||||
{% if sigs_total %}
|
||||
<div class="row center">
|
||||
<label>
|
||||
{% if wallet_type=="sh" %}
|
||||
<input type="radio" name="type" value="sh" class="hidden" checked>
|
||||
{% else %}
|
||||
<input type="radio" name="type" value="sh" class="hidden">
|
||||
{%endif%}
|
||||
<div class="btn radio left">Legacy</div>
|
||||
</label>
|
||||
<label>
|
||||
{% if wallet_type=="sh-wsh" %}
|
||||
<input type="radio" name="type" value="sh-wsh" class="hidden" checked>
|
||||
{% else %}
|
||||
<input type="radio" name="type" value="sh-wsh" class="hidden">
|
||||
{%endif%}
|
||||
<div class="btn radio">Nested Segwit</div>
|
||||
</label>
|
||||
<label>
|
||||
{% if wallet_type=="wsh" %}
|
||||
<input type="radio" name="type" value="wsh" class="hidden" checked>
|
||||
{% else %}
|
||||
<input type="radio" name="type" value="wsh" class="hidden">
|
||||
{%endif%}
|
||||
<div class="btn radio right">Segwit</div>
|
||||
</label>
|
||||
{% else %}
|
||||
<div class="row center">
|
||||
<label>
|
||||
<input type="radio" name="type" value="pkh" class="hidden">
|
||||
<div class="btn radio left">Legacy</div>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="type" value="sh-wpkh" class="hidden">
|
||||
<div class="btn radio">Nested Segwit</div>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="type" value="wpkh" checked class="hidden">
|
||||
<div class="btn radio right">Segwit</div>
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="note"><center>
|
||||
<br><b>Segwit</b> uses bech32-encoded addresses (bc1), <b>Nested Segwit</b> makes it compatible with legacy software. Don't use legacy.
|
||||
</center></div>
|
||||
{% if sigs_total %}
|
||||
<br>
|
||||
<div class="center">
|
||||
Using {%if not cosigner_index %}<input class="inline" type="number" name="sigs_required" min=1 max={{sigs_total}} step=1 value="{{sigs_required}}" />{% else %}<div class="input inline">{{sigs_required}}</div><input type="hidden" name="sigs_required" value="{{sigs_required}}"/> {%endif%}
|
||||
of {%if not cosigner_index %}<input class="inline" type="number" name="sigs_total" min={{2}} max={{sigs_total}} step=1 value="{{sigs_total}}"/>{% else %}<div class="input inline">{{sigs_total}}</div><input type="hidden" name="sigs_total" value="{{sigs_total}}"/> {%endif%} multisig
|
||||
<input type="hidden" name="cosigner_index" value="{{cosigner_index}}"/>
|
||||
{% for cosigner in cosigners %}
|
||||
<input type="hidden" name="cosigner{{loop.index0}}" value="{{cosigner}}"/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<br>
|
||||
<h1>Pick the device you want to use as signer #{{cosigner_index+1}}</h1>
|
||||
{% else %}
|
||||
<div class="spacer"></div>
|
||||
<h1>Pick the device you want to use</h1>
|
||||
{% endif %}
|
||||
<div class="row overflow">
|
||||
{% for k in specter.devices.names() %}
|
||||
<label>
|
||||
{% if cosigners and k in cosigners %}{% else %}
|
||||
{% if device["name"] == k %}
|
||||
<input type="radio" name="device" value="{{k}}" class="hidden" checked>
|
||||
{% else %}
|
||||
<input type="radio" name="device" value="{{k}}" class="hidden">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if cosigners and k in cosigners %}
|
||||
<div class="small-card radio highlighted">
|
||||
{% else %}
|
||||
<div class="small-card radio">
|
||||
{% endif %}
|
||||
<img src="/static/img/{{specter.devices[k]['type']}}_icon.svg" width="18px">
|
||||
{{ k }}
|
||||
{% if cosigners %}
|
||||
{% for cosigner in cosigners %}
|
||||
{% if cosigner==k %}
|
||||
- cosigner {{loop.index}}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="submit" name="action" value="device" class="btn centered">Continue</button>
|
||||
<div class="note"><br>
|
||||
{% if sigs_total %}
|
||||
<center>Remember the order of signers. <b>Order is important.</b> It is another piece of information you need to back up.<br>
|
||||
On the next page we will ask you to choose the next device.</center>
|
||||
{% else %}
|
||||
<center>On the next page we will ask you to choose the keys.</center>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
126
templates/new_simple_keys.html
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div class="spacer"></div>
|
||||
{% if device %}
|
||||
<h1>Select the {{device["name"] }}'s key to use in {{wallet_name}}.</h1>
|
||||
{% else %}
|
||||
<h1>Select the key of devices to use in {{wallet_name}}.</h1>
|
||||
{% endif %}
|
||||
|
||||
{% if cosigners %}
|
||||
<form action="./" method="POST" class="table-holder">
|
||||
<input type="hidden" name="wallet_name" value="{{wallet_name}}">
|
||||
{% for cosigner in cosigners %}
|
||||
<input type="hidden" name="cosigner{{loop.index0}}" value="{{cosigner['name']}}"/>
|
||||
{% endfor %}
|
||||
<input type="hidden" name="type" value="{{wallet_type}}">
|
||||
<input type="hidden" name="sigs_required" value="{{sigs_required}}">
|
||||
<input type="hidden" name="sigs_total" value="{{sigs_total}}">
|
||||
<input type="hidden" name="cosigner_index" value="{{cosigner_index}}">
|
||||
{% for device in cosigners %}
|
||||
<h1>Signer {{ loop.index }} - {{device["name"]}}</h1>
|
||||
{% set outer_loop = loop %}
|
||||
{% if device["keys"] | length > 0 %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Network</th><th>Purpose</th><th>Derivation</th><th>Key</th><th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for key in device["keys"] %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if key["xpub"].startswith("xpub") %}
|
||||
<svg class="core main" height="15" width="15" viewBox="0 0 64 64">
|
||||
{%else%}
|
||||
<svg class="core test" height="15" width="15" viewBox="0 0 64 64">
|
||||
{%endif%}
|
||||
<g>
|
||||
<path class="main" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||
<path fill="" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</g>
|
||||
</svg>
|
||||
{% if key["xpub"].startswith("xpub") %}
|
||||
Mainnet
|
||||
{%else%}
|
||||
Testnet
|
||||
{%endif%}
|
||||
</td>
|
||||
<td>{{ purposes[key["type"]] }}</td>
|
||||
<td>{{key["derivation"]}}</td>
|
||||
<td class="xpub">{{key["original"]}}</td>
|
||||
<td width="120px">
|
||||
<label>
|
||||
<input type="radio" name="key{{outer_loop.index0}}" value="{{key['original']}}" class="hidden" {% if loop.index == 1 %}checked{% endif %}/>
|
||||
<div class="btn inline hovering">Use this key</div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="center">Looks like this device doesn't have keys matching this type of wallet.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="spacer"></div>
|
||||
{% endfor %}
|
||||
{% if not error %}
|
||||
<button type="submit" onclick="showLoader()" name="action" value="key" class="btn centered action">Create wallet</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
|
||||
{% else %}
|
||||
<!-- UGLY COPY-PASTE -->
|
||||
<div class="spacer"></div>
|
||||
<div class="table-holder">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Network</th><th>Purpose</th><th>Derivation</th><th>Key</th><th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for key in device["keys"] %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if key["xpub"].startswith("xpub") %}
|
||||
<svg class="core main" height="15" width="15" viewBox="0 0 64 64">
|
||||
{%else%}
|
||||
<svg class="core test" height="15" width="15" viewBox="0 0 64 64">
|
||||
{%endif%}
|
||||
<g>
|
||||
<path class="main" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||
<path fill="" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</g>
|
||||
</svg>
|
||||
{% if key["xpub"].startswith("xpub") %}
|
||||
Main
|
||||
{%else%}
|
||||
Test
|
||||
{%endif%}
|
||||
</td>
|
||||
<td>{{ purposes[key["type"]] }}</td>
|
||||
<td>{{key["derivation"]}}</td>
|
||||
<td class="xpub">{{key["original"]}}</td>
|
||||
<td width="120px">
|
||||
<form action="./" method="POST">
|
||||
<input type="hidden" name="key" value="{{key['original']}}">
|
||||
<input type="hidden" name="wallet_name" value="{{wallet_name}}">
|
||||
<input type="hidden" name="device" value="{{device['name']}}">
|
||||
<input type="hidden" name="type" value="{{wallet_type}}">
|
||||
<button type="submit" onclick="showLoader()" name="action" value="key" class="btn hovering action">Use this key</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="spacer"></div>
|
||||
<div class="note">
|
||||
<center>Here we only show <b>{{purposes[wallet_type]}}</b> and <b>General</b> keys. Follow the standards.</center>
|
||||
</div>
|
||||
{% endblock %}
|
||||
18
templates/new_wallet.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div class="spacer"></div>
|
||||
<h1>Select the type of the wallet</h1>
|
||||
<div class="row">
|
||||
<a href="./simple/" class="small-card">
|
||||
<img src="/static/img/simple_icon.svg"/>
|
||||
Single key wallet
|
||||
</a>
|
||||
<a href="./multisig/" class="small-card">
|
||||
<img src="/static/img/multisig_icon.svg"/>
|
||||
Multisignature wallet
|
||||
</a>
|
||||
</div>
|
||||
<div class="note">
|
||||
<center>Multisig is better for larg-ish amounts.</center>
|
||||
</div>
|
||||
{% endblock %}
|
||||
45
templates/settings.html
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<form action="./" method="POST">
|
||||
<h1>App settings</h1>
|
||||
<div class="card">
|
||||
<h2>Bitcoin JSON-RPC configuration</h2>
|
||||
<br>
|
||||
Username:<br><input type="text" name="username" value="{{username}}">
|
||||
<br><br>
|
||||
Password:<br><input type="text" name="password" value="{{password}}">
|
||||
<br><br>
|
||||
Host:<br>
|
||||
<input type="text" name="host" type="text" value="{{host}}">
|
||||
<br><br>
|
||||
Port:<br>
|
||||
<input type="text" name="port" type="text" value="{{port}}">
|
||||
<div class="note">Default ports: <b>8332</b> for mainnet, <b>18332</b> for testnet, <b>18443</b> for Regtest</div>
|
||||
<div class="row">
|
||||
<button type="submit" class="btn" name="action" value="test">
|
||||
Test
|
||||
</button>
|
||||
<button type="submit" class="btn" name="action" value="save">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
{%if test %}
|
||||
<br><div class="log">Test results:<br>
|
||||
<code><pre>
|
||||
Process finished with code <b>{{ test.code }}</b>
|
||||
{% if test.code == 0 %}
|
||||
Output: {{ test.out }}
|
||||
{% else %}
|
||||
Error message: {{ test.err }}
|
||||
{% endif %}</pre></code>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="card">
|
||||
<h2>Command to run HWI tool</h2>
|
||||
<br>
|
||||
<div>Not implemented yet</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
80
templates/wallet_receive.html
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1>{{wallet.name}}</h1>
|
||||
<div class="row">
|
||||
<a href="/wallets/{{wallet['alias']}}/tx/" class="btn radio left">Transactions</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/receive/" class="btn radio checked">Receive</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/send/" class="btn radio">Send</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/settings/" class="btn radio right">Settings</a>
|
||||
</div>
|
||||
{% set url="https://blockstream.info/" %}
|
||||
{% if specter.chain == "test" %}
|
||||
{% set url="https://blockstream.info/testnet/" %}
|
||||
{% endif %}
|
||||
{% if specter.chain == "regtest" %}
|
||||
{% set url="#" %}
|
||||
{% endif %}
|
||||
<br>
|
||||
<div class="center">
|
||||
<span class="qr-trigger">
|
||||
Address #{{wallet['address_index']+1}}<br>
|
||||
<small>( hover
|
||||
<img src="/static/img/qr_tiny.svg"/>
|
||||
to verify
|
||||
<div class="qr-popup">
|
||||
<img src="{{qrcode(wallet | derivation)}}">
|
||||
</div>)
|
||||
</small>
|
||||
</span><br><br>
|
||||
<img src="{{ qrcode( 'bitcoin:'+wallet['address'] ) }}" class="qr alt-qr" width="300px" /><br>
|
||||
<br>{{wallet['address']}}
|
||||
</div><br><br>
|
||||
<form action="./" method="POST">
|
||||
<button type="submit" onclick="showLoader()" name="action" value="newaddress" class="btn centered">Get new address</button>
|
||||
<!-- <button type="submit" name="action" value="send" class="btn centered action">Send</button> -->
|
||||
</form>
|
||||
|
||||
<div class="note"><center>Specter can verify this address if you scan derivation path<br>( just hover over <img src="/static/img/qr_tiny.svg"/> symbol above the qr code of the address )</center></div>
|
||||
|
||||
{% if (wallet | txonaddr) > 0 %}
|
||||
<br>
|
||||
<h1>Transactions on this address ( {{wallet | txonaddr}} )</h1>
|
||||
<div class="table-holder">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th><th>TxID</th><th>Address</th><th>Amount</th><th>Confirmations</th><th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tx in wallet.transactions %}
|
||||
{% if tx['address'] == wallet['address'] %}
|
||||
{%if tx["confirmations"] == 0 %}
|
||||
<tr class="unconfirmed">
|
||||
{%else%}
|
||||
<tr>
|
||||
{%endif%}
|
||||
<td>
|
||||
{%if tx["confirmations"] == 0 %}
|
||||
<img src="/static/img/unconfirmed_{{tx['category']}}_icon.svg"/>
|
||||
{% else %}
|
||||
<img src="/static/img/{{tx['category']}}_icon.svg"/>
|
||||
{%endif%}
|
||||
</td>
|
||||
<td class="txid"><a target="blank" href="{{url}}tx/{{tx['txid']}}">{{tx["txid"]}}</a></td>
|
||||
<td class="txid"><a target="blank" href="{{url}}address/{{tx['address']}}">{{tx["address"]}}</a></td>
|
||||
<td>{{tx["amount"]}}</td><td>
|
||||
{%if tx["confirmations"] == 0 %}
|
||||
Pending
|
||||
{% else %}
|
||||
{{tx["confirmations"]}}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{tx["time"] | datetime}}</td></tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
86
templates/wallet_send.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1>{{wallet.name}}</h1>
|
||||
<div class="row">
|
||||
<a href="/wallets/{{wallet['alias']}}/tx/" class="btn radio left">Transactions</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/receive/" class="btn radio">Receive</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/send/" class="btn radio checked">Send</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/settings/" class="btn radio right">Settings</a>
|
||||
</div>
|
||||
<br>
|
||||
<form action="./" method="POST" class="full-width">
|
||||
{% if not psbt %}
|
||||
<br><h1>Sending to:</h1>
|
||||
<div class="card">
|
||||
Receipient address:<br>
|
||||
<div class="row">
|
||||
<input type="text" id="address" name="address" value="{{address}}">
|
||||
<a class="btn" id="scanme"><img src="/static/img/qr_tiny.svg"/> Scan</a>
|
||||
</div>
|
||||
<br><br>
|
||||
Amount:<br>
|
||||
<input type="number" name="amount" value="{{amount}}" id="amount" max="{{wallet.fullbalance}}" min=0 step="1e-8">
|
||||
<div class="note">Available: {{"%.8f" % wallet.fullbalance}}</div>
|
||||
<br><br>
|
||||
<button type="submit" onclick="showLoader()" name="action" value="createpsbt" class="btn centered">Create unsigned transaction</button>
|
||||
</div>
|
||||
<br>
|
||||
{% else %}
|
||||
<div>
|
||||
<div class="row">
|
||||
<!-- {% set chunk = 700 %}
|
||||
{%for i in range( (psbt["base64"] | length)//chunk+1)%}
|
||||
<img src="{{qrcode((loop.index0 | string)+''+psbt['base64'][loop.index0*chunk:(loop.index0+1)*chunk])}}" class="qr" width="300px"/>
|
||||
{%endfor%}
|
||||
-->
|
||||
<img src="{{qrcode(psbt['base64'])}}" class="qr broad" width="500px"/>
|
||||
</div>
|
||||
<br><a class="btn centered" download='to{{address}}{{amount}}.psbt' href="data:application/octet-stream;base64,{{psbt['base64']}}">Download transaction</a>
|
||||
</div>
|
||||
<div class="log"><code><pre>{{psbt | prettyjson}}</pre></code></div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<div class="popup" id="popup">
|
||||
<video muted playsinline id="qr-video" class="video"></video>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import QrScanner from "/static/qr/qr-scanner.min.js";
|
||||
QrScanner.WORKER_PATH = '/static/qr/qr-scanner-worker.min.js';
|
||||
|
||||
const video = document.getElementById('qr-video');
|
||||
|
||||
const scanner = new QrScanner(video, result => {
|
||||
scanner.stop();
|
||||
document.getElementById("popup").style.display = 'none';
|
||||
let addr = result;
|
||||
if(addr.indexOf("bitcoin:") >= 0){
|
||||
addr = addr.substr(addr.indexOf("bitcoin:")+8);
|
||||
}
|
||||
let arr = addr.split("?");
|
||||
addr = arr[0]
|
||||
document.getElementById("address").value = addr;
|
||||
if(arr.length > 1){
|
||||
arr = arr[1].split("&");
|
||||
arr.forEach((e)=>{
|
||||
if(e.startsWith("amount=")){
|
||||
document.getElementById("amount").value = parseFloat(e.substr(7));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
document.getElementById("scanme").addEventListener("click", function(){
|
||||
document.getElementById("popup").style.display = 'flex';
|
||||
scanner.start();
|
||||
});
|
||||
document.getElementById("popup").addEventListener("click", function(){
|
||||
document.getElementById("popup").style.display = 'none';
|
||||
scanner.stop();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
28
templates/wallet_settings.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1>{{wallet.name}}</h1>
|
||||
<div class="row">
|
||||
<a href="/wallets/{{wallet['alias']}}/tx/" class="btn radio left">Transactions</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/receive/" class="btn radio">Receive</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/send/" class="btn radio">Send</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/settings/" class="btn radio right checked">Settings</a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="note center">
|
||||
Import this wallet to the device by scanning QR code below.
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<img src="{{qrcode(qr_text)}}" class="qr" width="400px" />
|
||||
</div>
|
||||
{% if wallet.is_multisig() %}
|
||||
<br>
|
||||
<div>
|
||||
<a download="{{wallet['name']}}" href="data:text/plain;charset=US-ASCII,{{cc_file}}" class="btn centered">Download ColdCard file</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<code><pre>{{qr_text}}</pre></code>
|
||||
</div>
|
||||
<br>
|
||||
{% endblock %}
|
||||
65
templates/wallet_tx.html
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1>{{wallet.name}}</h1>
|
||||
<div class="row">
|
||||
<a href="/wallets/{{wallet['alias']}}/tx/" class="btn radio left checked">Transactions</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/receive/" class="btn radio">Receive</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/send/" class="btn radio">Send</a>
|
||||
<a href="/wallets/{{wallet['alias']}}/settings/" class="btn radio right">Settings</a>
|
||||
</div>
|
||||
{% set url="https://blockstream.info/" %}
|
||||
{% if specter.chain == "test" %}
|
||||
{% set url="https://blockstream.info/testnet/" %}
|
||||
{% endif %}
|
||||
{% if specter.chain == "regtest" %}
|
||||
{% set url="#" %}
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if ( wallet.fullbalance ) is not none %}
|
||||
<h1><small style="line-height:30px">Total balance:<br></small><span style="color: #fff">
|
||||
{{ "%0.8f" % ( wallet.fullbalance ) | float }}
|
||||
{% if specter.chain !='main' %}t{%endif%}BTC
|
||||
{% if wallet.balance["untrusted_pending"] > 0 %}</span><br><small>
|
||||
( {{ "%0.8f" % wallet.balance["trusted"] | float }} confirmed, {{"%0.8f" % wallet.balance["untrusted_pending"] | float}} pending )
|
||||
{% endif %}
|
||||
</small></h1>
|
||||
<br>
|
||||
<h1>Latest transactions</h1>
|
||||
<div class="table-holder">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th><th>TxID</th><th>Address</th><th>Amount</th><th>Confirmations</th><th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tx in wallet.transactions %}
|
||||
{%if tx["confirmations"] == 0 %}
|
||||
<tr class="unconfirmed">
|
||||
{%else%}
|
||||
<tr>
|
||||
{%endif%}
|
||||
<td>
|
||||
{%if tx["confirmations"] == 0 %}
|
||||
<img src="/static/img/unconfirmed_{{tx['category']}}_icon.svg"/>
|
||||
{% else %}
|
||||
<img src="/static/img/{{tx['category']}}_icon.svg"/>
|
||||
{%endif%}
|
||||
</td>
|
||||
<td class="txid"><a target="blank" href="{{url}}tx/{{tx['txid']}}">{{tx["txid"]}}</a></td>
|
||||
<td class="txid"><a target="blank" href="{{url}}address/{{tx['address']}}">{{tx["address"]}}</a></td>
|
||||
<td>{{tx["amount"]}}</td><td>
|
||||
{%if tx["confirmations"] == 0 %}
|
||||
Pending
|
||||
{% else %}
|
||||
{{tx["confirmations"]}}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{tx["time"] | datetime}}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||