diff --git a/docs/USAGE.md b/docs/USAGE.md
index 474d74e..d0fe187 100644
--- a/docs/USAGE.md
+++ b/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).
diff --git a/jmbitcoin/jmbitcoin/__init__.py b/jmbitcoin/jmbitcoin/__init__.py
index 7db6dc1..784673a 100644
--- a/jmbitcoin/jmbitcoin/__init__.py
+++ b/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 *
diff --git a/jmbitcoin/jmbitcoin/bip21.py b/jmbitcoin/jmbitcoin/bip21.py
new file mode 100644
index 0000000..58a726d
--- /dev/null
+++ b/jmbitcoin/jmbitcoin/bip21.py
@@ -0,0 +1,41 @@
+# https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
+# bitcoin:
[?amount=][?label=][?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
diff --git a/jmbitcoin/setup.py b/jmbitcoin/setup.py
index 7f711a8..dd9b055 100644
--- a/jmbitcoin/setup.py
+++ b/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)
diff --git a/jmbitcoin/test/test_bip21.py b/jmbitcoin/test/test_bip21.py
new file mode 100644
index 0000000..3faf4ca
--- /dev/null
+++ b/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')
+
diff --git a/jmclient/jmclient/cli_options.py b/jmclient/jmclient/cli_options.py
index f64e6ac..95dee6c 100644
--- a/jmclient/jmclient/cli_options.py
+++ b/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. '
diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py
index 1433d9b..5fc2c54 100644
--- a/scripts/sendpayment.py
+++ b/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: