Skip to content

Commit

Permalink
rebase and change MintOperationError constructor
Browse files Browse the repository at this point in the history
  • Loading branch information
gudnuf committed Dec 29, 2024
1 parent 9f9f83a commit 69ee478
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 4 deletions.
40 changes: 40 additions & 0 deletions src/model/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, string> = {
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);
}
}
23 changes: 19 additions & 4 deletions src/request.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpResponseError } from './model/Errors';
import { HttpResponseError, NetworkError, MintOperationError } from './model/Errors';

type RequestArgs = {
endpoint: string;
Expand Down Expand Up @@ -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 {
Expand Down
48 changes: 48 additions & 0 deletions test/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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');
});
});

0 comments on commit 69ee478

Please sign in to comment.