frontier-os-app-builder 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/agents/fos-executor.md +22 -65
- package/agents/fos-plan-checker.md +13 -12
- package/agents/fos-planner.md +20 -67
- package/agents/fos-researcher.md +14 -10
- package/agents/fos-verifier.md +11 -5
- package/bin/fos-tools.cjs +48 -11
- package/bin/install.js +8 -5
- package/commands/fos/add-feature.md +1 -2
- package/commands/fos/discuss.md +0 -1
- package/commands/fos/new-app.md +1 -3
- package/commands/fos/new-milestone.md +1 -1
- package/commands/fos/plan.md +0 -2
- package/package.json +7 -1
- package/references/app-patterns.md +46 -28
- package/references/deployment.md +40 -74
- package/references/module-index.md +32 -0
- package/references/sdk/chain.md +92 -0
- package/references/sdk/communities.md +159 -0
- package/references/sdk/events.md +212 -0
- package/references/sdk/init.md +126 -0
- package/references/sdk/navigation.md +49 -0
- package/references/sdk/offices.md +76 -0
- package/references/sdk/partnerships.md +111 -0
- package/references/sdk/storage.md +44 -0
- package/references/sdk/thirdparty.md +240 -0
- package/references/sdk/token-amount.md +99 -0
- package/references/sdk/types.md +27 -0
- package/references/sdk/ui-utils.md +39 -0
- package/references/sdk/user.md +208 -0
- package/references/sdk/wallet.md +334 -0
- package/references/verification-rules.md +18 -18
- package/templates/app/frontier-services.tsx +75 -18
- package/templates/app/layout.tsx +19 -9
- package/templates/app/package.json +2 -1
- package/templates/app/public/favicon.svg +3 -0
- package/templates/app/sdk-context.tsx +7 -9
- package/templates/app/sdk-services.tsx +92 -117
- package/templates/app/vercel.json +8 -47
- package/templates/state/plan.md +32 -14
- package/templates/state/roadmap.md +2 -2
- package/templates/state/summary.md +26 -29
- package/workflows/add-feature.md +6 -1
- package/workflows/discuss.md +9 -3
- package/workflows/execute-plan.md +3 -3
- package/workflows/execute.md +17 -6
- package/workflows/new-app.md +54 -18
- package/workflows/new-milestone.md +9 -2
- package/workflows/plan.md +14 -5
- package/workflows/ship.md +26 -10
- package/workflows/status.md +0 -1
- package/references/module-inference.md +0 -348
- package/references/sdk-surface.md +0 -1600
- package/templates/app/main-simple-standalone.tsx +0 -19
- package/templates/app/main-simple.tsx +0 -19
- package/templates/state/manifest.json +0 -12
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# Wallet Module
|
|
2
|
+
|
|
3
|
+
**Trigger keywords:** payment, pay, charge, pos, checkout, purchase, buy, sell, transfer, send money, balance, funds, money, wallet, fnd, swap, exchange, convert, token, deposit, on-ramp, fund, withdraw, off-ramp, bank, fiat, subscription, billing, price, cost, fee, tip, donate, donation
|
|
4
|
+
|
|
5
|
+
Access via `sdk.getWallet()`. All methods use the current chain from the chain manager. Write operations require biometric authentication.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Methods
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
getBalance(): Promise<WalletBalance>
|
|
13
|
+
```
|
|
14
|
+
Returns raw balance breakdown (bigint values). To display, format each bigint field with `formatAmount()` from `@frontiertower/frontier-sdk`. Permission: `wallet:getBalance`
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
getAddress(): Promise<string>
|
|
18
|
+
```
|
|
19
|
+
Returns the smart account contract address for the current chain. Permission: `wallet:getAddress`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
getSmartAccount(): Promise<SmartAccount>
|
|
23
|
+
```
|
|
24
|
+
Returns detailed smart account info including deployment status. Permission: `wallet:getSmartAccount`
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
transferERC20(
|
|
28
|
+
tokenAddress: string,
|
|
29
|
+
to: string,
|
|
30
|
+
amount: bigint,
|
|
31
|
+
overrides?: GasOverrides
|
|
32
|
+
): Promise<UserOperationReceipt>
|
|
33
|
+
```
|
|
34
|
+
Transfer ERC20 tokens. Amount in token's smallest unit. Permission: `wallet:transferERC20`
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
approveERC20(
|
|
38
|
+
tokenAddress: string,
|
|
39
|
+
spender: string,
|
|
40
|
+
amount: bigint,
|
|
41
|
+
overrides?: GasOverrides
|
|
42
|
+
): Promise<UserOperationReceipt>
|
|
43
|
+
```
|
|
44
|
+
Approve a spender for ERC20 tokens. Permission: `wallet:approveERC20`
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
transferNative(
|
|
48
|
+
to: string,
|
|
49
|
+
amount: bigint,
|
|
50
|
+
overrides?: GasOverrides
|
|
51
|
+
): Promise<UserOperationReceipt>
|
|
52
|
+
```
|
|
53
|
+
Transfer native currency (ETH). Amount in wei. Permission: `wallet:transferNative`
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
executeCall(
|
|
57
|
+
call: ExecuteCall,
|
|
58
|
+
overrides?: GasOverrides
|
|
59
|
+
): Promise<UserOperationReceipt>
|
|
60
|
+
```
|
|
61
|
+
Execute an arbitrary contract call. Permission: `wallet:executeCall`
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
executeBatchCall(
|
|
65
|
+
calls: ExecuteCall[],
|
|
66
|
+
overrides?: GasOverrides
|
|
67
|
+
): Promise<UserOperationReceipt>
|
|
68
|
+
```
|
|
69
|
+
Execute multiple calls atomically in a single transaction. Permission: `wallet:executeBatchCall`
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
transferFrontierDollar(
|
|
73
|
+
to: string,
|
|
74
|
+
amount: bigint,
|
|
75
|
+
overrides?: GasOverrides
|
|
76
|
+
): Promise<UserOperationReceipt>
|
|
77
|
+
```
|
|
78
|
+
Transfer FND (Frontier Network Dollar). Amount in base units — build with `parseAmount('10.5')` from `@frontiertower/frontier-sdk`. Permission: `wallet:transferFrontierDollar`
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
transferInternalFrontierDollar(
|
|
82
|
+
to: string,
|
|
83
|
+
amount: bigint,
|
|
84
|
+
overrides?: GasOverrides
|
|
85
|
+
): Promise<UserOperationReceipt>
|
|
86
|
+
```
|
|
87
|
+
Transfer iFND (Internal Frontier Network Dollar). Amount in base units — build with `parseAmount('10.5')` from `@frontiertower/frontier-sdk`. Permission: `wallet:transferInternalFrontierDollar`
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
transferOverallFrontierDollar(
|
|
91
|
+
to: string,
|
|
92
|
+
amount: bigint,
|
|
93
|
+
overrides?: GasOverrides
|
|
94
|
+
): Promise<UserOperationReceipt>
|
|
95
|
+
```
|
|
96
|
+
Transfer using iFND first, falling back to FND for the remainder. Permission: `wallet:transferOverallFrontierDollar`
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
getSupportedTokens(): Promise<string[]>
|
|
100
|
+
```
|
|
101
|
+
Returns token symbols supported for swaps on the current chain (e.g. `['FND', 'USDC', 'WETH']`). Permission: `wallet:getSupportedTokens`
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
swap(
|
|
105
|
+
sourceToken: string,
|
|
106
|
+
targetToken: string,
|
|
107
|
+
sourceNetwork: string,
|
|
108
|
+
targetNetwork: string,
|
|
109
|
+
amount: bigint
|
|
110
|
+
): Promise<SwapResult>
|
|
111
|
+
```
|
|
112
|
+
Execute a token swap (same-chain or cross-chain). Amount in base units — build with `parseAmount('10.5')` from `@frontiertower/frontier-sdk`. Permission: `wallet:swap`
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
quoteSwap(
|
|
116
|
+
sourceToken: string,
|
|
117
|
+
targetToken: string,
|
|
118
|
+
sourceNetwork: string,
|
|
119
|
+
targetNetwork: string,
|
|
120
|
+
amount: bigint
|
|
121
|
+
): Promise<SwapQuote>
|
|
122
|
+
```
|
|
123
|
+
Get a swap quote without executing. Permission: `wallet:quoteSwap`
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
getUsdDepositInstructions(): Promise<OnRampResponse<UsdDepositInstructions>>
|
|
127
|
+
```
|
|
128
|
+
Get US bank details for fiat-to-crypto on-ramp. Requires approved KYC. Permission: `wallet:getUsdDepositInstructions`
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
getEurDepositInstructions(): Promise<OnRampResponse<EurDepositInstructions>>
|
|
132
|
+
```
|
|
133
|
+
Get SEPA bank details for EUR fiat-to-crypto on-ramp. Requires approved KYC. Permission: `wallet:getEurDepositInstructions`
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
getLinkedBanks(): Promise<LinkedBanksResponse>
|
|
137
|
+
```
|
|
138
|
+
Get all linked bank accounts for off-ramp withdrawals. Requires approved KYC. Permission: `wallet:getLinkedBanks`
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
linkUsBankAccount(
|
|
142
|
+
accountOwnerName: string,
|
|
143
|
+
bankName: string,
|
|
144
|
+
routingNumber: string,
|
|
145
|
+
accountNumber: string,
|
|
146
|
+
checkingOrSavings: 'checking' | 'savings',
|
|
147
|
+
address: BillingAddress
|
|
148
|
+
): Promise<LinkBankResponse>
|
|
149
|
+
```
|
|
150
|
+
Link a US bank account for USD withdrawals via ACH. Requires approved KYC. Permission: `wallet:linkUsBankAccount`
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
linkEuroAccount(
|
|
154
|
+
accountOwnerName: string,
|
|
155
|
+
accountOwnerType: AccountOwnerType,
|
|
156
|
+
firstName: string,
|
|
157
|
+
lastName: string,
|
|
158
|
+
ibanAccountNumber: string,
|
|
159
|
+
bic?: string
|
|
160
|
+
): Promise<LinkBankResponse>
|
|
161
|
+
```
|
|
162
|
+
Link a EUR/IBAN bank account for SEPA withdrawals. Requires approved KYC. Permission: `wallet:linkEuroAccount`
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
deleteLinkedBank(bankId: string): Promise<void>
|
|
166
|
+
```
|
|
167
|
+
Delete a linked bank account. Permission: `wallet:deleteLinkedBank`
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
getDeprecatedSmartAccounts(): Promise<DeprecatedSmartAccount[]>
|
|
171
|
+
```
|
|
172
|
+
Get deprecated smart accounts that still have active gas sponsorship. Permission: `wallet:getDeprecatedSmartAccounts`
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Types
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
interface SmartAccount {
|
|
180
|
+
id: number;
|
|
181
|
+
ownerAddress: string;
|
|
182
|
+
contractAddress: string | null;
|
|
183
|
+
network: string;
|
|
184
|
+
status: string;
|
|
185
|
+
deploymentTransactionHash: string;
|
|
186
|
+
createdAt: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface WalletBalance {
|
|
190
|
+
total: bigint;
|
|
191
|
+
fnd: bigint;
|
|
192
|
+
internalFnd: bigint;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface UserOperationReceipt {
|
|
196
|
+
userOpHash: string;
|
|
197
|
+
transactionHash: string;
|
|
198
|
+
blockNumber: bigint;
|
|
199
|
+
success: boolean;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface GasOverrides {
|
|
203
|
+
maxFeePerGas?: bigint;
|
|
204
|
+
maxPriorityFeePerGas?: bigint;
|
|
205
|
+
gasLimit?: bigint;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface ExecuteCall {
|
|
209
|
+
target: string;
|
|
210
|
+
value?: bigint;
|
|
211
|
+
data: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
interface SwapParams {
|
|
215
|
+
sourceToken: string;
|
|
216
|
+
targetToken: string;
|
|
217
|
+
sourceNetwork: string;
|
|
218
|
+
targetNetwork: string;
|
|
219
|
+
amount: bigint;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
enum SwapResultStatus {
|
|
223
|
+
COMPLETED = 'COMPLETED',
|
|
224
|
+
SUBMITTED = 'SUBMITTED',
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface SwapResult {
|
|
228
|
+
sourceChain: object;
|
|
229
|
+
targetChain: object;
|
|
230
|
+
sourceToken: object;
|
|
231
|
+
targetToken: object;
|
|
232
|
+
status: SwapResultStatus;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface SwapQuote {
|
|
236
|
+
sourceChain: object;
|
|
237
|
+
targetChain: object;
|
|
238
|
+
sourceToken: object;
|
|
239
|
+
targetToken: object;
|
|
240
|
+
expectedAmountOut: bigint;
|
|
241
|
+
minAmountOut: bigint;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface UsdDepositInstructions {
|
|
245
|
+
currency: 'usd';
|
|
246
|
+
bankName: string;
|
|
247
|
+
bankAddress: string;
|
|
248
|
+
bankRoutingNumber: string;
|
|
249
|
+
bankAccountNumber: string;
|
|
250
|
+
bankBeneficiaryName: string;
|
|
251
|
+
paymentRail: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface EurDepositInstructions {
|
|
255
|
+
currency: 'eur';
|
|
256
|
+
iban: string;
|
|
257
|
+
bic: string;
|
|
258
|
+
accountHolderName: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface OnRampResponse<T = UsdDepositInstructions | EurDepositInstructions> {
|
|
262
|
+
currency: 'usd' | 'eur';
|
|
263
|
+
depositInstructions: T;
|
|
264
|
+
destinationAddress: string;
|
|
265
|
+
destinationNetwork: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface LinkedBank {
|
|
269
|
+
id: string;
|
|
270
|
+
bankName: string;
|
|
271
|
+
last4: string;
|
|
272
|
+
withdrawalAddress: string;
|
|
273
|
+
network: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface LinkedBanksResponse {
|
|
277
|
+
banks: LinkedBank[];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
interface LinkBankResponse {
|
|
281
|
+
externalAccountId: string;
|
|
282
|
+
bankName: string;
|
|
283
|
+
withdrawalAddress: string;
|
|
284
|
+
network: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
interface BillingAddress {
|
|
288
|
+
streetLine1: string;
|
|
289
|
+
streetLine2?: string;
|
|
290
|
+
city: string;
|
|
291
|
+
state: string;
|
|
292
|
+
postalCode: string;
|
|
293
|
+
country: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type AccountOwnerType = 'individual' | 'business';
|
|
297
|
+
|
|
298
|
+
interface DeprecatedSmartAccount {
|
|
299
|
+
id: number;
|
|
300
|
+
ownerAddress: string;
|
|
301
|
+
contractAddress: string;
|
|
302
|
+
network: string;
|
|
303
|
+
deprecatedAt: string;
|
|
304
|
+
version: number;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Permissions (21)
|
|
311
|
+
|
|
312
|
+
| Permission | Description |
|
|
313
|
+
|---|---|
|
|
314
|
+
| `wallet:getBalance` | Access wallet balance (raw bigint) |
|
|
315
|
+
| `wallet:getAddress` | Access wallet address |
|
|
316
|
+
| `wallet:getSmartAccount` | Access smart account details |
|
|
317
|
+
| `wallet:transferERC20` | Transfer ERC20 tokens |
|
|
318
|
+
| `wallet:approveERC20` | Approve ERC20 token spending |
|
|
319
|
+
| `wallet:transferNative` | Transfer native currency (ETH) |
|
|
320
|
+
| `wallet:transferFrontierDollar` | Transfer FND (Frontier Network Dollar) |
|
|
321
|
+
| `wallet:transferInternalFrontierDollar` | Transfer iFND (Internal Frontier Network Dollar) |
|
|
322
|
+
| `wallet:transferOverallFrontierDollar` | Transfer using iFND first, fallback to FND |
|
|
323
|
+
| `wallet:executeCall` | Execute arbitrary contract call |
|
|
324
|
+
| `wallet:executeBatchCall` | Execute multiple contract calls atomically |
|
|
325
|
+
| `wallet:getSupportedTokens` | Get supported token symbols for swaps |
|
|
326
|
+
| `wallet:swap` | Execute token swap (same-chain or cross-chain) |
|
|
327
|
+
| `wallet:quoteSwap` | Get swap quote without executing |
|
|
328
|
+
| `wallet:getUsdDepositInstructions` | Get USD bank deposit instructions (on-ramp) |
|
|
329
|
+
| `wallet:getEurDepositInstructions` | Get EUR/SEPA deposit instructions (on-ramp) |
|
|
330
|
+
| `wallet:getLinkedBanks` | Get linked bank accounts (off-ramp) |
|
|
331
|
+
| `wallet:linkUsBankAccount` | Link US bank account for USD withdrawals |
|
|
332
|
+
| `wallet:linkEuroAccount` | Link EUR/IBAN bank account for EUR withdrawals |
|
|
333
|
+
| `wallet:deleteLinkedBank` | Delete a linked bank account |
|
|
334
|
+
| `wallet:getDeprecatedSmartAccounts` | Get deprecated smart accounts with active gas sponsorship |
|
|
@@ -48,7 +48,7 @@ The `src/` directory must contain at minimum:
|
|
|
48
48
|
src/
|
|
49
49
|
main.tsx
|
|
50
50
|
lib/
|
|
51
|
-
|
|
51
|
+
frontier-services.tsx
|
|
52
52
|
views/
|
|
53
53
|
Layout.tsx
|
|
54
54
|
styles/
|
|
@@ -70,6 +70,8 @@ src/
|
|
|
70
70
|
components/
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
> **Note:** `src/lib/sdk-context.tsx` and `src/lib/sdk-services.tsx` exist only **after** the SDK Integration phase (Tier 2). They are not present during pre-integration phases, so S-02 (a Tier-1 check that runs after every phase) must not require them.
|
|
74
|
+
|
|
73
75
|
**Pass condition:** All required paths exist. Optional paths should be present when the corresponding feature is used (e.g., `router.tsx` when `react-router-dom` is a dependency).
|
|
74
76
|
|
|
75
77
|
### S-03: No extraneous top-level files
|
|
@@ -130,24 +132,26 @@ if (standaloneHtml) {
|
|
|
130
132
|
}
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
### I-03: SdkProvider wrapping children in Layout.tsx
|
|
135
|
+
### I-03: SdkProvider + FrontierServicesProvider wrapping children in Layout.tsx
|
|
134
136
|
|
|
135
|
-
When inside the Frontier app, the Layout must wrap its children
|
|
137
|
+
When inside the Frontier app, the Layout must wrap its children with `<SdkProvider>` AND bridge the SDK into `<FrontierServicesProvider>`. Feature code consumes `useServices()`, so without `FrontierServicesProvider` the app **crashes at runtime** ("useServices must be used within FrontierServicesProvider").
|
|
136
138
|
|
|
137
|
-
**Pass condition:**
|
|
139
|
+
**Pass condition:** Layout's "in Frontier" return path contains both providers:
|
|
138
140
|
```tsx
|
|
139
141
|
<SdkProvider>
|
|
140
|
-
<
|
|
142
|
+
<SdkServicesBridge> {/* useSdk() → createSdkServices(sdk) → FrontierServicesProvider */}
|
|
143
|
+
<Outlet />
|
|
144
|
+
</SdkServicesBridge>
|
|
141
145
|
</SdkProvider>
|
|
142
146
|
```
|
|
143
147
|
|
|
144
148
|
### I-04: useSdk() hook available and used
|
|
145
149
|
|
|
146
|
-
`src/lib/sdk-context.tsx` must export `useSdk` and `SdkProvider`.
|
|
150
|
+
`src/lib/sdk-context.tsx` must export `useSdk` and `SdkProvider`. Only `sdk-services.tsx` and `src/views/Layout.tsx` may consume them; feature code under `src/hooks/`, `src/views/`, and `src/components/` accesses the SDK indirectly through `useServices()` (see M-03), never `useSdk()` directly.
|
|
147
151
|
|
|
148
152
|
**Pass condition:**
|
|
149
153
|
- `sdk-context.tsx` exports `useSdk` and `SdkProvider`.
|
|
150
|
-
-
|
|
154
|
+
- `useSdk()` / `SdkProvider` are consumed only by `sdk-services.tsx` and `Layout.tsx`; feature code uses `useServices()` from `../lib/frontier-services` (see M-03), not `useSdk`.
|
|
151
155
|
- No direct `new FrontierSDK()` calls outside of `sdk-context.tsx`.
|
|
152
156
|
|
|
153
157
|
---
|
|
@@ -156,27 +160,23 @@ When inside the Frontier app, the Layout must wrap its children (either `<Outlet
|
|
|
156
160
|
|
|
157
161
|
These verify configuration files have the correct content.
|
|
158
162
|
|
|
159
|
-
### C-01: vercel.json has
|
|
163
|
+
### C-01: vercel.json has CORS + CSP frame-ancestors for the 3 origins
|
|
160
164
|
|
|
161
165
|
> **Tier 2 — SDK Integration phase only.**
|
|
162
166
|
|
|
163
|
-
The `vercel.json` file
|
|
164
|
-
|
|
165
|
-
1. `https://os.frontiertower.io`
|
|
166
|
-
2. `https://sandbox.os.frontiertower.io`
|
|
167
|
-
3. `http://localhost:5173`
|
|
168
|
-
|
|
169
|
-
Each block must include:
|
|
170
|
-
- `Access-Control-Allow-Origin` matching the origin
|
|
167
|
+
The `vercel.json` file uses a single unconditional header block containing:
|
|
168
|
+
- `Access-Control-Allow-Origin: https://os.frontiertower.io` (the production origin)
|
|
171
169
|
- `Access-Control-Allow-Methods: GET, OPTIONS`
|
|
172
170
|
- `Access-Control-Allow-Headers: Content-Type`
|
|
171
|
+
- `Content-Security-Policy` with `frame-ancestors` listing all 3 live origins (`https://os.frontiertower.io`, `https://sandbox.os.frontiertower.io`, `http://localhost:5173`) — this governs iframe embedding
|
|
172
|
+
- security headers: `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`
|
|
173
173
|
|
|
174
174
|
It must also include the SPA rewrite:
|
|
175
175
|
```json
|
|
176
176
|
{ "source": "/(.*)", "destination": "/index.html" }
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
**Pass condition:** All 3
|
|
179
|
+
**Pass condition:** All 3 origins appear in the `frame-ancestors` directive, the CORS and security headers are present, and the rewrite rule exists.
|
|
180
180
|
|
|
181
181
|
### C-02: tsconfig.json has strict mode and vitest types
|
|
182
182
|
|
|
@@ -443,7 +443,7 @@ Source files under `src/hooks/`, `src/views/`, and `src/components/` must NOT im
|
|
|
443
443
|
| I-02 | SDK | createStandaloneHTML() fallback in Layout.tsx | Tier 2 | Error |
|
|
444
444
|
| I-03 | SDK | SdkProvider wrapping children in Layout.tsx | Tier 2 | Error |
|
|
445
445
|
| I-04 | SDK | useSdk() hook available and used | Tier 2 | Error |
|
|
446
|
-
| C-01 | Configuration | vercel.json
|
|
446
|
+
| C-01 | Configuration | vercel.json: CORS + CSP frame-ancestors (3 origins) | Tier 2 | Error |
|
|
447
447
|
| C-02 | Configuration | tsconfig.json has strict mode and vitest types | Tier 1 | Error |
|
|
448
448
|
| C-03 | Configuration | postcss.config.js imports @tailwindcss/postcss | Tier 1 | Error |
|
|
449
449
|
| C-04 | Configuration | package.json has correct scripts | Tier 1 | Error |
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { createContext, useContext, type ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
// ── Token amounts (bigint base units) ────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Since Frontier SDK v0.23, all FND amounts are bigint base units (FND_DECIMALS = 6).
|
|
6
|
+
// WalletBalance fields and every transfer*/swap amount are bigint, NOT display strings.
|
|
7
|
+
// - Display a balance: formatAmount(b.fnd) → symbol-free decimal string (100_500000n → '100.5')
|
|
8
|
+
// - Parse user input: parseAmount('10.5') → bigint (throws InvalidAmountError on bad input)
|
|
9
|
+
// - Write: wallet.transferFrontierDollar(to, parseAmount('10.5'))
|
|
10
|
+
// Import formatAmount / parseAmount / FND_DECIMALS / InvalidAmountError from the package
|
|
11
|
+
// ROOT '@frontiertower/frontier-sdk' (NOT the /ui-utils subpath). The SDK never adds '$'.
|
|
12
|
+
|
|
3
13
|
// ── Shared Types ────────────────────────────────────────────────────────────
|
|
4
14
|
|
|
5
15
|
export interface PaginatedResponse<T> {
|
|
@@ -15,12 +25,6 @@ export interface WalletBalance {
|
|
|
15
25
|
internalFnd: bigint;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
export interface WalletBalanceFormatted {
|
|
19
|
-
total: string;
|
|
20
|
-
fnd: string;
|
|
21
|
-
internalFnd: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
28
|
export interface SmartAccount {
|
|
25
29
|
id: number;
|
|
26
30
|
ownerAddress: string;
|
|
@@ -63,8 +67,8 @@ export interface SwapQuote {
|
|
|
63
67
|
targetChain: object;
|
|
64
68
|
sourceToken: object;
|
|
65
69
|
targetToken: object;
|
|
66
|
-
expectedAmountOut:
|
|
67
|
-
minAmountOut:
|
|
70
|
+
expectedAmountOut: bigint;
|
|
71
|
+
minAmountOut: bigint;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export interface OnRampResponse<T> {
|
|
@@ -407,6 +411,57 @@ export interface FrontierEvent {
|
|
|
407
411
|
color: string;
|
|
408
412
|
reviewStatus: string;
|
|
409
413
|
status: string;
|
|
414
|
+
isHost?: boolean;
|
|
415
|
+
deposit?: EventDeposit | null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Events Deposit Types ──────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
export type DepositStatus =
|
|
421
|
+
| 'not_required'
|
|
422
|
+
| 'required'
|
|
423
|
+
| 'pending'
|
|
424
|
+
| 'secured'
|
|
425
|
+
| 'released'
|
|
426
|
+
| 'withheld'
|
|
427
|
+
| 'failed';
|
|
428
|
+
|
|
429
|
+
export type CryptoDepositStatus =
|
|
430
|
+
| 'secured'
|
|
431
|
+
| 'awaiting_payment'
|
|
432
|
+
| 'grant'
|
|
433
|
+
| 'released'
|
|
434
|
+
| 'withheld'
|
|
435
|
+
| 'failed';
|
|
436
|
+
|
|
437
|
+
export interface EventDeposit {
|
|
438
|
+
status: DepositStatus;
|
|
439
|
+
amount: number;
|
|
440
|
+
currency: string;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export interface DepositPreflightToken {
|
|
444
|
+
key: string;
|
|
445
|
+
address: string;
|
|
446
|
+
decimals: number;
|
|
447
|
+
baseUnits: string;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export interface DepositPreflight {
|
|
451
|
+
spender: string;
|
|
452
|
+
network: string;
|
|
453
|
+
amount: string;
|
|
454
|
+
currency: string;
|
|
455
|
+
tokens: DepositPreflightToken[];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export interface DepositResult {
|
|
459
|
+
provider: 'crypto';
|
|
460
|
+
status: CryptoDepositStatus;
|
|
461
|
+
amount: string;
|
|
462
|
+
currency: string;
|
|
463
|
+
reference: string;
|
|
464
|
+
statusReason: string;
|
|
410
465
|
}
|
|
411
466
|
|
|
412
467
|
export interface EventLocation {
|
|
@@ -481,7 +536,6 @@ export interface DeepLinkData {
|
|
|
481
536
|
|
|
482
537
|
export interface WalletService {
|
|
483
538
|
getBalance(): Promise<WalletBalance>;
|
|
484
|
-
getBalanceFormatted(): Promise<WalletBalanceFormatted>;
|
|
485
539
|
getAddress(): Promise<string>;
|
|
486
540
|
getSmartAccount(): Promise<SmartAccount>;
|
|
487
541
|
transferERC20(tokenAddress: string, to: string, amount: bigint, overrides?: GasOverrides): Promise<UserOperationReceipt>;
|
|
@@ -489,12 +543,12 @@ export interface WalletService {
|
|
|
489
543
|
transferNative(to: string, amount: bigint, overrides?: GasOverrides): Promise<UserOperationReceipt>;
|
|
490
544
|
executeCall(call: ExecuteCall, overrides?: GasOverrides): Promise<UserOperationReceipt>;
|
|
491
545
|
executeBatchCall(calls: ExecuteCall[], overrides?: GasOverrides): Promise<UserOperationReceipt>;
|
|
492
|
-
transferFrontierDollar(to: string, amount:
|
|
493
|
-
transferInternalFrontierDollar(to: string, amount:
|
|
494
|
-
transferOverallFrontierDollar(to: string, amount:
|
|
546
|
+
transferFrontierDollar(to: string, amount: bigint, overrides?: GasOverrides): Promise<UserOperationReceipt>;
|
|
547
|
+
transferInternalFrontierDollar(to: string, amount: bigint, overrides?: GasOverrides): Promise<UserOperationReceipt>;
|
|
548
|
+
transferOverallFrontierDollar(to: string, amount: bigint, overrides?: GasOverrides): Promise<UserOperationReceipt>;
|
|
495
549
|
getSupportedTokens(): Promise<string[]>;
|
|
496
|
-
swap(sourceToken: string, targetToken: string, sourceNetwork: string, targetNetwork: string, amount:
|
|
497
|
-
quoteSwap(sourceToken: string, targetToken: string, sourceNetwork: string, targetNetwork: string, amount:
|
|
550
|
+
swap(sourceToken: string, targetToken: string, sourceNetwork: string, targetNetwork: string, amount: bigint): Promise<SwapResult>;
|
|
551
|
+
quoteSwap(sourceToken: string, targetToken: string, sourceNetwork: string, targetNetwork: string, amount: bigint): Promise<SwapQuote>;
|
|
498
552
|
getUsdDepositInstructions(): Promise<OnRampResponse<UsdDepositInstructions>>;
|
|
499
553
|
getEurDepositInstructions(): Promise<OnRampResponse<EurDepositInstructions>>;
|
|
500
554
|
getLinkedBanks(): Promise<LinkedBanksResponse>;
|
|
@@ -505,7 +559,7 @@ export interface WalletService {
|
|
|
505
559
|
}
|
|
506
560
|
|
|
507
561
|
export interface StorageService {
|
|
508
|
-
get<T = any>(key: string): Promise<T>;
|
|
562
|
+
get<T = any>(key: string): Promise<T | null>;
|
|
509
563
|
set(key: string, value: any): Promise<void>;
|
|
510
564
|
remove(key: string): Promise<void>;
|
|
511
565
|
clear(): Promise<void>;
|
|
@@ -579,6 +633,8 @@ export interface EventsService {
|
|
|
579
633
|
listLocations(payload?: { locationType?: LocationType }): Promise<EventLocation[]>;
|
|
580
634
|
listRoomBookings(payload?: { locationId?: string; date?: string; startDate?: string; endDate?: string; page?: number }): Promise<PaginatedResponse<RoomBooking>>;
|
|
581
635
|
createRoomBooking(payload: { startsAt: string; endsAt: string; location: string }): Promise<RoomBooking>;
|
|
636
|
+
getCryptoDepositPreflight(payload: { eventId: number }): Promise<DepositPreflight>;
|
|
637
|
+
placeCryptoDeposit(payload: { eventId: number }): Promise<DepositResult>;
|
|
582
638
|
}
|
|
583
639
|
|
|
584
640
|
export interface OfficesService {
|
|
@@ -639,7 +695,6 @@ async function mockWriteOp(module: string, method: string): Promise<UserOperatio
|
|
|
639
695
|
export function createMockServices(): FrontierServices {
|
|
640
696
|
const wallet: WalletService = {
|
|
641
697
|
async getBalance() { mockLog('wallet', 'getBalance'); return { total: BigInt(10_500000), fnd: BigInt(7_500000), internalFnd: BigInt(3_000000) }; },
|
|
642
|
-
async getBalanceFormatted() { mockLog('wallet', 'getBalanceFormatted'); return { total: '$100.00', fnd: '$75.00', internalFnd: '$25.00' }; },
|
|
643
698
|
async getAddress() { mockLog('wallet', 'getAddress'); return '0x1234567890abcdef1234567890abcdef12345678'; },
|
|
644
699
|
async getSmartAccount() { mockLog('wallet', 'getSmartAccount'); return { id: 1, ownerAddress: '0xowner', contractAddress: '0xcontract', network: 'base-sepolia', status: 'deployed', deploymentTransactionHash: '0xdeploy', createdAt: new Date().toISOString() }; },
|
|
645
700
|
async transferERC20() { return mockWriteOp('wallet', 'transferERC20'); },
|
|
@@ -652,7 +707,7 @@ export function createMockServices(): FrontierServices {
|
|
|
652
707
|
async transferOverallFrontierDollar() { return mockWriteOp('wallet', 'transferOverallFrontierDollar'); },
|
|
653
708
|
async getSupportedTokens() { mockLog('wallet', 'getSupportedTokens'); return ['FND', 'USDC', 'WETH']; },
|
|
654
709
|
async swap() { mockLog('wallet', 'swap'); await delay(1000); return { sourceChain: {}, targetChain: {}, sourceToken: {}, targetToken: {}, status: 'COMPLETED' as const }; },
|
|
655
|
-
async quoteSwap() { mockLog('wallet', 'quoteSwap'); return { sourceChain: {}, targetChain: {}, sourceToken: {}, targetToken: {}, expectedAmountOut:
|
|
710
|
+
async quoteSwap() { mockLog('wallet', 'quoteSwap'); return { sourceChain: {}, targetChain: {}, sourceToken: {}, targetToken: {}, expectedAmountOut: BigInt(10_000000), minAmountOut: BigInt(9_950000) }; },
|
|
656
711
|
async getUsdDepositInstructions() { mockLog('wallet', 'getUsdDepositInstructions'); return { currency: 'usd', depositInstructions: { currency: 'usd', bankName: 'Mock Bank', bankAddress: '123 Mock St', bankRoutingNumber: '000000000', bankAccountNumber: '1234567890', bankBeneficiaryName: 'Frontier Mock', paymentRail: 'ach' }, destinationAddress: '0xdest', destinationNetwork: 'base' }; },
|
|
657
712
|
async getEurDepositInstructions() { mockLog('wallet', 'getEurDepositInstructions'); return { currency: 'eur', depositInstructions: { currency: 'eur', iban: 'DE00000000000000000000', bic: 'MOCKDEFF', accountHolderName: 'Frontier Mock' }, destinationAddress: '0xdest', destinationNetwork: 'base' }; },
|
|
658
713
|
async getLinkedBanks() { mockLog('wallet', 'getLinkedBanks'); return { banks: [] }; },
|
|
@@ -663,7 +718,7 @@ export function createMockServices(): FrontierServices {
|
|
|
663
718
|
};
|
|
664
719
|
|
|
665
720
|
const storage: StorageService = {
|
|
666
|
-
async get<T = any>(key: string): Promise<T> {
|
|
721
|
+
async get<T = any>(key: string): Promise<T | null> {
|
|
667
722
|
mockLog('storage', 'get');
|
|
668
723
|
const raw = localStorage.getItem(MOCK_PREFIX + key);
|
|
669
724
|
return raw ? JSON.parse(raw) : null;
|
|
@@ -765,6 +820,8 @@ export function createMockServices(): FrontierServices {
|
|
|
765
820
|
async listLocations() { mockLog('events', 'listLocations'); return []; },
|
|
766
821
|
async listRoomBookings() { mockLog('events', 'listRoomBookings'); return emptyPage(); },
|
|
767
822
|
async createRoomBooking() { mockLog('events', 'createRoomBooking'); await delay(1000); return { id: 1, startsAt: new Date().toISOString(), endsAt: new Date(Date.now() + 3600000).toISOString(), location: 'mock-room' }; },
|
|
823
|
+
async getCryptoDepositPreflight() { mockLog('events', 'getCryptoDepositPreflight'); return { spender: '0xspender', network: 'base-sepolia', amount: '400.00', currency: 'usd', tokens: [{ key: 'ifnd_token', address: '0xifnd', decimals: 6, baseUnits: '400000000' }, { key: 'fnd_token', address: '0xfnd', decimals: 6, baseUnits: '400000000' }] }; },
|
|
824
|
+
async placeCryptoDeposit() { mockLog('events', 'placeCryptoDeposit'); await delay(1000); return { provider: 'crypto', status: 'secured', amount: '400.00', currency: 'usd', reference: '0xmocktxhash', statusReason: '' }; },
|
|
768
825
|
};
|
|
769
826
|
|
|
770
827
|
const offices: OfficesService = {
|
package/templates/app/layout.tsx
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import { Outlet } from 'react-router-dom';
|
|
3
3
|
import { isInFrontierApp, createStandaloneHTML } from '@frontiertower/frontier-sdk/ui-utils';
|
|
4
|
-
import { SdkProvider } from '../lib/sdk-context';
|
|
4
|
+
import { SdkProvider, useSdk } from '../lib/sdk-context';
|
|
5
|
+
import { FrontierServicesProvider } from '../lib/frontier-services';
|
|
6
|
+
import { createSdkServices } from '../lib/sdk-services';
|
|
7
|
+
|
|
8
|
+
// Bridges the live SDK (from SdkProvider) into the FrontierServices seam that
|
|
9
|
+
// feature code consumes via useServices(). Keeps feature code SDK-agnostic: the
|
|
10
|
+
// exact same components run against mocks (standalone) and the real SDK (iframe).
|
|
11
|
+
const SdkServicesBridge = ({ children }: { children: ReactNode }) => {
|
|
12
|
+
const sdk = useSdk();
|
|
13
|
+
const services = useMemo(() => createSdkServices(sdk), [sdk]);
|
|
14
|
+
return <FrontierServicesProvider services={services}>{children}</FrontierServicesProvider>;
|
|
15
|
+
};
|
|
5
16
|
|
|
6
17
|
export const Layout = () => {
|
|
7
18
|
const [loading, setLoading] = useState(true);
|
|
8
19
|
const [standaloneHtml, setStandaloneHtml] = useState('');
|
|
9
20
|
|
|
10
21
|
useEffect(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!inFrontier) {
|
|
22
|
+
if (!isInFrontierApp()) {
|
|
14
23
|
setStandaloneHtml(createStandaloneHTML('{{APP_NAME}}'));
|
|
15
|
-
setLoading(false);
|
|
16
|
-
return;
|
|
17
24
|
}
|
|
18
|
-
|
|
19
25
|
setLoading(false);
|
|
20
26
|
}, []);
|
|
21
27
|
|
|
@@ -37,9 +43,13 @@ export const Layout = () => {
|
|
|
37
43
|
);
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
// In-frame: provide both the raw SDK (useSdk) and the SDK-backed services
|
|
47
|
+
// seam (useServices) so every feature component works unchanged from standalone.
|
|
40
48
|
return (
|
|
41
49
|
<SdkProvider>
|
|
42
|
-
<
|
|
50
|
+
<SdkServicesBridge>
|
|
51
|
+
<Outlet />
|
|
52
|
+
</SdkServicesBridge>
|
|
43
53
|
</SdkProvider>
|
|
44
54
|
);
|
|
45
55
|
};
|