diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 43692c5a5..d531f33f3 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1126,6 +1126,18 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("unreserveinputs", payload) + def fundpsbt(self, satoshi, feerate, minconf=None, reserve=True): + """ + Create a PSBT with inputs sufficient to give an output of satoshi. + """ + payload = { + "satoshi": satoshi, + "feerate": feerate, + "minconf": minconf, + "reserve": reserve, + } + return self.call("fundpsbt", payload) + def signpsbt(self, psbt): """ Add internal wallet's signatures to PSBT diff --git a/tests/test_wallet.py b/tests/test_wallet.py index f12c3abbd..611a05469 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -486,6 +486,64 @@ def test_reserveinputs(node_factory, bitcoind, chainparams): assert not any(o['reserved'] for o in l1.rpc.listfunds()['outputs']) +def test_fundpsbt(node_factory, bitcoind, chainparams): + amount = 1000000 + total_outs = 4 + l1 = node_factory.get_node() + + outputs = [] + # Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh + for i in range(total_outs // 2): + txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'], + amount / 10**8) + outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout'])) + txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'], + amount / 10**8) + outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout'])) + + bitcoind.generate_block(1) + wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == total_outs) + + feerate = '7500perkw' + + # Should get one input, plus some excess + funding = l1.rpc.fundpsbt(amount // 2, feerate, reserve=False) + psbt = bitcoind.rpc.decodepsbt(funding['psbt']) + assert len(psbt['tx']['vin']) == 1 + assert funding['excess_msat'] > Millisatoshi(0) + assert funding['excess_msat'] < Millisatoshi(amount // 2 * 1000) + + # Cannot afford this one (too much) + with pytest.raises(RpcError, match=r"not afford"): + l1.rpc.fundpsbt(amount * total_outs, feerate) + + # Nor this (depth insufficient) + with pytest.raises(RpcError, match=r"not afford"): + l1.rpc.fundpsbt(amount // 2, feerate, minconf=2) + + # Should get two inputs. + psbt = bitcoind.rpc.decodepsbt(l1.rpc.fundpsbt(amount, feerate, reserve=False)['psbt']) + assert len(psbt['tx']['vin']) == 2 + + # Should not use reserved outputs. + psbt = bitcoind.rpc.createpsbt([{'txid': out[0], 'vout': out[1]} for out in outputs], []) + l1.rpc.reserveinputs(psbt) + with pytest.raises(RpcError, match=r"not afford"): + l1.rpc.fundpsbt(amount // 2, feerate) + + # Will use first one if unreserved. + l1.rpc.unreserveinputs(bitcoind.rpc.createpsbt([{'txid': outputs[0][0], 'vout': outputs[0][1]}], [])) + psbt = l1.rpc.fundpsbt(amount // 2, feerate)['psbt'] + + # Should have passed to reserveinputs. + with pytest.raises(RpcError, match=r"already reserved"): + l1.rpc.reserveinputs(psbt) + + # And now we can't afford any more. + with pytest.raises(RpcError, match=r"not afford"): + l1.rpc.fundpsbt(amount // 2, feerate) + + @pytest.mark.xfail(strict=True) def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): """