diff --git a/doc/Makefile b/doc/Makefile index a0665fd19..79a0b8622 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -49,6 +49,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-listdatastore.7 \ doc/lightning-listforwards.7 \ doc/lightning-listfunds.7 \ + doc/lightning-listhtlcs.7 \ doc/lightning-listinvoices.7 \ doc/lightning-listoffers.7 \ doc/lightning-listpays.7 \ diff --git a/doc/index.rst b/doc/index.rst index 593bc8624..c7bd4bacc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -76,6 +76,7 @@ Core Lightning Documentation lightning-listdatastore lightning-listforwards lightning-listfunds + lightning-listhtlcs lightning-listinvoices lightning-listnodes lightning-listoffers diff --git a/doc/lightning-listhtlcs.7.md b/doc/lightning-listhtlcs.7.md new file mode 100644 index 000000000..6c8f265c5 --- /dev/null +++ b/doc/lightning-listhtlcs.7.md @@ -0,0 +1,49 @@ +lightning-listhtlcs -- Command for querying HTLCs +================================================= + +SYNOPSIS +-------- + +**listhtlcs** [*id*] + +DESCRIPTION +----------- + +The **listhtlcs** RPC command gets all HTLCs (which, generally, we +remember for as long as a channel is open, even if they've completed +long ago). If given a short channel id (e.g. 1x2x3) or full 64-byte +hex channel id, it will only list htlcs for that channel (which +must be known). + +RETURN VALUE +------------ + +[comment]: # (GENERATE-FROM-SCHEMA-START) +On success, an object containing **htlcs** is returned. It is an array of objects, where each object contains: + +- **short\_channel\_id** (short\_channel\_id): the channel that contains/contained the HTLC +- **id** (u64): the unique, incrementing HTLC id the creator gave this +- **expiry** (u32): the block number where this HTLC expires/expired +- **amount\_msat** (msat): the value of the HTLC +- **direction** (string): out if we offered this to the peer, in if they offered it (one of "out", "in") +- **payment\_hash** (hex): payment hash sought by HTLC (always 64 characters) +- **state** (string): The first 10 states are for `in`, the next 10 are for `out`. (one of "SENT_ADD_HTLC", "SENT_ADD_COMMIT", "RCVD_ADD_REVOCATION", "RCVD_ADD_ACK_COMMIT", "SENT_ADD_ACK_REVOCATION", "RCVD_REMOVE_HTLC", "RCVD_REMOVE_COMMIT", "SENT_REMOVE_REVOCATION", "SENT_REMOVE_ACK_COMMIT", "RCVD_REMOVE_ACK_REVOCATION", "RCVD_ADD_HTLC", "RCVD_ADD_COMMIT", "SENT_ADD_REVOCATION", "SENT_ADD_ACK_COMMIT", "RCVD_ADD_ACK_REVOCATION", "SENT_REMOVE_HTLC", "SENT_REMOVE_COMMIT", "RCVD_REMOVE_REVOCATION", "RCVD_REMOVE_ACK_COMMIT", "SENT_REMOVE_ACK_REVOCATION") + +[comment]: # (GENERATE-FROM-SCHEMA-END) + +AUTHOR +------ + +Rusty Russell <> is mainly responsible. + +SEE ALSO +-------- + +lightning-listforwards(7) + +RESOURCES +--------- + +Main web site: + +[comment]: # ( SHA256STAMP:6ef16f6e1f54522435130d99f224ca41a38fb3c5bc26886ccdaddc69f1abb946) diff --git a/doc/schemas/listhtlcs.request.json b/doc/schemas/listhtlcs.request.json new file mode 100644 index 000000000..df0312329 --- /dev/null +++ b/doc/schemas/listhtlcs.request.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": { + "id": { + "type": "string", + "description": "channel id or short_channel_id" + } + } +} diff --git a/doc/schemas/listhtlcs.schema.json b/doc/schemas/listhtlcs.schema.json new file mode 100644 index 000000000..469eb1588 --- /dev/null +++ b/doc/schemas/listhtlcs.schema.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "htlcs" + ], + "properties": { + "htlcs": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id", + "id", + "expiry", + "direction", + "amount_msat", + "payment_hash", + "state" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": "the channel that contains/contained the HTLC" + }, + "id": { + "type": "u64", + "description": "the unique, incrementing HTLC id the creator gave this" + }, + "expiry": { + "type": "u32", + "description": "the block number where this HTLC expires/expired" + }, + "amount_msat": { + "type": "msat", + "description": "the value of the HTLC" + }, + "direction": { + "type": "string", + "enum": [ + "out", + "in" + ], + "description": "out if we offered this to the peer, in if they offered it" + }, + "payment_hash": { + "type": "hex", + "description": "payment hash sought by HTLC", + "maxLength": 64, + "minLength": 64 + }, + "state": { + "type": "string", + "enum": [ + "SENT_ADD_HTLC", + "SENT_ADD_COMMIT", + "RCVD_ADD_REVOCATION", + "RCVD_ADD_ACK_COMMIT", + "SENT_ADD_ACK_REVOCATION", + "RCVD_REMOVE_HTLC", + "RCVD_REMOVE_COMMIT", + "SENT_REMOVE_REVOCATION", + "SENT_REMOVE_ACK_COMMIT", + "RCVD_REMOVE_ACK_REVOCATION", + "RCVD_ADD_HTLC", + "RCVD_ADD_COMMIT", + "SENT_ADD_REVOCATION", + "SENT_ADD_ACK_COMMIT", + "RCVD_ADD_ACK_REVOCATION", + "SENT_REMOVE_HTLC", + "SENT_REMOVE_COMMIT", + "RCVD_REMOVE_REVOCATION", + "RCVD_REMOVE_ACK_COMMIT", + "SENT_REMOVE_ACK_REVOCATION" + ], + "description": "The first 10 states are for `in`, the next 10 are for `out`." + } + } + } + } + } +} diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 2dbb18265..7eadf672a 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -2894,3 +2894,83 @@ static const struct json_command listforwards_command = { "List all forwarded payments and their information optionally filtering by [status], [in_channel] and [out_channel]" }; AUTODATA(json_command, &listforwards_command); + +static struct command_result *param_channel(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct channel **chan) +{ + struct channel_id cid; + struct short_channel_id scid; + + if (json_tok_channel_id(buffer, tok, &cid)) { + *chan = channel_by_cid(cmd->ld, &cid); + if (!*chan) + return command_fail_badparam(cmd, name, buffer, tok, + "unknown channel"); + return NULL; + } else if (json_to_short_channel_id(buffer, tok, &scid)) { + *chan = any_channel_by_scid(cmd->ld, &scid, true); + if (!*chan) + return command_fail_badparam(cmd, name, buffer, tok, + "unknown channel"); + return NULL; + } + return command_fail_badparam(cmd, name, buffer, tok, + "must be channel id or short channel id"); +} + +static struct command_result *json_listhtlcs(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct json_stream *response; + struct channel *chan; + struct wallet_htlc_iter *i; + struct short_channel_id scid; + u64 htlc_id; + int cltv_expiry; + enum side owner; + struct amount_msat msat; + struct sha256 payment_hash; + enum htlc_state hstate; + + if (!param(cmd, buffer, params, + p_opt("id", param_channel, &chan), + NULL)) + return command_param_failed(); + + response = json_stream_success(cmd); + json_array_start(response, "htlcs"); + for (i = wallet_htlcs_first(cmd, cmd->ld->wallet, chan, + &scid, &htlc_id, &cltv_expiry, &owner, &msat, + &payment_hash, &hstate); + i; + i = wallet_htlcs_next(cmd->ld->wallet, i, + &scid, &htlc_id, &cltv_expiry, &owner, &msat, + &payment_hash, &hstate)) { + json_object_start(response, NULL); + json_add_short_channel_id(response, "short_channel_id", &scid); + json_add_u64(response, "id", htlc_id); + json_add_u32(response, "expiry", cltv_expiry); + json_add_string(response, "direction", + owner == LOCAL ? "out": "in"); + json_add_amount_msat_only(response, "amount_msat", msat); + json_add_sha256(response, "payment_hash", &payment_hash); + json_add_string(response, "state", htlc_state_name(hstate)); + json_object_end(response); + } + json_array_end(response); + + return command_success(cmd, response); +} + +static const struct json_command listhtlcs_command = { + "listhtlcs", + "channels", + json_listhtlcs, + "List all known HTLCS (optionally, just for [id] (scid or channel id))" +}; +AUTODATA(json_command, &listhtlcs_command); diff --git a/tests/test_connection.py b/tests/test_connection.py index b880e2364..3ed032d4a 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -3992,12 +3992,13 @@ def test_multichan(node_factory, executor, bitcoind): # Now fund *second* channel l2->l3 (slightly larger) bitcoind.rpc.sendtoaddress(l2.rpc.newaddr()['bech32'], 0.1) bitcoind.generate_block(1) - sync_blockheight(bitcoind, [l2]) + sync_blockheight(bitcoind, [l1, l2, l3]) l2.rpc.fundchannel(l3.info['id'], '0.01001btc') assert(len(only_one(l2.rpc.listpeers(l3.info['id'])['peers'])['channels']) == 2) assert(len(only_one(l3.rpc.listpeers(l2.info['id'])['peers'])['channels']) == 2) bitcoind.generate_block(1, wait_for_mempool=1) + sync_blockheight(bitcoind, [l1, l2, l3]) # Make sure new channel is also CHANNELD_NORMAL wait_for(lambda: [c['state'] for c in only_one(l2.rpc.listpeers(l3.info['id'])['peers'])['channels']] == ["CHANNELD_NORMAL", "CHANNELD_NORMAL"]) @@ -4023,9 +4024,9 @@ def test_multichan(node_factory, executor, bitcoind): 'delay': 5, 'channel': scid23a}] before = only_one(l2.rpc.listpeers(l3.info['id'])['peers'])['channels'] - inv = l3.rpc.invoice(100000000, "invoice", "invoice") - l1.rpc.sendpay(route, inv['payment_hash'], payment_secret=inv['payment_secret']) - l1.rpc.waitsendpay(inv['payment_hash']) + inv1 = l3.rpc.invoice(100000000, "invoice", "invoice") + l1.rpc.sendpay(route, inv1['payment_hash'], payment_secret=inv1['payment_secret']) + l1.rpc.waitsendpay(inv1['payment_hash']) # Wait until HTLCs fully settled wait_for(lambda: [c['htlcs'] for c in only_one(l2.rpc.listpeers(l3.info['id'])['peers'])['channels']] == [[], []]) after = only_one(l2.rpc.listpeers(l3.info['id'])['peers'])['channels'] @@ -4049,9 +4050,9 @@ def test_multichan(node_factory, executor, bitcoind): before = only_one(l2.rpc.listpeers(l3.info['id'])['peers'])['channels'] route[1]['channel'] = scid23b - inv = l3.rpc.invoice(100000000, "invoice2", "invoice2") - l1.rpc.sendpay(route, inv['payment_hash'], payment_secret=inv['payment_secret']) - l1.rpc.waitsendpay(inv['payment_hash']) + inv2 = l3.rpc.invoice(100000000, "invoice2", "invoice2") + l1.rpc.sendpay(route, inv2['payment_hash'], payment_secret=inv2['payment_secret']) + l1.rpc.waitsendpay(inv2['payment_hash']) # Wait until HTLCs fully settled wait_for(lambda: [c['htlcs'] for c in only_one(l2.rpc.listpeers(l3.info['id'])['peers'])['channels']] == [[], []]) after = only_one(l2.rpc.listpeers(l3.info['id'])['peers'])['channels'] @@ -4062,6 +4063,7 @@ def test_multichan(node_factory, executor, bitcoind): # Make sure gossip works. bitcoind.generate_block(5) + sync_blockheight(bitcoind, [l1, l2, l3]) wait_for(lambda: len(l1.rpc.listchannels(source=l3.info['id'])['channels']) == 2) @@ -4084,6 +4086,7 @@ def test_multichan(node_factory, executor, bitcoind): l2.rpc.close(scid23b) bitcoind.generate_block(1, wait_for_mempool=1) + sync_blockheight(bitcoind, [l1, l2, l3]) # Gossip works as expected. wait_for(lambda: len(l1.rpc.listchannels(source=l3.info['id'])['channels']) == 1) @@ -4091,9 +4094,9 @@ def test_multichan(node_factory, executor, bitcoind): # We can actually pay by *closed* scid (at least until it's completely forgotten) route[1]['channel'] = scid23a - inv = l3.rpc.invoice(100000000, "invoice3", "invoice3") - l1.rpc.sendpay(route, inv['payment_hash'], payment_secret=inv['payment_secret']) - l1.rpc.waitsendpay(inv['payment_hash']) + inv3 = l3.rpc.invoice(100000000, "invoice3", "invoice3") + l1.rpc.sendpay(route, inv3['payment_hash'], payment_secret=inv3['payment_secret']) + l1.rpc.waitsendpay(inv3['payment_hash']) # Restart with multiple channels works. l3.restart() @@ -4103,8 +4106,48 @@ def test_multichan(node_factory, executor, bitcoind): except RpcError: wait_for(lambda: only_one(l3.rpc.listpeers(l2.info['id'])['peers'])['connected']) - inv = l3.rpc.invoice(100000000, "invoice4", "invoice4") - l1.rpc.pay(inv['bolt11']) + inv4 = l3.rpc.invoice(100000000, "invoice4", "invoice4") + l1.rpc.pay(inv4['bolt11']) + + # A good place to test listhtlcs! + wait_for(lambda: all([h['state'] == 'RCVD_REMOVE_ACK_REVOCATION' for h in l1.rpc.listhtlcs()['htlcs']])) + + l1htlcs = l1.rpc.listhtlcs()['htlcs'] + assert l1htlcs == l1.rpc.listhtlcs(scid12)['htlcs'] + assert l1htlcs == [{"short_channel_id": scid12, + "id": 0, + "expiry": 117, + "direction": "out", + "amount_msat": Millisatoshi(100001001), + "payment_hash": inv1['payment_hash'], + "state": "RCVD_REMOVE_ACK_REVOCATION"}, + {"short_channel_id": scid12, + "id": 1, + "expiry": 117, + "direction": "out", + "amount_msat": Millisatoshi(100001001), + "payment_hash": inv2['payment_hash'], + "state": "RCVD_REMOVE_ACK_REVOCATION"}, + {"short_channel_id": scid12, + "id": 2, + "expiry": 123, + "direction": "out", + "amount_msat": Millisatoshi(100001001), + "payment_hash": inv3['payment_hash'], + "state": "RCVD_REMOVE_ACK_REVOCATION"}, + {"short_channel_id": scid12, + "id": 3, + "expiry": 123, + "direction": "out", + "amount_msat": Millisatoshi(100001001), + "payment_hash": inv4['payment_hash'], + "state": "RCVD_REMOVE_ACK_REVOCATION"}] + + # Reverse direction, should match l2's view of channel. + for h in l1htlcs: + h['direction'] = 'in' + h['state'] = 'SENT_REMOVE_ACK_REVOCATION' + assert l2.rpc.listhtlcs(scid12)['htlcs'] == l1htlcs @pytest.mark.developer("dev-no-reconnect required") diff --git a/tests/test_misc.py b/tests/test_misc.py index b2c809c11..03f7605de 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2,7 +2,7 @@ from bitcoin.rpc import RawProxy from decimal import Decimal from fixtures import * # noqa: F401,F403 from fixtures import LightningNode, TEST_NETWORK -from pyln.client import RpcError +from pyln.client import RpcError, Millisatoshi from threading import Event from pyln.testing.utils import ( DEVELOPER, TIMEOUT, VALGRIND, DEPRECATED_APIS, sync_blockheight, only_one, @@ -2379,15 +2379,15 @@ def test_listfunds(node_factory): assert open_txid in txids -def test_listforwards(node_factory, bitcoind): - """Test listfunds command.""" +def test_listforwards_and_listhtlcs(node_factory, bitcoind): + """Test listforwards and listhtlcs commands.""" l1, l2, l3, l4 = node_factory.get_nodes(4, opts=[{}, {}, {}, {}]) l1.rpc.connect(l2.info['id'], 'localhost', l2.port) l2.rpc.connect(l3.info['id'], 'localhost', l3.port) l2.rpc.connect(l4.info['id'], 'localhost', l4.port) - c12, _ = l1.fundchannel(l2, 10**5) + c12, c12res = l1.fundchannel(l2, 10**5) c23, _ = l2.fundchannel(l3, 10**5) c24, _ = l2.fundchannel(l4, 10**5) @@ -2406,7 +2406,7 @@ def test_listforwards(node_factory, bitcoind): failed_inv = l3.rpc.invoice(4000, 'failed', 'desc') failed_route = l1.rpc.getroute(l3.info['id'], 4000, 1)['route'] - l2.rpc.close(c23, 1) + l2.rpc.close(c23) with pytest.raises(RpcError): l1.rpc.sendpay(failed_route, failed_inv['payment_hash'], payment_secret=failed_inv['payment_secret']) @@ -2447,6 +2447,52 @@ def test_listforwards(node_factory, bitcoind): c24_forwards = l2.rpc.listforwards(out_channel=c24)['forwards'] assert len(c24_forwards) == 1 + # listhtlcs on l1 is the same with or without id specifiers + c1htlcs = l1.rpc.listhtlcs()['htlcs'] + assert l1.rpc.listhtlcs(c12)['htlcs'] == c1htlcs + assert l1.rpc.listhtlcs(c12res['channel_id'])['htlcs'] == c1htlcs + c1htlcs.sort(key=lambda h: h['id']) + assert [h['id'] for h in c1htlcs] == [0, 1, 2] + assert [h['short_channel_id'] for h in c1htlcs] == [c12] * 3 + assert [h['amount_msat'] for h in c1htlcs] == [Millisatoshi(1001), + Millisatoshi(2001), + Millisatoshi(4001)] + assert [h['direction'] for h in c1htlcs] == ['out'] * 3 + assert [h['state'] for h in c1htlcs] == ['RCVD_REMOVE_ACK_REVOCATION'] * 3 + + # These should be a mirror! + c2c1htlcs = l2.rpc.listhtlcs(c12)['htlcs'] + for h in c2c1htlcs: + assert h['state'] == 'SENT_REMOVE_ACK_REVOCATION' + assert h['direction'] == 'in' + h['state'] = 'RCVD_REMOVE_ACK_REVOCATION' + h['direction'] = 'out' + assert c2c1htlcs == c1htlcs + + # One channel at a time should result in all htlcs. + allhtlcs = l2.rpc.listhtlcs()['htlcs'] + parthtlcs = (l2.rpc.listhtlcs(c12)['htlcs'] + + l2.rpc.listhtlcs(c23)['htlcs'] + + l2.rpc.listhtlcs(c24)['htlcs']) + assert len(allhtlcs) == len(parthtlcs) + for h in allhtlcs: + assert h in parthtlcs + + # Now, close and forget. + l2.rpc.close(c24) + l2.rpc.close(c12) + + bitcoind.generate_block(100, wait_for_mempool=3) + + # Once channels are gone, htlcs are gone. + for n in (l1, l2, l3, l4): + # They might reconnect, but still will have no channels + wait_for(lambda: all(p['channels'] == [] for p in n.rpc.listpeers()['peers'])) + assert n.rpc.listhtlcs() == {'htlcs': []} + + # But forwards are not forgotten! + assert l2.rpc.listforwards()['forwards'] == all_forwards + @pytest.mark.openchannel('v1') def test_version_reexec(node_factory, bitcoind): diff --git a/wallet/wallet.c b/wallet/wallet.c index 7e7f84c7d..1674a7222 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -5146,3 +5146,96 @@ struct db_stmt *wallet_datastore_next(const tal_t *ctx, return stmt; } + +/* We use a different query form if we only care about a single channel. */ +struct wallet_htlc_iter { + struct db_stmt *stmt; + /* Non-zero if they specified it */ + struct short_channel_id scid; +}; + +struct wallet_htlc_iter *wallet_htlcs_first(const tal_t *ctx, + struct wallet *w, + const struct channel *chan, + struct short_channel_id *scid, + u64 *htlc_id, + int *cltv_expiry, + enum side *owner, + struct amount_msat *msat, + struct sha256 *payment_hash, + enum htlc_state *hstate) +{ + struct wallet_htlc_iter *i = tal(ctx, struct wallet_htlc_iter); + + if (chan) { + i->scid = *channel_scid_or_local_alias(chan); + assert(i->scid.u64 != 0); + assert(chan->dbid != 0); + + i->stmt = db_prepare_v2(w->db, + SQL("SELECT h.channel_htlc_id" + ", h.cltv_expiry" + ", h.direction" + ", h.msatoshi" + ", h.payment_hash" + ", h.hstate" + " FROM channel_htlcs h" + " WHERE channel_id = ?")); + db_bind_u64(i->stmt, 0, chan->dbid); + } else { + i->scid.u64 = 0; + i->stmt = db_prepare_v2(w->db, + SQL("SELECT channels.scid" + ", channels.alias_local" + ", h.channel_htlc_id" + ", h.cltv_expiry" + ", h.direction" + ", h.msatoshi" + ", h.payment_hash" + ", h.hstate" + " FROM channel_htlcs h" + " JOIN channels ON channels.id = h.channel_id")); + } + /* FIXME: db_prepare should take ctx! */ + tal_steal(i, i->stmt); + db_query_prepared(i->stmt); + + return wallet_htlcs_next(w, i, + scid, htlc_id, cltv_expiry, owner, msat, + payment_hash, hstate); +} + +struct wallet_htlc_iter *wallet_htlcs_next(struct wallet *w, + struct wallet_htlc_iter *iter, + struct short_channel_id *scid, + u64 *htlc_id, + int *cltv_expiry, + enum side *owner, + struct amount_msat *msat, + struct sha256 *payment_hash, + enum htlc_state *hstate) +{ + if (!db_step(iter->stmt)) + return tal_free(iter); + + if (iter->scid.u64 != 0) + *scid = iter->scid; + else { + if (db_col_is_null(iter->stmt, "channels.scid")) + db_col_scid(iter->stmt, "channels.alias_local", scid); + else { + db_col_scid(iter->stmt, "channels.scid", scid); + db_col_ignore(iter->stmt, "channels.alias_local"); + } + } + *htlc_id = db_col_u64(iter->stmt, "h.channel_htlc_id"); + if (db_col_int(iter->stmt, "h.direction") == DIRECTION_INCOMING) + *owner = REMOTE; + else + *owner = LOCAL; + db_col_amount_msat(iter->stmt, "h.msatoshi", msat); + db_col_sha256(iter->stmt, "h.payment_hash", payment_hash); + *cltv_expiry = db_col_int(iter->stmt, "h.cltv_expiry"); + *hstate = db_col_int(iter->stmt, "h.hstate"); + return iter; +} diff --git a/wallet/wallet.h b/wallet/wallet.h index 94ec5c082..09b253148 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -1632,4 +1632,40 @@ struct db_stmt *wallet_datastore_next(const tal_t *ctx, const u8 **data, u64 *generation); +/** + * Iterate through the htlcs table. + * @w: the wallet + * @chan: optional channel to filter by + * + * Returns pointer to hand as @iter to wallet_htlcs_next(), or NULL. + * If you choose not to call wallet_htlcs_next() you must free it! + */ +struct wallet_htlc_iter *wallet_htlcs_first(const tal_t *ctx, + struct wallet *w, + const struct channel *chan, + struct short_channel_id *scid, + u64 *htlc_id, + int *cltv_expiry, + enum side *owner, + struct amount_msat *msat, + struct sha256 *payment_hash, + enum htlc_state *hstate); + +/** + * Iterate through the htlcs table. + * @w: the wallet + * @iter: the previous iter. + * + * Returns pointer to hand as @iter to wallet_htlcs_next(), or NULL. + * If you choose not to call wallet_htlcs_next() you must free it! + */ +struct wallet_htlc_iter *wallet_htlcs_next(struct wallet *w, + struct wallet_htlc_iter *iter, + struct short_channel_id *scid, + u64 *htlc_id, + int *cltv_expiry, + enum side *owner, + struct amount_msat *msat, + struct sha256 *payment_hash, + enum htlc_state *hstate); #endif /* LIGHTNING_WALLET_WALLET_H */