Skip to content

Commit

Permalink
feat: magic BIP-21 routing hints (#482)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Feb 8, 2024
1 parent a4180b9 commit d82bc20
Show file tree
Hide file tree
Showing 21 changed files with 715 additions and 47 deletions.
3 changes: 3 additions & 0 deletions lib/api/Controller.ts
Expand Up @@ -474,6 +474,7 @@ class Controller {
invoiceAmount,
onchainAmount,
claimPublicKey,
addressSignature,
} = validateRequest(req.body, [
{ name: 'pairId', type: 'string' },
{ name: 'orderSide', type: 'string' },
Expand All @@ -486,6 +487,7 @@ class Controller {
{ name: 'invoiceAmount', type: 'number', optional: true },
{ name: 'onchainAmount', type: 'number', optional: true },
{ name: 'claimPublicKey', type: 'string', hex: true, optional: true },
{ name: 'addressSignature', type: 'string', hex: true, optional: true },
]);

checkPreimageHashLength(preimageHash);
Expand All @@ -503,6 +505,7 @@ class Controller {
claimPublicKey,
userAddress: address,
version: SwapVersion.Legacy,
userAddressSignature: addressSignature,
});

await markSwap(this.countryCodes, req.ip, response.id);
Expand Down
80 changes: 80 additions & 0 deletions lib/api/v2/routers/SwapRouter.ts
Expand Up @@ -663,6 +663,12 @@ class SwapRouter extends RouterBase {
* type: string
* referralId:
* type: string
* address:
* type: string
* description: Address to be used for a BIP-21 direct payment
* addressSignature:
* type: string
* description: Signature of the claim public key of the SHA256 hash of the address for the direct payment
*/

/**
Expand Down Expand Up @@ -725,6 +731,56 @@ class SwapRouter extends RouterBase {
*/
router.post('/reverse', this.handleError(this.createReverse));

/**
* @openapi
* components:
* schemas:
* ReverseBip21:
* type: object
* properties:
* bip21:
* type: string
* description: BIP-21 for the Reverse Swap
* signature:
* type: string
* description: Signature of the address in the BIP-21 of the public key in the routing hint
*/

/**
* @openapi
* /swap/reverse/{id}/bip21:
* get:
* tags: [Reverse]
* description: Get the BIP-21 of a Reverse Swap for a direct payment
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: ID of the Reverse Swap
* responses:
* '200':
* description: BIP-21 and signature to prove the authenticity of the BIP-21
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ReverseBip21'
* '404':
* description: When no BIP-21 was set for the Reverse Swap
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* '400':
* description: Error that caused the request to fail
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get('/reverse/:id/bip21', this.handleError(this.getReverseBip21));

/**
* @openapi
* components:
Expand Down Expand Up @@ -1087,6 +1143,7 @@ class SwapRouter extends RouterBase {
const {
to,
from,
address,
pairHash,
referralId,
routingNode,
Expand All @@ -1095,17 +1152,20 @@ class SwapRouter extends RouterBase {
invoiceAmount,
onchainAmount,
claimPublicKey,
addressSignature,
} = validateRequest(req.body, [
{ name: 'to', type: 'string' },
{ name: 'from', type: 'string' },
{ name: 'preimageHash', type: 'string', hex: true },
{ name: 'address', type: 'string', optional: true },
{ name: 'pairHash', type: 'string', optional: true },
{ name: 'referralId', type: 'string', optional: true },
{ name: 'routingNode', type: 'string', optional: true },
{ name: 'claimAddress', type: 'string', optional: true },
{ name: 'invoiceAmount', type: 'number', optional: true },
{ name: 'onchainAmount', type: 'number', optional: true },
{ name: 'claimPublicKey', type: 'string', hex: true, optional: true },
{ name: 'addressSignature', type: 'string', hex: true, optional: true },
]);

checkPreimageHashLength(preimageHash);
Expand All @@ -1122,8 +1182,11 @@ class SwapRouter extends RouterBase {
invoiceAmount,
onchainAmount,
claimPublicKey,

userAddress: address,
prepayMinerFee: false,
version: SwapVersion.Taproot,
userAddressSignature: addressSignature,
});

await markSwap(this.countryCodes, req.ip, response.id);
Expand All @@ -1134,6 +1197,23 @@ class SwapRouter extends RouterBase {
createdResponse(res, response);
};

private getReverseBip21 = async (req: Request, res: Response) => {
const { id } = validateRequest(req.params, [
{ name: 'id', type: 'string' },
]);

const hint = await this.service.getReverseBip21(id);
if (hint === undefined) {
errorResponse(this.logger, req, res, 'no BIP-21 for swap', 404);
return;
}

successResponse(res, {
bip21: hint.bip21,
signature: hint.signature,
});
};

private getReverseTransaction = async (req: Request, res: Response) => {
const { id } = validateRequest(req.params, [
{ name: 'id', type: 'string' },
Expand Down
8 changes: 7 additions & 1 deletion lib/db/Database.ts
Expand Up @@ -12,6 +12,7 @@ import MarkedSwap from './models/MarkedSwap';
import Pair from './models/Pair';
import PendingEthereumTransaction from './models/PendingEthereumTransaction';
import Referral from './models/Referral';
import ReverseRoutingHint from './models/ReverseRoutingHint';
import ReverseSwap from './models/ReverseSwap';
import Swap from './models/Swap';

Expand Down Expand Up @@ -111,7 +112,11 @@ class Database {
]);

await Promise.all([Swap.sync(), ReverseSwap.sync()]);
await Promise.all([ChannelCreation.sync(), MarkedSwap.sync()]);
await Promise.all([
ChannelCreation.sync(),
ReverseRoutingHint.sync(),
MarkedSwap.sync(),
]);
};

public migrate = async (currencies: Map<string, Currency>): Promise<void> => {
Expand All @@ -132,6 +137,7 @@ class Database {
KeyProvider.load(Database.sequelize);
ChannelCreation.load(Database.sequelize);
DatabaseVersion.load(Database.sequelize);
ReverseRoutingHint.load(Database.sequelize);
PendingEthereumTransaction.load(Database.sequelize);
};
}
Expand Down
40 changes: 40 additions & 0 deletions lib/db/models/ReverseRoutingHint.ts
@@ -0,0 +1,40 @@
import { DataTypes, Model, Sequelize } from 'sequelize';
import ReverseSwap from './ReverseSwap';

type ReverseRoutingHintsType = {
swapId: string;
bip21: string;
signature: string;
};

class ReverseRoutingHint extends Model {
public swapId!: string;

public bip21!: string;
public signature!: string;

public static load = (sequelize: Sequelize) => {
ReverseRoutingHint.init(
{
swapId: {
type: new DataTypes.STRING(255),
primaryKey: true,
allowNull: false,
},
bip21: { type: new DataTypes.STRING(255), allowNull: false },
signature: { type: new DataTypes.STRING(255), allowNull: false },
},
{
sequelize,
tableName: 'reverseRoutingHints',
},
);

ReverseRoutingHint.belongsTo(ReverseSwap, {
foreignKey: 'swapId',
});
};
}

export default ReverseRoutingHint;
export { ReverseRoutingHintsType };
17 changes: 17 additions & 0 deletions lib/db/repositories/ReverseRoutingHintRepository.ts
@@ -0,0 +1,17 @@
import ReverseRoutingHint, {
ReverseRoutingHintsType,
} from '../models/ReverseRoutingHint';

class ReverseRoutingHintRepository {
public static addHint = (hints: ReverseRoutingHintsType) =>
ReverseRoutingHint.create(hints);

public static getHint = (swapId: string) =>
ReverseRoutingHint.findOne({
where: {
swapId,
},
});
}

export default ReverseRoutingHintRepository;
1 change: 0 additions & 1 deletion lib/grpc/GrpcServer.ts
Expand Up @@ -46,7 +46,6 @@ class GrpcServer {
if (error) {
reject(Errors.COULD_NOT_BIND(host, port, error.message));
} else {
this.server.start();
this.logger.info(`gRPC server listening on: ${host}:${bindPort}`);
resolve();
}
Expand Down
12 changes: 12 additions & 0 deletions lib/lightning/ChannelUtils.ts
Expand Up @@ -17,6 +17,18 @@ export const scidClnToLnd = (s: string): string => {
).toString();
};

export const transactionToLndScid = (
blockHeight: number,
transactionIndex: number,
outputIndex: number,
): string => {
const scid =
(BigInt(blockHeight) << 40n) |
(BigInt(transactionIndex) << 16n) |
BigInt(outputIndex);
return scid.toString();
};

export const msatToSat = (msat: number): number => {
return Math.round(msat / msatFactor);
};
Expand Down
17 changes: 16 additions & 1 deletion lib/service/Service.ts
Expand Up @@ -42,6 +42,7 @@ import Swap from '../db/models/Swap';
import ChannelCreationRepository from '../db/repositories/ChannelCreationRepository';
import PairRepository from '../db/repositories/PairRepository';
import ReferralRepository from '../db/repositories/ReferralRepository';
import ReverseRoutingHintRepository from '../db/repositories/ReverseRoutingHintRepository';
import ReverseSwapRepository from '../db/repositories/ReverseSwapRepository';
import SwapRepository from '../db/repositories/SwapRepository';
import {
Expand Down Expand Up @@ -586,6 +587,18 @@ class Service {
return response;
};

public getReverseBip21 = async (id: string) => {
const hint = await ReverseRoutingHintRepository.getHint(id);
if (!hint) {
return undefined;
}

return {
bip21: hint.bip21,
signature: hint.signature,
};
};

public deriveKeys = (symbol: string, index: number): DeriveKeysResponse => {
const wallet = this.walletManager.wallets.get(symbol.toUpperCase());

Expand Down Expand Up @@ -1316,6 +1329,7 @@ class Service {

// Address of the user to encode in the invoice memo
userAddress?: string;
userAddressSignature?: Buffer;
}): Promise<{
id: string;
invoice: string;
Expand Down Expand Up @@ -1551,8 +1565,8 @@ class Service {
lightningTimeoutBlockDelta,
prepayMinerFeeInvoiceAmount,
prepayMinerFeeOnchainAmount,
orderSide: side,

orderSide: side,
baseCurrency: base,
quoteCurrency: quote,
version: args.version,
Expand All @@ -1561,6 +1575,7 @@ class Service {
claimAddress: args.claimAddress,
preimageHash: args.preimageHash,
claimPublicKey: args.claimPublicKey,
userAddressSignature: args.userAddressSignature,
});

this.eventHandler.emitSwapCreation(id);
Expand Down
4 changes: 4 additions & 0 deletions lib/swap/Errors.ts
Expand Up @@ -102,4 +102,8 @@ export default {
message: 'blocked address',
code: concatErrorCode(ErrorCodePrefix.Swap, 20),
}),
INVALID_ADDRESS_SIGNATURE: (): Error => ({
message: 'invalid address signature',
code: concatErrorCode(ErrorCodePrefix.Swap, 21),
}),
};
9 changes: 8 additions & 1 deletion lib/swap/NodeFallback.ts
Expand Up @@ -35,6 +35,7 @@ class NodeFallback {
cltvExpiry?: number,
expiry?: number,
memo?: string,
routingHints?: HopHint[][],
): Promise<HolisticInvoice> => {
let nodeForSwap = this.nodeSwitch.getNodeForReverseSwap(
id,
Expand All @@ -57,6 +58,7 @@ class NodeFallback {
cltvExpiry,
expiry,
memo,
routingHints,
)),
};
} catch (e) {
Expand Down Expand Up @@ -96,8 +98,9 @@ class NodeFallback {
cltvExpiry?: number,
expiry?: number,
memo?: string,
externalHints?: HopHint[][],
): Promise<InvoiceWithRoutingHints> => {
const routingHints =
const nodeHints =
routingNode !== undefined
? await this.routingHints.getRoutingHints(
currency.symbol,
Expand All @@ -106,6 +109,10 @@ class NodeFallback {
)
: undefined;

const routingHints: HopHint[][] = [nodeHints, externalHints]
.filter((hints): hints is HopHint[][] => hints !== undefined)
.flat();

return lightningClient.raceCall<InvoiceWithRoutingHints>(
async (): Promise<InvoiceWithRoutingHints> => ({
routingHints,
Expand Down

0 comments on commit d82bc20

Please sign in to comment.