create-sbc-app 0.1.5 → 0.3.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -12
  3. package/bin/cli.js +63 -9
  4. package/package.json +1 -2
  5. package/templates/README.md +11 -4
  6. package/templates/react/.env.template +2 -1
  7. package/templates/react/package.json.template +3 -5
  8. package/templates/react/src/App.tsx.template +94 -10
  9. package/templates/react-dynamic/.env.template +7 -0
  10. package/templates/react-dynamic/README.md.template +24 -0
  11. package/templates/react-dynamic/eslint.config.js.template +34 -0
  12. package/templates/react-dynamic/index.html.template +14 -0
  13. package/templates/react-dynamic/package.json.template +33 -0
  14. package/templates/react-dynamic/postcss.config.js.template +8 -0
  15. package/templates/react-dynamic/public/sbc-logo.png +0 -0
  16. package/templates/react-dynamic/src/App.css.template +5 -0
  17. package/templates/react-dynamic/src/App.tsx.template +344 -0
  18. package/templates/react-dynamic/src/env.d.ts.template +14 -0
  19. package/templates/react-dynamic/src/index.css.template +15 -0
  20. package/templates/react-dynamic/src/main.tsx.template +12 -0
  21. package/templates/react-dynamic/tailwind.config.js.template +13 -0
  22. package/templates/react-dynamic/tsconfig.json.template +18 -0
  23. package/templates/react-dynamic/vite.config.ts.template +11 -0
  24. package/templates/react-para/.env.template +7 -0
  25. package/templates/react-para/README.md.template +24 -0
  26. package/templates/react-para/eslint.config.js.template +34 -0
  27. package/templates/react-para/index.html.template +14 -0
  28. package/templates/react-para/package.json.template +35 -0
  29. package/templates/react-para/postcss.config.js.template +8 -0
  30. package/templates/react-para/public/sbc-logo.png +0 -0
  31. package/templates/react-para/src/App.tsx.template +361 -0
  32. package/templates/react-para/src/components/ConnectButton.tsx.template +99 -0
  33. package/templates/react-para/src/env.d.ts.template +14 -0
  34. package/templates/react-para/src/hooks/usePara.ts.template +34 -0
  35. package/templates/react-para/src/hooks/useParaViem.ts.template +61 -0
  36. package/templates/react-para/src/index.css.template +5 -0
  37. package/templates/react-para/src/main.tsx.template +12 -0
  38. package/templates/react-para/src/providers.tsx.template +39 -0
  39. package/templates/react-para/src/utils/permit.ts.template +217 -0
  40. package/templates/react-para/tailwind.config.js.template +13 -0
  41. package/templates/react-para/tsconfig.json.template +18 -0
  42. package/templates/react-para/vite.config.ts.template +73 -0
@@ -0,0 +1,344 @@
1
+ import { DynamicContextProvider, useDynamicContext, DynamicUserProfile, DynamicWidget } from '@dynamic-labs/sdk-react-core';
2
+ import { EthereumWalletConnectors } from '@dynamic-labs/ethereum';
3
+ import { ZeroDevSmartWalletConnectors } from '@dynamic-labs/ethereum-aa';
4
+ import { useSbcDynamic } from '@stablecoin.xyz/react';
5
+ import { radiusTestnet, TestSBC_CONTRACT_ADDRESS } from '@stablecoin.xyz/core';
6
+ import { baseSepolia, base, type Chain } from 'viem/chains';
7
+ import { createPublicClient, http, getAddress, parseUnits, encodeFunctionData, erc20Abi } from 'viem';
8
+ import { useEffect, useState } from 'react';
9
+ import './App.css';
10
+
11
+ const getChain = () => {
12
+ const chainEnv = import.meta.env.VITE_CHAIN;
13
+ if (chainEnv === 'base') return base;
14
+ if (chainEnv === 'radiusTestnet') return radiusTestnet;
15
+ return baseSepolia;
16
+ };
17
+
18
+ const chain = getChain();
19
+ const rpcUrl = import.meta.env.VITE_RPC_URL;
20
+
21
+ // Radius testnet uses TestSBC (a test token for development)
22
+ const TEST_SBC_DECIMALS = 6;
23
+
24
+ const SBC_TOKEN_ADDRESS = (chain: Chain) => {
25
+ if (chain.id === baseSepolia.id) return '0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16';
26
+ if (chain.id === base.id) return '0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798';
27
+ if (chain.id === radiusTestnet.id) return TestSBC_CONTRACT_ADDRESS;
28
+ throw new Error('Unsupported chain');
29
+ };
30
+
31
+ const SBC_DECIMALS = (chain: Chain) => {
32
+ if (chain.id === baseSepolia.id) return 6;
33
+ if (chain.id === base.id) return 18;
34
+ if (chain.id === radiusTestnet.id) return TEST_SBC_DECIMALS;
35
+ throw new Error('Unsupported chain');
36
+ };
37
+
38
+ const getTokenSymbol = (chain: Chain) => {
39
+ if (chain.id === radiusTestnet.id) return 'TestSBC';
40
+ return 'SBC';
41
+ };
42
+
43
+ const chainExplorer = (chain: Chain) => {
44
+ if (chain.id === baseSepolia.id) return 'https://sepolia.basescan.org';
45
+ if (chain.id === base.id) return 'https://basescan.org';
46
+ if (chain.id === radiusTestnet.id) return 'https://testnet.radiustech.xyz/testnet/explorer';
47
+ return '';
48
+ };
49
+
50
+ const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
51
+
52
+ // ERC20 + EIP-2612 nonces helper ABI
53
+ const erc20PermitAbi = [
54
+ ...erc20Abi,
55
+ {
56
+ "inputs": [
57
+ { "internalType": "address", "name": "owner", "type": "address" }
58
+ ],
59
+ "name": "nonces",
60
+ "outputs": [
61
+ { "internalType": "uint256", "name": "", "type": "uint256" }
62
+ ],
63
+ "stateMutability": "view",
64
+ "type": "function"
65
+ }
66
+ ] as const;
67
+
68
+ // Wrapper to render DynamicWidget only after SDK is ready
69
+ const DynamicWidgetWrapper = () => {
70
+ const { sdkHasLoaded } = useDynamicContext();
71
+ if (!sdkHasLoaded) return null;
72
+ return <DynamicWidget />;
73
+ };
74
+
75
+ // Wrapper to render DynamicUserProfile only after SDK is ready
76
+ const DynamicUserProfileWrapper = () => {
77
+ const { sdkHasLoaded } = useDynamicContext();
78
+ if (!sdkHasLoaded) return null;
79
+ return <DynamicUserProfile />;
80
+ };
81
+
82
+ function WalletStatus() {
83
+ const { primaryWallet } = useDynamicContext();
84
+ const [balances, setBalances] = useState<{ eth: string | null; sbc: string | null }>({ eth: null, sbc: null });
85
+
86
+ useEffect(() => {
87
+ if (!primaryWallet?.address) return;
88
+ (async () => {
89
+ try {
90
+ const [ethBalance, sbcBalance] = await Promise.all([
91
+ publicClient.getBalance({ address: primaryWallet.address as `0x${string}` }),
92
+ publicClient.readContract({
93
+ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
94
+ abi: erc20Abi,
95
+ functionName: 'balanceOf',
96
+ args: [primaryWallet.address as `0x${string}`],
97
+ })
98
+ ]);
99
+ setBalances({ eth: ethBalance.toString(), sbc: (sbcBalance as bigint).toString() });
100
+ } catch (error) {
101
+ console.error('Failed to fetch EOA balances:', error);
102
+ setBalances({ eth: null, sbc: null });
103
+ }
104
+ })();
105
+ }, [primaryWallet?.address]);
106
+
107
+ if (!primaryWallet) return null;
108
+ const fmtEth = (v: string | null) => v ? (Number(v) / 1e18).toFixed(4) : '0.0000';
109
+ const fmtSbc = (v: string | null) => v ? (Number(v) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(4) : '0.0000';
110
+
111
+ return (
112
+ <div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
113
+ <div className="flex justify-between items-start mb-3">
114
+ <div className="flex-1">
115
+ <h3 className="font-semibold text-green-800 mb-1">✅ Dynamic Wallet Connected</h3>
116
+ <p className="text-xs text-green-600 font-mono break-all mb-2">EOA: {primaryWallet.address}</p>
117
+ <p className="text-xs text-green-600 mb-2">Connected via Dynamic SDK</p>
118
+ <p className="text-xs text-green-600 mb-2"><strong>Chain:</strong> {chain.name} (ID: {chain.id})</p>
119
+ <div className="mt-2 pt-2 border-t border-green-200">
120
+ <p className="text-xs font-medium text-green-700 mb-1">EOA Wallet Balances:</p>
121
+ <div className="flex gap-4">
122
+ <span className="text-xs text-green-600"><strong>ETH:</strong> {fmtEth(balances.eth)}</span>
123
+ <span className="text-xs text-green-600"><strong>{getTokenSymbol(chain)}:</strong> {fmtSbc(balances.sbc)}</span>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ function SmartAccountInfo({ account, refreshAccount, isLoadingAccount, accountError }: any) {
133
+ const [sbcBalance, setSbcBalance] = useState<string | null>(null);
134
+ useEffect(() => {
135
+ if (!account?.address) return;
136
+ (async () => {
137
+ try {
138
+ const bal = await publicClient.readContract({
139
+ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
140
+ abi: erc20Abi,
141
+ functionName: 'balanceOf',
142
+ args: [account.address as `0x${string}`],
143
+ });
144
+ setSbcBalance((bal as bigint).toString());
145
+ } catch (error) {
146
+ console.error('Failed to fetch smart account SBC balance:', error);
147
+ setSbcBalance('0');
148
+ }
149
+ })();
150
+ }, [account?.address]);
151
+
152
+ if (!account) return null;
153
+ const fmtEth = (v: string | null) => v ? (Number(v) / 1e18).toFixed(6) : '0.000000';
154
+ const fmtSbc = (v: string | null) => v ? (Number(v) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(2) : '0.00';
155
+
156
+ return (
157
+ <div className="mb-6 p-4 bg-purple-50 border border-purple-200 rounded-lg">
158
+ <div className="flex justify-between items-center mb-2">
159
+ <h3 className="font-semibold text-purple-800">🔐 Smart Account Status</h3>
160
+ <button onClick={refreshAccount} disabled={isLoadingAccount} className="text-xs bg-purple-600 text-white px-3 py-1 rounded hover:bg-purple-700 disabled:opacity-50">{isLoadingAccount ? '🔄 Refreshing...' : '🔄 Refresh'}</button>
161
+ </div>
162
+ <div className="space-y-2 text-sm">
163
+ <div className="flex justify-between"><span className="text-purple-700">Smart Account Address:</span><span className="font-mono text-xs text-purple-600 break-all">{account.address}</span></div>
164
+ <div className="flex justify-between"><span className="text-purple-700">Deployed:</span><span className="text-purple-600">{account.isDeployed ? '✅ Yes' : '⏳ On first transaction'}</span></div>
165
+ <div className="flex justify-between"><span className="text-purple-700">Nonce:</span><span className="text-purple-600">{account.nonce}</span></div>
166
+ <div className="flex justify-between"><span className="text-purple-700">ETH Balance:</span><span className="text-purple-600 font-mono text-xs">{fmtEth(account.balance)} ETH</span></div>
167
+ <div className="flex justify-between"><span className="text-purple-700">{getTokenSymbol(chain)} Balance:</span><span className="text-purple-600 font-mono text-xs">{fmtSbc(sbcBalance)} {getTokenSymbol(chain)}</span></div>
168
+ </div>
169
+ {accountError && <p className="mt-2 text-xs text-red-600">{String(accountError)}</p>}
170
+ </div>
171
+ );
172
+ }
173
+
174
+ function TransactionForm({ account, sbcAppKit }: { account: any; sbcAppKit: any }) {
175
+ const [recipient, setRecipient] = useState('');
176
+ const [amount, setAmount] = useState('1');
177
+ const [status, setStatus] = useState<'idle'|'loading'|'success'|'error'>('idle');
178
+ const [error, setError] = useState<string | null>(null);
179
+ const [result, setResult] = useState<any>(null);
180
+ const isValid = recipient && /^0x[a-fA-F0-9]{40}$/.test(recipient) && parseFloat(amount) > 0;
181
+
182
+ const sendTx = async () => {
183
+ if (!isValid || !account || !sbcAppKit) return;
184
+ try {
185
+ setStatus('loading'); setError(null);
186
+ const owner = sbcAppKit.getOwnerAddress();
187
+ const value = parseUnits(amount, SBC_DECIMALS(chain));
188
+ const deadline = Math.floor(Date.now() / 1000) + 60 * 30;
189
+ const { publicClient: pc, walletClient: wc } = (sbcAppKit as any);
190
+ const [nonce, tokenName] = await Promise.all([
191
+ pc.readContract({ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, abi: erc20PermitAbi, functionName: 'nonces', args: [owner] }),
192
+ pc.readContract({ address: SBC_TOKEN_ADDRESS(chain), abi: erc20Abi, functionName: 'name' })
193
+ ]);
194
+ const domain = { name: tokenName as string, version: '1', chainId: chain.id, verifyingContract: SBC_TOKEN_ADDRESS(chain) };
195
+ const types = { Permit: [
196
+ { name: 'owner', type: 'address' },
197
+ { name: 'spender', type: 'address' },
198
+ { name: 'value', type: 'uint256' },
199
+ { name: 'nonce', type: 'uint256' },
200
+ { name: 'deadline', type: 'uint256' },
201
+ ] } as const;
202
+ const message = { owner, spender: account.address, value, nonce, deadline: BigInt(deadline) } as const;
203
+ const signature = await wc.signTypedData({ domain, types, primaryType: 'Permit', message });
204
+ const { r, s, v } = (await import('viem')).parseSignature(signature);
205
+ const permitData = encodeFunctionData({
206
+ abi: [{ name: 'permit', type: 'function', inputs: [
207
+ { name: 'owner', type: 'address' },
208
+ { name: 'spender', type: 'address' },
209
+ { name: 'value', type: 'uint256' },
210
+ { name: 'deadline', type: 'uint256' },
211
+ { name: 'v', type: 'uint8' },
212
+ { name: 'r', type: 'bytes32' },
213
+ { name: 's', type: 'bytes32' }
214
+ ] }],
215
+ functionName: 'permit',
216
+ args: [owner, account.address, value, BigInt(deadline), v, r, s]
217
+ });
218
+ const transferFromData = encodeFunctionData({
219
+ abi: erc20Abi,
220
+ functionName: 'transferFrom',
221
+ args: [owner, recipient as `0x${string}`, value]
222
+ });
223
+ const res = await sbcAppKit.sendUserOperation({
224
+ calls: [
225
+ { to: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, data: permitData },
226
+ { to: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, data: transferFromData }
227
+ ]
228
+ });
229
+ setResult(res);
230
+ setStatus('success');
231
+ } catch (e: any) {
232
+ setError(e?.message || 'Transaction failed');
233
+ setStatus('error');
234
+ }
235
+ };
236
+
237
+ if (!account) return null;
238
+
239
+ return (
240
+ <div className="p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
241
+ <h3 className="font-semibold text-gray-800 mb-4">💸 Send {getTokenSymbol(chain)} Tokens</h3>
242
+ <div className="space-y-4">
243
+ <div>
244
+ <label className="block text-sm font-medium text-gray-700 mb-2">Recipient Address</label>
245
+ <input type="text" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="0x..." className="w-full px-3 py-2 text-xs font-mono border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 border-gray-300" />
246
+ </div>
247
+ <div>
248
+ <label className="block text-sm font-medium text-gray-700 mb-2">Amount ({getTokenSymbol(chain)})</label>
249
+ <input type="number" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="1.0" step="0.000001" min="0" className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 border-gray-300" />
250
+ </div>
251
+ <div className="p-3 bg-gray-50 rounded">
252
+ <div className="flex justify-between text-sm"><span>Amount:</span><span className="font-medium">{amount} {getTokenSymbol(chain)}</span></div>
253
+ <div className="flex justify-between text-xs text-gray-600"><span>Gas fees:</span><span>Covered by SBC Paymaster ✨</span></div>
254
+ <div className="flex justify-between text-xs text-gray-600"><span>Signing:</span><span>Your Dynamic wallet will prompt to sign 🖊️</span></div>
255
+ </div>
256
+ <button onClick={sendTx} disabled={!isValid || status==='loading' || !account} className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
257
+ {status==='loading' ? 'Waiting for signature...' : `Send ${amount} ${getTokenSymbol(chain)}`}
258
+ </button>
259
+ {status==='success' && result && (
260
+ <div className="p-3 bg-green-50 border border-green-200 rounded">
261
+ <p className="text-sm text-green-800 font-medium">✅ Transaction Submitted</p>
262
+ <p className="text-xs text-green-600 font-mono break-all mt-1">
263
+ <a href={`${chainExplorer(chain)}/tx/${result.transactionHash}`} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">View transaction: {result.transactionHash}</a>
264
+ </p>
265
+ </div>
266
+ )}
267
+ {status==='error' && error && (
268
+ <div className="p-3 bg-red-50 border border-red-200 rounded">
269
+ <p className="text-sm text-red-800 font-medium">❌ Transaction Failed</p>
270
+ <p className="text-xs text-red-600 mt-1">{error}</p>
271
+ </div>
272
+ )}
273
+ </div>
274
+ </div>
275
+ );
276
+ }
277
+
278
+ function DynamicApp() {
279
+ const { primaryWallet } = useDynamicContext();
280
+ const { sbcAppKit, isInitialized, error, account, isLoadingAccount, accountError, refreshAccount } = useSbcDynamic({
281
+ apiKey: import.meta.env.VITE_SBC_API_KEY,
282
+ chain,
283
+ primaryWallet,
284
+ rpcUrl,
285
+ debug: true
286
+ });
287
+
288
+ return (
289
+ <>
290
+ <div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
291
+ <h3 className="font-semibold text-blue-800 mb-2">🔗 Connect to Dynamic</h3>
292
+ <p className="text-sm text-blue-600 mb-3">
293
+ Connect your wallet or sign in with email to create a smart account with Dynamic
294
+ </p>
295
+ <DynamicWidgetWrapper />
296
+ </div>
297
+ {primaryWallet && isInitialized && (
298
+ <>
299
+ <WalletStatus />
300
+ <SmartAccountInfo account={account} refreshAccount={refreshAccount} isLoadingAccount={isLoadingAccount} accountError={accountError} />
301
+ <TransactionForm account={account} sbcAppKit={sbcAppKit} />
302
+ </>
303
+ )}
304
+ {error && <div className="error">{error.message}</div>}
305
+ </>
306
+ );
307
+ }
308
+
309
+ export default function App() {
310
+ return (
311
+ <DynamicContextProvider
312
+ settings={{
313
+ environmentId: import.meta.env.VITE_DYNAMIC_ENVIRONMENT_ID || '',
314
+ walletConnectors: [EthereumWalletConnectors, ZeroDevSmartWalletConnectors],
315
+ }}
316
+ >
317
+ <div className="min-h-screen bg-gray-50 py-8">
318
+ <div className="max-w-2xl mx-auto px-4">
319
+ <div className="text-center mb-8">
320
+ <h1 className="text-3xl font-bold text-gray-900 mb-2 flex items-center justify-center gap-3">
321
+ <img src="/sbc-logo.png" alt="SBC Logo" width={36} height={36} />
322
+ SBC (Dynamic) Integration
323
+ </h1>
324
+ <p className="text-gray-600">Gasless transactions with Dynamic SDK integration</p>
325
+ </div>
326
+
327
+ <DynamicApp />
328
+
329
+ <DynamicUserProfileWrapper />
330
+
331
+ <div className="mt-8 text-center text-xs text-gray-500">
332
+ <p>
333
+ Powered by{' '}
334
+ <a href="https://github.com/stablecoinxyz/app-kit" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">SBC AppKit</a>
335
+ {' '}• Dynamic SDK integration
336
+ </p>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ </DynamicContextProvider>
341
+ );
342
+ }
343
+
344
+
@@ -0,0 +1,14 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_SBC_API_KEY: string
5
+ readonly VITE_DYNAMIC_ENVIRONMENT_ID: string
6
+ readonly VITE_CHAIN?: string
7
+ readonly VITE_RPC_URL?: string
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv
12
+ }
13
+
14
+
@@ -0,0 +1,15 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
+ sans-serif;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ }
9
+
10
+ code {
11
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
+ monospace;
13
+ }
14
+
15
+
@@ -0,0 +1,12 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './App.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
11
+
12
+
@@ -0,0 +1,13 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
12
+
13
+
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+ "moduleResolution": "bundler",
8
+ "allowImportingTsExtensions": true,
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "jsx": "react-jsx",
13
+ "strict": true
14
+ },
15
+ "include": ["src"]
16
+ }
17
+
18
+
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 3000
8
+ }
9
+ })
10
+
11
+
@@ -0,0 +1,7 @@
1
+ # Get your SBC API Key at https://dashboard.stablecoin.xyz
2
+ VITE_SBC_API_KEY={{apiKey}}
3
+ # Get your Para API Key at https://developer.getpara.com/
4
+ VITE_PARA_API_KEY=your_para_api_key
5
+ # Supported chains: "baseSepolia" | "base" | "radiusTestnet"
6
+ VITE_CHAIN={{chain}}
7
+ VITE_RPC_URL=
@@ -0,0 +1,24 @@
1
+ # React + Para + SBC App Kit
2
+
3
+ Gasless user operation using EIP-2612 permit signed via Para and executed via SBC App Kit.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm dev
10
+ ```
11
+
12
+ ## Environment
13
+
14
+ Create `.env` from `.env.template`:
15
+
16
+ ```env
17
+ VITE_SBC_API_KEY={{apiKey}}
18
+ VITE_PARA_API_KEY=your_para_api_key
19
+ # Optional
20
+ VITE_CHAIN=baseSepolia # or "base"
21
+ VITE_RPC_URL=
22
+ ```
23
+
24
+
@@ -0,0 +1,34 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from '@typescript-eslint/eslint-plugin'
6
+ import tsparser from '@typescript-eslint/parser'
7
+
8
+ export default [
9
+ { ignores: ['dist'] },
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ parser: tsparser,
16
+ },
17
+ plugins: {
18
+ '@typescript-eslint': tseslint,
19
+ 'react-hooks': reactHooks,
20
+ 'react-refresh': reactRefresh,
21
+ },
22
+ rules: {
23
+ ...js.configs.recommended.rules,
24
+ ...tseslint.configs.recommended.rules,
25
+ ...reactHooks.configs.recommended.rules,
26
+ 'react-refresh/only-export-components': [
27
+ 'warn',
28
+ { allowConstantExport: true },
29
+ ],
30
+ },
31
+ },
32
+ ]
33
+
34
+
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{projectName}} – SBC + Para</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
13
+
14
+
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@getpara/react-sdk": "2.0.0-alpha.40",
13
+ "@getpara/viem-v2-integration": "2.0.0-alpha.40",
14
+ "@tanstack/react-query": "^5.0.0",
15
+ "wagmi": "^2.0.0",
16
+ "@stablecoin.xyz/core": "latest",
17
+ "@stablecoin.xyz/react": "latest",
18
+ "react": "^19.1.0",
19
+ "react-dom": "^19.1.0",
20
+ "viem": "^2.33.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^18.2.0",
24
+ "@types/react-dom": "^18.2.0",
25
+ "autoprefixer": "^10.4.0",
26
+ "postcss": "^8.4.0",
27
+ "tailwindcss": "^3.4.17",
28
+ "@vitejs/plugin-react": "^4.0.0",
29
+ "typescript": "^5.0.0",
30
+ "vite": "^5.0.0",
31
+ "vite-plugin-node-polyfills": "^0.22.0"
32
+ }
33
+ }
34
+
35
+
@@ -0,0 +1,8 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
7
+
8
+