Browse Source

Add BIP21 bitcoin payment URI support to sendpayment.py

master
Kristaps Kaupe 6 years ago
parent
commit
faaf51eada
No known key found for this signature in database
GPG Key ID: D47B1B4232B55437
  1. 6
      docs/USAGE.md
  2. 1
      jmbitcoin/jmbitcoin/__init__.py
  3. 41
      jmbitcoin/jmbitcoin/bip21.py
  4. 2
      jmbitcoin/setup.py
  5. 58
      jmbitcoin/test/test_bip21.py
  6. 3
      jmclient/jmclient/cli_options.py
  7. 28
      scripts/sendpayment.py

6
docs/USAGE.md

@ -382,7 +382,11 @@ Here is an example:
(jmvenv)$ python sendpayment.py wallet.jmdat 5000000 mprGzBA9rQk82Ly41TsmpQGa8UPpZb2w8c
This sends 5000000 satoshi (0.05btc) to the address *mprGzBA9rQk82Ly41TsmpQGa8UPpZb2w8c* (testnet), with the default 5-7 (randomized) other parties from the default 0-th mixing depth from the wallet contained in the file *wallet.jmdat*. This will take some time, since Joinmarket will connect to remote messaging servers and do end to end encrypted communication with other bots, and also you will be paying some fees (more on this later in this section).
Or you can use BIP21 bitcoin payment URI:
(jmvenv)$ python sendpayment.py wallet.jmdat bitcoin:mprGzBA9rQk82Ly41TsmpQGa8UPpZb2w8c?amount=0.05
These send 5000000 satoshi (0.05btc) to the address *mprGzBA9rQk82Ly41TsmpQGa8UPpZb2w8c* (testnet), with the default 5-7 (randomized) other parties from the default 0-th mixing depth from the wallet contained in the file *wallet.jmdat*. This will take some time, since Joinmarket will connect to remote messaging servers and do end to end encrypted communication with other bots, and also you will be paying some fees (more on this later in this section).
<a name="no-coinjoin-sending-funds" />

1
jmbitcoin/jmbitcoin/__init__.py

@ -5,4 +5,5 @@ from jmbitcoin.secp256k1_deterministic import *
from jmbitcoin.btscript import *
from jmbitcoin.bech32 import *
from jmbitcoin.amount import *
from jmbitcoin.bip21 import *

41
jmbitcoin/jmbitcoin/bip21.py

@ -0,0 +1,41 @@
# https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
# bitcoin:<address>[?amount=<amount>][?label=<label>][?message=<message>]
# We don't check validity of Bitcoin address here, as all the tools using
# this are expected to do address validation independently anyway.
from jmbitcoin import amount_to_sat
from urllib.parse import parse_qs, urlparse
from url_decode import urldecode
import re
def is_bip21_uri(uri):
parsed = urlparse(uri)
return parsed.scheme == 'bitcoin' and parsed.path != ''
def is_bip21_amount_str(amount):
return re.compile("^[0-9]{1,8}(\.[0-9]{1,8})?$").match(amount) != None
def decode_bip21_uri(uri):
if not is_bip21_uri(uri):
raise ValueError("Not a valid BIP21 URI: " + uri)
result = {}
parsed = urlparse(uri)
result['address'] = parsed.path
params = parse_qs(parsed.query)
for key in params:
if key.startswith('req-'):
raise ValueError("Unknown required parameter " + key +
" in BIP21 URI.")
if key == 'amount':
amount_str = params['amount'][0]
if not is_bip21_amount_str(amount_str):
raise ValueError("Invalid BTC amount " + amount_str +
" vs. " + str(amount_to_sat(amount_str)) + " sat")
# Convert amount to sats, as used internally by JM
result['amount'] = amount_to_sat(amount_str + "btc")
else:
result[key] = urldecode(params[key][0])
return result

2
jmbitcoin/setup.py

@ -9,5 +9,5 @@ setup(name='joinmarketbitcoin',
author_email='',
license='GPL',
packages=['jmbitcoin'],
install_requires=['future', 'coincurve',],
install_requires=['future', 'coincurve', 'urldecode'],
zip_safe=False)

58
jmbitcoin/test/test_bip21.py

@ -0,0 +1,58 @@
import jmbitcoin as btc
import pytest
def test_bip21():
# These should raise exception because of not being valid BIP21 URI's
with pytest.raises(ValueError):
btc.decode_bip21_uri('')
btc.decode_bip21_uri('nfdjksnfjkdsnfjkds')
btc.decode_bip21_uri('175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W')
btc.decode_bip21_uri(
'175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=20.3')
btc.decode_bip21_uri('bitcoin:')
btc.decode_bip21_uri('bitcoin:?amount=20.3')
btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=')
btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=XYZ')
btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=100\'000')
btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=100,000')
btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=100000000')
assert(btc.decode_bip21_uri('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W'
)['address'] == '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W')
parsed = btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?label=Luke-Jr')
assert(parsed['address'] == '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W')
assert(parsed['label'] == 'Luke-Jr')
parsed = btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=20.3&label=Luke-Jr')
assert(parsed['address'] == '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W')
assert(parsed['amount'] == 2030000000)
assert(parsed['label'] == 'Luke-Jr')
parsed = btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz')
assert(parsed['address'] == '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W')
assert(parsed['amount'] == 5000000000)
assert(parsed['label'] == 'Luke-Jr')
assert(parsed['message'] == 'Donation for project xyz')
# This should raise exception because of unknown req-* parameters
with pytest.raises(ValueError):
btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?req-somethingyoudontunderstand=50&req-somethingelseyoudontget=999')
parsed = btc.decode_bip21_uri(
'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?somethingyoudontunderstand=50&somethingelseyoudontget=999')
assert(parsed['address'] == '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W')
assert(parsed['somethingyoudontunderstand'] == '50')
assert(parsed['somethingelseyoudontget'] == '999')

3
jmclient/jmclient/cli_options.py

@ -442,7 +442,8 @@ def get_tumbler_parser():
def get_sendpayment_parser():
parser = OptionParser(
usage=
'usage: %prog [options] [wallet file] [amount] [destaddr]',
'usage: %prog [options] wallet_file amount destaddr\n' +
' %prog [options] wallet_file bitcoin_uri',
description='Sends a single payment from a given mixing depth of your '
+
'wallet to an given address using coinjoin and then switches off. '

28
scripts/sendpayment.py

@ -54,19 +54,31 @@ def main():
parser.error("PayJoin requires exactly three arguments: "
"wallet, amount and destination address.")
sys.exit(EXIT_ARGERROR)
elif options.schedule == '' and len(args) != 3:
parser.error("Joinmarket sendpayment (coinjoin) needs arguments:"
" wallet, amount and destination address")
sys.exit(EXIT_ARGERROR)
elif options.schedule == '':
if ((len(args) < 2) or
(btc.is_bip21_uri(args[1]) and len(args) != 2) or
(not btc.is_bip21_uri(args[1]) and len(args) != 3)):
parser.error("Joinmarket sendpayment (coinjoin) needs arguments:"
" wallet, amount, destination address or wallet, bitcoin_uri.")
sys.exit(EXIT_ARGERROR)
#without schedule file option, use the arguments to create a schedule
#of a single transaction
sweeping = False
if options.schedule == '':
amount = btc.amount_to_sat(args[1])
if amount == 0:
sweeping = True
destaddr = args[2]
if btc.is_bip21_uri(args[1]):
parsed = btc.decode_bip21_uri(args[1])
try:
amount = parsed['amount']
except KeyError:
parser.error("Given BIP21 URI does not contain amount.")
sys.exit(EXIT_ARGERROR)
destaddr = parsed['address']
else:
amount = btc.amount_to_sat(args[1])
if amount == 0:
sweeping = True
destaddr = args[2]
mixdepth = options.mixdepth
addr_valid, errormsg = validate_address(destaddr)
if not addr_valid:

Loading…
Cancel
Save