From 798b4a72c05395b8c3645eeace71b09294cb4b74 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Sun, 14 Jan 2024 13:18:08 +0100 Subject: [PATCH] docs: swagger specs for API V2 endpoints --- lib/api/v2/routers/SwapRouter.ts | 395 ++++++++++++++++--- lib/service/Errors.ts | 4 + lib/service/MusigSigner.ts | 6 +- lib/service/Service.ts | 28 ++ package.json | 1 + swagger-spec.json | 359 ++++++++++++++++- test/integration/service/MusigSigner.spec.ts | 31 ++ test/unit/service/Service.spec.ts | 31 +- 8 files changed, 773 insertions(+), 82 deletions(-) diff --git a/lib/api/v2/routers/SwapRouter.ts b/lib/api/v2/routers/SwapRouter.ts index 29fe05c8..7b87e1fa 100644 --- a/lib/api/v2/routers/SwapRouter.ts +++ b/lib/api/v2/routers/SwapRouter.ts @@ -22,13 +22,41 @@ class SwapRouter extends RouterBase { public getRouter = () => { /** * @openapi - * tags: - * name: Swap - * description: Swap related endpoints + * components: + * schemas: + * SwapTreeLeaf: + * type: object + * properties: + * version: + * type: number + * description: Tapscript version + * output: + * type: string + * description: Script encoded as HEX + */ + + /** + * @openapi + * components: + * schemas: + * SwapTree: + * type: object + * properties: + * claimLeaf: + * $ref: '#/components/schemas/SwapTreeLeaf' + * refundLeaf: + * $ref: '#/components/schemas/SwapTreeLeaf' */ const router = Router(); + /** + * @openapi + * tags: + * name: Submarine + * description: Submarine Swap related endpoints + */ + /** * @openapi * components: @@ -36,34 +64,69 @@ class SwapRouter extends RouterBase { * SubmarineRequest: * type: object * properties: - * pairId: + * from: * type: string * required: true - * orderSide: - * type: string - * required: true - * enum: - * - buy - * - sell - * refundPublicKey: + * description: The asset that is sent onchain + * to: * type: string * required: true + * description: The asset that is received on lightning * invoice: * type: string - * preimageHash: + * required: true + * description: BOLT11 invoice that should be paid on lightning + * refundPublicKey: * type: string + * required: true + * description: Public key with which the Submarine Swap can be refunded encoded as HEX * pairHash: * type: string * referralId: * type: string */ + /** + * @openapi + * components: + * schemas: + * SubmarineResponse: + * type: object + * properties: + * id: + * type: string + * description: ID of the created Submarine Swap + * bip21: + * type: string + * description: BIP21 for the onchain payment request + * address: + * type: string + * description: Onchain HTLC address + * swapTree: + * $ref: '#/components/schemas/SwapTree' + * claimPublicKey: + * type: string + * description: Public key of Boltz that will be used to sweep the onchain HTLC + * timeoutBlockHeight: + * type: number + * description: Timeout block height of the onchain HTLC + * acceptZeroConf: + * type: boolean + * description: Whether 0-conf will be accepted assuming the transaction does not signal RBF and has a reasonably high fee + * expectedAmount: + * type: number + * description: Amount that is expected to be sent to the onchain HTLC address in satoshis + * blindingKey: + * type: string + * description: Liquid blinding private key encoded as HEX + */ + /** * @openapi * /swap/submarine: * post: * description: Create a new Submarine Swap from onchain to lightning - * tags: [Swap] + * tags: [Submarine] * requestBody: * required: true * content: @@ -71,12 +134,12 @@ class SwapRouter extends RouterBase { * schema: * $ref: '#/components/schemas/SubmarineRequest' * responses: - * '200': + * '201': * description: The created Submarine Swap * content: * application/json: * schema: - * type: object + * $ref: '#/components/schemas/SubmarineResponse' * '400': * description: Error that caused the Submarine Swap creation to fail * content: @@ -86,70 +149,267 @@ class SwapRouter extends RouterBase { */ router.post('/submarine', this.handleError(this.createSubmarine)); + /** + * @openapi + * components: + * schemas: + * SubmarineRefundRequest: + * type: object + * properties: + * id: + * type: string + * required: true + * description: ID of the Submarine Swap that should be refunded + * pubNonce: + * type: string + * required: true + * description: Public nonce of the client for the session encoded as HEX + * transaction: + * type: string + * required: true + * description: Transaction which should be signed encoded as HEX + * index: + * type: number + * required: true + * description: Index of the input of the transaction that should be signed + */ + + /** + * @openapi + * components: + * schemas: + * PartialSignature: + * type: object + * properties: + * pubNonce: + * type: string + * description: Public nonce of Boltz encoded as HEX + * partialSignature: + * type: string + * description: Partial signature encoded as HEX + */ + + /** + * @openapi + * /swap/submarine/refund: + * post: + * description: Requests a partial signature for a cooperative Submarine Swap refund transaction + * tags: [Submarine] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubmarineRefundRequest' + * responses: + * '200': + * description: A partial signature + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PartialSignature' + * '400': + * description: Error that caused signature request to fail + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ router.post('/submarine/refund', this.handleError(this.refundSubmarine)); + /** + * @openapi + * tags: + * name: Reverse + * description: Reverse Swap related endpoints + */ + + /** + * @openapi + * components: + * schemas: + * ReverseRequest: + * type: object + * properties: + * from: + * type: string + * required: true + * description: The asset that is sent on lightning + * to: + * type: string + * required: true + * description: The asset that is received onchain + * preimageHash: + * type: string + * required: true + * description: SHA-256 hash of the preimage of the Reverse Swap encoded as HEX + * claimPublicKey: + * type: string + * required: true + * description: Public key with which the Reverse Swap can be claimed encoded as HEX + * invoiceAmount: + * type: string + * description: Amount for which the invoice should be; conflicts with "onchainAmount" + * onchainAmount: + * type: string + * description: Amount that should be locked in the onchain HTLC; conflicts with "invoiceAmount" + * pairHash: + * type: string + * referralId: + * type: string + */ + + /** + * @openapi + * components: + * schemas: + * ReverseResponse: + * type: object + * properties: + * id: + * type: string + * description: ID of the created Reverse Swap + * invoice: + * type: string + * description: Hold invoice of the Reverse Swap + * swapTree: + * $ref: '#/components/schemas/SwapTree' + * lockupAddress: + * type: string + * description: HTLC address in which coins will be locked + * refundPublicKey: + * type: string + * description: Public key of Boltz that will be used to refund the onchain HTLC + * timeoutBlockHeight: + * type: number + * description: Timeout block height of the onchain HTLC + * onchainAmount: + * type: number + * description: Amount that will be locked in the onchain HTLC + * blindingKey: + * type: string + * description: Liquid blinding private key encoded as HEX + */ + + /** + * @openapi + * /swap/reverse: + * post: + * description: Create a new Reverse Swap from lightning to onchain + * tags: [Reverse] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ReverseRequest' + * responses: + * '201': + * description: The created Reverse Swap + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ReverseResponse' + * '400': + * description: Error that caused the Reverse Swap creation to fail + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ router.post('/reverse', this.handleError(this.createReverse)); + /** + * @openapi + * components: + * schemas: + * ReverseClaimRequest: + * type: object + * properties: + * id: + * type: string + * required: true + * description: ID of the Reverse Swap that should be refunded + * preimage: + * type: string + * required: true + * description: Preimage of the Reverse Swap encoded as HEX + * pubNonce: + * type: string + * required: true + * description: Public nonce of the client for the session encoded as HEX + * transaction: + * type: string + * required: true + * description: Transaction which should be signed encoded as HEX + * index: + * type: number + * required: true + * description: Index of the input of the transaction that should be signed + */ + + /** + * @openapi + * /swap/reverse/claim: + * post: + * description: Requests a partial signature for a cooperative Reverse Swap claim transaction + * tags: [Reverse] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ReverseClaimRequest' + * responses: + * '200': + * description: A partial signature + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PartialSignature' + * '400': + * description: Error that caused signature request to fail + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ router.post('/reverse/claim', this.handleError(this.claimReverse)); return router; }; private createSubmarine = async (req: Request, res: Response) => { - const { - pairId, - invoice, - pairHash, - orderSide, - referralId, - preimageHash, - refundPublicKey, - } = validateRequest(req.body, [ - { name: 'pairId', type: 'string' }, - { name: 'orderSide', type: 'string' }, - { name: 'refundPublicKey', type: 'string', hex: true }, - { name: 'invoice', type: 'string', optional: true }, - { name: 'pairHash', type: 'string', optional: true }, - { name: 'referralId', type: 'string', optional: true }, - { name: 'preimageHash', type: 'string', hex: true, optional: true }, - ]); - - let response: any; - - if (invoice) { - response = await this.service.createSwapWithInvoice( - pairId, - orderSide, - refundPublicKey, - invoice.toLowerCase(), - pairHash, - referralId, - undefined, - SwapVersion.Taproot, - ); - } else { - // Check that the preimage hash was set + const { to, from, invoice, pairHash, referralId, refundPublicKey } = validateRequest(req.body, [ - { name: 'preimageHash', type: 'string', hex: true }, + { name: 'to', type: 'string' }, + { name: 'from', type: 'string' }, + { name: 'invoice', type: 'string' }, + { name: 'refundPublicKey', type: 'string', hex: true }, + { name: 'pairHash', type: 'string', optional: true }, + { name: 'referralId', type: 'string', optional: true }, ]); - checkPreimageHashLength(preimageHash); + const { pairId, orderSide } = this.service.convertToPairAndSide( + from, + to, + false, + ); - response = await this.service.createSwap({ - pairId, - orderSide, - referralId, - preimageHash, - refundPublicKey, - version: SwapVersion.Taproot, - }); - } + const response = await this.service.createSwapWithInvoice( + pairId, + orderSide, + refundPublicKey, + invoice.toLowerCase(), + pairHash, + referralId, + undefined, + SwapVersion.Taproot, + ); this.logger.verbose(`Created new Swap with id: ${response.id}`); this.logger.silly(`Swap ${response.id}: ${stringify(response)}`); - delete response.canBeRouted; - createdResponse(res, response); }; @@ -176,9 +436,9 @@ class SwapRouter extends RouterBase { private createReverse = async (req: Request, res: Response) => { const { - pairId, + to, + from, pairHash, - orderSide, referralId, routingNode, claimAddress, @@ -187,8 +447,8 @@ class SwapRouter extends RouterBase { onchainAmount, claimPublicKey, } = validateRequest(req.body, [ - { name: 'pairId', type: 'string' }, - { name: 'orderSide', type: 'string' }, + { name: 'to', type: 'string' }, + { name: 'from', type: 'string' }, { name: 'preimageHash', type: 'string', hex: true }, { name: 'claimPublicKey', type: 'string', hex: true }, { name: 'pairHash', type: 'string', optional: true }, @@ -201,6 +461,11 @@ class SwapRouter extends RouterBase { checkPreimageHashLength(preimageHash); + const { pairId, orderSide } = this.service.convertToPairAndSide( + from, + to, + true, + ); const response = await this.service.createReverseSwap({ pairId, pairHash, diff --git a/lib/service/Errors.ts b/lib/service/Errors.ts index fa6a3559..3ab52a7a 100644 --- a/lib/service/Errors.ts +++ b/lib/service/Errors.ts @@ -141,4 +141,8 @@ export default { message: 'incorrect preimage', code: concatErrorCode(ErrorCodePrefix.Service, 36), }), + INVALID_VIN: (): Error => ({ + message: 'input index is out of range', + code: concatErrorCode(ErrorCodePrefix.Service, 37), + }), }; diff --git a/lib/service/MusigSigner.ts b/lib/service/MusigSigner.ts index d6e51f56..443f8d10 100644 --- a/lib/service/MusigSigner.ts +++ b/lib/service/MusigSigner.ts @@ -144,6 +144,11 @@ class MusigSigner { rawTransaction: Buffer | string, vin: number, ): Promise => { + const tx = parseTransaction(currency.type, rawTransaction); + if (vin < 0 || tx.ins.length <= vin) { + throw Errors.INVALID_VIN(); + } + const wallet = this.walletManager.wallets.get(currency.symbol)!; const ourKeys = wallet.getKeysByIndex(keyIndex); @@ -153,7 +158,6 @@ class MusigSigner { musig.aggregateNonces([[theirPublicKey, theirNonce]]); - const tx = parseTransaction(currency.type, rawTransaction); const hash = await hashForWitnessV1(currency, tx, vin); musig.initializeSession(hash); diff --git a/lib/service/Service.ts b/lib/service/Service.ts index 2174bbbe..a5df56db 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -218,6 +218,34 @@ class Service { await this.nodeInfo.init(); }; + public convertToPairAndSide = ( + from: string, + to: string, + isReverse: boolean, + ): { pairId: string; orderSide: string } => { + const pair = ( + [ + [getPairId({ base: from, quote: to }), false], + [getPairId({ base: to, quote: from }), true], + ] as [string, boolean][] + ).find(([val]) => this.rateProvider.pairs.has(val)); + + if (pair === undefined) { + throw Errors.PAIR_NOT_FOUND(getPairId({ base: from, quote: to })); + } + + return { + pairId: pair[0], + orderSide: isReverse + ? pair[1] + ? 'sell' + : 'buy' + : pair[1] + ? 'buy' + : 'sell', + }; + }; + /** * Gets general information about this Boltz instance and the nodes it is connected to */ diff --git a/package.json b/package.json index 941b90dd..ca06abb1 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "precompile": "node parseGitCommit.js", "compile": "npm run swagger && tsc && cross-os postcompile", "swagger": "node swagger.js", + "swagger-ui": "docker run --rm -p 8082:8080 -e SWAGGER_JSON=/boltz-backend/swagger-spec.json -v .:/boltz-backend swaggerapi/swagger-ui", "compile:watch": "tsc -w", "start": "node bin/boltzd", "dev": "npm run compile && npm run start", diff --git a/swagger-spec.json b/swagger-spec.json index 0d29ab82..5fee8cfc 100644 --- a/swagger-spec.json +++ b/swagger-spec.json @@ -66,7 +66,7 @@ "post": { "description": "Create a new Submarine Swap from onchain to lightning", "tags": [ - "Swap" + "Submarine" ], "requestBody": { "required": true, @@ -79,12 +79,12 @@ } }, "responses": { - "200": { + "201": { "description": "The created Submarine Swap", "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/SubmarineResponse" } } } @@ -101,6 +101,126 @@ } } } + }, + "/swap/submarine/refund": { + "post": { + "description": "Requests a partial signature for a cooperative Submarine Swap refund transaction", + "tags": [ + "Submarine" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmarineRefundRequest" + } + } + } + }, + "responses": { + "200": { + "description": "A partial signature", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartialSignature" + } + } + } + }, + "400": { + "description": "Error that caused signature request to fail", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/swap/reverse": { + "post": { + "description": "Create a new Reverse Swap from lightning to onchain", + "tags": [ + "Reverse" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReverseRequest" + } + } + } + }, + "responses": { + "201": { + "description": "The created Reverse Swap", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReverseResponse" + } + } + } + }, + "400": { + "description": "Error that caused the Reverse Swap creation to fail", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/swap/reverse/claim": { + "post": { + "description": "Requests a partial signature for a cooperative Reverse Swap claim transaction", + "tags": [ + "Reverse" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReverseClaimRequest" + } + } + } + }, + "responses": { + "200": { + "description": "A partial signature", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartialSignature" + } + } + } + }, + "400": { + "description": "Error that caused signature request to fail", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } } }, "components": { @@ -145,30 +265,169 @@ } } }, + "SwapTreeLeaf": { + "type": "object", + "properties": { + "version": { + "type": "number", + "description": "Tapscript version" + }, + "output": { + "type": "string", + "description": "Script encoded as HEX" + } + } + }, + "SwapTree": { + "type": "object", + "properties": { + "claimLeaf": { + "$ref": "#/components/schemas/SwapTreeLeaf" + }, + "refundLeaf": { + "$ref": "#/components/schemas/SwapTreeLeaf" + } + } + }, "SubmarineRequest": { "type": "object", "properties": { - "pairId": { + "from": { "type": "string", - "required": true + "required": true, + "description": "The asset that is sent onchain" }, - "orderSide": { + "to": { "type": "string", "required": true, - "enum": [ - "buy", - "sell" - ] + "description": "The asset that is received on lightning" + }, + "invoice": { + "type": "string", + "required": true, + "description": "BOLT11 invoice that should be paid on lightning" }, "refundPublicKey": { "type": "string", - "required": true + "required": true, + "description": "Public key with which the Submarine Swap can be refunded encoded as HEX" }, - "invoice": { + "pairHash": { "type": "string" }, - "preimageHash": { + "referralId": { "type": "string" + } + } + }, + "SubmarineResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the created Submarine Swap" + }, + "bip21": { + "type": "string", + "description": "BIP21 for the onchain payment request" + }, + "address": { + "type": "string", + "description": "Onchain HTLC address" + }, + "swapTree": { + "$ref": "#/components/schemas/SwapTree" + }, + "claimPublicKey": { + "type": "string", + "description": "Public key of Boltz that will be used to sweep the onchain HTLC" + }, + "timeoutBlockHeight": { + "type": "number", + "description": "Timeout block height of the onchain HTLC" + }, + "acceptZeroConf": { + "type": "boolean", + "description": "Whether 0-conf will be accepted assuming the transaction does not signal RBF and has a reasonably high fee" + }, + "expectedAmount": { + "type": "number", + "description": "Amount that is expected to be sent to the onchain HTLC address in satoshis" + }, + "blindingKey": { + "type": "string", + "description": "Liquid blinding private key encoded as HEX" + } + } + }, + "SubmarineRefundRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "required": true, + "description": "ID of the Submarine Swap that should be refunded" + }, + "pubNonce": { + "type": "string", + "required": true, + "description": "Public nonce of the client for the session encoded as HEX" + }, + "transaction": { + "type": "string", + "required": true, + "description": "Transaction which should be signed encoded as HEX" + }, + "index": { + "type": "number", + "required": true, + "description": "Index of the input of the transaction that should be signed" + } + } + }, + "PartialSignature": { + "type": "object", + "properties": { + "pubNonce": { + "type": "string", + "description": "Public nonce of Boltz encoded as HEX" + }, + "partialSignature": { + "type": "string", + "description": "Partial signature encoded as HEX" + } + } + }, + "ReverseRequest": { + "type": "object", + "properties": { + "from": { + "type": "string", + "required": true, + "description": "The asset that is sent on lightning" + }, + "to": { + "type": "string", + "required": true, + "description": "The asset that is received onchain" + }, + "preimageHash": { + "type": "string", + "required": true, + "description": "SHA-256 hash of the preimage of the Reverse Swap encoded as HEX" + }, + "claimPublicKey": { + "type": "string", + "required": true, + "description": "Public key with which the Reverse Swap can be claimed encoded as HEX" + }, + "invoiceAmount": { + "type": "string", + "description": "Amount for which the invoice should be; conflicts with \"onchainAmount\"" + }, + "onchainAmount": { + "type": "string", + "description": "Amount that should be locked in the onchain HTLC; conflicts with \"invoiceAmount\"" }, "pairHash": { "type": "string" @@ -177,6 +436,72 @@ "type": "string" } } + }, + "ReverseResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the created Reverse Swap" + }, + "invoice": { + "type": "string", + "description": "Hold invoice of the Reverse Swap" + }, + "swapTree": { + "$ref": "#/components/schemas/SwapTree" + }, + "lockupAddress": { + "type": "string", + "description": "HTLC address in which coins will be locked" + }, + "refundPublicKey": { + "type": "string", + "description": "Public key of Boltz that will be used to refund the onchain HTLC" + }, + "timeoutBlockHeight": { + "type": "number", + "description": "Timeout block height of the onchain HTLC" + }, + "onchainAmount": { + "type": "number", + "description": "Amount that will be locked in the onchain HTLC" + }, + "blindingKey": { + "type": "string", + "description": "Liquid blinding private key encoded as HEX" + } + } + }, + "ReverseClaimRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "required": true, + "description": "ID of the Reverse Swap that should be refunded" + }, + "preimage": { + "type": "string", + "required": true, + "description": "Preimage of the Reverse Swap encoded as HEX" + }, + "pubNonce": { + "type": "string", + "required": true, + "description": "Public nonce of the client for the session encoded as HEX" + }, + "transaction": { + "type": "string", + "required": true, + "description": "Transaction which should be signed encoded as HEX" + }, + "index": { + "type": "number", + "required": true, + "description": "Index of the input of the transaction that should be signed" + } + } } } }, @@ -186,8 +511,12 @@ "description": "Lightning nodes" }, { - "name": "Swap", - "description": "Swap related endpoints" + "name": "Submarine", + "description": "Submarine Swap related endpoints" + }, + { + "name": "Reverse", + "description": "Reverse Swap related endpoints" } ], "servers": [ diff --git a/test/integration/service/MusigSigner.spec.ts b/test/integration/service/MusigSigner.spec.ts index 09d3dee0..560bc5a9 100644 --- a/test/integration/service/MusigSigner.spec.ts +++ b/test/integration/service/MusigSigner.spec.ts @@ -12,6 +12,7 @@ import { reverseSwapTree, swapTree, } from 'boltz-core'; +import { SwapTree } from 'boltz-core/dist/lib/consts/Types'; import { randomBytes } from 'crypto'; import { hashForWitnessV1, setup, tweakMusig, zkp } from '../../../lib/Core'; import { ECPair } from '../../../lib/ECPairHelper'; @@ -395,4 +396,34 @@ describe('MusigSigner', () => { ), ).rejects.toEqual(Errors.INCORRECT_PREIMAGE()); }); + + test.each` + vin + ${-1} + ${-23234} + ${5} + ${123} + `( + 'should not create partial signature when vin ($vin) is out of bounds', + async ({ vin }) => { + const tx = await bitcoinClient.getRawTransaction( + await bitcoinClient.sendToAddress( + await bitcoinClient.getNewAddress(), + 100_000, + ), + ); + + await expect( + signer['createPartialSignature']( + btcCurrency, + {} as SwapTree, + 1, + Buffer.alloc(0), + Buffer.alloc(0), + tx, + vin, + ), + ).rejects.toEqual(Errors.INVALID_VIN()); + }, + ); }); diff --git a/test/unit/service/Service.spec.ts b/test/unit/service/Service.spec.ts index 44f191e6..d8bd4626 100644 --- a/test/unit/service/Service.spec.ts +++ b/test/unit/service/Service.spec.ts @@ -4,7 +4,12 @@ import { Provider } from 'ethers'; import { ConfigType } from '../../../lib/Config'; import { ECPair } from '../../../lib/ECPairHelper'; import Logger from '../../../lib/Logger'; -import { decodeInvoice, getHexBuffer, getHexString } from '../../../lib/Utils'; +import { + decodeInvoice, + getHexBuffer, + getHexString, + getPairId, +} from '../../../lib/Utils'; import ApiErrors from '../../../lib/api/Errors'; import ChainClient from '../../../lib/chain/ChainClient'; import { @@ -671,6 +676,30 @@ describe('Service', () => { expect(mockInitRateProvider).toHaveBeenCalledWith(configPairs); }); + test.each` + from | to | isReverse | expected + ${'LTC'} | ${'BTC'} | ${false} | ${{ pairId: 'LTC/BTC', orderSide: 'sell' }} + ${'BTC'} | ${'LTC'} | ${false} | ${{ pairId: 'LTC/BTC', orderSide: 'buy' }} + ${'LTC'} | ${'BTC'} | ${true} | ${{ pairId: 'LTC/BTC', orderSide: 'buy' }} + ${'BTC'} | ${'LTC'} | ${true} | ${{ pairId: 'LTC/BTC', orderSide: 'sell' }} + `( + 'should convert from/to to pairId and order side', + ({ from, to, isReverse, expected }) => { + expect(service.convertToPairAndSide(from, to, isReverse)).toEqual( + expected, + ); + }, + ); + + test('should throw when converting non existent from/to to pairId and order side', () => { + const from = 'DOGE'; + const to = 'BTC'; + + expect(() => service.convertToPairAndSide(from, to, false)).toThrow( + Errors.PAIR_NOT_FOUND(getPairId({ base: from, quote: to })).message, + ); + }); + test('should get info', async () => { const info = (await service.getInfo()).toObject();