mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #165 from newtonick/ur_output_descriptor
Ur output descriptor
This commit is contained in:
@@ -1,181 +0,0 @@
|
||||
### bc-ur encoding stuff
|
||||
from io import BytesIO
|
||||
import hashlib
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
|
||||
def bech32_polymod(values):
|
||||
"""Internal function that computes the Bech32 checksum."""
|
||||
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
|
||||
chk = 1
|
||||
for value in values:
|
||||
top = chk >> 25
|
||||
chk = (chk & 0x1FFFFFF) << 5 ^ value
|
||||
for i in range(5):
|
||||
chk ^= generator[i] if ((top >> i) & 1) else 0
|
||||
return chk
|
||||
|
||||
|
||||
def bech32_hrp_expand(hrp):
|
||||
"""Expand the HRP into values for checksum computation."""
|
||||
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
||||
|
||||
|
||||
def bech32_verify_checksum(hrp, data):
|
||||
"""Verify a checksum given HRP and converted data characters."""
|
||||
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
|
||||
|
||||
|
||||
def bech32_create_checksum(hrp, data):
|
||||
"""Compute the checksum values given HRP and data."""
|
||||
values = bech32_hrp_expand(hrp) + data
|
||||
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
|
||||
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
||||
|
||||
|
||||
def bech32_encode(hrp, data):
|
||||
"""Compute a Bech32 string given HRP and data values."""
|
||||
combined = data + bech32_create_checksum(hrp, data)
|
||||
return hrp + "1" + "".join([CHARSET[d] for d in combined])
|
||||
|
||||
|
||||
def bech32_decode(bech):
|
||||
"""Validate a Bech32 string, and determine HRP and data."""
|
||||
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
|
||||
bech.lower() != bech and bech.upper() != bech
|
||||
):
|
||||
return (None, None)
|
||||
bech = bech.lower()
|
||||
pos = bech.rfind("1")
|
||||
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
|
||||
return (None, None)
|
||||
if not all(x in CHARSET for x in bech[pos + 1 :]):
|
||||
return (None, None)
|
||||
hrp = bech[:pos]
|
||||
data = [CHARSET.find(x) for x in bech[pos + 1 :]]
|
||||
if not bech32_verify_checksum(hrp, data):
|
||||
return (None, None)
|
||||
return (hrp, data[:-6])
|
||||
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True):
|
||||
"""General power-of-2 base conversion."""
|
||||
acc = 0
|
||||
bits = 0
|
||||
ret = []
|
||||
maxv = (1 << tobits) - 1
|
||||
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||
for value in data:
|
||||
if value < 0 or (value >> frombits):
|
||||
return None
|
||||
acc = ((acc << frombits) | value) & max_acc
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if pad:
|
||||
if bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
def decode(hrp, addr):
|
||||
"""Decode a segwit address."""
|
||||
hrpgot, data = bech32_decode(addr)
|
||||
if hrpgot != hrp:
|
||||
return (None, None)
|
||||
decoded = convertbits(data[1:], 5, 8, False)
|
||||
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
|
||||
return (None, None)
|
||||
if data[0] > 16:
|
||||
return (None, None)
|
||||
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
|
||||
return (None, None)
|
||||
return (data[0], decoded)
|
||||
|
||||
|
||||
def encode(hrp, witver, witprog):
|
||||
"""Encode a segwit address."""
|
||||
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
|
||||
if decode(hrp, ret) == (None, None):
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
def bc32encode(data: bytes) -> str:
|
||||
"""
|
||||
bc32 encoding
|
||||
see https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-004-bc32.md
|
||||
"""
|
||||
dd = convertbits(data, 8, 5)
|
||||
polymod = bech32_polymod([0] + dd + [0, 0, 0, 0, 0, 0]) ^ 0x3FFFFFFF
|
||||
chk = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
||||
return "".join([CHARSET[d] for d in dd + chk])
|
||||
|
||||
|
||||
def bc32decode(bc32: str) -> bytes:
|
||||
"""
|
||||
bc32 decoding
|
||||
see https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-004-bc32.md
|
||||
"""
|
||||
if bc32.lower() != bc32 and bc32.upper() != bc32:
|
||||
return None
|
||||
bc32 = bc32.lower()
|
||||
if not all([x in CHARSET for x in bc32]):
|
||||
return None
|
||||
res = [CHARSET.find(c) for c in bc32.lower()]
|
||||
if bech32_polymod([0] + res) != 0x3FFFFFFF:
|
||||
return None
|
||||
return bytes(convertbits(res[:-6], 5, 8, False))
|
||||
|
||||
|
||||
def cbor_encode(data):
|
||||
l = len(data)
|
||||
if l <= 23:
|
||||
prefix = bytes([0x40 + l])
|
||||
elif l <= 255:
|
||||
prefix = bytes([0x58, l])
|
||||
elif l <= 65535:
|
||||
prefix = b"\x59" + l.to_bytes(2, "big")
|
||||
else:
|
||||
prefix = b"\x60" + l.to_bytes(4, "big")
|
||||
return prefix + data
|
||||
|
||||
|
||||
def cbor_decode(data):
|
||||
s = BytesIO(data)
|
||||
b = s.read(1)[0]
|
||||
if b >= 0x40 and b < 0x58:
|
||||
l = b - 0x40
|
||||
return s.read(l)
|
||||
if b == 0x58:
|
||||
l = s.read(1)[0]
|
||||
return s.read(l)
|
||||
if b == 0x59:
|
||||
l = int.from_bytes(s.read(2), "big")
|
||||
return s.read(l)
|
||||
if b == 0x60:
|
||||
l = int.from_bytes(s.read(4), "big")
|
||||
return s.read(l)
|
||||
return None
|
||||
|
||||
|
||||
def bcur_encode(data):
|
||||
"""Returns bcur encoded string and hash digest"""
|
||||
cbor = cbor_encode(data)
|
||||
enc = bc32encode(cbor)
|
||||
h = hashlib.sha256(cbor).digest()
|
||||
enc_hash = bc32encode(h)
|
||||
return enc, enc_hash
|
||||
|
||||
|
||||
def bcur_decode(data, checksum=None):
|
||||
"""Returns decoded data, verifies hash digest if provided"""
|
||||
cbor = bc32decode(data)
|
||||
if checksum is not None:
|
||||
h = bc32decode(checksum)
|
||||
assert h == hashlib.sha256(cbor).digest()
|
||||
return cbor_decode(cbor)
|
||||
@@ -9,9 +9,10 @@ from embit import psbt, bip39
|
||||
from pyzbar import pyzbar
|
||||
from pyzbar.pyzbar import ZBarSymbol
|
||||
from urtypes.crypto import PSBT as UR_PSBT
|
||||
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
|
||||
from urtypes.bytes import Bytes
|
||||
|
||||
from seedsigner.helpers.ur2.ur_decoder import URDecoder
|
||||
from seedsigner.helpers.bcur import (cbor_decode, bc32decode)
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
|
||||
from . import QRType, Seed
|
||||
@@ -64,15 +65,12 @@ class DecodeQR:
|
||||
if self.qr_type == None:
|
||||
self.qr_type = qr_type
|
||||
|
||||
if self.qr_type == QRType.PSBT__UR2:
|
||||
self.decoder = URDecoder() # UR2 decoder
|
||||
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
|
||||
self.decoder = URDecoder() # BCUR Decoder
|
||||
|
||||
elif self.qr_type == QRType.PSBT__SPECTER:
|
||||
self.decoder = SpecterPsbtQrDecoder() # Specter Desktop PSBT QR base64 decoder
|
||||
|
||||
elif self.qr_type == QRType.PSBT__LEGACY_UR:
|
||||
self.decoder = LegacyUrPsbtQrDecoder() # UR Legacy decoder
|
||||
|
||||
elif self.qr_type == QRType.PSBT__BASE64:
|
||||
self.decoder = Base64PsbtQrDecoder() # Single Segments Base64
|
||||
|
||||
@@ -93,6 +91,9 @@ class DecodeQR:
|
||||
|
||||
elif self.qr_type == QRType.WALLET__GENERIC:
|
||||
self.decoder = GenericWalletQrDecoder()
|
||||
|
||||
elif self.qr_type == QRType.WALLET__CONFIGFILE:
|
||||
self.decoder = MultiSigConfigFileQRDecoder()
|
||||
|
||||
elif self.qr_type != qr_type:
|
||||
raise Exception('QR Fragment Unexpected Type Change')
|
||||
@@ -114,7 +115,7 @@ class DecodeQR:
|
||||
# it's already str data
|
||||
qr_str = data
|
||||
|
||||
if self.qr_type == QRType.PSBT__UR2:
|
||||
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
|
||||
self.decoder.receive_part(qr_str)
|
||||
if self.decoder.is_complete():
|
||||
self.complete = True
|
||||
@@ -192,17 +193,29 @@ class DecodeQR:
|
||||
|
||||
def get_wallet_descriptor(self):
|
||||
if self.is_wallet_descriptor:
|
||||
return self.decoder.get_wallet_descriptor()
|
||||
if self.qr_type in [QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
|
||||
cbor = self.decoder.result_message().cbor
|
||||
if self.qr_type == QRType.OUTPUT__UR:
|
||||
return Output.from_cbor(cbor).descriptor()
|
||||
elif self.qr_type == QRType.ACCOUNT__UR:
|
||||
return Account.from_cbor(cbor).output_descriptors[0].descriptor()
|
||||
elif self.qr_type == QRType.BYTES__UR:
|
||||
raw = Bytes.from_cbor(cbor).data
|
||||
descriptor = DecodeQR.multisig_setup_file_to_descriptor(raw.decode("utf-8"))
|
||||
return descriptor
|
||||
else:
|
||||
# All the other wallet output descriptor decoder types use the same method signature
|
||||
return self.decoder.get_wallet_descriptor()
|
||||
|
||||
|
||||
def get_percent_complete(self) -> int:
|
||||
if not self.decoder:
|
||||
return 0
|
||||
|
||||
if self.qr_type == QRType.PSBT__UR2:
|
||||
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR]:
|
||||
return int(self.decoder.estimated_percent_complete() * 100)
|
||||
|
||||
elif self.qr_type in [QRType.PSBT__SPECTER, QRType.PSBT__LEGACY_UR]:
|
||||
elif self.qr_type in [QRType.PSBT__SPECTER]:
|
||||
if self.decoder.total_segments == None:
|
||||
return 0
|
||||
return int((self.decoder.collected_segments / self.decoder.total_segments) * 100)
|
||||
@@ -233,12 +246,10 @@ class DecodeQR:
|
||||
return self.qr_type in [
|
||||
QRType.PSBT__UR2,
|
||||
QRType.PSBT__SPECTER,
|
||||
QRType.PSBT__LEGACY_UR,
|
||||
QRType.PSBT__BASE64,
|
||||
QRType.PSBT__BASE43,
|
||||
]
|
||||
|
||||
|
||||
@property
|
||||
def is_seed(self):
|
||||
return self.qr_type in [
|
||||
@@ -262,8 +273,14 @@ class DecodeQR:
|
||||
|
||||
@property
|
||||
def is_wallet_descriptor(self):
|
||||
return self.qr_type in [QRType.WALLET__SPECTER, QRType.WALLET__UR, QRType.WALLET__BLUEWALLET, QRType.WALLET__GENERIC]
|
||||
|
||||
check = self.qr_type in [QRType.WALLET__SPECTER, QRType.WALLET__UR, QRType.WALLET__CONFIGFILE, QRType.WALLET__GENERIC, QRType.OUTPUT__UR]
|
||||
|
||||
if self.qr_type in [QRType.BYTES__UR]:
|
||||
cbor = self.decoder.result_message().cbor
|
||||
raw = Bytes.from_cbor(cbor).data
|
||||
check = 'multisig setup file' in raw.decode("utf-8").lower()
|
||||
|
||||
return check
|
||||
|
||||
@property
|
||||
def is_settings(self):
|
||||
@@ -303,12 +320,18 @@ class DecodeQR:
|
||||
# PSBT
|
||||
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
|
||||
return QRType.PSBT__UR2
|
||||
|
||||
elif re.search("^UR:CRYPTO-OUTPUT/", s, re.IGNORECASE):
|
||||
return QRType.OUTPUT__UR
|
||||
|
||||
elif re.search("^UR:CRYPTO-ACCOUNT/", s, re.IGNORECASE):
|
||||
return QRType.ACCOUNT__UR
|
||||
|
||||
elif re.search(r'^p(\d+)of(\d+) ([A-Za-z0-9+\/=]+$)', s, re.IGNORECASE): #must be base64 characters only in segment
|
||||
return QRType.PSBT__SPECTER
|
||||
|
||||
elif re.search("^UR:BYTES/", s, re.IGNORECASE):
|
||||
return QRType.PSBT__LEGACY_UR
|
||||
return QRType.BYTES__UR
|
||||
|
||||
elif DecodeQR.is_base64_psbt(s):
|
||||
return QRType.PSBT__BASE64
|
||||
@@ -323,6 +346,9 @@ class DecodeQR:
|
||||
# if json starting with label and contains descriptor, assume specter wallet json
|
||||
return QRType.WALLET__SPECTER
|
||||
|
||||
elif "multisig setup file" in s.lower():
|
||||
return QRType.WALLET__CONFIGFILE
|
||||
|
||||
elif "sortedmulti" in s:
|
||||
return QRType.WALLET__GENERIC
|
||||
|
||||
@@ -453,8 +479,101 @@ class DecodeQR:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def multisig_setup_file_to_descriptor(text) -> str:
|
||||
# sample text file, parse the contents and create descriptor
|
||||
"""
|
||||
Name: SeedSigner Dev Funds
|
||||
Policy: 4 of 6
|
||||
Derivation: m/48'/0'/0'/2'
|
||||
Format: P2WSH
|
||||
|
||||
E0811B6B: xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt
|
||||
852B308F: xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh
|
||||
7EDF9C59: xpub6DaFfKoe7WpofrbYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs
|
||||
B433E095: xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS
|
||||
184D07EB: xpub6EEoTpcQu7N4R8D84pJjZ69j3minevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy
|
||||
3E451EFE: xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g
|
||||
"""
|
||||
|
||||
lines = text.split('\n')
|
||||
|
||||
m = 0
|
||||
n = 0
|
||||
xpubs = []
|
||||
x = 0
|
||||
derivation = ''
|
||||
descriptor = ''
|
||||
|
||||
lines = text.split('\n')
|
||||
|
||||
for l in lines:
|
||||
if l.find('#') == 0:
|
||||
# skip comments
|
||||
continue
|
||||
|
||||
l = l.strip()
|
||||
|
||||
if ':' not in l:
|
||||
# when label/value divider not found, skip line
|
||||
continue
|
||||
|
||||
label, value = l.split(':', 1)
|
||||
label = label.strip().lower()
|
||||
value = value.strip()
|
||||
|
||||
if label == 'policy':
|
||||
try:
|
||||
match = re.search(r'(\d+)\D*(\d+)', value)
|
||||
m = int(match.group(1))
|
||||
n = int(match.group(2))
|
||||
except:
|
||||
raise Exception(f"Policy line not supported")
|
||||
elif label == 'derivation':
|
||||
derivation = value
|
||||
elif label == 'format':
|
||||
if value.lower() in ['p2wsh', 'p2sh-p2wsh', 'p2wsh-p2sh']:
|
||||
script_type = value.lower()
|
||||
elif len(label) == 8:
|
||||
if len(xpubs) == 0:
|
||||
xpubs = [None] * n
|
||||
|
||||
xpubs[x] = {'xfp': label, 'key': value}
|
||||
x += 1
|
||||
|
||||
if None in xpubs or len(xpubs) != n:
|
||||
raise Exception(f"bad or missing xpub")
|
||||
|
||||
if m <= 0 or m > 9 or n <= 0 or n > 9:
|
||||
raise Exception(f"bad or missing policy")
|
||||
|
||||
if len(derivation) == 0:
|
||||
raise Exception(f"bad or missing derivation path")
|
||||
|
||||
if script_type not in ['p2wsh', 'p2sh-p2wsh', 'p2wsh-p2sh']:
|
||||
raise Exception(f"bad or missing script format")
|
||||
|
||||
# create descriptor string
|
||||
|
||||
if script_type == "p2wsh":
|
||||
script_open = "wsh(sortedmulti(" + str(m)
|
||||
script_close = "))"
|
||||
elif script_type in ["p2sh-p2wsh", 'p2wsh-p2sh']:
|
||||
script_open = "sh(wsh(sortedmulti(" + str(m)
|
||||
script_close = ")))"
|
||||
|
||||
descriptor = script_open
|
||||
|
||||
for x in xpubs:
|
||||
if derivation[0] == 'm':
|
||||
derivation = derivation[1:]
|
||||
derivation = derivation.replace("'", "h")
|
||||
descriptor += ',[' + x['xfp'] + derivation + "]" + x['key'] + "/{0,1}/*"
|
||||
|
||||
descriptor += script_close
|
||||
|
||||
return descriptor
|
||||
|
||||
class BaseQrDecoder:
|
||||
def __init__(self):
|
||||
@@ -551,51 +670,6 @@ class SpecterPsbtQrDecoder(BaseAnimatedQrDecoder):
|
||||
def parse_segment(self, segment) -> str:
|
||||
return segment.split(" ")[-1].strip()
|
||||
|
||||
|
||||
|
||||
class LegacyUrPsbtQrDecoder(BaseAnimatedQrDecoder):
|
||||
"""
|
||||
Decodes Legacy UR animated qr encoding
|
||||
"""
|
||||
def get_base64_data(self) -> str:
|
||||
bc32_cbor = "".join(self.segments)
|
||||
raw = cbor_decode(bc32decode(bc32_cbor))
|
||||
base64 = b2a_base64(raw)
|
||||
|
||||
if self.complete:
|
||||
return base64
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_data(self):
|
||||
if not self.complete:
|
||||
return None
|
||||
|
||||
bc32_cbor = "".join(self.segments)
|
||||
raw = cbor_decode(bc32decode(bc32_cbor))
|
||||
return raw
|
||||
|
||||
|
||||
def parse_current_segment_num(self, segment) -> int:
|
||||
if re.search(r'^UR:BYTES/(\d+)OF(\d+)', segment, re.IGNORECASE) != None:
|
||||
return int(re.search(r'^UR:BYTES/(\d+)OF(\d+)', segment, re.IGNORECASE).group(1))
|
||||
else:
|
||||
raise Exception('Unexpected Legacy UR Error')
|
||||
|
||||
|
||||
def parse_total_segment_nums(self, segment) -> int:
|
||||
if re.search(r'^UR:BYTES/(\d+)OF(\d+)', segment, re.IGNORECASE) != None:
|
||||
return int(re.search(r'^UR:BYTES/(\d+)OF(\d+)', segment, re.IGNORECASE).group(2))
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
def parse_segment(self, segment) -> str:
|
||||
return segment.split("/")[-1].strip()
|
||||
|
||||
|
||||
|
||||
class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder):
|
||||
"""
|
||||
Decodes single frame base64 encoded qr image.
|
||||
@@ -1005,6 +1079,7 @@ class GenericWalletQrDecoder(BaseSingleFrameQrDecoder):
|
||||
# Validate via embit
|
||||
Descriptor.from_string(segment)
|
||||
self.descriptor = segment
|
||||
self.complete = True
|
||||
return DecodeQRStatus.COMPLETE
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
@@ -1013,4 +1088,11 @@ class GenericWalletQrDecoder(BaseSingleFrameQrDecoder):
|
||||
|
||||
def get_wallet_descriptor(self):
|
||||
return self.descriptor
|
||||
|
||||
class MultiSigConfigFileQRDecoder(GenericWalletQrDecoder):
|
||||
|
||||
def add(self, segment, qr_type=QRType.WALLET__CONFIGFILE):
|
||||
descriptor = DecodeQR.multisig_setup_file_to_descriptor(segment)
|
||||
return super().add(descriptor,qr_type=QRType.WALLET__CONFIGFILE)
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ class QRType:
|
||||
"""
|
||||
PSBT__BASE64 = "psbt__base64"
|
||||
PSBT__SPECTER = "psbt__specter"
|
||||
PSBT__LEGACY_UR = "psbt__legacy_ur"
|
||||
PSBT__BASE43 = "psbt__base43"
|
||||
PSBT__UR2 = "psbt__ur2"
|
||||
|
||||
@@ -24,7 +23,10 @@ class QRType:
|
||||
|
||||
WALLET__SPECTER = "wallet__specter"
|
||||
WALLET__UR = "wallet__ur"
|
||||
WALLET__BLUEWALLET = "wallet__bluewallet"
|
||||
WALLET__CONFIGFILE = "wallet__configfile"
|
||||
WALLET__GENERIC = "wallet__generic"
|
||||
OUTPUT__UR = "output__ur"
|
||||
ACCOUNT__UR = "account__ur"
|
||||
BYTES__UR = "bytes__ur"
|
||||
|
||||
INVALID = "invalid"
|
||||
Reference in New Issue
Block a user