diff --git a/lib/swap/NodeSwitch.ts b/lib/swap/NodeSwitch.ts index 9351d91e..88bab133 100644 --- a/lib/swap/NodeSwitch.ts +++ b/lib/swap/NodeSwitch.ts @@ -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'; @@ -14,6 +17,8 @@ type NodeSwitchConfig = { swapNode?: string; referralsIds?: Record; + + preferredForNode?: Record; }; class NodeSwitch { @@ -24,6 +29,7 @@ class NodeSwitch { private readonly referralIds = new Map(); private readonly swapNode?: NodeType; + private readonly preferredForNode = new Map(); constructor( private readonly logger: Logger, @@ -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 = ( @@ -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) { @@ -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, diff --git a/test/unit/service/Service.spec.ts b/test/unit/service/Service.spec.ts index d9152245..e68aeba3 100644 --- a/test/unit/service/Service.spec.ts +++ b/test/unit/service/Service.spec.ts @@ -748,6 +748,7 @@ describe('Service', () => { )!.data as string; return { + routingHints: [], type: InvoiceType.Bolt11, features: new Set(), paymentHash: getHexBuffer(preimageHash), diff --git a/test/unit/swap/NodeSwitch.spec.ts b/test/unit/swap/NodeSwitch.spec.ts index d97afa76..e8a177c9 100644 --- a/test/unit/swap/NodeSwitch.spec.ts +++ b/test/unit/swap/NodeSwitch.spec.ts @@ -62,6 +62,11 @@ describe('NodeSwitch', () => { }); test('should parse config', () => { + const nodeOne = + '026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2'; + const nodeTwo = + '02d96eadea3d780104449aca5c93461ce67c1564e2e1d73225fa67dd3b997a6018'.toUpperCase(); + const config = { clnAmountThreshold: 21, swapNode: 'LND', @@ -70,6 +75,11 @@ describe('NodeSwitch', () => { breez: 'LND', other: 'notFound', }, + preferredForNode: { + [nodeOne]: 'LND', + [nodeTwo]: 'CLN', + unparseable: 'notFound', + }, }; const ns = new NodeSwitch(Logger.disabledLogger, config); @@ -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` @@ -108,7 +123,8 @@ describe('NodeSwitch', () => { { type: InvoiceType.Bolt11, amountMsat: satToMsat(amount), - } as DecodedInvoice, + routingHints: [], + } as unknown as DecodedInvoice, { referral, } as Swap, @@ -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); @@ -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); @@ -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() @@ -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}