Skip to content

Commit 24dd5e3

Browse files
committed
unify contract handling
1 parent 9a9baea commit 24dd5e3

6 files changed

Lines changed: 68 additions & 45 deletions

File tree

src/components/swap/SwapContainer.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function SwapContainer() {
5151
dismissError: dismissSwapError,
5252
} = useSwap();
5353

54-
const subscriptionStatus = useSubscriptionStatus(isSwapping, swapPhase, dripPhase);
54+
const subscriptionStatus = useSubscriptionStatus(swapPhase, dripPhase);
5555
const isBlocked = subscriptionStatus.kind === 'full' || subscriptionStatus.kind === 'depleted';
5656

5757
// Drip success banner
@@ -108,7 +108,8 @@ export function SwapContainer() {
108108
}
109109
}, [onboardingStatus, refetchBalances]);
110110

111-
// Refetch balances when swap succeeds
111+
// Batched refresh after swap or drip success — single wallet roundtrip for
112+
// exchange rate + balances + subscription status
112113
useEffect(() => {
113114
if (swapPhase === 'success') {
114115
refetchBalances();

src/contexts/contracts/ContractsContext.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createContext, useContext, useEffect, type ReactNode, useCallback } fro
77
import type { AztecAddress } from '@aztec/aztec.js/addresses';
88
import type { TxReceipt } from '@aztec/stdlib/tx';
99
import type { AMMContract } from '../../../contracts/target/AMM';
10+
import type { SubscriptionFPC } from '@gregojuice/contracts/subscription-fpc';
1011
import { useWallet } from '../wallet';
1112
import { useNetwork } from '../network';
1213
import * as contractService from '../../services/contractService';
@@ -21,6 +22,7 @@ interface ContractsContextType {
2122

2223
// Utility methods
2324
getAmm: () => AMMContract | null;
25+
getFpc: () => SubscriptionFPC | null;
2426
getExchangeRate: () => Promise<number>;
2527
swap: (amountOut: number, amountInMax: number) => Promise<TxReceipt>;
2628
unsponsoredSwap: (amountOut: number, amountInMax: number) => Promise<TxReceipt>;
@@ -88,6 +90,11 @@ export function ContractsProvider({ children }: ContractsProviderProps) {
8890
return state.contracts.amm ?? null;
8991
}, [state.contracts.amm]);
9092

93+
// Get FPC wrapper instance (for hooks that need it)
94+
const getFpc = useCallback((): SubscriptionFPC | null => {
95+
return state.contracts.fpc ?? null;
96+
}, [state.contracts.fpc]);
97+
9198
// Get exchange rate
9299
const getExchangeRate = useCallback(async (): Promise<number> => {
93100
if (
@@ -106,6 +113,7 @@ export function ContractsProvider({ children }: ContractsProviderProps) {
106113
gregoCoin: state.contracts.gregoCoin,
107114
gregoCoinPremium: state.contracts.gregoCoinPremium,
108115
amm: state.contracts.amm,
116+
fpc: state.contracts.fpc,
109117
},
110118
currentAddress,
111119
);
@@ -119,17 +127,18 @@ export function ContractsProvider({ children }: ContractsProviderProps) {
119127
!currentAddress ||
120128
!state.contracts.amm ||
121129
!state.contracts.gregoCoin ||
122-
!state.contracts.gregoCoinPremium
130+
!state.contracts.gregoCoinPremium ||
131+
!state.contracts.fpc
123132
) {
124133
throw new Error('Contracts not initialized');
125134
}
126135

127136
return contractService.executeSponsoredSwap(
128-
wallet,
129137
activeNetwork,
130138
state.contracts.amm,
131139
state.contracts.gregoCoin,
132140
state.contracts.gregoCoinPremium,
141+
state.contracts.fpc,
133142
currentAddress,
134143
amountOut,
135144
amountInMax,
@@ -156,6 +165,7 @@ export function ContractsProvider({ children }: ContractsProviderProps) {
156165
gregoCoin: state.contracts.gregoCoin,
157166
gregoCoinPremium: state.contracts.gregoCoinPremium,
158167
amm: state.contracts.amm,
168+
fpc: state.contracts.fpc,
159169
},
160170
currentAddress,
161171
amountOut,
@@ -177,6 +187,7 @@ export function ContractsProvider({ children }: ContractsProviderProps) {
177187
gregoCoin: state.contracts.gregoCoin,
178188
gregoCoinPremium: state.contracts.gregoCoinPremium,
179189
amm: state.contracts.amm!,
190+
fpc: state.contracts.fpc,
180191
},
181192
currentAddress,
182193
);
@@ -200,6 +211,7 @@ export function ContractsProvider({ children }: ContractsProviderProps) {
200211
gregoCoin: state.contracts.gregoCoin,
201212
gregoCoinPremium: state.contracts.gregoCoinPremium,
202213
amm: state.contracts.amm,
214+
fpc: state.contracts.fpc,
203215
},
204216
currentAddress,
205217
);
@@ -210,13 +222,20 @@ export function ContractsProvider({ children }: ContractsProviderProps) {
210222
// Execute drip
211223
const drip = useCallback(
212224
async (password: string, recipient: AztecAddress): Promise<TxReceipt> => {
213-
if (!wallet || !node || !state.contracts.pop) {
225+
if (!wallet || !node || !state.contracts.pop || !state.contracts.fpc) {
214226
throw new Error('ProofOfPassword contract not initialized');
215227
}
216228

217-
return contractService.executeDrip(wallet, activeNetwork, state.contracts.pop, password, recipient);
229+
return contractService.executeDrip(
230+
wallet,
231+
activeNetwork,
232+
state.contracts.pop,
233+
state.contracts.fpc,
234+
password,
235+
recipient,
236+
);
218237
},
219-
[wallet, activeNetwork, state.contracts.pop],
238+
[wallet, activeNetwork, state.contracts.pop, state.contracts.fpc],
220239
);
221240

222241
// Initialize contracts for embedded wallet
@@ -247,6 +266,7 @@ export function ContractsProvider({ children }: ContractsProviderProps) {
247266
registerBaseContracts,
248267
registerDripContracts,
249268
getAmm,
269+
getFpc,
250270
getExchangeRate,
251271
swap,
252272
unsponsoredSwap,

src/contexts/contracts/reducer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import type { TokenContract } from '@aztec/noir-contracts.js/Token';
77
import type { AMMContract } from '../../../contracts/target/AMM';
88
import type { ProofOfPasswordContract } from '../../../contracts/target/ProofOfPassword';
9+
import type { SubscriptionFPC } from '@gregojuice/contracts/subscription-fpc';
910
import { createReducerHook, type ActionsFrom } from '../utils';
1011

1112
// =============================================================================
@@ -17,6 +18,7 @@ export interface Contracts {
1718
gregoCoinPremium: TokenContract | null;
1819
amm: AMMContract | null;
1920
pop: ProofOfPasswordContract | null;
21+
fpc: SubscriptionFPC | null;
2022
}
2123

2224
export type ContractRegistrationStage = 'base' | 'drip';
@@ -32,6 +34,7 @@ export const initialContractsState: ContractsState = {
3234
gregoCoinPremium: null,
3335
amm: null,
3436
pop: null,
37+
fpc: null,
3538
},
3639
isLoading: true,
3740
};

src/contexts/swap/SwapContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ export function SwapProvider({ children }: SwapProviderProps) {
214214
}
215215
}
216216
},
217-
[state.exchangeRate, actions]
217+
[state.exchangeRate, actions],
218218
);
219219

220220
const setToAmount = useCallback(
@@ -230,7 +230,7 @@ export function SwapProvider({ children }: SwapProviderProps) {
230230
}
231231
}
232232
},
233-
[state.exchangeRate, actions]
233+
[state.exchangeRate, actions],
234234
);
235235

236236
// Computed values

src/hooks/useSubscriptionStatus.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { useContracts } from '../contexts/contracts';
66
import { useNetwork } from '../contexts/network';
77
import { useOnboarding } from '../contexts/onboarding';
88

9-
export function useSubscriptionStatus(isSwapping: boolean, swapPhase: string, dripPhase: string): SubscriptionStatus {
10-
const { wallet, currentAddress } = useWallet();
11-
const { getAmm } = useContracts();
9+
export function useSubscriptionStatus(swapPhase: string, dripPhase: string): SubscriptionStatus {
10+
const { currentAddress } = useWallet();
11+
const { getAmm, getFpc } = useContracts();
1212
const { activeNetwork } = useNetwork();
1313
const { status: onboardingStatus } = useOnboarding();
1414
const [status, setStatus] = useState<SubscriptionStatus>({ kind: 'no_fpc' });
@@ -25,22 +25,23 @@ export function useSubscriptionStatus(isSwapping: boolean, swapPhase: string, dr
2525

2626
const fetchStatus = useCallback(async () => {
2727
const amm = getAmm();
28-
if (!wallet || !currentAddress || !amm || !isOnboarded) return;
28+
const fpc = getFpc();
29+
if (!currentAddress || !amm || !isOnboarded) return;
2930
if (!activeNetwork.subscriptionFPC) {
3031
setStatus({ kind: 'no_fpc' });
3132
return;
3233
}
3334
if (isFetchingRef.current) return;
3435
isFetchingRef.current = true;
3536
try {
36-
const result = await querySubscriptionStatus(wallet, activeNetwork, amm, currentAddress);
37+
const result = await querySubscriptionStatus(activeNetwork, amm, currentAddress, fpc);
3738
setStatus(result);
3839
} catch {
3940
// Leave previous status on transient error to avoid flicker
4041
} finally {
4142
isFetchingRef.current = false;
4243
}
43-
}, [wallet, currentAddress, activeNetwork, getAmm, isOnboarded]);
44+
}, [currentAddress, activeNetwork, getAmm, getFpc, isOnboarded]);
4445

4546
// Fetch after onboarding completes
4647
useEffect(() => {

src/services/contractService.ts

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@
33
* Pure functions for contract-related operations
44
*/
55

6-
import type { Wallet } from '@aztec/aztec.js/wallet';
6+
import type { BatchedMethod, Wallet, TxSimulationResultWithAppOffset } from '@aztec/aztec.js/wallet';
77
import type { AztecNode } from '@aztec/aztec.js/node';
88
import { AztecAddress } from '@aztec/aztec.js/addresses';
99
import { AztecAddress as AztecAddressClass } from '@aztec/aztec.js/addresses';
1010
import { Fr } from '@aztec/aztec.js/fields';
1111
import { FunctionSelector } from '@aztec/aztec.js/abi';
1212
import { BatchCall, getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts';
1313
import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon';
14+
import { type FunctionCall, decodeFromAbi } from '@aztec/stdlib/abi';
15+
import { ExecutionPayload } from '@aztec/stdlib/tx';
16+
import { UtilityExecutionResult } from '@aztec/stdlib/tx';
1417
import type { TxReceipt } from '@aztec/stdlib/tx';
1518
import type { TokenContract } from '@aztec/noir-contracts.js/Token';
1619
import type { AMMContract } from '../../contracts/target/AMM';
1720
import type { ProofOfPasswordContract } from '../../contracts/target/ProofOfPassword';
21+
import { SubscriptionFPC } from '@gregojuice/contracts/subscription-fpc';
1822
import { BigDecimal } from '../utils/bigDecimal';
1923
import type { NetworkConfig } from '../config/networks';
2024
import type { OnboardingResult } from '../contexts/onboarding/reducer';
@@ -26,13 +30,15 @@ export interface SwapContracts {
2630
gregoCoin: TokenContract;
2731
gregoCoinPremium: TokenContract;
2832
amm: AMMContract;
33+
fpc: SubscriptionFPC | null;
2934
}
3035

3136
/**
3237
* Contracts returned after drip registration
3338
*/
3439
export interface DripContracts {
3540
pop: ProofOfPasswordContract;
41+
fpc: SubscriptionFPC | null;
3642
}
3743

3844
/**
@@ -138,7 +144,10 @@ export async function registerSwapContracts(
138144
const gregoCoinPremium = TokenContract.at(gregoCoinPremiumAddress, wallet);
139145
const amm = AMMContract.at(ammAddress, wallet);
140146

141-
return { gregoCoin, gregoCoinPremium, amm };
147+
// Instantiate FPC wrapper if configured
148+
const fpc = subFPC && fpcAddress ? SubscriptionFPC.at(fpcAddress, wallet) : null;
149+
150+
return { gregoCoin, gregoCoinPremium, amm, fpc };
142151
}
143152

144153
/**
@@ -206,7 +215,11 @@ export async function registerDripContracts(
206215
// Instantiate the ProofOfPassword contract
207216
const pop = ProofOfPasswordContract.at(popAddress, wallet);
208217

209-
return { pop };
218+
// Instantiate FPC wrapper if configured
219+
const fpcAddr = subFPC ? AztecAddressClass.fromString(subFPC.address) : undefined;
220+
const fpc = fpcAddr ? SubscriptionFPC.at(fpcAddr, wallet) : null;
221+
222+
return { pop, fpc };
210223
}
211224

212225
/**
@@ -339,11 +352,11 @@ function markSubscribed(fpcAddress: string, configIndex: number, userAddress: st
339352
* Uses subscribe on first call, sponsor on subsequent calls.
340353
*/
341354
export async function executeSponsoredSwap(
342-
wallet: Wallet,
343355
network: NetworkConfig,
344356
amm: SwapContracts['amm'],
345357
gregoCoin: SwapContracts['gregoCoin'],
346358
gregoCoinPremium: SwapContracts['gregoCoinPremium'],
359+
fpc: SubscriptionFPC,
347360
userAddress: AztecAddress,
348361
amountOut: number,
349362
amountInMax: number,
@@ -372,12 +385,6 @@ export async function executeSponsoredSwap(
372385
);
373386
}
374387

375-
const fpcAddress = AztecAddressClass.fromString(subFPC.address);
376-
const { SubscriptionFPCContract } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC');
377-
const { SubscriptionFPC } = await import('@gregojuice/contracts/subscription-fpc');
378-
const rawFPC = SubscriptionFPCContract.at(fpcAddress, wallet);
379-
const fpc = new SubscriptionFPC(rawFPC);
380-
381388
const subscribed = hasSubscription(subFPC.address, configIndex, userAddress.toString());
382389

383390
if (subscribed) {
@@ -422,12 +429,12 @@ export async function executeUnsponsoredSwap(
422429
}
423430

424431
export type SubscriptionStatusKind =
425-
| 'loading' // query in flight
426-
| 'no_fpc' // no FPC configured for this network — hide everything
427-
| 'sponsored' // user not yet subscribed, slots available — first swap will be free
428-
| 'active' // user has subscription with uses remaining — swap is free
429-
| 'full' // no slots left, user never subscribed — must bridge
430-
| 'depleted'; // user's uses exhausted — must bridge
432+
| 'loading' // query in flight
433+
| 'no_fpc' // no FPC configured for this network — hide everything
434+
| 'sponsored' // user not yet subscribed, slots available — first swap will be free
435+
| 'active' // user has subscription with uses remaining — swap is free
436+
| 'full' // no slots left, user never subscribed — must bridge
437+
| 'depleted'; // user's uses exhausted — must bridge
431438

432439
export interface SubscriptionStatus {
433440
kind: SubscriptionStatusKind;
@@ -440,13 +447,13 @@ export interface SubscriptionStatus {
440447
* Returns the status kind based on available slots and user subscription state.
441448
*/
442449
export async function querySubscriptionStatus(
443-
wallet: Wallet,
444450
network: NetworkConfig,
445451
amm: SwapContracts['amm'],
446452
userAddress: AztecAddress,
453+
fpc: SubscriptionFPC | null,
447454
): Promise<SubscriptionStatus> {
448455
const subFPC = network.subscriptionFPC;
449-
if (!subFPC) return { kind: 'no_fpc' };
456+
if (!subFPC || !fpc) return { kind: 'no_fpc' };
450457

451458
// Derive configIndex + selector from the AMM's function map — take the first entry
452459
const ammFunctions = subFPC.functions[amm.address.toString()];
@@ -458,15 +465,11 @@ export async function querySubscriptionStatus(
458465
const selector = FunctionSelector.fromString(selectorHex);
459466
const configId = await poseidon2Hash([amm.address.toField(), selector.toField(), new Fr(configIndex)]);
460467

461-
const fpcAddress = AztecAddressClass.fromString(subFPC.address);
462-
const { SubscriptionFPCContract } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC');
463-
const rawFPC = SubscriptionFPCContract.at(fpcAddress, wallet);
464-
465468
// SlotNote is owned by the FPC — must simulate from fpc.address
466469
// SubscriptionNote is owned by the user — must simulate from userAddress
467470
const [{ result: slotsResult }, { result: subInfoResult }] = await Promise.all([
468-
rawFPC.methods.count_available_slots(configId).simulate({ from: fpcAddress }),
469-
rawFPC.methods.get_subscription_info(userAddress, configId).simulate({ from: userAddress }),
471+
fpc.methods.count_available_slots(configId).simulate({ from: fpc.address }),
472+
fpc.methods.get_subscription_info(userAddress, configId).simulate({ from: userAddress }),
470473
]);
471474

472475
const availableSlots = Number(slotsResult);
@@ -510,6 +513,7 @@ export async function executeDrip(
510513
wallet: Wallet,
511514
network: NetworkConfig,
512515
pop: ProofOfPasswordContract,
516+
fpc: SubscriptionFPC,
513517
password: string,
514518
recipient: AztecAddress,
515519
): Promise<TxReceipt> {
@@ -524,12 +528,6 @@ export async function executeDrip(
524528
throw new Error(`No subscription config found for ${pop.address.toString()} selector ${call.selector.toString()}`);
525529
}
526530

527-
const fpcAddress = AztecAddressClass.fromString(subFPC.address);
528-
const { SubscriptionFPCContract } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC');
529-
const { SubscriptionFPC } = await import('@gregojuice/contracts/subscription-fpc');
530-
const rawFPC = SubscriptionFPCContract.at(fpcAddress, wallet);
531-
const fpc = new SubscriptionFPC(rawFPC);
532-
533531
const accounts = await wallet.getAccounts();
534532
const userAddress = accounts[0]?.item ?? recipient;
535533

0 commit comments

Comments
 (0)