Skip to content
Closed
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
9 changes: 5 additions & 4 deletions app/features/accounts/account-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
getInitializedSparkWallet,
sparkMnemonicQueryOptions,
} from '../shared/spark';
import type { Account, AccountPurpose, CashuAccount } from './account';
import type { Account, CashuAccount, StoredAccountPurpose } from './account';
import type { CashuProof } from './cashu-account';

type AccountOmit<
Expand All @@ -38,9 +38,10 @@ type AccountOmit<

type AccountInput<T extends Account> = {
userId: string;
purpose: StoredAccountPurpose;
} & (T extends CashuAccount
? AccountOmit<CashuAccount, 'wallet' | 'proofs'>
: AccountOmit<T>);
? AccountOmit<CashuAccount, 'wallet' | 'proofs' | 'purpose'>
: AccountOmit<T, 'purpose'>);

type Options = {
abortSignal?: AbortSignal;
Expand Down Expand Up @@ -229,7 +230,7 @@ export class AccountRepository {
private async getInitializedCashuWallet(
mintUrl: string,
currency: Currency,
purpose: AccountPurpose,
purpose: StoredAccountPurpose,
) {
const seed = await this.getCashuWalletSeed();
return getInitializedCashuWallet({
Expand Down
12 changes: 10 additions & 2 deletions app/features/accounts/account-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
getKeysetExpiry,
} from '~/lib/cashu';
import type { User } from '../user/user';
import type { Account, CashuAccount, ExtendedAccount } from './account';
import type {
Account,
CashuAccount,
ExtendedAccount,
StoredAccountPurpose,
} from './account';
import {
type AccountRepository,
useAccountRepository,
Expand Down Expand Up @@ -53,7 +58,9 @@ export class AccountService {
account,
}: {
userId: string;
account: DistributedOmit<
account: {
purpose: StoredAccountPurpose;
} & DistributedOmit<
CashuAccount,
| 'id'
| 'createdAt'
Expand All @@ -65,6 +72,7 @@ export class AccountService {
| 'wallet'
| 'isOnline'
| 'state'
| 'purpose'
>;
}) {
const isTestMint = checkIsTestMint(account.mintUrl);
Expand Down
8 changes: 7 additions & 1 deletion app/features/accounts/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ export type AccountType = z.infer<typeof AccountTypeSchema>;

export type AccountState = 'active' | 'expired';

export const AccountPurposeSchema = z.enum([
export const StoredAccountPurposeSchema = z.enum([
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is needed. We could just keep the same schema

'transactional',
'gift-card',
'offer',
]);
export type StoredAccountPurpose = z.infer<typeof StoredAccountPurposeSchema>;

export const AccountPurposeSchema = z.enum([
...StoredAccountPurposeSchema.options,
'unknown',
]);
export type AccountPurpose = z.infer<typeof AccountPurposeSchema>;

export type Account = {
Expand Down
9 changes: 8 additions & 1 deletion app/features/receive/claim-cashu-token-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,16 @@ export class ClaimCashuTokenService {
}

if (receiveAccount.isUnknown && receiveAccount.type === 'cashu') {
if (!receiveAccount.isOnline || receiveAccount.purpose === 'unknown') {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it even possible that account has isOnline true and purpose unknown?

return {
success: false,
message: 'The mint that issued this ecash is offline',
};
}
const { purpose } = receiveAccount;
const addedAccount = await this.accountService.addCashuAccount({
userId: user.id,
account: receiveAccount,
account: { ...receiveAccount, purpose },
});
this.accountsCache.upsert(addedAccount);
receiveAccount = { ...receiveAccount, ...addedAccount };
Expand Down
6 changes: 5 additions & 1 deletion app/features/receive/receive-cashu-token-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,11 @@ export function useReceiveCashuTokenAccounts(
): Promise<Account> => {
let newAccount: Account;
if (accountToAdd.type === 'cashu') {
newAccount = await addCashuAccount(accountToAdd);
if (!accountToAdd.isOnline || accountToAdd.purpose === 'unknown') {
throw new Error('Cannot add a mint that is offline');
}
const { purpose } = accountToAdd;
newAccount = await addCashuAccount({ ...accountToAdd, purpose });
} else {
// Only cashu accounts can be unknown, this should never happen
throw new Error('Invalid account type');
Expand Down
10 changes: 7 additions & 3 deletions app/features/receive/receive-cashu-token-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '~/lib/cashu';
import type { Currency } from '~/lib/money';
import {
type AccountPurpose,
type ExtendedAccount,
type ExtendedCashuAccount,
canReceiveFromLightning,
Expand Down Expand Up @@ -45,8 +46,11 @@ export class ReceiveCashuTokenService {
currency,
});

// wallet.purpose throws when offline
const purpose: AccountPurpose = isOnline ? wallet.purpose : 'unknown';

let expiresAt: string | null = null;
if (wallet.purpose === 'offer') {
if (purpose === 'offer') {
const activeKeyset = findFirstActiveKeyset(
wallet.keyChain.getKeysets(),
currency,
Expand All @@ -61,7 +65,7 @@ export class ReceiveCashuTokenService {
const baseAccount = {
id: 'cashu-account-placeholder-id',
type: 'cashu' as const,
purpose: wallet.purpose,
purpose,
state: isExpired ? ('expired' as const) : ('active' as const),
name: mintUrl.replace('https://', '').replace('http://', ''),
mintUrl,
Expand Down Expand Up @@ -100,7 +104,7 @@ export class ReceiveCashuTokenService {

const isValid = validationResult === true;
const isGatedGiftCard =
wallet.purpose === 'gift-card' && !getFeatureFlag('GIFT_CARDS');
purpose === 'gift-card' && !getFeatureFlag('GIFT_CARDS');

return {
...baseAccount,
Expand Down
9 changes: 8 additions & 1 deletion app/features/shared/cashu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,19 @@ export const allMintKeysetsQueryOptions = (mintUrl: string) =>

/**
* Extract and decode a cashu token from arbitrary content.
* Fetches keyset IDs from the token's mint for v2 keyset resolution.
* Tokens with v1 keyset IDs decode without contacting the mint. v2 tokens use
* truncated keyset IDs that have to be resolved against the mint's keyset list.
*/
export async function decodeCashuToken(content: string): Promise<Token | null> {
const result = extractCashuToken(content);
if (!result) return null;

try {
return getDecodedToken(result.encoded);
} catch {
// v2 keyset IDs are truncated and need resolution from the mint
}

try {
const queryClient = getQueryClient();
const data = await queryClient.fetchQuery(
Expand Down
8 changes: 7 additions & 1 deletion app/features/user/user-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import type { DistributedOmit } from 'type-fest';
import type { z } from 'zod';
import { normalizeMintUrl } from '~/lib/cashu';
import type { Currency } from '~/lib/money';
import type { Account, RedactedAccount } from '../accounts/account';
import type {
Account,
RedactedAccount,
StoredAccountPurpose,
} from '../accounts/account';
import {
type AccountRepository,
useAccountRepository,
Expand Down Expand Up @@ -42,6 +46,7 @@ type Options = {

type AccountInput = {
isDefault?: boolean;
purpose: StoredAccountPurpose;
} & DistributedOmit<
Account,
| 'id'
Expand All @@ -53,6 +58,7 @@ type AccountInput = {
| 'isOnline'
| 'balance'
| 'state'
| 'purpose'
>;

/**
Expand Down
Loading