Skip to content

Commit b47e062

Browse files
authored
fix: gate FWSS approval action by readiness
1 parent 2029dbb commit b47e062

5 files changed

Lines changed: 203 additions & 12 deletions

File tree

ui/src/lib/wallet-operation-events.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export function applyWalletOperationEventData(queryClient: QueryClient, raw: str
2424
}
2525
queryClient.invalidateQueries({ queryKey: ['walletOperations'] })
2626
queryClient.invalidateQueries({ queryKey: ['wallet'] })
27+
if (operation.type === 'approve' && operation.status === 'confirmed') {
28+
queryClient.invalidateQueries({ queryKey: ['filecoinReadiness'] })
29+
}
2730
}
2831

2932
function applyWalletOperationSnapshot(

ui/src/lib/wallet-operations.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1-
import type { PaymentAccountData, WalletOperation, WalletOperationType } from '@/api/client'
1+
import type {
2+
FilecoinReadinessData,
3+
FilecoinReadinessStatus,
4+
PaymentAccountData,
5+
WalletOperation,
6+
WalletOperationType,
7+
} from '@/api/client'
28
import { formatTokenAmount } from './utils.ts'
39

410
export type WalletRunwayTone = 'danger' | 'warning' | 'neutral'
511

12+
export interface WalletFwssApprovalState {
13+
status: FilecoinReadinessStatus
14+
message: string
15+
action?: string
16+
canApprove: boolean
17+
}
18+
619
export interface WalletOperationConfirmation {
720
title: string
821
description: string
@@ -67,6 +80,58 @@ export function walletOperationMutationError(
6780
return null
6881
}
6982

83+
export function walletFwssApprovalState({
84+
readiness,
85+
error,
86+
isLoading,
87+
}: {
88+
readiness?: FilecoinReadinessData
89+
error?: unknown
90+
isLoading?: boolean
91+
}): WalletFwssApprovalState {
92+
if (error) {
93+
return {
94+
status: 'unknown',
95+
message: 'FWSS approval status could not be checked.',
96+
canApprove: false,
97+
}
98+
}
99+
if (isLoading) {
100+
return {
101+
status: 'unknown',
102+
message: 'Checking FWSS approval status.',
103+
canApprove: false,
104+
}
105+
}
106+
if (!readiness) {
107+
return {
108+
status: 'unknown',
109+
message: 'FWSS approval status unavailable.',
110+
canApprove: false,
111+
}
112+
}
113+
const check = readiness.checks.find((item) => item.id === 'fwss_approval')
114+
if (!check) {
115+
return {
116+
status: 'unknown',
117+
message: 'FWSS approval status unavailable.',
118+
canApprove: false,
119+
}
120+
}
121+
return {
122+
status: check.status,
123+
message:
124+
check.message ||
125+
(check.status === 'ready'
126+
? 'FWSS approval is sufficient.'
127+
: check.status === 'blocked'
128+
? 'FWSS approval is required.'
129+
: 'FWSS approval status unavailable.'),
130+
action: check.action,
131+
canApprove: check.status === 'blocked',
132+
}
133+
}
134+
70135
export function buildWalletOperationConfirmation({
71136
type,
72137
amountBaseUnits,

ui/src/routes/wallet.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ import { Label } from '@/components/ui/label'
2525
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
2626
import { Skeleton } from '@/components/ui/skeleton'
2727
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
28-
import { useWallet, useWalletApprove, useWalletFund, useWalletOperations, useWalletWithdraw } from '@/hooks/queries'
28+
import {
29+
useFilecoinReadiness,
30+
useWallet,
31+
useWalletApprove,
32+
useWalletFund,
33+
useWalletOperations,
34+
useWalletWithdraw,
35+
} from '@/hooks/queries'
36+
import { filecoinReadinessStatusLabel, filecoinReadinessStatusTone } from '@/lib/filecoin-readiness'
2937
import { cn, formatAttoFIL, formatDuration, formatTokenAmount, timeAgo } from '@/lib/utils'
3038
import {
3139
baseUnitsToDecimal,
@@ -37,6 +45,7 @@ import {
3745
type WalletOperationDialogCloseReason,
3846
type WalletOperationDraft,
3947
type WalletRunwayTone,
48+
walletFwssApprovalState,
4049
walletOperationDetail,
4150
walletOperationDialogShouldClearDraft,
4251
walletOperationMutationError,
@@ -62,6 +71,7 @@ interface PendingWalletOperation extends WalletOperationDraft {
6271

6372
function WalletPage() {
6473
const { data, isLoading, error } = useWallet()
74+
const readiness = useFilecoinReadiness(Boolean(data?.configured))
6575
const [operationsLimit, setOperationsLimit] = useState(walletOperationsDefaultLimit)
6676
const { data: operationsData } = useWalletOperations(operationsLimit)
6777
const fundMutation = useWalletFund()
@@ -77,6 +87,11 @@ function WalletPage() {
7787
const paymentAccount = data?.payment_account ?? null
7888
const decimals = data?.contracts?.usdfc_decimals ?? usdfcDecimals
7989
const operations = operationsData?.operations ?? []
90+
const fwssApproval = walletFwssApprovalState({
91+
readiness: readiness.data,
92+
error: readiness.error,
93+
isLoading: readiness.isLoading || readiness.isFetching,
94+
})
8095
const mutationError = walletOperationMutationError(
8196
pendingOperation?.type,
8297
fundMutation.error,
@@ -297,15 +312,21 @@ function WalletPage() {
297312
<div className="rounded-md border border-border p-4">
298313
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
299314
<div>
300-
<div className="text-sm font-medium">FWSS approval</div>
301-
<p className="mt-1 text-sm text-muted-foreground">
302-
Allow FWSS to spend USDFC for storage payments. This does not deposit or withdraw funds.
303-
</p>
315+
<div className="flex flex-wrap items-center gap-2">
316+
<div className="text-sm font-medium">FWSS approval</div>
317+
<StatusBadge tone={filecoinReadinessStatusTone(fwssApproval.status)}>
318+
{filecoinReadinessStatusLabel(fwssApproval.status)}
319+
</StatusBadge>
320+
</div>
321+
<p className="mt-1 text-sm text-muted-foreground">{fwssApproval.message}</p>
322+
{fwssApproval.action && <p className="mt-1 text-xs text-muted-foreground">{fwssApproval.action}</p>}
304323
</div>
305-
<Button type="button" variant="outline" onClick={submitApprove} disabled={isMutating}>
306-
<ShieldCheck data-icon="inline-start" />
307-
Approve FWSS
308-
</Button>
324+
{fwssApproval.canApprove && (
325+
<Button type="button" variant="outline" onClick={submitApprove} disabled={isMutating}>
326+
<ShieldCheck data-icon="inline-start" />
327+
Approve FWSS
328+
</Button>
329+
)}
309330
</div>
310331
</div>
311332
<div className="flex flex-col gap-2">

ui/test/wallet-operation-events.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { QueryClient } from '@tanstack/react-query'
55
import type { WalletOperation, WalletOperationsResponse } from '../src/api/client.ts'
66
import { applyWalletOperationEventData } from '../src/lib/wallet-operation-events.ts'
77

8-
function walletOperation(id: number, createdAt: string): WalletOperation {
8+
function walletOperation(id: number, createdAt: string, overrides: Partial<WalletOperation> = {}): WalletOperation {
99
return {
1010
id,
1111
type: 'fund',
@@ -14,6 +14,7 @@ function walletOperation(id: number, createdAt: string): WalletOperation {
1414
status: 'submitted',
1515
created_at: createdAt,
1616
updated_at: createdAt,
17+
...overrides,
1718
}
1819
}
1920

@@ -64,3 +65,34 @@ test('wallet operation events grow up to the selected operation count', () => {
6465
[4, 3, 2, 1]
6566
)
6667
})
68+
69+
test('confirmed approve events invalidate Filecoin readiness', () => {
70+
const qc = new QueryClient()
71+
const readinessKey = ['filecoinReadiness']
72+
qc.setQueryData(readinessKey, { status: 'blocked' })
73+
74+
applyWalletOperationEventData(
75+
qc,
76+
JSON.stringify({
77+
topic: 'wallet_operation_updated',
78+
operation: walletOperation(1, '2026-06-22T00:00:00Z', {
79+
type: 'approve',
80+
amount: '0',
81+
}),
82+
})
83+
)
84+
assert.equal(qc.getQueryState(readinessKey)?.isInvalidated, false)
85+
86+
applyWalletOperationEventData(
87+
qc,
88+
JSON.stringify({
89+
topic: 'wallet_operation_updated',
90+
operation: walletOperation(1, '2026-06-22T00:00:00Z', {
91+
type: 'approve',
92+
amount: '0',
93+
status: 'confirmed',
94+
}),
95+
})
96+
)
97+
assert.equal(qc.getQueryState(readinessKey)?.isInvalidated, true)
98+
})

ui/test/wallet-operations.test.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import assert from 'node:assert/strict'
22
import test from 'node:test'
33

4-
import type { WalletOperation } from '../src/api/client.ts'
4+
import type { FilecoinReadinessData, WalletOperation } from '../src/api/client.ts'
55
import {
66
buildWalletOperationConfirmation,
77
createWalletOperationDraft,
88
decimalToBaseUnits,
99
fundedUntilCaption,
1010
fundedUntilTone,
1111
topUpNeeded,
12+
walletFwssApprovalState,
1213
walletOperationDetail,
1314
walletOperationDialogShouldClearDraft,
1415
walletOperationMutationError,
@@ -71,6 +72,63 @@ test('wallet operation mutation error follows the active operation type', () =>
7172
assert.equal(walletOperationMutationError(null, fundError, withdrawError, approveError), null)
7273
})
7374

75+
test('wallet FWSS approval action follows readiness state', () => {
76+
const ready = walletFwssApprovalState({
77+
readiness: readinessData('ready', [
78+
{ id: 'fwss_approval', status: 'ready', message: 'FWSS approval is sufficient.' },
79+
]),
80+
})
81+
assert.equal(ready.status, 'ready')
82+
assert.equal(ready.canApprove, false)
83+
84+
const blocked = walletFwssApprovalState({
85+
readiness: readinessData('blocked', [
86+
{
87+
id: 'fwss_approval',
88+
status: 'blocked',
89+
message: 'FWSS payment approval is missing or too low.',
90+
action: 'Approve FWSS spending before uploading to Filecoin.',
91+
},
92+
]),
93+
})
94+
assert.equal(blocked.status, 'blocked')
95+
assert.equal(blocked.canApprove, true)
96+
assert.equal(blocked.action, 'Approve FWSS spending before uploading to Filecoin.')
97+
98+
const blockedWithoutMessage = walletFwssApprovalState({
99+
readiness: readinessData('blocked', [{ id: 'fwss_approval', status: 'blocked', message: '' }]),
100+
})
101+
assert.equal(blockedWithoutMessage.message, 'FWSS approval is required.')
102+
103+
const unknown = walletFwssApprovalState({
104+
readiness: readinessData('unknown', [
105+
{ id: 'fwss_approval', status: 'unknown', message: 'FWSS approval could not be checked.' },
106+
]),
107+
})
108+
assert.equal(unknown.status, 'unknown')
109+
assert.equal(unknown.canApprove, false)
110+
111+
assert.equal(walletFwssApprovalState({ readiness: readinessData('ready', []) }).canApprove, false)
112+
assert.equal(
113+
walletFwssApprovalState({
114+
readiness: readinessData('blocked', [
115+
{ id: 'fwss_approval', status: 'blocked', message: 'FWSS approval is required.' },
116+
]),
117+
isLoading: true,
118+
}).canApprove,
119+
false
120+
)
121+
assert.equal(
122+
walletFwssApprovalState({
123+
readiness: readinessData('blocked', [
124+
{ id: 'fwss_approval', status: 'blocked', message: 'FWSS approval is required.' },
125+
]),
126+
error: new Error('readiness unavailable'),
127+
}).canApprove,
128+
false
129+
)
130+
})
131+
74132
test('wallet operation detail exposes failure reason', () => {
75133
const operation: WalletOperation = {
76134
id: 1,
@@ -179,3 +237,15 @@ test('funded until tone highlights low runway ranges', () => {
179237
assert.equal(fundedUntilTone({ ...base, runway_seconds: 45 * 86_400 }), 'neutral')
180238
assert.equal(fundedUntilTone({ ...base, no_active_spend: true, runway_seconds: undefined }), 'neutral')
181239
})
240+
241+
function readinessData(
242+
status: FilecoinReadinessData['status'],
243+
checks: FilecoinReadinessData['checks']
244+
): FilecoinReadinessData {
245+
return {
246+
status,
247+
mode: 'runtime',
248+
checked_at: '2026-06-22T00:00:00Z',
249+
checks,
250+
}
251+
}

0 commit comments

Comments
 (0)