Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/web-wallet/app/features/receive/cashu-receive-swap-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useSelectItemsWithOnlineAccount,
} from '../accounts/account-hooks';
import type { AgicashDbCashuReceiveSwap } from '../agicash-db/database';
import { ConcurrencyError } from '../shared/error';
import { useUser } from '../user/user-hooks';
import type { CashuReceiveSwap } from './cashu-receive-swap';
import { useCashuReceiveSwapRepository } from './cashu-receive-swap-repository';
Expand Down Expand Up @@ -163,8 +164,13 @@ export function useProcessCashuReceiveSwapTasks() {
const account = getCashuAccount(swap.accountId);
await receiveSwapService.completeSwap(account, swap);
},
retry: 3,
throwOnError: true,
retry: (failureCount, error) => {
if (error instanceof ConcurrencyError) {
return true;
}
return failureCount < 3;
},
throwOnError: (error) => !(error instanceof ConcurrencyError),
onError: (error, tokenHash) => {
console.error('Error finalizing receive swap', {
cause: error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
CashuErrorCodes,
areMintUrlsEqual,
getCashuUnit,
isTransientCashuSwapError,
sumProofs,
} from '~/lib/cashu';
import { Money } from '~/lib/money';
import type { CashuAccount } from '../accounts/account';
import { tokenToMoney } from '../shared/cashu';
import { ConcurrencyError } from '../shared/error';
import type { CashuReceiveSwap } from './cashu-receive-swap';
import {
type CashuReceiveSwapRepository,
Expand Down Expand Up @@ -244,6 +246,11 @@ export class CashuReceiveSwapService {
// TODO: make sure these proofs are not already in our balance and that they are not spent
return proofs;
}

if (isTransientCashuSwapError(error)) {
throw new ConcurrencyError(error.message);
}

throw error;
}
}
Expand Down
9 changes: 7 additions & 2 deletions apps/web-wallet/app/features/send/cashu-send-swap-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,13 @@ export function useProcessCashuSendSwapTasks() {
account,
});
},
retry: 3,
throwOnError: true,
retry: (failureCount, error) => {
if (error instanceof ConcurrencyError) {
return true;
}
return failureCount < 3;
},
throwOnError: (error) => !(error instanceof ConcurrencyError),
onError: (error, swapId) => {
console.error('Error swapping for proofs to send', {
cause: error,
Expand Down
7 changes: 6 additions & 1 deletion apps/web-wallet/app/features/send/cashu-send-swap-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type ExtendedCashuWallet,
getCashuProtocolUnit,
getCashuUnit,
isTransientCashuSwapError,
sumProofs,
} from '~/lib/cashu';
import { Money } from '~/lib/money';
Expand All @@ -21,7 +22,7 @@ import {
} from '../receive/cashu-receive-swap-service';
import { getTokenHash } from '../shared/cashu';
import { getDefaultUnit } from '../shared/currencies';
import { DomainError } from '../shared/error';
import { ConcurrencyError, DomainError } from '../shared/error';
import type { CashuSendSwap } from './cashu-send-swap';
import {
type CashuSendSwapRepository,
Expand Down Expand Up @@ -457,6 +458,10 @@ export class CashuSendSwapService {
};
}

if (isTransientCashuSwapError(error)) {
throw new ConcurrencyError(error.message);
}

throw error;
}
}
Expand Down
51 changes: 51 additions & 0 deletions apps/web-wallet/app/lib/cashu/error-codes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, test } from 'bun:test';
import { MintOperationError } from '@cashu/cashu-ts';
import { CashuErrorCodes, isTransientCashuSwapError } from './error-codes';

const mintError = (code: CashuErrorCodes) =>
new MintOperationError(code, `mint error ${code}`);

describe('isTransientCashuSwapError', () => {
test('treats OUTPUTS_ARE_PENDING (11004) as transient', () => {
expect(
isTransientCashuSwapError(mintError(CashuErrorCodes.OUTPUTS_ARE_PENDING)),
).toBe(true);
});

test('treats PROOFS_ARE_PENDING (11002) as transient', () => {
expect(
isTransientCashuSwapError(mintError(CashuErrorCodes.PROOFS_ARE_PENDING)),
).toBe(true);
});

test('does not treat OUTPUT_ALREADY_SIGNED (11003) as transient', () => {
expect(
isTransientCashuSwapError(
mintError(CashuErrorCodes.OUTPUT_ALREADY_SIGNED),
),
).toBe(false);
});

test('does not treat TOKEN_ALREADY_SPENT (11001) as transient', () => {
expect(
isTransientCashuSwapError(mintError(CashuErrorCodes.TOKEN_ALREADY_SPENT)),
).toBe(false);
});

test('does not treat other mint operation errors as transient', () => {
expect(
isTransientCashuSwapError(
mintError(CashuErrorCodes.TRANSACTION_NOT_BALANCED),
),
).toBe(false);
expect(
isTransientCashuSwapError(mintError(CashuErrorCodes.KEYSET_INACTIVE)),
).toBe(false);
});

test('does not treat non-mint errors as transient', () => {
expect(isTransientCashuSwapError(new Error('boom'))).toBe(false);
expect(isTransientCashuSwapError('outputs are pending')).toBe(false);
expect(isTransientCashuSwapError(undefined)).toBe(false);
});
});
17 changes: 17 additions & 0 deletions apps/web-wallet/app/lib/cashu/error-codes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { MintOperationError } from '@cashu/cashu-ts';

/**
* Based on https://github.com/cashubtc/nuts/blob/main/error_codes.md
*/
Expand Down Expand Up @@ -216,3 +218,18 @@ export enum CashuErrorCodes {
*/
BAT_MINT_RATE_LIMIT_EXCEEDED = 31004,
}

/**
* Whether the error is a transient swap error returned by the mint: the outputs or
* input proofs are still in flight in a parallel operation (codes 11002 and 11004).
* The mint settles them on its own, so the swap should be retried rather than failed.
* @see https://github.com/cashubtc/nuts/blob/main/error_codes.md
*/
export const isTransientCashuSwapError = (
error: unknown,
): error is MintOperationError =>
error instanceof MintOperationError &&
[
CashuErrorCodes.PROOFS_ARE_PENDING,
CashuErrorCodes.OUTPUTS_ARE_PENDING,
].includes(error.code);
Loading