diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index ca068e2fb5e7..b035318b8daf 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -33917,6 +33917,73 @@ } ] }, + "splicein.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "added": "v26.04", + "rpc": "splicein", + "title": "Command to splice funds into a channel", + "warning": "experimental-splicing only", + "description": [ + "`splicein` is the command to move funds into a channel." + ], + "request": { + "required": [ + "channel", + "amount" + ], + "properties": { + "channel": { + "type": "string", + "description": [ + "channel identifier or channel query. Format is the same as is used in `dev-splice`." + ] + }, + "amount": { + "type": "string", + "description": [ + "Amount in satoshis taken from the internal wallet and add to the channel. Format is the same as is used in `dev-splice`." + ] + } + } + }, + "response": { + "required": [], + "properties": { + "psbt": { + "type": "string", + "description": [ + "The final psbt" + ] + }, + "tx": { + "type": "string", + "description": [ + "The final transaction in hex" + ] + }, + "txid": { + "type": "string", + "description": [ + "The txid of the final transaction" + ] + } + } + }, + "usage": [ + "`splicein` is the command to add funds to a channel. It takes `amount` funds from your onchain wallet and places them into `channel`." + ], + "author": [ + "Dusty [@dusty_daemon](mailto:@dustydaemon) is mainly responsible." + ], + "see_also": [ + "lightning-dev-splice(7)" + ], + "resources": [ + "Main web site: [https://github.com/ElementsProject/lightning](https://github.com/ElementsProject/lightning)" + ] + }, "sql-template.json": { "$schema": "../rpc-schema-draft.json", "type": "object", diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index f656b60020b2..58419afc57af 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1226,6 +1226,14 @@ def splice(self, script_or_json, dryrun=None, force_feerate=None, debug_log=None } return self.call("dev-splice", payload) + def splicein(self, channel, amount): + """ Execute a splice """ + payload = { + "channel": channel, + "amount": amount, + } + return self.call("splicein", payload) + def stfu_channels(self, channel_ids): """ STFU multiple channels """ payload = { diff --git a/doc/Makefile b/doc/Makefile index e99c3ded0808..8fa6fa491465 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -135,6 +135,7 @@ MARKDOWNPAGES := doc/addgossip.7 \ doc/signmessage.7 \ doc/signmessagewithkey.7 \ doc/signpsbt.7 \ + doc/splicein.7 \ doc/splice_init.7 \ doc/splice_signed.7 \ doc/splice_update.7 \ diff --git a/doc/index.rst b/doc/index.rst index 3a495cfe57ae..dcc2ea10a0d8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -154,6 +154,7 @@ Core Lightning Documentation splice_init splice_signed splice_update + splicein sql staticbackup stop diff --git a/doc/schemas/splicein.json b/doc/schemas/splicein.json new file mode 100644 index 000000000000..df3166d0f5f6 --- /dev/null +++ b/doc/schemas/splicein.json @@ -0,0 +1,67 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "added": "v26.04", + "rpc": "splicein", + "title": "Command to splice funds into a channel", + "warning": "experimental-splicing only", + "description": [ + "`splicein` is the command to move funds into a channel." + ], + "request": { + "required": [ + "channel", + "amount" + ], + "properties": { + "channel": { + "type": "string", + "description": [ + "channel identifier or channel query. Format is the same as is used in `dev-splice`." + ] + }, + "amount": { + "type": "string", + "description": [ + "Amount in satoshis taken from the internal wallet and add to the channel. Format is the same as is used in `dev-splice`." + ] + } + } + }, + "response": { + "required": [], + "properties": { + "psbt": { + "type": "string", + "description": [ + "The final psbt" + ] + }, + "tx": { + "type": "string", + "description": [ + "The final transaction in hex" + ] + }, + "txid": { + "type": "string", + "description": [ + "The txid of the final transaction" + ] + } + } + }, + "usage": [ + "`splicein` is the command to add funds to a channel. It takes `amount` funds from your onchain wallet and places them into `channel`." + ], + "author": [ + "Dusty [@dusty_daemon](mailto:@dustydaemon) is mainly responsible." + ], + "see_also": [ + "lightning-dev-splice(7)" + ], + "resources": [ + "Main web site: [https://github.com/ElementsProject/lightning](https://github.com/ElementsProject/lightning)" + ] +} diff --git a/plugins/spender/splice.c b/plugins/spender/splice.c index 35098c0c7f31..a9b849898df6 100644 --- a/plugins/spender/splice.c +++ b/plugins/spender/splice.c @@ -1462,10 +1462,56 @@ json_splice(struct command *cmd, const char *buf, const jsmntok_t *params) return send_outreq(req); } +static struct command_result * +json_splicein(struct command *cmd, const char *buf, const jsmntok_t *params) +{ + struct out_req *req; + const char *channel, *amount; + struct splice_cmd *splice_cmd; + char *script; + + if (!param(cmd, buf, params, + p_req("channel", param_string, &channel), + p_req("amount", param_string, &amount), + NULL)) + return command_param_failed(); + + script = tal_fmt(NULL, + "wallet -> %s + fee; %s -> %s", + amount, channel, amount); + + splice_cmd = tal(cmd, struct splice_cmd); + + splice_cmd->cmd = cmd; + splice_cmd->script = tal_steal(splice_cmd, script); + splice_cmd->psbt = create_psbt(splice_cmd, 0, 0, 0); + splice_cmd->dryrun = false; + splice_cmd->wetrun = false; + splice_cmd->feerate_per_kw = 0; + splice_cmd->force_feerate = false; + splice_cmd->wallet_inputs_to_signed = 0; + splice_cmd->fee_calculated = false; + splice_cmd->initial_funds = AMOUNT_SAT(0); + splice_cmd->emergency_sat = AMOUNT_SAT(0); + splice_cmd->debug_log = NULL; + splice_cmd->debug_counter = 0; + memset(&splice_cmd->final_txid, 0, sizeof(splice_cmd->final_txid)); + + req = jsonrpc_request_start(cmd, "listpeerchannels", + listpeerchannels_get_result, + splice_error, splice_cmd); + + return send_outreq(req); +} + const struct plugin_command splice_commands[] = { { "dev-splice", json_splice }, + { + "splicein", + json_splicein + }, }; const size_t num_splice_commands = ARRAY_SIZE(splice_commands); diff --git a/tests/test_splice.py b/tests/test_splice.py index a26c983166ad..6a7407cb3d59 100644 --- a/tests/test_splice.py +++ b/tests/test_splice.py @@ -195,3 +195,42 @@ def test_script_splice_in(node_factory, bitcoind, chainparams): l1.wait_for_channel_onchain(l2.info['id']) account_info = only_one([acct for acct in l1.rpc.bkpr_listbalances()['accounts'] if acct['account'] == account_id]) assert not account_info['account_closed'] + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_easy_splice_in(node_factory, bitcoind, chainparams): + fundamt = 1000000 + + coin_mvt_plugin = Path(__file__).parent / "plugins" / "coin_movements.py" + l1, l2 = node_factory.line_graph(2, fundamount=fundamt, wait_for_announce=True, + opts={'experimental-splicing': None, + 'plugin': coin_mvt_plugin}) + + # Splice in 100k sats into first channel + spliceamt = 100000 + + l1.rpc.splicein("*:?", f"{spliceamt}") + p1 = only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels']) + p2 = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels']) + assert p1['inflight'][0]['splice_amount'] == spliceamt + assert p1['inflight'][0]['total_funding_msat'] == (fundamt + spliceamt) * 1000 + assert p1['inflight'][0]['our_funding_msat'] == fundamt * 1000 + assert p2['inflight'][0]['splice_amount'] == 0 + assert p2['inflight'][0]['total_funding_msat'] == (fundamt + spliceamt) * 1000 + assert p2['inflight'][0]['our_funding_msat'] == 0 + bitcoind.generate_block(6, wait_for_mempool=1) + l2.daemon.wait_for_log(r'lightningd, splice_locked clearing inflights') + + p1 = only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels']) + p2 = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels']) + assert p1['to_us_msat'] == (fundamt + spliceamt) * 1000 + assert p1['total_msat'] == (fundamt + spliceamt) * 1000 + assert p2['to_us_msat'] == 0 + assert p2['total_msat'] == (fundamt + spliceamt) * 1000 + assert 'inflight' not in p1 + assert 'inflight' not in p2 + + wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1) + wait_for(lambda: len(l1.rpc.listfunds()['channels']) == 1)