Skip to content

Commit

Permalink
feat: renegotiation of over- and underpaid chain swaps (#657)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Aug 29, 2024
1 parent 924eddf commit b93f0e8
Show file tree
Hide file tree
Showing 26 changed files with 2,230 additions and 590 deletions.
8 changes: 6 additions & 2 deletions lib/api/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,13 @@ export const errorResponse = (
}
};

export const successResponse = (res: Response, data: unknown) => {
export const successResponse = (
res: Response,
data: unknown,
statusCode = 200,
) => {
setContentTypeJson(res);
res.status(200);
res.status(statusCode);

if (typeof data === 'object') {
res.json(data);
Expand Down
111 changes: 110 additions & 1 deletion lib/api/v2/routers/SwapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,93 @@ class SwapRouter extends RouterBase {
),
);

/**
* @openapi
* components:
* schemas:
* Quote:
* type: object
* properties:
* amount:
* type: number
* description: New quote for a Swap. Amount that the server will lock for Chain Swaps
*/

/**
* @openapi
* /swap/chain/{id}/quote:
* get:
* tags: [Chain Swap]
* description: Gets a new quote for an overpaid or underpaid Chain Swap
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: ID of the Swap
* responses:
* '200':
* description: The new quote
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Quote'
* '400':
* description: When the Chain Swap is not eligible for a new quote
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get('/chain/:id/quote', this.handleError(this.chainSwapQuote));

/**
* @openapi
* components:
* schemas:
* QuoteResponse:
* type: object
*/

/**
* @openapi
* /swap/chain/{id}/quote:
* post:
* tags: [Chain Swap]
* description: Accepts a new quote for a Chain Swap
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: ID of the Swap
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Quote'
* responses:
* '202':
* description: The new quote was accepted
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/QuoteResponse'
* '400':
* description: When the Chain Swap is not eligible for a new quote
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.post(
'/chain/:id/quote',
this.handleError(this.chainSwapAcceptQuote),
);

/**
* @openapi
* tags:
Expand Down Expand Up @@ -2034,10 +2121,32 @@ class SwapRouter extends RouterBase {
]);

successResponse(res, {
signature: await this.service.eipSigner.signSwapRefund(id),
signature: await this.service.swapManager.eipSigner.signSwapRefund(id),
});
};

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

successResponse(res, {
amount: await this.service.swapManager.renegotiator.getQuote(id),
});
};

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

await this.service.swapManager.renegotiator.acceptQuote(id, amount);
successResponse(res, {}, 202);
};

private getSwapStatus = async (req: Request, res: Response) => {
const { id } = validateRequest(req.params, [
{ name: 'id', type: 'string' },
Expand Down
33 changes: 32 additions & 1 deletion lib/db/Migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '../Utils';
import { SwapVersion } from '../consts/Enums';
import { Currency } from '../wallet/WalletManager';
import ChainSwap from './models/ChainSwap';
import ChannelCreation from './models/ChannelCreation';
import DatabaseVersion from './models/DatabaseVersion';
import LightningPayment from './models/LightningPayment';
Expand All @@ -23,7 +24,7 @@ import DatabaseVersionRepository from './repositories/DatabaseVersionRepository'

// TODO: integration tests for actual migrations
class Migration {
private static latestSchemaVersion = 10;
private static latestSchemaVersion = 11;

constructor(
private logger: Logger,
Expand Down Expand Up @@ -458,6 +459,36 @@ class Migration {
break;
}

case 10: {
await this.sequelize.transaction(async (tx) => {
await this.sequelize.getQueryInterface().addColumn(
ChainSwap.tableName,
'createdRefundSignature',
{
type: new DataTypes.BOOLEAN(),
allowNull: false,
defaultValue: false,
},
{ transaction: tx },
);

// To make sure we do not allow renegotiation of amounts and potentially
// accept a transaction, we created a refund signature before
await ChainSwap.update(
{
createdRefundSignature: true,
},
{
where: {},
transaction: tx,
},
);
});

await this.finishMigration(versionRow.version, currencies);
break;
}

default:
throw `found unexpected database version ${versionRow.version}`;
}
Expand Down
9 changes: 9 additions & 0 deletions lib/db/models/ChainSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type ChainSwapType = {

preimageHash: string;
preimage?: string;

createdRefundSignature: boolean;
};

class ChainSwap extends Model implements ChainSwapType {
Expand All @@ -36,6 +38,8 @@ class ChainSwap extends Model implements ChainSwapType {
public preimageHash!: string;
public preimage?: string;

public createdRefundSignature!: boolean;

public createdAt!: Date;
public updatedAt!: Date;

Expand All @@ -60,6 +64,11 @@ class ChainSwap extends Model implements ChainSwapType {
unique: true,
},
preimage: { type: new DataTypes.STRING(64), allowNull: true },
createdRefundSignature: {
type: DataTypes.BOOLEAN(),
allowNull: false,
defaultValue: false,
},
},
{
sequelize,
Expand Down
45 changes: 45 additions & 0 deletions lib/db/repositories/ChainSwapRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class ChainSwapInfo {
return this.chainSwap.acceptZeroConf;
}

get createdRefundSignature() {
return this.chainSwap.createdRefundSignature;
}

get failureReason() {
return this.chainSwap.failureReason;
}
Expand Down Expand Up @@ -244,6 +248,18 @@ class ChainSwapRepository {
);
};

public static setRefundSignatureCreated = (id: string) =>
ChainSwap.update(
{
createdRefundSignature: true,
},
{
where: {
id,
},
},
);

public static setUserLockupTransaction = (
swap: ChainSwapInfo,
lockupTransactionId: string,
Expand Down Expand Up @@ -272,6 +288,35 @@ class ChainSwapRepository {
return swap;
});

public static setExpectedAmounts = (
swap: ChainSwapInfo,
fee: number,
userLockAmount: number,
serverLockAmount: number,
): Promise<ChainSwapInfo> =>
Database.sequelize.transaction(async (transaction) => {
swap.chainSwap = await swap.chainSwap.update(
{
fee,
},
{ transaction },
);
swap.receivingData = await swap.receivingData.update(
{
expectedAmount: userLockAmount,
},
{ transaction },
);
swap.sendingData = await swap.sendingData.update(
{
expectedAmount: serverLockAmount,
},
{ transaction },
);

return swap;
});

public static setClaimMinerFee = async (
swap: ChainSwapInfo,
preimage: Buffer,
Expand Down
16 changes: 16 additions & 0 deletions lib/service/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,20 @@ export default {
message: 'server claim succeeded already',
code: concatErrorCode(ErrorCodePrefix.Service, 46),
}),
REFUND_SIGNED_ALREADY: (): Error => ({
message: 'a refund for this swap was signed already',
code: concatErrorCode(ErrorCodePrefix.Service, 47),
}),
LOCKUP_NOT_REJECTED: (): Error => ({
message: 'lockup transaction was not rejected because of the amount',
code: concatErrorCode(ErrorCodePrefix.Service, 48),
}),
TIME_UNTIL_EXPIRY_TOO_SHORT: (): Error => ({
message: 'time until expiry too short',
code: concatErrorCode(ErrorCodePrefix.Service, 49),
}),
INVALID_QUOTE: (): Error => ({
message: 'invalid quote',
code: concatErrorCode(ErrorCodePrefix.Service, 50),
}),
};
Loading

0 comments on commit b93f0e8

Please sign in to comment.