Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: select node to pay invoices based on routing hints #799

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions lib/swap/NodeSwitch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Logger from '../Logger';
import { getHexString } from '../Utils';
import { SwapType, swapTypeToPrettyString } from '../consts/Enums';
import ReverseSwap, { NodeType } from '../db/models/ReverseSwap';
import ReverseSwap, {
NodeType,
nodeTypeToPrettyString,
} from '../db/models/ReverseSwap';
import LightningPaymentRepository from '../db/repositories/LightningPaymentRepository';
import { msatToSat } from '../lightning/ChannelUtils';
import { LightningClient } from '../lightning/LightningClient';
Expand All @@ -14,6 +17,8 @@ type NodeSwitchConfig = {

swapNode?: string;
referralsIds?: Record<string, string>;

preferredForNode?: Record<string, string>;
};

class NodeSwitch {
Expand All @@ -24,6 +29,7 @@ class NodeSwitch {
private readonly referralIds = new Map<string, NodeType>();

private readonly swapNode?: NodeType;
private readonly preferredForNode = new Map<string, NodeType>();

constructor(
private readonly logger: Logger,
Expand Down Expand Up @@ -52,6 +58,17 @@ class NodeSwitch {

this.referralIds.set(referralId, nt);
}

for (const [node, nodeType] of Object.entries(
cfg?.preferredForNode || {},
)) {
const nt = this.parseNodeType(nodeType, `preferred for node ${node}`);
if (nt === undefined) {
continue;
}

this.preferredForNode.set(node.toLowerCase(), nt);
}
}

public static getReverseSwapNode = (
Expand Down Expand Up @@ -93,7 +110,7 @@ class NodeSwitch {
);
};

let client = selectNode(this.swapNode);
let client = selectNode(this.getPreferredNode(decoded));

// Go easy on CLN xpay
if (client.type === NodeType.CLN && decoded.type === InvoiceType.Bolt11) {
Expand Down Expand Up @@ -173,6 +190,27 @@ class NodeSwitch {
);
};

private getPreferredNode = (
invoice: DecodedInvoice,
): NodeType | undefined => {
const nodes = invoice.routingHints.flat().map((h) => h.nodeId);
if (invoice.payee !== undefined) {
nodes.push(getHexString(invoice.payee!));
}

for (const node of nodes) {
const nt = this.preferredForNode.get(node);
if (nt !== undefined) {
this.logger.debug(
`Preferring node ${nodeTypeToPrettyString(nt)} because of ${node}`,
);
return nt;
}
}

return this.swapNode;
};

private parseNodeType = (
nodeType: any,
valueContext: string,
Expand Down
1 change: 1 addition & 0 deletions test/unit/service/Service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ describe('Service', () => {
)!.data as string;

return {
routingHints: [],
type: InvoiceType.Bolt11,
features: new Set<InvoiceFeature>(),
paymentHash: getHexBuffer(preimageHash),
Expand Down
74 changes: 70 additions & 4 deletions test/unit/swap/NodeSwitch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ describe('NodeSwitch', () => {
});

test('should parse config', () => {
const nodeOne =
'026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2';
const nodeTwo =
'02d96eadea3d780104449aca5c93461ce67c1564e2e1d73225fa67dd3b997a6018'.toUpperCase();

const config = {
clnAmountThreshold: 21,
swapNode: 'LND',
Expand All @@ -70,6 +75,11 @@ describe('NodeSwitch', () => {
breez: 'LND',
other: 'notFound',
},
preferredForNode: {
[nodeOne]: 'LND',
[nodeTwo]: 'CLN',
unparseable: 'notFound',
},
};
const ns = new NodeSwitch(Logger.disabledLogger, config);

Expand All @@ -80,6 +90,11 @@ describe('NodeSwitch', () => {
expect(referrals.size).toEqual(2);
expect(referrals.get('test')).toEqual(NodeType.CLN);
expect(referrals.get('breez')).toEqual(NodeType.LND);

const preferredNodes = ns['preferredForNode'];
expect(preferredNodes.size).toEqual(2);
expect(preferredNodes.get(nodeOne)).toEqual(NodeType.LND);
expect(preferredNodes.get(nodeTwo.toLowerCase())).toEqual(NodeType.CLN);
});

test.each`
Expand Down Expand Up @@ -108,7 +123,8 @@ describe('NodeSwitch', () => {
{
type: InvoiceType.Bolt11,
amountMsat: satToMsat(amount),
} as DecodedInvoice,
routingHints: [],
} as unknown as DecodedInvoice,
{
referral,
} as Swap,
Expand All @@ -127,7 +143,11 @@ describe('NodeSwitch', () => {
await expect(
new NodeSwitch(Logger.disabledLogger, {}).getSwapNode(
currency,
{ type, amountMsat: satToMsat(1_000_001) } as DecodedInvoice,
{
type,
amountMsat: satToMsat(1_000_001),
routingHints: [],
} as never as DecodedInvoice,
{},
),
).resolves.toEqual(client);
Expand All @@ -147,7 +167,10 @@ describe('NodeSwitch', () => {
swapNode,
}).getSwapNode(
currency,
{ type: InvoiceType.Bolt11 } as DecodedInvoice,
{
type: InvoiceType.Bolt11,
routingHints: [],
} as never as DecodedInvoice,
{} as Swap,
),
).resolves.toEqual(expected);
Expand Down Expand Up @@ -179,7 +202,8 @@ describe('NodeSwitch', () => {
type: InvoiceType.Bolt11,
paymentHash: randomBytes(32),
amountMsat: satToMsat(21_000),
} as DecodedInvoice;
routingHints: [],
} as unknown as DecodedInvoice;

LightningPaymentRepository.findByPreimageHashAndNode = jest
.fn()
Expand Down Expand Up @@ -253,6 +277,48 @@ describe('NodeSwitch', () => {
expect(NodeSwitch.hasClient(currency)).toEqual(has);
});

describe('getPreferredNode', () => {
test('should get preferred node for payee', () => {
const payee = randomBytes(32);

expect(
new NodeSwitch(Logger.disabledLogger, {
preferredForNode: {
[getHexString(payee)]: 'CLN',
},
})['getPreferredNode']({
payee,
routingHints: [],
} as unknown as DecodedInvoice),
).toEqual(NodeType.CLN);
});

test('should get preferred node for routing hint', () => {
const nodeId = randomBytes(32);

expect(
new NodeSwitch(Logger.disabledLogger, {
preferredForNode: {
[getHexString(nodeId)]: 'CLN',
},
})['getPreferredNode']({
routingHints: [[{ nodeId: getHexString(nodeId) }]],
} as unknown as DecodedInvoice),
).toEqual(NodeType.CLN);
});

test('should default to swapNode when no preference is configured', () => {
expect(
new NodeSwitch(Logger.disabledLogger, {
swapNode: 'LND',
})['getPreferredNode']({
payee: randomBytes(32),
routingHints: [[{ nodeId: getHexString(randomBytes(32)) }]],
} as unknown as DecodedInvoice),
).toEqual(NodeType.LND);
});
});

test.each`
currency | client | expected
${{ lndClient: lndClient, clnClient: clnClient }} | ${lndClient} | ${lndClient}
Expand Down