From 69ee478b18b5e554a43294c8cd4a4408b439f529 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Sun, 29 Dec 2024 12:46:26 -0800 Subject: [PATCH] rebase and change MintOperationError constructor --- src/model/Errors.ts | 40 ++++++++++++++++++++++++++++++++++++ src/request.ts | 23 +++++++++++++++++---- test/request.test.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/src/model/Errors.ts b/src/model/Errors.ts index d0e7bcd45..ac8a42d1c 100644 --- a/src/model/Errors.ts +++ b/src/model/Errors.ts @@ -3,5 +3,45 @@ export class HttpResponseError extends Error { constructor(message: string, status: number) { super(message); this.status = status; + this.name = 'HttpResponseError'; + Object.setPrototypeOf(this, HttpResponseError.prototype); + } +} + +export class NetworkError extends Error { + status: number; + constructor(message: string, status: number) { + super(message); + this.status = status; + this.name = 'NetworkError'; + Object.setPrototypeOf(this, NetworkError.prototype); + } +} +export class MintOperationError extends Error { + code: number; + detail: string; + + constructor(code: number, detail: string) { + const messages: Record = { + 10002: 'Blinded message of output already signed', + 10003: 'Token could not be verified', + 11001: 'Token is already spent', + 11002: 'Transaction is not balanced (inputs != outputs)', + 11005: 'Unit in request is not supported', + 11006: 'Amount outside of limit range', + 12001: 'Keyset is not known', + 12002: 'Keyset is inactive, cannot sign messages', + 20001: 'Quote request is not paid', + 20002: 'Tokens have already been issued for quote', + 20003: 'Minting is disabled', + 20005: 'Quote is pending', + 20006: 'Invoice already paid', + 20007: 'Quote is expired' + }; + super(messages[code] || 'Unknown mint operation error'); + this.code = code; + this.detail = detail; + this.name = 'MintOperationError'; + Object.setPrototypeOf(this, MintOperationError.prototype); } } diff --git a/src/request.ts b/src/request.ts index d74630aed..ce6ab5cf2 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,4 +1,4 @@ -import { HttpResponseError } from './model/Errors'; +import { HttpResponseError, NetworkError, MintOperationError } from './model/Errors'; type RequestArgs = { endpoint: string; @@ -31,13 +31,28 @@ async function _request({ ...requestHeaders }; - const response = await fetch(endpoint, { body, headers, ...options }); + let response: Response; + try { + response = await fetch(endpoint, { body, headers, ...options }); + } catch (err) { + // A fetch() promise only rejects when the request fails, + // for example, because of a badly-formed request URL or a network error. + throw new NetworkError(err instanceof Error ? err.message : 'Network request failed', 0); + } if (!response.ok) { // expecting: { error: '', code: 0 } // or: { detail: '' } (cashuBtc via pythonApi) - const { error, detail } = await response.json().catch(() => ({ error: 'bad response' })); - throw new HttpResponseError(error || detail || 'bad response', response.status); + const errorData = await response.json().catch(() => ({ error: 'bad response' })); + + if (response.status === 400 && 'code' in errorData && 'detail' in errorData) { + throw new MintOperationError(errorData.code as number, errorData.detail as string); + } + + throw new HttpResponseError( + errorData.error || errorData.detail || 'HTTP request failed', + response.status + ); } try { diff --git a/test/request.test.ts b/test/request.test.ts index 56e2c76fe..4e77aa67b 100644 --- a/test/request.test.ts +++ b/test/request.test.ts @@ -4,6 +4,8 @@ import { CashuWallet } from '../src/CashuWallet.js'; import { HttpResponse, http } from 'msw'; import { setupServer } from 'msw/node'; import { setGlobalRequestOptions } from '../src/request.js'; +import { MeltQuoteResponse } from '../src/model/types/index.js'; +import { HttpResponseError, NetworkError, MintOperationError } from '../src/model/Errors'; const mintUrl = 'https://localhost:3338'; const unit = 'sats'; @@ -46,6 +48,7 @@ describe('requests', () => { // expect(request!['content-type']).toContain('application/json'); expect(headers!.get('accept')).toContain('application/json, text/plain, */*'); }); + test('global custom headers can be set', async () => { let headers: Headers; const mint = new CashuMint(mintUrl); @@ -70,4 +73,49 @@ describe('requests', () => { expect(headers!).toBeDefined(); expect(headers!.get('x-cashu')).toContain('xyz-123-abc'); }); + + test('handles HttpResponseError on non-200 response', async () => { + const mint = new CashuMint(mintUrl); + server.use( + http.get(mintUrl + '/v1/melt/quote/bolt11/test', () => { + return new HttpResponse( + JSON.stringify({ error: 'Not Found' }), + { status: 404 } + ); + }) + ); + + const wallet = new CashuWallet(mint, { unit }); + await expect(wallet.checkMeltQuote('test')).rejects.toThrowError(HttpResponseError); + }); + test('handles NetworkError on network failure', async () => { + const mint = new CashuMint(mintUrl); + server.use( + http.get(mintUrl + '/v1/melt/quote/bolt11/test', () => { + // This simulates a network failure at the fetch level + return Response.error(); + }) + ); + + const wallet = new CashuWallet(mint, { unit }); + await expect(wallet.checkMeltQuote('test')).rejects.toThrow(NetworkError); + }); + + test('handles MintOperationError on 400 response with code and detail', async () => { + const mint = new CashuMint(mintUrl); + server.use( + http.get(mintUrl + '/v1/melt/quote/bolt11/test', () => { + return new HttpResponse( + JSON.stringify({ code: 20003, detail: 'Minting disabled' }), + { status: 400 } + ); + }) + ); + + const wallet = new CashuWallet(mint, { unit }); + const promise = wallet.checkMeltQuote('test'); + await expect(promise).rejects.toThrow(MintOperationError); + // assert that the error message is set correctly by the code + await expect(promise).rejects.toThrow('Minting is disabled'); + }); });