diff --git a/cmd/loop/loopin.go b/cmd/loop/loopin.go index d0512040d..d29f1345c 100644 --- a/cmd/loop/loopin.go +++ b/cmd/loop/loopin.go @@ -31,10 +31,11 @@ var ( "with our reserved prefix: %v.", labels.MaxLength, labels.Reserved), } - routeHintsFlag = &cli.StringSliceFlag{ + routeHintsFlag = &cli.StringFlag{ Name: "route_hints", - Usage: "route hints that can each be individually used " + - "to assist in reaching the invoice's destination", + Usage: "a JSON array of route hints that can each be " + + "individually used to assist in reaching the invoice's " + + "destination", } privateFlag = &cli.BoolFlag{ Name: "private", diff --git a/cmd/loop/testdata/sessions/AGENTS.md b/cmd/loop/testdata/sessions/AGENTS.md index b5a695732..6c3c75e1e 100644 --- a/cmd/loop/testdata/sessions/AGENTS.md +++ b/cmd/loop/testdata/sessions/AGENTS.md @@ -69,6 +69,7 @@ Base URL: `http://127.0.0.1:12345` | `loopout/` | `loop out` (forced success), `loop out` (invalid amount), `loop out` (addr + account error), `loop out` (invalid account address type), `loop out` (amt flag + channel + max routing fee + payment timeout), `loop out` (addr flag), `loop out` (positional addr), `loop out` (account + account_addr_type) | | `misc/` | `loop terms` | | `quote/` | `loop quote out` (success + verbose), `loop quote in` (help + verbose), `loop quote out` (help), `loop quote in` (deposit_outpoint success), `loop quote in` (positional + last_hop) | +| `route-hints/` | `loop quote in`, `loop in`, and `loop static in` with empty, single, multiple, and multi-hop route-hint arrays; malformed and invalid hints; comma-separated deposit and UTXO selections | | `static/` | `loop static withdraw` (no selection error), `loop static withdraw` (invalid utxo), `loop static withdraw` (all success), `loop static withdraw` (utxo + dest_addr success), `loop static listwithdrawals`, `loop static listswaps` | | `static-autoloop/` | `loop setparams --loopinsource static-address` (success + no-experimental error), `loop getparams` (static-address loop-in source), `loop suggestswaps` (static loop-in suggestion) | | `static-loop-in/` | `loop static new`, `loop static` (help), `loop static listunspent` (incl alias), `loop static listdeposits`, `loop static summary`, `loop static in` (multiple args/flags cases), `loop static in` (duplicate outpoints), `loop static in` (positional low amount error), `loop static in` (positional + last_hop + payment_timeout), `loop static in` (all cancel), `loop static in` (explicit and automatically selected low-confirmation warnings) | diff --git a/cmd/loop/testdata/sessions/quote/02_loop-quote-in-help.json b/cmd/loop/testdata/sessions/quote/02_loop-quote-in-help.json index b7aa504d4..fa7f5076b 100644 --- a/cmd/loop/testdata/sessions/quote/02_loop-quote-in-help.json +++ b/cmd/loop/testdata/sessions/quote/02_loop-quote-in-help.json @@ -20,16 +20,7 @@ "kind": "stdout", "data": { "lines": [ - "NAME:\n", - " loop quote in - get a quote for the cost of a loop in swap\n", - "\n", - "USAGE:\n", - " loop quote in amt\n", - "\n", - "DESCRIPTION:\n", - " Allows to determine the cost of a swap up front.Either specify an amount or deposit outpoints.\n", - "\n", - "OPTIONS:\n" + "NAME:\n" ] } }, @@ -38,11 +29,9 @@ "kind": "stdout", "data": { "lines": [ - " --last_hop string the pubkey of the last hop to use for the quote\n", - " --conf_target uint the target number of blocks the on-chain htlc broadcast by the swap client should confirm within (default: 0)\n", - " --verbose, -v show expanded details (default: false)\n", - " --private generates and passes routehints. Should be used if the connected node is only reachable via private channels (default: false)\n", - " --route_hints string [ --route_hints string ] " + " loop quote in - get a quote for the cost of a loop in swap\n", + "\n", + "USAGE:\n" ] } }, @@ -51,8 +40,9 @@ "kind": "stdout", "data": { "lines": [ - " route hints that can each be individually used to assist in reaching the invoice's destination\n", - " --deposit_outpoint string [ --deposit_outpoint string ] one or more static address deposit outpoints to quote for. Deposit outpoints are not to be used in combination with an amount. Eachadditional outpoint can be added by specifying --deposit_outpoint tx_id:idx\n" + " loop quote in amt\n", + "\n", + "DESCRIPTION:\n" ] } }, @@ -61,7 +51,9 @@ "kind": "stdout", "data": { "lines": [ - " --help, -h " + " Allows to determine the cost of a swap up front.Either specify an amount or deposit outpoints.\n", + "\n", + "OPTIONS:\n" ] } }, @@ -70,7 +62,12 @@ "kind": "stdout", "data": { "lines": [ - " " + " --last_hop string the pubkey of the last hop to use for the quote\n", + " --conf_target uint the target number of blocks the on-chain htlc broadcast by the swap client should confirm within (default: 0)\n", + " --verbose, -v show expanded details (default: false)\n", + " --private generates and passes routehints. Should be used if the connected node is only reachable via private channels (default: false)\n", + " --route_hints string a JSON array of route hints that can each be individually used to assist in reaching the invoice's destination\n", + " --deposit_outpoint string [ --deposit_outpoint string ] one or more static address deposit outpoints to quote for. Deposit outpoints are not to be used in combination with an amount. Eachadditional outpoint can be added by specifying --deposit_outpoint tx_id:idx" ] } }, @@ -79,7 +76,8 @@ "kind": "stdout", "data": { "lines": [ - "show help\n" + "\n", + " --help, -h show help\n" ] } }, diff --git a/cmd/loop/testdata/sessions/route-hints/01_quote-one-route.json b/cmd/loop/testdata/sessions/route-hints/01_quote-one-route.json new file mode 100644 index 000000000..aa3e2913c --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/01_quote-one-route.json @@ -0,0 +1,85 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"111111111111111\",\"fee_base_msat\":1000,\"fee_proportional_millionths\":1,\"cltv_expiry_delta\":80}]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "duration": 15981312, + "clock_start_unix": 1787783813 + }, + "events": [ + { + "time_ms": 1, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "111111111111111", + "fee_base_msat": 1000, + "fee_proportional_millionths": 1, + "cltv_expiry_delta": 80 + } + ] + } + ], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 15, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "response", + "message_type": "looprpc.InQuoteResponse", + "payload": { + "swap_fee_sat": "1825", + "htlc_publish_fee_sat": "4125", + "cltv_delta": 0, + "conf_target": 6, + "quoted_amt": "250000" + } + } + }, + { + "time_ms": 15, + "kind": "stdout", + "data": { + "lines": [ + "Send on-chain: 250000 sat\n", + "Receive off-chain: 244050 sat\n", + "Estimated total fee: 5950 sat\n" + ] + } + }, + { + "time_ms": 15, + "kind": "exit", + "data": {} + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/02_quote-multiple-routes-multi-hop.json b/cmd/loop/testdata/sessions/route-hints/02_quote-multiple-routes-multi-hop.json new file mode 100644 index 000000000..8b18d9e30 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/02_quote-multiple-routes-multi-hop.json @@ -0,0 +1,103 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"111111111111111\",\"fee_base_msat\":1000,\"fee_proportional_millionths\":1,\"cltv_expiry_delta\":80}]},{\"hopHints\":[{\"nodeId\":\"023333333333333333333333333333333333333333333333333333333333333333\",\"chanId\":\"222222222222222\",\"feeBaseMsat\":1000,\"feeProportionalMillionths\":1,\"cltvExpiryDelta\":80},{\"nodeId\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chanId\":\"111111111111111\",\"feeBaseMsat\":1000,\"feeProportionalMillionths\":1,\"cltvExpiryDelta\":80}]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "duration": 14344073, + "clock_start_unix": 1787783836 + }, + "events": [ + { + "time_ms": 0, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "111111111111111", + "fee_base_msat": 1000, + "fee_proportional_millionths": 1, + "cltv_expiry_delta": 80 + } + ] + }, + { + "hop_hints": [ + { + "node_id": "023333333333333333333333333333333333333333333333333333333333333333", + "chan_id": "222222222222222", + "fee_base_msat": 1000, + "fee_proportional_millionths": 1, + "cltv_expiry_delta": 80 + }, + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "111111111111111", + "fee_base_msat": 1000, + "fee_proportional_millionths": 1, + "cltv_expiry_delta": 80 + } + ] + } + ], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 14, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "response", + "message_type": "looprpc.InQuoteResponse", + "payload": { + "swap_fee_sat": "1825", + "htlc_publish_fee_sat": "4125", + "cltv_delta": 0, + "conf_target": 6, + "quoted_amt": "250000" + } + } + }, + { + "time_ms": 14, + "kind": "stdout", + "data": { + "lines": [ + "Send on-chain: 250000 sat\n", + "Receive off-chain: 244050 sat\n", + "Estimated total fee: 5950 sat\n" + ] + } + }, + { + "time_ms": 14, + "kind": "exit", + "data": {} + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/03_loop-in-one-route-cancel.json b/cmd/loop/testdata/sessions/route-hints/03_loop-in-one-route-cancel.json new file mode 100644 index 000000000..deea892df --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/03_loop-in-one-route-cancel.json @@ -0,0 +1,113 @@ +{ + "metadata": { + "args": [ + "loop", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"111111111111111\",\"fee_base_msat\":1000,\"fee_proportional_millionths\":1,\"cltv_expiry_delta\":80}]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "swap canceled", + "duration": 14666178, + "clock_start_unix": 1787783843 + }, + "events": [ + { + "time_ms": 0, + "kind": "stdin", + "data": { + "text": "n\n" + } + }, + { + "time_ms": 0, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "111111111111111", + "fee_base_msat": 1000, + "fee_proportional_millionths": 1, + "cltv_expiry_delta": 80 + } + ] + } + ], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 14, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "response", + "message_type": "looprpc.InQuoteResponse", + "payload": { + "swap_fee_sat": "1825", + "htlc_publish_fee_sat": "4125", + "cltv_delta": 0, + "conf_target": 6, + "quoted_amt": "250000" + } + } + }, + { + "time_ms": 14, + "kind": "stdout", + "data": { + "lines": [ + "Send on-chain: 250000 sat\n", + "Receive off-chain: 244050 sat\n", + "Estimated total fee: 5950 sat\n" + ] + } + }, + { + "time_ms": 14, + "kind": "stdout", + "data": { + "lines": [ + "\n", + "CONTINUE SWAP? (y/n): " + ] + } + }, + { + "time_ms": 14, + "kind": "stderr", + "data": { + "lines": [ + "[loop] swap canceled\n" + ] + } + }, + { + "time_ms": 14, + "kind": "exit", + "data": { + "run_error": "swap canceled" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/04_quote-empty-array.json b/cmd/loop/testdata/sessions/route-hints/04_quote-empty-array.json new file mode 100644 index 000000000..3e8d2861e --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/04_quote-empty-array.json @@ -0,0 +1,73 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "duration": 15065438, + "clock_start_unix": 1787783850 + }, + "events": [ + { + "time_ms": 0, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 14, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "response", + "message_type": "looprpc.InQuoteResponse", + "payload": { + "swap_fee_sat": "1825", + "htlc_publish_fee_sat": "4125", + "cltv_delta": 0, + "conf_target": 6, + "quoted_amt": "250000" + } + } + }, + { + "time_ms": 14, + "kind": "stdout", + "data": { + "lines": [ + "Send on-chain: 250000 sat\n", + "Receive off-chain: 244050 sat\n", + "Estimated total fee: 5950 sat\n" + ] + } + }, + { + "time_ms": 15, + "kind": "exit", + "data": {} + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/05_quote-malformed-json.json b/cmd/loop/testdata/sessions/route-hints/05_quote-malformed-json.json new file mode 100644 index 000000000..8756c13a7 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/05_quote-malformed-json.json @@ -0,0 +1,36 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "unable to parse route hints: unexpected end of JSON input", + "duration": 662211, + "clock_start_unix": 1787783861 + }, + "events": [ + { + "time_ms": 0, + "kind": "stderr", + "data": { + "lines": [ + "[loop] unable to parse route hints: unexpected end of JSON input\n" + ] + } + }, + { + "time_ms": 0, + "kind": "exit", + "data": { + "run_error": "unable to parse route hints: unexpected end of JSON input" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/06_quote-object-not-array.json b/cmd/loop/testdata/sessions/route-hints/06_quote-object-not-array.json new file mode 100644 index 000000000..adfb4f5f0 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/06_quote-object-not-array.json @@ -0,0 +1,36 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "{\"hop_hints\":[]}", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "unable to parse route hints: json: cannot unmarshal object into Go value of type []json.RawMessage", + "duration": 764451, + "clock_start_unix": 1787783866 + }, + "events": [ + { + "time_ms": 0, + "kind": "stderr", + "data": { + "lines": [ + "[loop] unable to parse route hints: json: cannot unmarshal object into Go value of type []json.RawMessage\n" + ] + } + }, + { + "time_ms": 0, + "kind": "exit", + "data": { + "run_error": "unable to parse route hints: json: cannot unmarshal object into Go value of type []json.RawMessage" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/07_quote-null-not-array.json b/cmd/loop/testdata/sessions/route-hints/07_quote-null-not-array.json new file mode 100644 index 000000000..80f02e5d1 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/07_quote-null-not-array.json @@ -0,0 +1,36 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "null", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "route hints must be a JSON array", + "duration": 839369, + "clock_start_unix": 1787783872 + }, + "events": [ + { + "time_ms": 0, + "kind": "stderr", + "data": { + "lines": [ + "[loop] route hints must be a JSON array\n" + ] + } + }, + { + "time_ms": 0, + "kind": "exit", + "data": { + "run_error": "route hints must be a JSON array" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/08_quote-unknown-field.json b/cmd/loop/testdata/sessions/route-hints/08_quote-unknown-field.json new file mode 100644 index 000000000..de932e185 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/08_quote-unknown-field.json @@ -0,0 +1,36 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"111111111111111\",\"cltv_expiry_delat\":80}]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "unable to parse route hint 0: proto: (line 1:123): unknown field \"cltv_expiry_delat\"", + "duration": 1073888, + "clock_start_unix": 1787783878 + }, + "events": [ + { + "time_ms": 1, + "kind": "stderr", + "data": { + "lines": [ + "[loop] unable to parse route hint 0: proto: (line 1:123): unknown field \"cltv_expiry_delat\"\n" + ] + } + }, + { + "time_ms": 1, + "kind": "exit", + "data": { + "run_error": "unable to parse route hint 0: proto: (line 1:123): unknown field \"cltv_expiry_delat\"" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/09_quote-wrong-field-type.json b/cmd/loop/testdata/sessions/route-hints/09_quote-wrong-field-type.json new file mode 100644 index 000000000..282bcea25 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/09_quote-wrong-field-type.json @@ -0,0 +1,36 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":\"not-an-array\"}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "unable to parse route hint 0: proto: syntax error (line 1:14): unexpected token \"not-an-array\"", + "duration": 1265213, + "clock_start_unix": 1787783883 + }, + "events": [ + { + "time_ms": 1, + "kind": "stderr", + "data": { + "lines": [ + "[loop] unable to parse route hint 0: proto: syntax error (line 1:14): unexpected token \"not-an-array\"\n" + ] + } + }, + { + "time_ms": 1, + "kind": "exit", + "data": { + "run_error": "unable to parse route hint 0: proto: syntax error (line 1:14): unexpected token \"not-an-array\"" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/10_quote-empty-route.json b/cmd/loop/testdata/sessions/route-hints/10_quote-empty-route.json new file mode 100644 index 000000000..56c83f76e --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/10_quote-empty-route.json @@ -0,0 +1,75 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "rpc error: code = Unknown desc = route hint 0 has no hop hints", + "duration": 3785371, + "clock_start_unix": 1787783888 + }, + "events": [ + { + "time_ms": 0, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [] + } + ], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 3, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "error", + "error": "rpc error: code = Unknown desc = route hint 0 has no hop hints", + "status": { + "code": 2, + "message": "route hint 0 has no hop hints" + } + } + }, + { + "time_ms": 3, + "kind": "stderr", + "data": { + "lines": [ + "[loop] rpc error: code = Unknown desc = route hint 0 has no hop hints\n" + ] + } + }, + { + "time_ms": 3, + "kind": "exit", + "data": { + "run_error": "rpc error: code = Unknown desc = route hint 0 has no hop hints" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/11_quote-invalid-node-id.json b/cmd/loop/testdata/sessions/route-hints/11_quote-invalid-node-id.json new file mode 100644 index 000000000..c78c46a20 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/11_quote-invalid-node-id.json @@ -0,0 +1,83 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"invalid\",\"chan_id\":\"1\",\"cltv_expiry_delta\":80}]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: encoding/hex: invalid byte: U+0069 'i'", + "duration": 5383027, + "clock_start_unix": 1787783896 + }, + "events": [ + { + "time_ms": 0, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "invalid", + "chan_id": "1", + "fee_base_msat": 0, + "fee_proportional_millionths": 0, + "cltv_expiry_delta": 80 + } + ] + } + ], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 4, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "error", + "error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: encoding/hex: invalid byte: U+0069 'i'", + "status": { + "code": 2, + "message": "invalid hop hint 0 in route hint 0: encoding/hex: invalid byte: U+0069 'i'" + } + } + }, + { + "time_ms": 5, + "kind": "stderr", + "data": { + "lines": [ + "[loop] rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: encoding/hex: invalid byte: U+0069 'i'\n" + ] + } + }, + { + "time_ms": 5, + "kind": "exit", + "data": { + "run_error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: encoding/hex: invalid byte: U+0069 'i'" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/12_quote-zero-channel-id.json b/cmd/loop/testdata/sessions/route-hints/12_quote-zero-channel-id.json new file mode 100644 index 000000000..0108a8d10 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/12_quote-zero-channel-id.json @@ -0,0 +1,83 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"0\",\"cltv_expiry_delta\":80}]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: channel ID is zero", + "duration": 4886392, + "clock_start_unix": 1787783902 + }, + "events": [ + { + "time_ms": 0, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "0", + "fee_base_msat": 0, + "fee_proportional_millionths": 0, + "cltv_expiry_delta": 80 + } + ] + } + ], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 4, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "error", + "error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: channel ID is zero", + "status": { + "code": 2, + "message": "invalid hop hint 0 in route hint 0: channel ID is zero" + } + } + }, + { + "time_ms": 4, + "kind": "stderr", + "data": { + "lines": [ + "[loop] rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: channel ID is zero\n" + ] + } + }, + { + "time_ms": 4, + "kind": "exit", + "data": { + "run_error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: channel ID is zero" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/13_quote-zero-cltv-delta.json b/cmd/loop/testdata/sessions/route-hints/13_quote-zero-cltv-delta.json new file mode 100644 index 000000000..4a43c287f --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/13_quote-zero-cltv-delta.json @@ -0,0 +1,83 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"1\",\"cltv_expiry_delta\":0}]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: CLTV expiry delta is zero", + "duration": 5080281, + "clock_start_unix": 1787783909 + }, + "events": [ + { + "time_ms": 1, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "1", + "fee_base_msat": 0, + "fee_proportional_millionths": 0, + "cltv_expiry_delta": 0 + } + ] + } + ], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 4, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "error", + "error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: CLTV expiry delta is zero", + "status": { + "code": 2, + "message": "invalid hop hint 0 in route hint 0: CLTV expiry delta is zero" + } + } + }, + { + "time_ms": 5, + "kind": "stderr", + "data": { + "lines": [ + "[loop] rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: CLTV expiry delta is zero\n" + ] + } + }, + { + "time_ms": 5, + "kind": "exit", + "data": { + "run_error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: CLTV expiry delta is zero" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/14_quote-cltv-overflow.json b/cmd/loop/testdata/sessions/route-hints/14_quote-cltv-overflow.json new file mode 100644 index 000000000..2fa224c02 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/14_quote-cltv-overflow.json @@ -0,0 +1,83 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"1\",\"cltv_expiry_delta\":65536}]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: CLTV expiry delta exceeds 65535", + "duration": 5909131, + "clock_start_unix": 1787783917 + }, + "events": [ + { + "time_ms": 1, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "250000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "1", + "fee_base_msat": 0, + "fee_proportional_millionths": 0, + "cltv_expiry_delta": 65536 + } + ] + } + ], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 5, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "error", + "error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: CLTV expiry delta exceeds 65535", + "status": { + "code": 2, + "message": "invalid hop hint 0 in route hint 0: CLTV expiry delta exceeds 65535" + } + } + }, + { + "time_ms": 5, + "kind": "stderr", + "data": { + "lines": [ + "[loop] rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: CLTV expiry delta exceeds 65535\n" + ] + } + }, + { + "time_ms": 5, + "kind": "exit", + "data": { + "run_error": "rpc error: code = Unknown desc = invalid hop hint 0 in route hint 0: CLTV expiry delta exceeds 65535" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/15_quote-null-hop.json b/cmd/loop/testdata/sessions/route-hints/15_quote-null-hop.json new file mode 100644 index 000000000..5817b5847 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/15_quote-null-hop.json @@ -0,0 +1,36 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[null]}]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "unable to parse route hint 0: proto: syntax error (line 1:15): unexpected token null", + "duration": 678372, + "clock_start_unix": 1787783923 + }, + "events": [ + { + "time_ms": 0, + "kind": "stderr", + "data": { + "lines": [ + "[loop] unable to parse route hint 0: proto: syntax error (line 1:15): unexpected token null\n" + ] + } + }, + { + "time_ms": 0, + "kind": "exit", + "data": { + "run_error": "unable to parse route hint 0: proto: syntax error (line 1:15): unexpected token null" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/16_quote-null-route.json b/cmd/loop/testdata/sessions/route-hints/16_quote-null-route.json new file mode 100644 index 000000000..f3641ea76 --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/16_quote-null-route.json @@ -0,0 +1,36 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[null]", + "250000" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "unable to parse route hint 0: proto: syntax error (line 1:1): unexpected token null", + "duration": 859410, + "clock_start_unix": 1787783929 + }, + "events": [ + { + "time_ms": 0, + "kind": "stderr", + "data": { + "lines": [ + "[loop] unable to parse route hint 0: proto: syntax error (line 1:1): unexpected token null\n" + ] + } + }, + { + "time_ms": 0, + "kind": "exit", + "data": { + "run_error": "unable to parse route hint 0: proto: syntax error (line 1:1): unexpected token null" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/17_quote-with-deposit-outpoints.json b/cmd/loop/testdata/sessions/route-hints/17_quote-with-deposit-outpoints.json new file mode 100644 index 000000000..f74bcd2ff --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/17_quote-with-deposit-outpoints.json @@ -0,0 +1,136 @@ +{ + "metadata": { + "args": [ + "loop", + "quote", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"111111111111111\",\"fee_base_msat\":1000,\"fee_proportional_millionths\":1,\"cltv_expiry_delta\":80}]}]", + "--deposit_outpoint", + "1111111111111111111111111111111111111111111111111111111111111111:0,2222222222222222222222222222222222222222222222222222222222222222:0" + ], + "version": "0.35.0-beta commit= commit_hash=", + "duration": 19677447, + "clock_start_unix": 1787783959 + }, + "events": [ + { + "time_ms": 0, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/ListStaticAddressDeposits", + "event": "request", + "message_type": "looprpc.ListStaticAddressDepositsRequest", + "payload": { + "state_filter": "UNKNOWN_STATE", + "outpoints": [ + "1111111111111111111111111111111111111111111111111111111111111111:0", + "2222222222222222222222222222222222222222222222222222222222222222:0" + ] + } + } + }, + { + "time_ms": 8, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/ListStaticAddressDeposits", + "event": "response", + "message_type": "looprpc.ListStaticAddressDepositsResponse", + "payload": { + "filtered_deposits": [ + { + "id": "ERERERERERERERERERERERERERERERERERERERERERE=", + "state": "DEPOSITED", + "outpoint": "1111111111111111111111111111111111111111111111111111111111111111:0", + "value": "300000", + "confirmation_height": "120", + "blocks_until_expiry": "14395", + "swap_hash": "" + }, + { + "id": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=", + "state": "DEPOSITED", + "outpoint": "2222222222222222222222222222222222222222222222222222222222222222:0", + "value": "300000", + "confirmation_height": "120", + "blocks_until_expiry": "14395", + "swap_hash": "" + } + ] + } + } + }, + { + "time_ms": 8, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "0", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "111111111111111", + "fee_base_msat": 1000, + "fee_proportional_millionths": 1, + "cltv_expiry_delta": 80 + } + ] + } + ], + "private": false, + "deposit_outpoints": [ + "1111111111111111111111111111111111111111111111111111111111111111:0", + "2222222222222222222222222222222222222222222222222222222222222222:0" + ], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 19, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "response", + "message_type": "looprpc.InQuoteResponse", + "payload": { + "swap_fee_sat": "3297", + "htlc_publish_fee_sat": "0", + "cltv_delta": 0, + "conf_target": 0, + "quoted_amt": "600000" + } + } + }, + { + "time_ms": 19, + "kind": "stdout", + "data": { + "lines": [ + "Previously deposited on-chain: 600000 sat\n", + "Receive off-chain: 596703 sat\n", + "Estimated total fee: 3297 sat\n" + ] + } + }, + { + "time_ms": 19, + "kind": "exit", + "data": {} + } + ] +} diff --git a/cmd/loop/testdata/sessions/route-hints/18_static-in-with-utxos.json b/cmd/loop/testdata/sessions/route-hints/18_static-in-with-utxos.json new file mode 100644 index 000000000..8e66d9add --- /dev/null +++ b/cmd/loop/testdata/sessions/route-hints/18_static-in-with-utxos.json @@ -0,0 +1,185 @@ +{ + "metadata": { + "args": [ + "loop", + "static", + "in", + "--network", + "regtest", + "--route_hints", + "[{\"hop_hints\":[{\"node_id\":\"022222222222222222222222222222222222222222222222222222222222222222\",\"chan_id\":\"111111111111111\",\"fee_base_msat\":1000,\"fee_proportional_millionths\":1,\"cltv_expiry_delta\":80}]}]", + "--utxo", + "1111111111111111111111111111111111111111111111111111111111111111:0,2222222222222222222222222222222222222222222222222222222222222222:0" + ], + "version": "0.35.0-beta commit= commit_hash=", + "run_error": "swap canceled", + "duration": 21546917, + "clock_start_unix": 1787783968 + }, + "events": [ + { + "time_ms": 0, + "kind": "stdin", + "data": { + "text": "n\n" + } + }, + { + "time_ms": 0, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/ListStaticAddressDeposits", + "event": "request", + "message_type": "looprpc.ListStaticAddressDepositsRequest", + "payload": { + "state_filter": "DEPOSITED", + "outpoints": [] + } + } + }, + { + "time_ms": 7, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/ListStaticAddressDeposits", + "event": "response", + "message_type": "looprpc.ListStaticAddressDepositsResponse", + "payload": { + "filtered_deposits": [ + { + "id": "ERERERERERERERERERERERERERERERERERERERERERE=", + "state": "DEPOSITED", + "outpoint": "1111111111111111111111111111111111111111111111111111111111111111:0", + "value": "300000", + "confirmation_height": "120", + "blocks_until_expiry": "14395", + "swap_hash": "" + }, + { + "id": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=", + "state": "DEPOSITED", + "outpoint": "2222222222222222222222222222222222222222222222222222222222222222:0", + "value": "300000", + "confirmation_height": "120", + "blocks_until_expiry": "14395", + "swap_hash": "" + } + ] + } + } + }, + { + "time_ms": 7, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "0", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [ + { + "hop_hints": [ + { + "node_id": "022222222222222222222222222222222222222222222222222222222222222222", + "chan_id": "111111111111111", + "fee_base_msat": 1000, + "fee_proportional_millionths": 1, + "cltv_expiry_delta": 80 + } + ] + } + ], + "private": false, + "deposit_outpoints": [ + "1111111111111111111111111111111111111111111111111111111111111111:0", + "2222222222222222222222222222222222222222222222222222222222222222:0" + ], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 20, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "response", + "message_type": "looprpc.InQuoteResponse", + "payload": { + "swap_fee_sat": "3297", + "htlc_publish_fee_sat": "0", + "cltv_delta": 0, + "conf_target": 0, + "quoted_amt": "600000" + } + } + }, + { + "time_ms": 20, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "request", + "message_type": "looprpc.StaticAddressSummaryRequest", + "payload": {} + } + }, + { + "time_ms": 21, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "response", + "message_type": "looprpc.StaticAddressSummaryResponse", + "payload": { + "static_address": "bcrt1pg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zqapvxeu", + "relative_expiry_blocks": "14400", + "total_num_deposits": 2, + "value_unconfirmed_satoshis": "0", + "value_deposited_satoshis": "600000", + "value_expired_satoshis": "0", + "value_withdrawn_satoshis": "0", + "value_looped_in_satoshis": "0", + "value_htlc_timeout_sweeps_satoshis": "0", + "value_channels_opened": "0" + } + } + }, + { + "time_ms": 21, + "kind": "stdout", + "data": { + "lines": [ + "Previously deposited on-chain: 600000 sat\n", + "Receive off-chain: 596703 sat\n", + "Estimated total fee: 3297 sat\n", + "\n", + "CONTINUE SWAP? (y/n): " + ] + } + }, + { + "time_ms": 21, + "kind": "stderr", + "data": { + "lines": [ + "[loop] swap canceled\n" + ] + } + }, + { + "time_ms": 21, + "kind": "exit", + "data": { + "run_error": "swap canceled" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/static-loop-in/14_loop-static-in.json b/cmd/loop/testdata/sessions/static-loop-in/14_loop-static-in.json index f79e0547e..ad2dfca0c 100644 --- a/cmd/loop/testdata/sessions/static-loop-in/14_loop-static-in.json +++ b/cmd/loop/testdata/sessions/static-loop-in/14_loop-static-in.json @@ -19,136 +19,41 @@ "kind": "stdout", "data": { "lines": [ - "NAME:\n" - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - " loop static in - Loop in funds from static address deposits." - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - "\n", + "NAME:\n", + " loop static in - Loop in funds from static address deposits.\n", "\n", - "USAGE:\n" - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ + "USAGE:\n", " loop static in [options] [amt] [--all | --utxo xxx:xx]\n", "\n", "DESCRIPTION:\n", - " " - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - "\n", + " \n", " Requests a loop-in swap based on static address deposits. After the\n", " creation of a static address funds can be sent to it. Once the funds are\n", - " " - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - " confirmed on-chain they can be swapped instantaneously. If deposited\n", + " confirmed on-chain they can be swapped instantaneously. If deposited\n", " funds are not needed they can we withdrawn back to the local lnd wallet.\n", " \n", "\n", - "OPTIONS:\n" - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - " --utxo string [ --utxo string ] specify the utxos of deposits as outpoints(tx:idx) that should be looped in.\n", - " --all loop in all static address deposits. (default: false)\n", - " --payment_timeout duration " - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - " the maximum time in seconds that the server is allowed to take for the swap payment. The client can retry the swap with adjusted parameters after the payment timed out. (default: 0s)\n", - " --amt uint, --amount uint the number of satoshis that should be swapped from the selected deposits. If thereis change it is sent back to the static address. (default: 0)\n", - " --fast Usage: complete the swap faster by paying a higher fee, so the change output is available sooner (default: false)\n", - " --max_swap_fee_sat uint the maximum swap fee in satoshis. If set, the swap is rejected when the quoted fee exceeds this cap. The maximum allowed value is 10000000. On-chain fees for creating static deposits are unaffected. (default: 0)\n", - " --max_swap_fee_ppm uint the maximum swap fee expressed in parts per million of the swap amount. If set together with --max_swap_fee_sat the tighter cap is used. (default: 0)\n", - " --last_hop string the pubkey of the last hop to use for this swap\n", - " --label string an optional label for this swap,limited to 500 characters. The label may not start with our reserved prefix: [reserved].\n", - " --route_hints string [ --route_hints string ]" - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - " route hints that can each be individually used to assist in reaching the invoice's destination\n", - " --private generates and passes routehints. Should be used if the connected node is only reachable via private channels (default: false)\n", - " --force Assumes yes during confirmation. Using this option will result in an immediate swap (default: false)\n", - " --verbose, -v show expanded details (default: false)\n", - " --help, -h show help\n", - "\n" - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ + "OPTIONS:\n", + " --utxo string [ --utxo string ] specify the utxos of deposits as outpoints(tx:idx) that should be looped in.\n", + " --all loop in all static address deposits. (default: false)\n", + " --payment_timeout duration the maximum time in seconds that the server is allowed to take for the swap payment. The client can retry the swap with adjusted parameters after the payment timed out. (default: 0s)\n", + " --amt uint, --amount uint the number of satoshis that should be swapped from the selected deposits. If thereis change it is sent back to the static address. (default: 0)\n", + " --fast Usage: complete the swap faster by paying a higher fee, so the change output is available sooner (default: false)\n", + " --max_swap_fee_sat uint the maximum swap fee in satoshis. If set, the swap is rejected when the quoted fee exceeds this cap. The maximum allowed value is 10000000. On-chain fees for creating static deposits are unaffected. (default: 0)\n", + " --max_swap_fee_ppm uint the maximum swap fee expressed in parts per million of the swap amount. If set together with --max_swap_fee_sat the tighter cap is used. (default: 0)\n", + " --last_hop string the pubkey of the last hop to use for this swap\n", + " --label string an optional label for this swap,limited to 500 characters. The label may not start with our reserved prefix: [reserved].\n", + " --route_hints string a JSON array of route hints that can each be individually used to assist in reaching the invoice's destination\n", + " --private generates and passes routehints. Should be used if the connected node is only reachable via private channels (default: false)\n", + " --force Assumes yes during confirmation. Using this option will result in an immediate swap (default: false)\n", + " --verbose, -v show expanded details (default: false)\n", + " --help, -h show help\n", + "\n", "GLOBAL OPTIONS:\n", - " --rpcserver string " - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - "loopd daemon address host:port (default: \"localhost:11010\") [$LOOPCLI_RPCSERVER]\n", + " --rpcserver string loopd daemon address host:port (default: \"localhost:11010\") [$LOOPCLI_RPCSERVER]\n", " --network string, -n string the network loop is running on e.g. mainnet, testnet, etc. (default: \"mainnet\") [$LOOPCLI_NETWORK]\n", " --loopdir string path to loop's base directory (default: ~/.loop) [$LOOPCLI_LOOPDIR]\n", - " --tlscertpath string" - ] - } - }, - { - "time_ms": 1, - "kind": "stdout", - "data": { - "lines": [ - " path to loop's TLS certificate (default: ~/.loop/mainnet/tls.cert) [$LOOPCLI_TLSCERTPATH]\n", + " --tlscertpath string path to loop's TLS certificate (default: ~/.loop/mainnet/tls.cert) [$LOOPCLI_TLSCERTPATH]\n", " --macaroonpath string path to macaroon file (default: ~/.loop/mainnet/loop.macaroon) [$LOOPCLI_MACAROONPATH]\n" ] } diff --git a/cmd/loop/utils.go b/cmd/loop/utils.go index 07a6016a6..a7a18caf1 100644 --- a/cmd/loop/utils.go +++ b/cmd/loop/utils.go @@ -4,9 +4,11 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/lightninglabs/loop/swapserverrpc" "github.com/urfave/cli/v3" + "google.golang.org/protobuf/encoding/protojson" ) // showCommandHelp prints help for the current command by delegating to the @@ -35,15 +37,29 @@ func validateRouteHints(cmd *cli.Command) ([]*swapserverrpc.RouteHint, error) { ) } - jsonHints := cmd.StringSlice(routeHintsFlag.Name) + var jsonHints []json.RawMessage + err := json.Unmarshal( + []byte(cmd.String(routeHintsFlag.Name)), &jsonHints, + ) + if err != nil { + return nil, fmt.Errorf("unable to parse route hints: %w", err) + } + if jsonHints == nil { + return nil, fmt.Errorf("route hints must be a JSON array") + } - hints := make([]*swapserverrpc.RouteHint, len(jsonHints)) + hints = make([]*swapserverrpc.RouteHint, len(jsonHints)) for i, jsonHint := range jsonHints { var h swapserverrpc.RouteHint - err := json.Unmarshal([]byte(jsonHint), &h) + err := protojson.Unmarshal(jsonHint, &h) if err != nil { - return nil, fmt.Errorf("unable to parse %d-th "+ - "hint json %v: %w", i, jsonHint, err) + // Protobuf deliberately varies this separator across + // builds. Normalize it to keep CLI errors stable. + errText := strings.ReplaceAll( + err.Error(), "\u00a0", " ", + ) + return nil, fmt.Errorf("unable to parse route hint "+ + "%d: %s", i, errText) } hints[i] = &h } diff --git a/docs/loop.1 b/docs/loop.1 index 1e21905b8..b906c4a99 100644 --- a/docs/loop.1 +++ b/docs/loop.1 @@ -128,7 +128,7 @@ perform an on-chain to off-chain swap (loop in) \fB--private\fP: generates and passes routehints. Should be used if the connected node is only reachable via private channels .PP -\fB--route_hints\fP="": route hints that can each be individually used to assist in reaching the invoice's destination (default: []) +\fB--route_hints\fP="": a JSON array of route hints that can each be individually used to assist in reaching the invoice's destination .PP \fB--verbose, -v\fP: show expanded details @@ -170,7 +170,7 @@ get a quote for the cost of a loop in swap \fB--private\fP: generates and passes routehints. Should be used if the connected node is only reachable via private channels .PP -\fB--route_hints\fP="": route hints that can each be individually used to assist in reaching the invoice's destination (default: []) +\fB--route_hints\fP="": a JSON array of route hints that can each be individually used to assist in reaching the invoice's destination .PP \fB--verbose, -v\fP: show expanded details @@ -539,7 +539,7 @@ Loop in funds from static address deposits. \fB--private\fP: generates and passes routehints. Should be used if the connected node is only reachable via private channels .PP -\fB--route_hints\fP="": route hints that can each be individually used to assist in reaching the invoice's destination (default: []) +\fB--route_hints\fP="": a JSON array of route hints that can each be individually used to assist in reaching the invoice's destination .PP \fB--utxo\fP="": specify the utxos of deposits as outpoints(tx:idx) that should be looped in. (default: []) diff --git a/docs/loop.md b/docs/loop.md index fc075b093..3c1c37642 100644 --- a/docs/loop.md +++ b/docs/loop.md @@ -107,7 +107,7 @@ The following flags are supported: | `--label="…"` | an optional label for this swap,limited to 500 characters. The label may not start with our reserved prefix: [reserved] | string | | `--force` | Assumes yes during confirmation. Using this option will result in an immediate swap | bool | `false` | | `--verbose` (`-v`) | show expanded details | bool | `false` | -| `--route_hints="…"` | route hints that can each be individually used to assist in reaching the invoice's destination | string | `[]` | +| `--route_hints="…"` | a JSON array of route hints that can each be individually used to assist in reaching the invoice's destination | string | | `--private` | generates and passes routehints. Should be used if the connected node is only reachable via private channels | bool | `false` | | `--help` (`-h`) | show help | bool | `false` | @@ -181,7 +181,7 @@ The following flags are supported: | `--conf_target="…"` | the target number of blocks the on-chain htlc broadcast by the swap client should confirm within | uint | `0` | | `--verbose` (`-v`) | show expanded details | bool | `false` | | `--private` | generates and passes routehints. Should be used if the connected node is only reachable via private channels | bool | `false` | -| `--route_hints="…"` | route hints that can each be individually used to assist in reaching the invoice's destination | string | `[]` | +| `--route_hints="…"` | a JSON array of route hints that can each be individually used to assist in reaching the invoice's destination | string | | `--deposit_outpoint="…"` | one or more static address deposit outpoints to quote for. Deposit outpoints are not to be used in combination with an amount. Eachadditional outpoint can be added by specifying --deposit_outpoint tx_id:idx | string | `[]` | | `--help` (`-h`) | show help | bool | `false` | @@ -690,7 +690,7 @@ The following flags are supported: | `--max_swap_fee_ppm="…"` | the maximum swap fee expressed in parts per million of the swap amount. If set together with --max_swap_fee_sat the tighter cap is used | uint | `0` | | `--last_hop="…"` | the pubkey of the last hop to use for this swap | string | | `--label="…"` | an optional label for this swap,limited to 500 characters. The label may not start with our reserved prefix: [reserved] | string | -| `--route_hints="…"` | route hints that can each be individually used to assist in reaching the invoice's destination | string | `[]` | +| `--route_hints="…"` | a JSON array of route hints that can each be individually used to assist in reaching the invoice's destination | string | | `--private` | generates and passes routehints. Should be used if the connected node is only reachable via private channels | bool | `false` | | `--force` | Assumes yes during confirmation. Using this option will result in an immediate swap | bool | `false` | | `--verbose` (`-v`) | show expanded details | bool | `false` | diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index 8cea37ddd..a5adaceeb 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -16,6 +16,9 @@ #### Bug Fixes +* Loop In commands now parse `--route_hints` as a single JSON array and pass + every route and hop through unchanged. + * Loop Out requests now account for channel reserves when checking outbound capacity, preventing swaps from starting when their off-chain payment cannot be funded. diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 72c3d7dd7..00e28c316 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "fmt" + "math" "reflect" "slices" "sort" @@ -1244,14 +1245,27 @@ func unmarshallRouteHints(rpcRouteHints []*swapserverrpc.RouteHint) ( [][]zpay32.HopHint, error) { routeHints := make([][]zpay32.HopHint, 0, len(rpcRouteHints)) - for _, rpcRouteHint := range rpcRouteHints { + for routeIndex, rpcRouteHint := range rpcRouteHints { + if rpcRouteHint == nil { + return nil, fmt.Errorf("route hint %d is nil", routeIndex) + } + + // An empty route would be encoded as an empty BOLT 11 r field, + // which zpay32 rejects when decoding the invoice. + if len(rpcRouteHint.HopHints) == 0 { + return nil, fmt.Errorf( + "route hint %d has no hop hints", routeIndex, + ) + } + routeHint := make( []zpay32.HopHint, 0, len(rpcRouteHint.HopHints), ) - for _, rpcHint := range rpcRouteHint.HopHints { + for hopIndex, rpcHint := range rpcRouteHint.HopHints { hint, err := unmarshallHopHint(rpcHint) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid hop hint %d in route "+ + "hint %d: %w", hopIndex, routeIndex, err) } routeHint = append(routeHint, hint) @@ -1264,6 +1278,21 @@ func unmarshallRouteHints(rpcRouteHints []*swapserverrpc.RouteHint) ( // unmarshallHopHint unmarshalls a single hop hint. func unmarshallHopHint(rpcHint *swapserverrpc.HopHint) (zpay32.HopHint, error) { + if rpcHint == nil { + return zpay32.HopHint{}, fmt.Errorf("hop hint is nil") + } + if rpcHint.ChanId == 0 { + return zpay32.HopHint{}, fmt.Errorf("channel ID is zero") + } + if rpcHint.CltvExpiryDelta == 0 { + return zpay32.HopHint{}, fmt.Errorf("CLTV expiry delta is zero") + } + if rpcHint.CltvExpiryDelta > math.MaxUint16 { + return zpay32.HopHint{}, fmt.Errorf( + "CLTV expiry delta exceeds %d", uint32(math.MaxUint16), + ) + } + pubBytes, err := hex.DecodeString(rpcHint.NodeId) if err != nil { return zpay32.HopHint{}, err