SendOnion
SendOnion provides an idempotent API for dispatching a pre-formed onion packet, which is the primary entry point for a remote router.
To safely handle network failures, a client can and should retry this RPC after a timeout or disconnection. Retries MUST use the exact same attempt_id to allow the server to correctly detect duplicate requests.
A client interacting with this RPC must handle four distinct categories of outcomes, determined by the gRPC status code:
SUCCESS (gRPC code OK): A definitive confirmation that the HTLC has been successfully dispatched. The client can proceed to track the payment's final result via the TrackOnion RPC.
DUPLICATE ACKNOWLEDGMENT (gRPC code AlreadyExists): A definitive acknowledgment that a request with the same attempt_id has already been successfully processed. A retrying client should interpret this as a success and proceed to tracking the payment's result.
INDEFINITE FAILURE (gRPC code Unavailable or DeadlineExceeded): The state of the HTLC dispatch is unknown. The client MUST retry the exact same request to resolve the ambiguity. A client MUST NOT treat this as a definitive failure and move on to a new attempt ID; doing so risks a duplicate in-flight HTLC.
DEFINITIVE FAILURE (gRPC code FailedPrecondition, InvalidArgument, etc.): The HTLC was not and will not be dispatched. The client should fail the attempt and may retry with a new route and/or attempt_id.
In the case of a FailedPrecondition or Unavailable failure, the gRPC error may include a SendOnionFailureDetails message with additional context for logging and pathfinding.
Source: switchrpc/switch.proto
gRPC
rpc SendOnion (SendOnionRequest) returns (SendOnionResponse);
REST
| HTTP Method | Path |
|---|---|
| POST | /v2/switch/onion |
Code Samples
- gRPC
- REST
- Javascript
- Python
- grpcurl
const fs = require('fs');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const GRPC_HOST = 'localhost:10009'
const MACAROON_PATH = 'LND_DIR/data/chain/bitcoin/regtest/admin.macaroon'
const TLS_PATH = 'LND_DIR/tls.cert'
const loaderOptions = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
};
const packageDefinition = protoLoader.loadSync(['lightning.proto', 'switchrpc/switch.proto'], loaderOptions);
const switchrpc = grpc.loadPackageDefinition(packageDefinition).switchrpc;
process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA';
const tlsCert = fs.readFileSync(TLS_PATH);
const sslCreds = grpc.credentials.createSsl(tlsCert);
const macaroon = fs.readFileSync(MACAROON_PATH).toString('hex');
const macaroonCreds = grpc.credentials.createFromMetadataGenerator(function(args, callback) {
let metadata = new grpc.Metadata();
metadata.add('macaroon', macaroon);
callback(null, metadata);
});
let creds = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds);
let client = new switchrpc.Switch(GRPC_HOST, creds);
let request = {
onion_blob: <bytes>,
attempt_id: <uint64>,
first_hop_pubkey: <bytes>,
first_hop_chan_id: <uint64>,
amount: <int64>,
timelock: <uint32>,
payment_hash: <bytes>,
blinding_point: <bytes>,
custom_records: <CustomRecordsEntry>,
extra_data: <bytes>,
};
client.sendOnion(request, function(err, response) {
console.log(response);
});
// Console output:
// {
// }
import codecs, grpc, os
# Generate the following 2 modules by compiling the switchrpc/switch.proto with the grpcio-tools.
# See https://github.com/lightningnetwork/lnd/blob/master/docs/grpc/python.md for instructions.
import switch_pb2 as switchrpc, switch_pb2_grpc as switchstub
GRPC_HOST = 'localhost:10009'
MACAROON_PATH = 'LND_DIR/data/chain/bitcoin/regtest/admin.macaroon'
TLS_PATH = 'LND_DIR/tls.cert'
# create macaroon credentials
macaroon = codecs.encode(open(MACAROON_PATH, 'rb').read(), 'hex')
def metadata_callback(context, callback):
callback([('macaroon', macaroon)], None)
auth_creds = grpc.metadata_call_credentials(metadata_callback)
# create SSL credentials
os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
cert = open(TLS_PATH, 'rb').read()
ssl_creds = grpc.ssl_channel_credentials(cert)
# combine macaroon and SSL credentials
combined_creds = grpc.composite_channel_credentials(ssl_creds, auth_creds)
# make the request
channel = grpc.secure_channel(GRPC_HOST, combined_creds)
stub = switchstub.SwitchStub(channel)
request = switchrpc.SendOnionRequest(
onion_blob=<bytes>,
attempt_id=<uint64>,
first_hop_pubkey=<bytes>,
first_hop_chan_id=<uint64>,
amount=<int64>,
timelock=<uint32>,
payment_hash=<bytes>,
blinding_point=<bytes>,
custom_records=<CustomRecordsEntry>,
extra_data=<bytes>,
)
response = stub.SendOnion(request)
print(response)
# {
# }
# grpcurl docs: https://github.com/fullstorydev/grpcurl
# Proto source: https://github.com/lightningnetwork/lnd
GRPC_HOST=localhost:10009
LND_DIR=~/.lnd
LND_SOURCE=path/to/lnd
NETWORK=mainnet
MACAROON_PATH="$LND_DIR/data/chain/bitcoin/$NETWORK/admin.macaroon"
TLS_PATH="$LND_DIR/tls.cert"
grpcurl \
-import-path $LND_SOURCE/lnrpc/ \
-proto switchrpc/switch.proto \
-cacert $TLS_PATH \
-H "macaroon: $(xxd -ps -u -c 1000 $MACAROON_PATH)" \
-d '{ "onion_blob": BASE64_ENCODED_VALUE, "attempt_id": <uint64>, "first_hop_pubkey": BASE64_ENCODED_VALUE, "first_hop_chan_id": <uint64>, "amount": <int64>, "timelock": <uint32>, "payment_hash": BASE64_ENCODED_VALUE, "blinding_point": BASE64_ENCODED_VALUE, "custom_records": <CustomRecordsEntry>, "extra_data": BASE64_ENCODED_VALUE }' \
$GRPC_HOST \
switchrpc.Switch/SendOnion
- Javascript
- Python
- curl
const fs = require('fs');
const request = require('request');
const REST_HOST = 'localhost:8080'
const MACAROON_PATH = 'LND_DIR/data/chain/bitcoin/regtest/admin.macaroon'
let requestBody = {
onion_blob: <string>, // <bytes> (base64 encoded)
attempt_id: <string>, // <uint64>
first_hop_pubkey: <string>, // <bytes> (base64 encoded)
first_hop_chan_id: <string>, // <uint64>
amount: <string>, // <int64>
timelock: <integer>, // <uint32>
payment_hash: <string>, // <bytes> (base64 encoded)
blinding_point: <string>, // <bytes> (base64 encoded)
custom_records: <object>, // <CustomRecordsEntry>
extra_data: <string>, // <bytes> (base64 encoded)
};
let options = {
url: `https://${REST_HOST}/v2/switch/onion`,
// Work-around for self-signed certificates.
rejectUnauthorized: false,
json: true,
headers: {
'Grpc-Metadata-macaroon': fs.readFileSync(MACAROON_PATH).toString('hex'),
},
form: JSON.stringify(requestBody),
}
request.post(options, function(error, response, body) {
console.log(body);
});
// Console output:
// {
// }
import base64, codecs, json, requests
REST_HOST = 'localhost:8080'
MACAROON_PATH = 'LND_DIR/data/chain/bitcoin/regtest/admin.macaroon'
TLS_PATH = 'LND_DIR/tls.cert'
url = f'https://{REST_HOST}/v2/switch/onion'
macaroon = codecs.encode(open(MACAROON_PATH, 'rb').read(), 'hex')
headers = {'Grpc-Metadata-macaroon': macaroon}
data = {
'onion_blob': base64.b64encode(<bytes>),
'attempt_id': <uint64>,
'first_hop_pubkey': base64.b64encode(<bytes>),
'first_hop_chan_id': <uint64>,
'amount': <int64>,
'timelock': <uint32>,
'payment_hash': base64.b64encode(<bytes>),
'blinding_point': base64.b64encode(<bytes>),
'custom_records': <CustomRecordsEntry>,
'extra_data': base64.b64encode(<bytes>),
}
r = requests.post(url, headers=headers, data=json.dumps(data), verify=TLS_PATH)
print(r.json())
# {
# }
REST_HOST=localhost:8080
LND_DIR=~/.lnd
NETWORK=mainnet
MACAROON_PATH="$LND_DIR/data/chain/bitcoin/$NETWORK/admin.macaroon"
TLS_PATH="$LND_DIR/tls.cert"
curl -X POST \
--cacert $TLS_PATH \
-H "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 $MACAROON_PATH)" \
-d '{ "onion_blob": BASE64_ENCODED_VALUE, "attempt_id": <uint64>, "first_hop_pubkey": BASE64_ENCODED_VALUE, "first_hop_chan_id": <uint64>, "amount": <int64>, "timelock": <uint32>, "payment_hash": BASE64_ENCODED_VALUE, "blinding_point": BASE64_ENCODED_VALUE, "custom_records": <CustomRecordsEntry>, "extra_data": BASE64_ENCODED_VALUE }' \
https://$REST_HOST/v2/switch/onion
Messages
switchrpc.SendOnionRequest
Source: switchrpc/switch.proto
| Field | gRPC Type | REST Type | REST Placement |
|---|---|---|---|
onion_blob | bytes | string | body |
attempt_id | uint64 | string | body |
first_hop_pubkey | bytes | string | body |
first_hop_chan_id | uint64 | string | body |
amount | int64 | string | body |
timelock | uint32 | integer | body |
payment_hash | bytes | string | body |
blinding_point | bytes | string | body |
custom_records | CustomRecordsEntry[] | object | body |
extra_data | bytes | string | body |
switchrpc.SendOnionResponse
Source: switchrpc/switch.proto
This response has no parameters.
Nested Messages
switchrpc.SendOnionRequest.CustomRecordsEntry
| Field | gRPC Type | REST Type |
|---|---|---|
key | uint64 | unknown |
value | bytes | unknown |