Skip to content

Commit

Permalink
feat: select node to pay invoices based on routing hints
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Jan 29, 2025
1 parent c7c1f9a commit c320216
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 6 deletions.
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

0 comments on commit c320216

Please sign in to comment.