Skip to content
Open
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
5 changes: 4 additions & 1 deletion app/features/send/cashu-send-swap-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useSuspenseQuery,
} from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react';
import type { SpendingConditionData } from '~/lib/cashu/types';
import type { SpendingConditionData, UnlockingData } from '~/lib/cashu/types';
import type { Money } from '~/lib/money';
import { useSupabaseRealtimeSubscription } from '~/lib/supabase/supabase-realtime';
import { useLatest } from '~/lib/use-latest';
Expand Down Expand Up @@ -137,11 +137,13 @@ export function useCreateCashuSendSwap({
amount,
accountId,
spendingConditionData,
unlockingData,
senderPaysFee = true,
}: {
amount: Money;
accountId: string;
spendingConditionData?: SpendingConditionData;
unlockingData?: UnlockingData;
senderPaysFee?: boolean;
}) => {
const account = await getLatestCashuAccount(accountId);
Expand All @@ -151,6 +153,7 @@ export function useCreateCashuSendSwap({
account,
senderPaysFee,
spendingConditionData,
unlockingData,
});
},
onSuccess: (swap) => {
Expand Down
30 changes: 29 additions & 1 deletion app/features/send/cashu-send-swap-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { sumProofs } from '~/lib/cashu';
import {
type SpendingConditionData,
SpendingConditionDataSchema,
type UnlockingData,
UnlockingDataSchema,
} from '~/lib/cashu/types';
import { Money } from '~/lib/money';
import {
Expand Down Expand Up @@ -94,6 +96,10 @@ type CreateSendSwap = {
* The version seen by the client for optimistic concurrency control.
*/
accountVersion: number;
/**
* The unlocking data to reverse the swap.
*/
unlockingData?: UnlockingData;
};

export class CashuSendSwapRepository {
Expand All @@ -120,6 +126,7 @@ export class CashuSendSwapRepository {
keysetCounter,
outputAmounts,
accountVersion,
unlockingData,
}: CreateSendSwap,
options?: Options,
) {
Expand All @@ -139,6 +146,7 @@ export class CashuSendSwapRepository {
encryptedProofsToSend,
encryptedTransactionDetails,
encryptedSpendingConditionData,
encryptedUnlockingData,
] = await Promise.all([
this.encryption.encrypt(inputProofs),
this.encryption.encrypt(accountProofs),
Expand All @@ -147,6 +155,7 @@ export class CashuSendSwapRepository {
spendingConditionData
? this.encryption.encrypt(spendingConditionData)
: undefined,
unlockingData ? this.encryption.encrypt(unlockingData) : undefined,
]);

const updatedKeysetCounter =
Expand Down Expand Up @@ -181,6 +190,7 @@ export class CashuSendSwapRepository {
p_proofs_to_send: encryptedProofsToSend,
p_token_hash: tokenHash,
p_spending_condition_data: encryptedSpendingConditionData,
p_unlocking_data: encryptedUnlockingData,
});

if (options?.abortSignal) {
Expand Down Expand Up @@ -368,13 +378,16 @@ export class CashuSendSwapRepository {
data: AgicashDbCashuSendSwap,
decrypt: Encryption['decrypt'],
): Promise<CashuSendSwap> {
const [inputProofs, proofsToSend, spendingConditionData] =
const [inputProofs, proofsToSend, spendingConditionData, unlockingData] =
await Promise.all([
decrypt<Proof[]>(data.input_proofs),
data.proofs_to_send ? decrypt<Proof[]>(data.proofs_to_send) : undefined,
data.spending_condition_data
? decrypt<SpendingConditionData>(data.spending_condition_data)
: undefined,
data.unlocking_data
? decrypt<UnlockingData>(data.unlocking_data)
: undefined,
]);

let validatedSpendingConditionData: SpendingConditionData | null = null;
Expand All @@ -393,6 +406,20 @@ export class CashuSendSwapRepository {
validatedSpendingConditionData = validationResult.data;
}

let validatedUnlockingData: UnlockingData | null = null;
if (unlockingData) {
const validationResult = UnlockingDataSchema.safeParse(unlockingData);
if (!validationResult.success) {
throw new Error('Invalid unlocking data', {
cause: {
data: unlockingData,
errors: validationResult.error.errors,
},
});
}
validatedUnlockingData = validationResult.data;
}

const toMoney = (amount: number) => {
return new Money({
amount,
Expand All @@ -414,6 +441,7 @@ export class CashuSendSwapRepository {
inputProofs,
inputAmount: toMoney(data.input_amount),
spendingConditionData: validatedSpendingConditionData,
unlockingData: validatedUnlockingData,
currency: data.currency,
version: data.version,
state: data.state,
Expand Down
34 changes: 27 additions & 7 deletions app/features/send/cashu-send-swap-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { MintOperationError, OutputData, type Proof } from '@cashu/cashu-ts';
import {
MintOperationError,
OutputData,
type Proof,
type Token,
} from '@cashu/cashu-ts';
import type { CashuAccount } from '~/features/accounts/account';
import {
CashuErrorCodes,
Expand All @@ -8,9 +13,10 @@ import {
getCashuUnit,
isCashuError,
sumProofs,
validateTokenSpendingConditions,
} from '~/lib/cashu';
import { createDeterministicP2PKData } from '~/lib/cashu/crypto';
import type { SpendingConditionData } from '~/lib/cashu/types';
import type { SpendingConditionData, UnlockingData } from '~/lib/cashu/types';
import { Money } from '~/lib/money';
import {
type CashuTokenSwapService,
Expand Down Expand Up @@ -108,6 +114,7 @@ export class CashuSendSwapService {
amount,
senderPaysFee,
spendingConditionData,
unlockingData,
}: {
/** The id of the user creating the swap */
userId: string;
Expand All @@ -118,6 +125,7 @@ export class CashuSendSwapService {
/** Whether the sender pays the fee for the swap by including the fee in the proofs to send */
senderPaysFee: boolean;
spendingConditionData?: SpendingConditionData;
unlockingData?: UnlockingData;
}): Promise<CashuSendSwap> {
if (account.currency !== amount.currency) {
throw new Error(
Expand Down Expand Up @@ -204,6 +212,7 @@ export class CashuSendSwapService {
keysetCounter: sendKeysetCounter,
tokenHash,
spendingConditionData,
unlockingData,
outputAmounts: {
send: amountsFromOutputData(sendOutputData),
keep: amountsFromOutputData(keepOutputData),
Expand Down Expand Up @@ -300,15 +309,26 @@ export class CashuSendSwapService {
throw new Error('Swap does not belong to account');
}

const token: Token = {
mint: account.mintUrl,
proofs: swap.proofsToSend,
unit: getCashuProtocolUnit(swap.currency),
};

const unlockingData = swap.unlockingData ?? undefined;
if (unlockingData) {
const result = validateTokenSpendingConditions(token, unlockingData);
if (!result.success) {
throw new Error(result.error);
}
}

return this.cashuTokenSwapService.create({
account,
userId: swap.userId,
token: {
mint: account.mintUrl,
proofs: swap.proofsToSend,
unit: getCashuProtocolUnit(swap.currency),
},
token,
reversedTransactionId: swap.transactionId,
unlockingData,
});
}

Expand Down
7 changes: 6 additions & 1 deletion app/features/send/cashu-send-swap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Proof } from '@cashu/cashu-ts';
import type { SpendingConditionData } from '~/lib/cashu/types';
import type { SpendingConditionData, UnlockingData } from '~/lib/cashu/types';
import type { Currency, Money } from '~/lib/money';

/**
Expand Down Expand Up @@ -68,6 +68,11 @@ export type CashuSendSwap = {
* All the data required to encumber the proofs with the specified spending conditions.
*/
spendingConditionData: SpendingConditionData | null;
/**
* All the data required to reclaim the proofs in the case that the user wants to reverse the swap.
* ie. refund keys
*/
unlockingData: UnlockingData | null;
/**
* - DRAFT: The swap entity has been created, but there are no proofs to send yet. At this point,
* we have only taken the inputProofs from the account
Expand Down
4 changes: 4 additions & 0 deletions app/routes/_protected.demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ function useDemoActions(
amount: amountMoney,
accountId: account.id,
spendingConditionData,
unlockingData: {
kind: 'P2PK',
signingKeys: [state.privateKey],
},
});
};

Expand Down
5 changes: 5 additions & 0 deletions supabase/database.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ export type Database = {
total_amount: number
transaction_id: string
unit: string
unlocking_data: string | null
user_id: string
version: number
}
Expand All @@ -297,6 +298,7 @@ export type Database = {
total_amount: number
transaction_id: string
unit: string
unlocking_data?: string | null
user_id: string
version?: number
}
Expand All @@ -323,6 +325,7 @@ export type Database = {
total_amount?: number
transaction_id?: string
unit?: string
unlocking_data?: string | null
user_id?: string
version?: number
}
Expand Down Expand Up @@ -796,6 +799,7 @@ export type Database = {
p_token_hash?: string
p_total_amount: number
p_unit: string
p_unlocking_data?: string
p_updated_keyset_counter?: number
p_user_id: string
}
Expand All @@ -822,6 +826,7 @@ export type Database = {
total_amount: number
transaction_id: string
unit: string
unlocking_data: string | null
user_id: string
version: number
}
Expand Down
Loading