Skip to content

Commit

Permalink
wip add more verbose error types
Browse files Browse the repository at this point in the history
  • Loading branch information
gudnuf committed Dec 3, 2024
1 parent 02ea20e commit 7c5d049
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 6 deletions.
138 changes: 138 additions & 0 deletions src/model/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,141 @@ export class HttpResponseError extends Error {
this.status = status;
}
}

export class NetworkError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}

export class MintOperationError extends Error {
code: number;
detail: string;

constructor(message: string, code: number, detail: string) {
super(message);
this.code = code;
this.detail = detail;
}
}

export class BlindedMessageAlreadySignedError extends MintOperationError {
constructor(detail: string) {
super('Blinded message of output already signed', 10002, detail);
}
}

export class TokenVerificationError extends MintOperationError {
constructor(detail: string) {
super('Token could not be verified', 10003, detail);
}
}

export class TokenAlreadySpentError extends MintOperationError {
constructor(detail: string) {
super('Token is already spent', 11001, detail);
}
}

export class TransactionNotBalancedError extends MintOperationError {
constructor(detail: string) {
super('Transaction is not balanced (inputs != outputs)', 11002, detail);
}
}

export class UnsupportedUnitError extends MintOperationError {
constructor(detail: string) {
super('Unit in request is not supported', 11005, detail);
}
}

export class AmountOutOfLimitError extends MintOperationError {
constructor(detail: string) {
super('Amount outside of limit range', 11006, detail);
}
}

export class KeysetUnknownError extends MintOperationError {
constructor(detail: string) {
super('Keyset is not known', 12001, detail);
}
}

export class KeysetInactiveError extends MintOperationError {
constructor(detail: string) {
super('Keyset is inactive, cannot sign messages', 12002, detail);
}
}

export class QuoteRequestNotPaidError extends MintOperationError {
constructor(detail: string) {
super('Quote request is not paid', 20001, detail);
}
}

export class TokensAlreadyIssuedError extends MintOperationError {
constructor(detail: string) {
super('Tokens have already been issued for quote', 20002, detail);
}
}

export class MintingDisabledError extends MintOperationError {
constructor(detail: string) {
super('Minting is disabled', 20003, detail);
}
}

export class QuotePendingError extends MintOperationError {
constructor(detail: string) {
super('Quote is pending', 20005, detail);
}
}

export class InvoiceAlreadyPaidError extends MintOperationError {
constructor(detail: string) {
super('Invoice already paid', 20006, detail);
}
}

export class QuoteExpiredError extends MintOperationError {
constructor(detail: string) {
super('Quote is expired', 20007, detail);
}
}

export function createMintOperationError(code: number, detail: string): MintOperationError {
switch (code) {
case 10002:
return new BlindedMessageAlreadySignedError(detail);
case 10003:
return new TokenVerificationError(detail);
case 11001:
return new TokenAlreadySpentError(detail);
case 11002:
return new TransactionNotBalancedError(detail);
case 11005:
return new UnsupportedUnitError(detail);
case 11006:
return new AmountOutOfLimitError(detail);
case 12001:
return new KeysetUnknownError(detail);
case 12002:
return new KeysetInactiveError(detail);
case 20001:
return new QuoteRequestNotPaidError(detail);
case 20002:
return new TokensAlreadyIssuedError(detail);
case 20003:
return new MintingDisabledError(detail);
case 20005:
return new QuotePendingError(detail);
case 20006:
return new InvoiceAlreadyPaidError(detail);
case 20007:
return new QuoteExpiredError(detail);
default:
return new MintOperationError('Unknown mint operation error', code, detail);
}
}
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, createMintOperationError } 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 createMintOperationError(errorData.code as number, errorData.detail as string);
}

throw new HttpResponseError(
errorData.error || errorData.detail || 'HTTP request failed',
response.status
);
}

try {
Expand Down
35 changes: 33 additions & 2 deletions test/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import { CashuMint } from '../src/CashuMint.js';
import { CashuWallet } from '../src/CashuWallet.js';
import { setGlobalRequestOptions } from '../src/request.js';
import { MeltQuoteResponse } from '../src/model/types/index.js';
import { HttpResponseError, NetworkError, MintOperationError } from '../src/model/Errors';

let request: Record<string, string> | undefined;
const mintUrl = 'https://localhost:3338';
const unit = 'sats';
const invoice =
'lnbc20u1p3u27nppp5pm074ffk6m42lvae8c6847z7xuvhyknwgkk7pzdce47grf2ksqwsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzpgxqyz5vqsp5sw6n7cztudpl5m5jv3z6dtqpt2zhd3q6dwgftey9qxv09w82rgjq9qyyssqhtfl8wv7scwp5flqvmgjjh20nf6utvv5daw5h43h69yqfwjch7wnra3cn94qkscgewa33wvfh7guz76rzsfg9pwlk8mqd27wavf2udsq3yeuju';

beforeAll(() => {
nock.disableNetConnect();
Expand Down Expand Up @@ -43,6 +42,7 @@ describe('requests', () => {
// expect(request!['content-type']).toContain('application/json');
expect(request!['accept']).toContain('application/json, text/plain, */*');
});

test('global custom headers can be set', async () => {
const mint = new CashuMint(mintUrl);
nock(mintUrl)
Expand All @@ -66,4 +66,35 @@ describe('requests', () => {
expect(request).toBeDefined();
expect(request!['x-cashu']).toContain('xyz-123-abc');
});

test('handles HttpResponseError on non-200 response', async () => {
const mint = new CashuMint(mintUrl);
nock(mintUrl)
.get('/v1/melt/quote/bolt11/test')
.reply(404, function () {
request = this.req.headers;
return { error: 'Not Found' };
});

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);
nock(mintUrl).get('/v1/melt/quote/bolt11/test').replyWithError('Network 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);
nock(mintUrl)
.get('/v1/melt/quote/bolt11/test')
.reply(400, { code: 4, detail: 'Invalid operation' });

const wallet = new CashuWallet(mint, { unit });
await expect(wallet.checkMeltQuote('test')).rejects.toThrow(MintOperationError);
});
});

0 comments on commit 7c5d049

Please sign in to comment.