create-sbc-app 0.2.0 → 0.3.1
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/README.md +58 -12
- package/bin/cli.js +33 -2
- package/package.json +1 -1
- package/templates/README.md +62 -4
- package/templates/react/.env.template +2 -1
- package/templates/react/README.md.template +8 -9
- package/templates/react/src/App.tsx.template +94 -10
- package/templates/react-dynamic/.env.template +2 -2
- package/templates/react-dynamic/README.md.template +131 -13
- package/templates/react-dynamic/src/App.tsx.template +40 -18
- package/templates/react-para/.env.template +2 -2
- package/templates/react-para/README.md.template +154 -13
- package/templates/react-para/src/App.tsx.template +44 -16
- package/templates/react-para/vite.config.ts.template +0 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ pnpm dev # or npm run dev
|
|
|
24
24
|
## CLI Options
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
Usage: create-sbc-app [project-directory] [options]
|
|
27
|
+
Usage: npx create-sbc-app [project-directory] [options]
|
|
28
28
|
|
|
29
29
|
Create a new SBC App Kit project with an opinionated template
|
|
30
30
|
|
|
@@ -34,21 +34,26 @@ Arguments:
|
|
|
34
34
|
Options:
|
|
35
35
|
-V, --version output the version number
|
|
36
36
|
-t, --template <type> Template to use: react, react-dynamic, or react-para
|
|
37
|
+
-c, --chain <chain> Chain to use: baseSepolia, base, or radiusTestnet
|
|
37
38
|
--api-key <apiKey> Your SBC API key for immediate configuration
|
|
38
39
|
--wallet <wallet> Wallet integration (not yet implemented)
|
|
39
40
|
-h, --help display help for command
|
|
40
41
|
|
|
41
42
|
Examples:
|
|
42
|
-
$ create-sbc-app my-app
|
|
43
|
-
$ create-sbc-app my-app --template react
|
|
44
|
-
$ create-sbc-app my-app --template react-dynamic
|
|
45
|
-
$ create-sbc-app my-app --template react-para
|
|
46
|
-
# Next.js template removed for now
|
|
43
|
+
$ npx create-sbc-app my-app
|
|
44
|
+
$ npx create-sbc-app my-app --template react --chain radiusTestnet
|
|
45
|
+
$ npx create-sbc-app my-app --template react-dynamic --chain base
|
|
46
|
+
$ npx create-sbc-app my-app --template react-para --api-key your-key
|
|
47
47
|
|
|
48
48
|
Available Templates:
|
|
49
49
|
- react React + Vite template with SBC integration
|
|
50
50
|
- react-dynamic React + Vite with Dynamic wallet integration
|
|
51
51
|
- react-para React + Vite with Para wallet integration
|
|
52
|
+
|
|
53
|
+
Available Chains:
|
|
54
|
+
- baseSepolia Base Sepolia testnet (default)
|
|
55
|
+
- base Base mainnet
|
|
56
|
+
- radiusTestnet Radius testnet (react template only)
|
|
52
57
|
```
|
|
53
58
|
|
|
54
59
|
## ✨ Features
|
|
@@ -81,7 +86,9 @@ The React template includes comprehensive, production-ready examples:
|
|
|
81
86
|
- **Hot Reload** - Fast development iteration
|
|
82
87
|
- **Production Ready** - Optimized builds and deployment preparation
|
|
83
88
|
|
|
84
|
-
## 🚀
|
|
89
|
+
## 🚀 Templates
|
|
90
|
+
|
|
91
|
+
### React Template (Default)
|
|
85
92
|
|
|
86
93
|
**Best for:** Client-side applications, rapid prototyping, and production use
|
|
87
94
|
|
|
@@ -89,14 +96,52 @@ The React template includes comprehensive, production-ready examples:
|
|
|
89
96
|
npx create-sbc-app my-app
|
|
90
97
|
```
|
|
91
98
|
|
|
92
|
-
**
|
|
99
|
+
**Supported Chains:** Base Sepolia, Base, Radius Testnet
|
|
93
100
|
|
|
101
|
+
**Features:**
|
|
94
102
|
- Fast development setup
|
|
95
103
|
- Hot module replacement
|
|
104
|
+
- Direct wallet connection (MetaMask, Coinbase, WalletConnect)
|
|
96
105
|
- Built-in testing framework
|
|
97
106
|
- Easy deployment to static hosts
|
|
98
107
|
- Modern React patterns and hooks
|
|
99
108
|
|
|
109
|
+
### React + Dynamic Template
|
|
110
|
+
|
|
111
|
+
**Best for:** Applications requiring embedded wallets with social logins
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx create-sbc-app my-app --template react-dynamic
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Supported Chains:** Base Sepolia, Base (Radius Testnet not supported)
|
|
118
|
+
|
|
119
|
+
**Features:**
|
|
120
|
+
- Dynamic SDK integration for embedded wallets
|
|
121
|
+
- Social login support (Google, Twitter, Discord, etc.)
|
|
122
|
+
- Email/SMS wallet creation
|
|
123
|
+
- All standard SBC features
|
|
124
|
+
|
|
125
|
+
**Additional Requirements:** Dynamic Environment ID from [Dynamic Dashboard](https://app.dynamic.xyz/)
|
|
126
|
+
|
|
127
|
+
### React + Para Template
|
|
128
|
+
|
|
129
|
+
**Best for:** DeFi applications leveraging EIP-2612 permits
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx create-sbc-app my-app --template react-para
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Supported Chains:** Base Sepolia, Base (Radius Testnet not supported)
|
|
136
|
+
|
|
137
|
+
**Features:**
|
|
138
|
+
- Para wallet integration
|
|
139
|
+
- EIP-2612 permit signatures
|
|
140
|
+
- Gasless token approvals
|
|
141
|
+
- All standard SBC features
|
|
142
|
+
|
|
143
|
+
**Additional Requirements:** Para API Key from [Para](https://para.xyz/)
|
|
144
|
+
|
|
100
145
|
## 📝 Configuration
|
|
101
146
|
|
|
102
147
|
### Environment Variables
|
|
@@ -108,7 +153,8 @@ The template includes comprehensive environment configuration:
|
|
|
108
153
|
```bash
|
|
109
154
|
# Your SBC API key (get from SBC dashboard)
|
|
110
155
|
VITE_SBC_API_KEY=your_api_key_here
|
|
111
|
-
|
|
156
|
+
|
|
157
|
+
# Supported chains: "baseSepolia" | "base" | "radiusTestnet"
|
|
112
158
|
VITE_CHAIN="baseSepolia"
|
|
113
159
|
```
|
|
114
160
|
|
|
@@ -139,9 +185,9 @@ cp .env.template .env
|
|
|
139
185
|
|
|
140
186
|
# then ensure your .env has the environment variables set up
|
|
141
187
|
|
|
142
|
-
# "base"
|
|
143
|
-
VITE_CHAIN="baseSepolia"
|
|
144
|
-
# Custom RPC URL (optional) - e.g. get one from
|
|
188
|
+
# Supported chains: "baseSepolia" | "base" | "radiusTestnet"
|
|
189
|
+
VITE_CHAIN="baseSepolia"
|
|
190
|
+
# Custom RPC URL (optional) - e.g. get one from Alchemy at https://dashboard.alchemy.com/apps
|
|
145
191
|
VITE_RPC_URL=
|
|
146
192
|
# Get your SBC API Key at https://dashboard.stablecoin.xyz
|
|
147
193
|
VITE_SBC_API_KEY=
|
package/bin/cli.js
CHANGED
|
@@ -13,18 +13,24 @@ program
|
|
|
13
13
|
.version('0.2.0')
|
|
14
14
|
.argument('[project-directory]', 'Directory to create the new app in')
|
|
15
15
|
.option('-t, --template <template>', 'Template to use: react, react-dynamic, or react-para')
|
|
16
|
+
.option('-c, --chain <chain>', 'Chain to use: baseSepolia, base, or radiusTestnet')
|
|
16
17
|
.option('--api-key <apiKey>', 'Your SBC API key for immediate configuration')
|
|
17
18
|
.option('--wallet <wallet>', 'Wallet integration (not yet implemented)')
|
|
18
19
|
.addHelpText('after', `
|
|
19
20
|
Examples:
|
|
20
21
|
$ create-sbc-app my-app
|
|
21
|
-
$ create-sbc-app my-app --template react
|
|
22
|
+
$ create-sbc-app my-app --template react --chain radiusTestnet
|
|
22
23
|
$ create-sbc-app my-app --template react --api-key your-api-key
|
|
23
24
|
|
|
24
25
|
Available Templates:
|
|
25
26
|
- react React + Vite template with SBC integration
|
|
26
27
|
- react-dynamic React + Vite with Dynamic wallet integration
|
|
27
28
|
- react-para React + Vite with Para wallet integration
|
|
29
|
+
|
|
30
|
+
Available Chains:
|
|
31
|
+
- baseSepolia Base Sepolia testnet (default)
|
|
32
|
+
- base Base mainnet
|
|
33
|
+
- radiusTestnet Radius testnet
|
|
28
34
|
`)
|
|
29
35
|
.action(async (dir, options) => {
|
|
30
36
|
if (options.wallet) {
|
|
@@ -36,6 +42,11 @@ Available Templates:
|
|
|
36
42
|
{ title: 'React (Dynamic wallet)', value: 'react-dynamic' },
|
|
37
43
|
{ title: 'React (Para wallet)', value: 'react-para' }
|
|
38
44
|
];
|
|
45
|
+
const chainChoices = [
|
|
46
|
+
{ title: 'Base Sepolia (testnet)', value: 'baseSepolia' },
|
|
47
|
+
{ title: 'Base (mainnet)', value: 'base' },
|
|
48
|
+
{ title: 'Radius Testnet', value: 'radiusTestnet' }
|
|
49
|
+
];
|
|
39
50
|
// Use provided argument or prompt for project directory
|
|
40
51
|
let projectDir = dir && dir.trim() ? dir.trim() : '';
|
|
41
52
|
if (!projectDir) {
|
|
@@ -69,6 +80,26 @@ Available Templates:
|
|
|
69
80
|
process.exit(1);
|
|
70
81
|
}
|
|
71
82
|
}
|
|
83
|
+
// Use provided option or prompt for chain
|
|
84
|
+
let chain = options.chain && ['baseSepolia', 'base', 'radiusTestnet'].includes(options.chain) ? options.chain : '';
|
|
85
|
+
if (!chain) {
|
|
86
|
+
const res = await prompts({
|
|
87
|
+
type: 'select',
|
|
88
|
+
name: 'chain',
|
|
89
|
+
message: 'Which chain?',
|
|
90
|
+
choices: chainChoices,
|
|
91
|
+
initial: 0
|
|
92
|
+
});
|
|
93
|
+
if (res.chain === undefined) {
|
|
94
|
+
console.log('Chain selection is required.');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
chain = res.chain;
|
|
98
|
+
if (!chain || !['baseSepolia', 'base', 'radiusTestnet'].includes(chain)) {
|
|
99
|
+
console.log('Chain selection is required.');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
72
103
|
// Use provided option or prompt for API key
|
|
73
104
|
let apiKey = options.apiKey && options.apiKey.trim() ? options.apiKey.trim() : '';
|
|
74
105
|
if (!apiKey) {
|
|
@@ -96,7 +127,7 @@ Available Templates:
|
|
|
96
127
|
}
|
|
97
128
|
await copyTemplate(templateDir, targetDir, {
|
|
98
129
|
projectName: projectDir,
|
|
99
|
-
chain:
|
|
130
|
+
chain: chain,
|
|
100
131
|
apiKey: apiKey
|
|
101
132
|
});
|
|
102
133
|
// Ensure SBC logo exists in public/ for all templates
|
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -1,17 +1,75 @@
|
|
|
1
|
-
# create-sbc-app
|
|
1
|
+
# create-sbc-app Templates
|
|
2
2
|
|
|
3
3
|
This directory contains ready-to-use templates for quickly starting new projects with the SBC App Kit. Each template is a minimal, fully functional app that demonstrates best practices for integrating @stablecoin.xyz/core and @stablecoin.xyz/react.
|
|
4
4
|
|
|
5
5
|
## Available Templates
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### react/ – React + Vite + SBC App Kit (Default)
|
|
8
|
+
|
|
9
|
+
**Best for:** Applications with custom wallet integrations, rapid prototyping, production use
|
|
10
|
+
|
|
11
|
+
**Supported Chains:** Base Sepolia, Base, Radius Testnet
|
|
12
|
+
|
|
13
|
+
**Features:**
|
|
14
|
+
- Direct wallet connection (MetaMask, Coinbase Wallet, WalletConnect)
|
|
15
|
+
- Complete SBC App Kit integration for gasless transactions
|
|
16
|
+
- Full control over wallet connection UI/UX
|
|
17
|
+
- TypeScript support
|
|
18
|
+
- Vite for fast development
|
|
19
|
+
|
|
20
|
+
**Use when:** You want maximum flexibility and plan to implement your own wallet connection logic or use standard Web3 wallets.
|
|
21
|
+
|
|
22
|
+
### react-dynamic/ – React + Dynamic + SBC App Kit
|
|
23
|
+
|
|
24
|
+
**Best for:** Applications requiring embedded wallets with social logins
|
|
25
|
+
|
|
26
|
+
**Supported Chains:** Base Sepolia, Base (Radius Testnet not supported by Dynamic)
|
|
27
|
+
|
|
28
|
+
**Features:**
|
|
29
|
+
- Dynamic SDK integration for embedded wallets
|
|
30
|
+
- Social authentication (Google, Twitter, Discord, GitHub, Apple, etc.)
|
|
31
|
+
- Email and SMS wallet creation
|
|
32
|
+
- Seamless onboarding for non-crypto users
|
|
33
|
+
- Multi-chain support (Base and Base Sepolia)
|
|
34
|
+
- All SBC App Kit features
|
|
35
|
+
|
|
36
|
+
**Use when:** You want to provide the easiest possible onboarding experience with social logins and don't want users to install a wallet extension.
|
|
37
|
+
|
|
38
|
+
**Additional Requirements:**
|
|
39
|
+
- Dynamic Environment ID (get from https://app.dynamic.xyz/)
|
|
40
|
+
|
|
41
|
+
### react-para/ – React + Para + SBC App Kit
|
|
42
|
+
|
|
43
|
+
**Best for:** DeFi applications leveraging EIP-2612 permits and advanced gasless patterns
|
|
44
|
+
|
|
45
|
+
**Supported Chains:** Base Sepolia, Base (Radius Testnet not supported by Para)
|
|
46
|
+
|
|
47
|
+
**Features:**
|
|
48
|
+
- Para wallet integration
|
|
49
|
+
- EIP-2612 permit signature support
|
|
50
|
+
- Gasless token approvals
|
|
51
|
+
- Single-step token operations (no separate approval transaction)
|
|
52
|
+
- Advanced meta-transaction patterns
|
|
53
|
+
- All SBC App Kit features
|
|
54
|
+
|
|
55
|
+
**Use when:** You're building DeFi applications (DEX, lending, staking) and want to optimize the user experience by eliminating approval transactions.
|
|
56
|
+
|
|
57
|
+
**Additional Requirements:**
|
|
58
|
+
- Para API Key (get from https://para.xyz/)
|
|
8
59
|
|
|
9
60
|
## How to Use a Template
|
|
10
61
|
|
|
11
62
|
1. **Copy the template directory** you want to use:
|
|
12
63
|
|
|
13
64
|
```bash
|
|
65
|
+
# Plain React template
|
|
14
66
|
cp -r create-sbc-app/react my-new-sbc-app
|
|
67
|
+
|
|
68
|
+
# Dynamic wallet template
|
|
69
|
+
cp -r create-sbc-app/react-dynamic my-dynamic-app
|
|
70
|
+
|
|
71
|
+
# Para wallet template
|
|
72
|
+
cp -r create-sbc-app/react-para my-para-app
|
|
15
73
|
```
|
|
16
74
|
|
|
17
75
|
2. **Install dependencies:**
|
|
@@ -32,8 +90,8 @@ This directory contains ready-to-use templates for quickly starting new projects
|
|
|
32
90
|
```
|
|
33
91
|
|
|
34
92
|
4. **Customize as needed:**
|
|
35
|
-
- Update the API key and config in `src/App.tsx
|
|
36
|
-
- Follow
|
|
93
|
+
- Update the API key and config in `src/App.tsx`.
|
|
94
|
+
- Follow each template’s README for specific details and environment variables.
|
|
37
95
|
|
|
38
96
|
## Keeping Templates Up to Date
|
|
39
97
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# SBC App Kit Configuration
|
|
2
|
-
|
|
2
|
+
# Supported chains: "baseSepolia" | "base" | "radiusTestnet"
|
|
3
|
+
VITE_CHAIN="{{chain}}"
|
|
3
4
|
# Custom RPC URL (optional) - e.g. get one from Alchemey at https://dashboard.alchemy.com/apps
|
|
4
5
|
VITE_RPC_URL=
|
|
5
6
|
# Get your SBC API Key at https://dashboard.stablecoin.xyz
|
|
@@ -18,12 +18,12 @@ cp .env.example .env # Optional: for local overrides
|
|
|
18
18
|
|
|
19
19
|
Edit `.env` and add your SBC API key and Chain, with the optional for a custom RPC:
|
|
20
20
|
```bash
|
|
21
|
-
# "base"
|
|
22
|
-
VITE_CHAIN=
|
|
23
|
-
# Custom RPC URL (optional) - e.g. get one from
|
|
21
|
+
# Supported chains: "baseSepolia" | "base" | "radiusTestnet"
|
|
22
|
+
VITE_CHAIN={{chain}}
|
|
23
|
+
# Custom RPC URL (optional) - e.g. get one from Alchemy at https://dashboard.alchemy.com/apps
|
|
24
24
|
VITE_RPC_URL=
|
|
25
25
|
# Get your SBC API Key at https://dashboard.stablecoin.xyz
|
|
26
|
-
VITE_SBC_API_KEY=
|
|
26
|
+
VITE_SBC_API_KEY={{apiKey}}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
> **Get your API key:** Visit the [SBC Dashboard](https://dashboard.stablecoin.xyz) to create an account and get your API key.
|
|
@@ -108,13 +108,12 @@ This app is configured for **{{chain}}**. To use a different chain:
|
|
|
108
108
|
|
|
109
109
|
1. Update `.env`:
|
|
110
110
|
```bash
|
|
111
|
-
|
|
111
|
+
VITE_CHAIN=base # Base mainnet
|
|
112
|
+
VITE_CHAIN=baseSepolia # Base Sepolia testnet
|
|
113
|
+
VITE_CHAIN=radiusTestnet # Radius testnet
|
|
112
114
|
```
|
|
113
115
|
|
|
114
|
-
2.
|
|
115
|
-
```typescript
|
|
116
|
-
import { yourChain } from 'viem/chains';
|
|
117
|
-
```
|
|
116
|
+
2. Restart your development server after changing the chain
|
|
118
117
|
|
|
119
118
|
## 📖 Learn More
|
|
120
119
|
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, createContext, useContext } from 'react';
|
|
2
2
|
import { SbcProvider, WalletButton, useSbcApp, useUserOperation } from '@stablecoin.xyz/react';
|
|
3
|
+
import { radiusTestnet, TestSBC_CONTRACT_ADDRESS } from '@stablecoin.xyz/core';
|
|
3
4
|
import { base, baseSepolia } from 'viem/chains';
|
|
4
5
|
import { createPublicClient, http, getAddress, parseSignature, WalletClient, PublicClient } from 'viem';
|
|
5
6
|
import { parseUnits, encodeFunctionData, erc20Abi } from 'viem';
|
|
6
7
|
import './App.css';
|
|
7
8
|
|
|
8
9
|
// Chain selection helpers
|
|
9
|
-
const
|
|
10
|
+
const getChain = () => {
|
|
11
|
+
const chainEnv = import.meta.env.VITE_CHAIN;
|
|
12
|
+
if (chainEnv === 'base') return base;
|
|
13
|
+
if (chainEnv === 'radiusTestnet') return radiusTestnet;
|
|
14
|
+
return baseSepolia;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const chain = getChain();
|
|
10
18
|
const rpcUrl = import.meta.env.VITE_RPC_URL;
|
|
11
19
|
|
|
20
|
+
// Radius testnet uses TestSBC (a test token for development)
|
|
21
|
+
const TEST_SBC_DECIMALS = 6;
|
|
22
|
+
|
|
12
23
|
const SBC_TOKEN_ADDRESS = (chain) => {
|
|
13
24
|
if (chain.id === baseSepolia.id) {
|
|
14
25
|
return '0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16';
|
|
15
26
|
} else if (chain.id === base.id) {
|
|
16
27
|
return '0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798';
|
|
28
|
+
} else if (chain.id === radiusTestnet.id) {
|
|
29
|
+
return TestSBC_CONTRACT_ADDRESS;
|
|
17
30
|
}
|
|
18
31
|
throw new Error('Unsupported chain');
|
|
19
32
|
};
|
|
@@ -23,15 +36,26 @@ const SBC_DECIMALS = (chain) => {
|
|
|
23
36
|
return 6;
|
|
24
37
|
} else if (chain.id === base.id) {
|
|
25
38
|
return 18;
|
|
39
|
+
} else if (chain.id === radiusTestnet.id) {
|
|
40
|
+
return TEST_SBC_DECIMALS;
|
|
26
41
|
}
|
|
27
42
|
throw new Error('Unsupported chain');
|
|
28
43
|
};
|
|
29
44
|
|
|
45
|
+
const getTokenSymbol = (chain) => {
|
|
46
|
+
if (chain.id === radiusTestnet.id) {
|
|
47
|
+
return 'TestSBC';
|
|
48
|
+
}
|
|
49
|
+
return 'SBC';
|
|
50
|
+
};
|
|
51
|
+
|
|
30
52
|
const chainExplorer = (chain) => {
|
|
31
53
|
if (chain.id === baseSepolia.id) {
|
|
32
54
|
return 'https://sepolia.basescan.org';
|
|
33
55
|
} else if (chain.id === base.id) {
|
|
34
56
|
return 'https://basescan.org';
|
|
57
|
+
} else if (chain.id === radiusTestnet.id) {
|
|
58
|
+
return 'https://testnet.radiustech.xyz/testnet/explorer';
|
|
35
59
|
}
|
|
36
60
|
throw new Error('Unsupported chain');
|
|
37
61
|
};
|
|
@@ -73,6 +97,54 @@ const permitAbi = [
|
|
|
73
97
|
|
|
74
98
|
function WalletStatus({ onDisconnect }: { onDisconnect: () => void }) {
|
|
75
99
|
const { ownerAddress } = useSbcApp();
|
|
100
|
+
const [eoaBalances, setEoaBalances] = useState<{ eth: string | null; sbc: string | null }>({ eth: null, sbc: null });
|
|
101
|
+
const [isLoadingEoaBalances, setIsLoadingEoaBalances] = useState(false);
|
|
102
|
+
|
|
103
|
+
// Fetch ETH and TestSBC balances for EOA wallet
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!ownerAddress) return;
|
|
106
|
+
|
|
107
|
+
const fetchEoaBalances = async () => {
|
|
108
|
+
setIsLoadingEoaBalances(true);
|
|
109
|
+
try {
|
|
110
|
+
const [ethBalance, sbcBalance] = await Promise.all([
|
|
111
|
+
publicClient.getBalance({ address: ownerAddress as `0x${string}` }),
|
|
112
|
+
publicClient.readContract({
|
|
113
|
+
address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
|
|
114
|
+
abi: erc20Abi,
|
|
115
|
+
functionName: 'balanceOf',
|
|
116
|
+
args: [ownerAddress as `0x${string}`],
|
|
117
|
+
})
|
|
118
|
+
]);
|
|
119
|
+
setEoaBalances({ eth: ethBalance.toString(), sbc: (sbcBalance as bigint).toString() });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('Failed to fetch EOA balances:', error);
|
|
122
|
+
setEoaBalances({ eth: '0', sbc: '0' });
|
|
123
|
+
} finally {
|
|
124
|
+
setIsLoadingEoaBalances(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
fetchEoaBalances();
|
|
129
|
+
}, [ownerAddress]);
|
|
130
|
+
|
|
131
|
+
const formatEthBalance = (balance: string | null): string => {
|
|
132
|
+
if (!balance) return '0.0000';
|
|
133
|
+
try {
|
|
134
|
+
return (Number(balance) / 1e18).toFixed(4);
|
|
135
|
+
} catch {
|
|
136
|
+
return '0.0000';
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const formatSbcBalance = (balance: string | null): string => {
|
|
141
|
+
if (!balance) return '0.00';
|
|
142
|
+
try {
|
|
143
|
+
return (Number(balance) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(2);
|
|
144
|
+
} catch {
|
|
145
|
+
return '0.00';
|
|
146
|
+
}
|
|
147
|
+
};
|
|
76
148
|
|
|
77
149
|
if (!ownerAddress) return null;
|
|
78
150
|
|
|
@@ -94,6 +166,18 @@ function WalletStatus({ onDisconnect }: { onDisconnect: () => void }) {
|
|
|
94
166
|
<label>Chain:</label>
|
|
95
167
|
<div className="value">{chain.name}</div>
|
|
96
168
|
</div>
|
|
169
|
+
<div className="info-row">
|
|
170
|
+
<label>EOA ETH Balance:</label>
|
|
171
|
+
<div className="value">
|
|
172
|
+
{isLoadingEoaBalances ? 'Loading...' : `${formatEthBalance(eoaBalances.eth)} ETH`}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="info-row">
|
|
176
|
+
<label>EOA {getTokenSymbol(chain)} Balance:</label>
|
|
177
|
+
<div className="value">
|
|
178
|
+
{isLoadingEoaBalances ? 'Loading...' : `${formatSbcBalance(eoaBalances.sbc)} ${getTokenSymbol(chain)}`}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
97
181
|
</div>
|
|
98
182
|
);
|
|
99
183
|
}
|
|
@@ -207,9 +291,9 @@ function SmartAccountInfo() {
|
|
|
207
291
|
<div className="value">{formatEthBalance(account.balance)} ETH</div>
|
|
208
292
|
</div>
|
|
209
293
|
<div className="info-row">
|
|
210
|
-
<label>
|
|
294
|
+
<label>{getTokenSymbol(chain)} Balance:</label>
|
|
211
295
|
<div className="value">
|
|
212
|
-
{isLoadingBalance ? 'Loading...' : `${formatSbcBalance(sbcBalance)}
|
|
296
|
+
{isLoadingBalance ? 'Loading...' : `${formatSbcBalance(sbcBalance)} ${getTokenSymbol(chain)}`}
|
|
213
297
|
</div>
|
|
214
298
|
</div>
|
|
215
299
|
</div>
|
|
@@ -229,7 +313,7 @@ function SendSBCForm() {
|
|
|
229
313
|
try {
|
|
230
314
|
const ownerChecksum = getAddress(ownerAddress);
|
|
231
315
|
const spenderChecksum = getAddress(account.address);
|
|
232
|
-
const value = parseUnits('1', SBC_DECIMALS(chain)); // Send 1
|
|
316
|
+
const value = parseUnits('1', SBC_DECIMALS(chain)); // Send 1 token
|
|
233
317
|
const deadline = Math.floor(Date.now() / 1000) + 60 * 30; // 30 min
|
|
234
318
|
|
|
235
319
|
const signature = await getPermitSignature({
|
|
@@ -275,7 +359,7 @@ function SendSBCForm() {
|
|
|
275
359
|
|
|
276
360
|
return (
|
|
277
361
|
<div className="card">
|
|
278
|
-
<h3>💸 Send 1
|
|
362
|
+
<h3>💸 Send 1 {getTokenSymbol(chain)} Token</h3>
|
|
279
363
|
<div className="form-group">
|
|
280
364
|
<label>Recipient Address</label>
|
|
281
365
|
<input
|
|
@@ -289,11 +373,11 @@ function SendSBCForm() {
|
|
|
289
373
|
<span className="error-text">Invalid Ethereum address</span>
|
|
290
374
|
)}
|
|
291
375
|
</div>
|
|
292
|
-
|
|
376
|
+
|
|
293
377
|
<div className="status-section">
|
|
294
378
|
<div className="info-row">
|
|
295
379
|
<label>Amount:</label>
|
|
296
|
-
<div className="value">1.00
|
|
380
|
+
<div className="value">1.00 {getTokenSymbol(chain)}</div>
|
|
297
381
|
</div>
|
|
298
382
|
<div className="info-row">
|
|
299
383
|
<label>Gas fees:</label>
|
|
@@ -310,18 +394,18 @@ function SendSBCForm() {
|
|
|
310
394
|
disabled={!isFormValid || isLoading || !account}
|
|
311
395
|
className="primary"
|
|
312
396
|
>
|
|
313
|
-
{isLoading ? 'Waiting for signature...' :
|
|
397
|
+
{isLoading ? 'Waiting for signature...' : `Send 1 ${getTokenSymbol(chain)}`}
|
|
314
398
|
</button>
|
|
315
399
|
|
|
316
400
|
{isSuccess && data && (
|
|
317
401
|
<div className="success-message">
|
|
318
402
|
<p>✅ Transaction Successful!</p>
|
|
319
|
-
<a
|
|
403
|
+
<a
|
|
320
404
|
href={`${chainExplorer(chain)}/tx/${data.transactionHash}`}
|
|
321
405
|
target="_blank"
|
|
322
406
|
rel="noopener noreferrer"
|
|
323
407
|
>
|
|
324
|
-
View
|
|
408
|
+
View transaction: {data.transactionHash}
|
|
325
409
|
</a>
|
|
326
410
|
</div>
|
|
327
411
|
)}
|
|
@@ -1,24 +1,142 @@
|
|
|
1
|
-
#
|
|
1
|
+
# {{projectName}}
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React + Vite app with Dynamic wallet integration and SBC App Kit for gasless transactions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This template combines:
|
|
8
|
+
- **Dynamic SDK** - Embedded wallet with social logins (Google, Twitter, Discord, email, SMS)
|
|
9
|
+
- **SBC App Kit** - Account abstraction and gasless transactions
|
|
10
|
+
- **React + Vite** - Fast, modern development experience
|
|
11
|
+
|
|
12
|
+
Perfect for applications that need a seamless onboarding experience with social authentication and gasless transactions.
|
|
4
13
|
|
|
5
14
|
## Quick Start
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
1. **Install dependencies:**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm install
|
|
20
|
+
# or
|
|
21
|
+
npm install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. **Configure environment:**
|
|
25
|
+
|
|
26
|
+
Copy `.env.template` to `.env` and fill in your API keys:
|
|
27
|
+
|
|
28
|
+
```env
|
|
29
|
+
# Your SBC API key from https://dashboard.stablecoin.xyz
|
|
30
|
+
VITE_SBC_API_KEY={{apiKey}}
|
|
31
|
+
|
|
32
|
+
# Your Dynamic Environment ID from https://app.dynamic.xyz/
|
|
33
|
+
VITE_DYNAMIC_ENVIRONMENT_ID=your_dynamic_env_id
|
|
34
|
+
|
|
35
|
+
# Supported chains: "baseSepolia" | "base"
|
|
36
|
+
VITE_CHAIN={{chain}}
|
|
37
|
+
|
|
38
|
+
# Optional: Custom RPC URL (e.g., from Alchemy)
|
|
39
|
+
VITE_RPC_URL=
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
3. **Start the development server:**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm dev
|
|
46
|
+
# or
|
|
47
|
+
npm run dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
4. **Open your browser:**
|
|
51
|
+
Visit [http://localhost:5173](http://localhost:5173) (or the port shown in your terminal).
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
### Dynamic Wallet Integration
|
|
56
|
+
- Social logins (Google, Twitter, Discord, GitHub, Apple, etc.)
|
|
57
|
+
- Email and SMS authentication
|
|
58
|
+
- Embedded wallet creation
|
|
59
|
+
- Multi-chain support
|
|
60
|
+
- Customizable authentication UI
|
|
11
61
|
|
|
12
|
-
|
|
62
|
+
### SBC App Kit Integration
|
|
63
|
+
- Automatic smart account creation
|
|
64
|
+
- Gasless transactions
|
|
65
|
+
- Gas estimation and previews
|
|
66
|
+
- Batch transactions
|
|
67
|
+
- Real-time balance tracking
|
|
68
|
+
- Transaction history
|
|
13
69
|
|
|
14
|
-
|
|
70
|
+
### Developer Experience
|
|
71
|
+
- TypeScript support with full type safety
|
|
72
|
+
- Hot module replacement for fast development
|
|
73
|
+
- Environment-based configuration
|
|
74
|
+
- Comprehensive error handling
|
|
75
|
+
- Debug logging
|
|
15
76
|
|
|
77
|
+
## Getting Your API Keys
|
|
78
|
+
|
|
79
|
+
### SBC API Key
|
|
80
|
+
1. Visit [https://dashboard.stablecoin.xyz](https://dashboard.stablecoin.xyz)
|
|
81
|
+
2. Sign up or log in
|
|
82
|
+
3. Create a new API key
|
|
83
|
+
4. Add it to your `.env` file as `VITE_SBC_API_KEY`
|
|
84
|
+
|
|
85
|
+
### Dynamic Environment ID
|
|
86
|
+
1. Visit [https://app.dynamic.xyz/](https://app.dynamic.xyz/)
|
|
87
|
+
2. Create an account and a new project
|
|
88
|
+
3. Copy your Environment ID
|
|
89
|
+
4. Add it to your `.env` file as `VITE_DYNAMIC_ENVIRONMENT_ID`
|
|
90
|
+
5. Configure your allowed authentication methods in the Dynamic dashboard
|
|
91
|
+
|
|
92
|
+
## Customization
|
|
93
|
+
|
|
94
|
+
### Modifying Authentication Methods
|
|
95
|
+
Edit the Dynamic configuration in `src/App.tsx`:
|
|
96
|
+
```typescript
|
|
97
|
+
// Configure which authentication methods to enable
|
|
98
|
+
walletConnectors={[EthereumWalletConnectors]}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Styling
|
|
102
|
+
- Edit `src/App.css` for component styles
|
|
103
|
+
- Edit `src/index.css` for global styles
|
|
104
|
+
- Customize Tailwind configuration in `tailwind.config.js`
|
|
105
|
+
|
|
106
|
+
### Chain Configuration
|
|
107
|
+
Change the chain in your `.env` file:
|
|
16
108
|
```env
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Optional
|
|
20
|
-
VITE_CHAIN=baseSepolia # or "base"
|
|
21
|
-
VITE_RPC_URL=
|
|
109
|
+
VITE_CHAIN=base # Base mainnet
|
|
110
|
+
VITE_CHAIN=baseSepolia # Base Sepolia testnet
|
|
22
111
|
```
|
|
23
112
|
|
|
113
|
+
**Note:** Radius Testnet is not currently supported by the Dynamic wallet provider. Use the base `react` template for Radius Testnet support.
|
|
114
|
+
|
|
115
|
+
## Building for Production
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pnpm build
|
|
119
|
+
# or
|
|
120
|
+
npm run build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The built files will be in the `dist/` directory, ready for deployment to any static host (Vercel, Netlify, Cloudflare Pages, etc.).
|
|
124
|
+
|
|
125
|
+
## Learn More
|
|
126
|
+
|
|
127
|
+
- [SBC Documentation](https://docs.stablecoin.xyz)
|
|
128
|
+
- [Dynamic Documentation](https://docs.dynamic.xyz/)
|
|
129
|
+
- [SBC App Kit GitHub](https://github.com/stablecoinxyz/app-kit)
|
|
130
|
+
- [React Documentation](https://react.dev)
|
|
131
|
+
- [Vite Documentation](https://vitejs.dev)
|
|
132
|
+
|
|
133
|
+
## Support
|
|
134
|
+
|
|
135
|
+
If you encounter any issues:
|
|
136
|
+
1. Check the [SBC Documentation](https://docs.stablecoin.xyz)
|
|
137
|
+
2. Review the [Dynamic Documentation](https://docs.dynamic.xyz/)
|
|
138
|
+
3. Open an issue on the [SBC App Kit repository](https://github.com/stablecoinxyz/app-kit/issues)
|
|
139
|
+
|
|
140
|
+
## License
|
|
24
141
|
|
|
142
|
+
MIT
|
|
@@ -2,25 +2,48 @@ import { DynamicContextProvider, useDynamicContext, DynamicUserProfile, DynamicW
|
|
|
2
2
|
import { EthereumWalletConnectors } from '@dynamic-labs/ethereum';
|
|
3
3
|
import { ZeroDevSmartWalletConnectors } from '@dynamic-labs/ethereum-aa';
|
|
4
4
|
import { useSbcDynamic } from '@stablecoin.xyz/react';
|
|
5
|
+
import { radiusTestnet, TestSBC_CONTRACT_ADDRESS } from '@stablecoin.xyz/core';
|
|
5
6
|
import { baseSepolia, base, type Chain } from 'viem/chains';
|
|
6
7
|
import { createPublicClient, http, getAddress, parseUnits, encodeFunctionData, erc20Abi } from 'viem';
|
|
7
8
|
import { useEffect, useState } from 'react';
|
|
8
9
|
import './App.css';
|
|
9
10
|
|
|
10
|
-
const
|
|
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();
|
|
11
19
|
const rpcUrl = import.meta.env.VITE_RPC_URL;
|
|
12
20
|
|
|
21
|
+
// Radius testnet uses TestSBC (a test token for development)
|
|
22
|
+
const TEST_SBC_DECIMALS = 6;
|
|
23
|
+
|
|
13
24
|
const SBC_TOKEN_ADDRESS = (chain: Chain) => {
|
|
14
25
|
if (chain.id === baseSepolia.id) return '0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16';
|
|
15
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;
|
|
16
35
|
throw new Error('Unsupported chain');
|
|
17
36
|
};
|
|
18
37
|
|
|
19
|
-
const
|
|
38
|
+
const getTokenSymbol = (chain: Chain) => {
|
|
39
|
+
if (chain.id === radiusTestnet.id) return 'TestSBC';
|
|
40
|
+
return 'SBC';
|
|
41
|
+
};
|
|
20
42
|
|
|
21
43
|
const chainExplorer = (chain: Chain) => {
|
|
22
44
|
if (chain.id === baseSepolia.id) return 'https://sepolia.basescan.org';
|
|
23
45
|
if (chain.id === base.id) return 'https://basescan.org';
|
|
46
|
+
if (chain.id === radiusTestnet.id) return 'https://testnet.radiustech.xyz/testnet/explorer';
|
|
24
47
|
return '';
|
|
25
48
|
};
|
|
26
49
|
|
|
@@ -74,7 +97,8 @@ function WalletStatus() {
|
|
|
74
97
|
})
|
|
75
98
|
]);
|
|
76
99
|
setBalances({ eth: ethBalance.toString(), sbc: (sbcBalance as bigint).toString() });
|
|
77
|
-
} catch {
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Failed to fetch EOA balances:', error);
|
|
78
102
|
setBalances({ eth: null, sbc: null });
|
|
79
103
|
}
|
|
80
104
|
})();
|
|
@@ -93,10 +117,10 @@ function WalletStatus() {
|
|
|
93
117
|
<p className="text-xs text-green-600 mb-2">Connected via Dynamic SDK</p>
|
|
94
118
|
<p className="text-xs text-green-600 mb-2"><strong>Chain:</strong> {chain.name} (ID: {chain.id})</p>
|
|
95
119
|
<div className="mt-2 pt-2 border-t border-green-200">
|
|
96
|
-
<p className="text-xs font-medium text-green-700 mb-1">Wallet Balances:</p>
|
|
120
|
+
<p className="text-xs font-medium text-green-700 mb-1">EOA Wallet Balances:</p>
|
|
97
121
|
<div className="flex gap-4">
|
|
98
122
|
<span className="text-xs text-green-600"><strong>ETH:</strong> {fmtEth(balances.eth)}</span>
|
|
99
|
-
<span className="text-xs text-green-600"><strong>
|
|
123
|
+
<span className="text-xs text-green-600"><strong>{getTokenSymbol(chain)}:</strong> {fmtSbc(balances.sbc)}</span>
|
|
100
124
|
</div>
|
|
101
125
|
</div>
|
|
102
126
|
</div>
|
|
@@ -118,7 +142,10 @@ function SmartAccountInfo({ account, refreshAccount, isLoadingAccount, accountEr
|
|
|
118
142
|
args: [account.address as `0x${string}`],
|
|
119
143
|
});
|
|
120
144
|
setSbcBalance((bal as bigint).toString());
|
|
121
|
-
} catch
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('Failed to fetch smart account SBC balance:', error);
|
|
147
|
+
setSbcBalance('0');
|
|
148
|
+
}
|
|
122
149
|
})();
|
|
123
150
|
}, [account?.address]);
|
|
124
151
|
|
|
@@ -136,13 +163,8 @@ function SmartAccountInfo({ account, refreshAccount, isLoadingAccount, accountEr
|
|
|
136
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>
|
|
137
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>
|
|
138
165
|
<div className="flex justify-between"><span className="text-purple-700">Nonce:</span><span className="text-purple-600">{account.nonce}</span></div>
|
|
139
|
-
<div className="
|
|
140
|
-
|
|
141
|
-
<div className="space-y-1">
|
|
142
|
-
<div className="flex justify-between"><span className="text-purple-700">ETH:</span><span className="text-purple-600 font-mono text-xs">{fmtEth(account.balance)} ETH</span></div>
|
|
143
|
-
<div className="flex justify-between"><span className="text-purple-700">SBC:</span><span className="text-purple-600 font-mono text-xs">{fmtSbc(sbcBalance)} SBC</span></div>
|
|
144
|
-
</div>
|
|
145
|
-
</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>
|
|
146
168
|
</div>
|
|
147
169
|
{accountError && <p className="mt-2 text-xs text-red-600">{String(accountError)}</p>}
|
|
148
170
|
</div>
|
|
@@ -216,29 +238,29 @@ function TransactionForm({ account, sbcAppKit }: { account: any; sbcAppKit: any
|
|
|
216
238
|
|
|
217
239
|
return (
|
|
218
240
|
<div className="p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
|
|
219
|
-
<h3 className="font-semibold text-gray-800 mb-4">💸 Send
|
|
241
|
+
<h3 className="font-semibold text-gray-800 mb-4">💸 Send {getTokenSymbol(chain)} Tokens</h3>
|
|
220
242
|
<div className="space-y-4">
|
|
221
243
|
<div>
|
|
222
244
|
<label className="block text-sm font-medium text-gray-700 mb-2">Recipient Address</label>
|
|
223
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" />
|
|
224
246
|
</div>
|
|
225
247
|
<div>
|
|
226
|
-
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (
|
|
248
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">Amount ({getTokenSymbol(chain)})</label>
|
|
227
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" />
|
|
228
250
|
</div>
|
|
229
251
|
<div className="p-3 bg-gray-50 rounded">
|
|
230
|
-
<div className="flex justify-between text-sm"><span>Amount:</span><span className="font-medium">{amount}
|
|
252
|
+
<div className="flex justify-between text-sm"><span>Amount:</span><span className="font-medium">{amount} {getTokenSymbol(chain)}</span></div>
|
|
231
253
|
<div className="flex justify-between text-xs text-gray-600"><span>Gas fees:</span><span>Covered by SBC Paymaster ✨</span></div>
|
|
232
254
|
<div className="flex justify-between text-xs text-gray-600"><span>Signing:</span><span>Your Dynamic wallet will prompt to sign 🖊️</span></div>
|
|
233
255
|
</div>
|
|
234
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">
|
|
235
|
-
{status==='loading' ? 'Waiting for signature...' : `Send ${amount}
|
|
257
|
+
{status==='loading' ? 'Waiting for signature...' : `Send ${amount} ${getTokenSymbol(chain)}`}
|
|
236
258
|
</button>
|
|
237
259
|
{status==='success' && result && (
|
|
238
260
|
<div className="p-3 bg-green-50 border border-green-200 rounded">
|
|
239
261
|
<p className="text-sm text-green-800 font-medium">✅ Transaction Submitted</p>
|
|
240
262
|
<p className="text-xs text-green-600 font-mono break-all mt-1">
|
|
241
|
-
<a href={`${chainExplorer(chain)}/tx/${result.transactionHash}`} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">View
|
|
263
|
+
<a href={`${chainExplorer(chain)}/tx/${result.transactionHash}`} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">View transaction: {result.transactionHash}</a>
|
|
242
264
|
</p>
|
|
243
265
|
</div>
|
|
244
266
|
)}
|
|
@@ -1,24 +1,165 @@
|
|
|
1
|
-
#
|
|
1
|
+
# {{projectName}}
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React + Vite app with Para wallet integration and SBC App Kit for advanced gasless transactions using EIP-2612 permits.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This template combines:
|
|
8
|
+
- **Para** - Advanced wallet integration with EIP-2612 permit support
|
|
9
|
+
- **SBC App Kit** - Account abstraction and gasless transactions
|
|
10
|
+
- **React + Vite** - Fast, modern development experience
|
|
11
|
+
|
|
12
|
+
Perfect for applications that need advanced gasless transaction patterns using EIP-2612 permit signatures, enabling token approvals without separate transactions.
|
|
13
|
+
|
|
14
|
+
## What are EIP-2612 Permits?
|
|
15
|
+
|
|
16
|
+
EIP-2612 permits allow users to sign approval messages off-chain, eliminating the need for a separate approval transaction. This creates a superior user experience:
|
|
17
|
+
- No approval transaction fees
|
|
18
|
+
- Single-step token operations
|
|
19
|
+
- Enhanced security through time-limited permits
|
|
20
|
+
- Better UX for DeFi interactions
|
|
4
21
|
|
|
5
22
|
## Quick Start
|
|
6
23
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
24
|
+
1. **Install dependencies:**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm install
|
|
28
|
+
# or
|
|
29
|
+
npm install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. **Configure environment:**
|
|
33
|
+
|
|
34
|
+
Copy `.env.template` to `.env` and fill in your API keys:
|
|
35
|
+
|
|
36
|
+
```env
|
|
37
|
+
# Your SBC API key from https://dashboard.stablecoin.xyz
|
|
38
|
+
VITE_SBC_API_KEY={{apiKey}}
|
|
39
|
+
|
|
40
|
+
# Your Para API key
|
|
41
|
+
VITE_PARA_API_KEY=your_para_api_key
|
|
42
|
+
|
|
43
|
+
# Supported chains: "baseSepolia" | "base"
|
|
44
|
+
VITE_CHAIN={{chain}}
|
|
45
|
+
|
|
46
|
+
# Optional: Custom RPC URL (e.g., from Alchemy)
|
|
47
|
+
VITE_RPC_URL=
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
3. **Start the development server:**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm dev
|
|
54
|
+
# or
|
|
55
|
+
npm run dev
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
4. **Open your browser:**
|
|
59
|
+
Visit [http://localhost:5173](http://localhost:5173) (or the port shown in your terminal).
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
### Para Wallet Integration
|
|
64
|
+
- EIP-2612 permit signature support
|
|
65
|
+
- Gasless token approvals
|
|
66
|
+
- Advanced transaction patterns
|
|
67
|
+
- Seamless wallet experience
|
|
68
|
+
- Optimized for DeFi applications
|
|
11
69
|
|
|
12
|
-
|
|
70
|
+
### SBC App Kit Integration
|
|
71
|
+
- Automatic smart account creation
|
|
72
|
+
- Gasless transactions
|
|
73
|
+
- Gas estimation and previews
|
|
74
|
+
- Batch transactions
|
|
75
|
+
- Real-time balance tracking
|
|
76
|
+
- Transaction history
|
|
13
77
|
|
|
14
|
-
|
|
78
|
+
### Advanced Use Cases
|
|
79
|
+
- Single-step token swaps (no separate approval)
|
|
80
|
+
- Gasless staking operations
|
|
81
|
+
- Permit-based token transfers
|
|
82
|
+
- Enhanced DeFi interactions
|
|
83
|
+
- Meta-transactions with permits
|
|
15
84
|
|
|
85
|
+
### Developer Experience
|
|
86
|
+
- TypeScript support with full type safety
|
|
87
|
+
- Hot module replacement for fast development
|
|
88
|
+
- Environment-based configuration
|
|
89
|
+
- Comprehensive error handling
|
|
90
|
+
- Debug logging
|
|
91
|
+
|
|
92
|
+
## Getting Your API Keys
|
|
93
|
+
|
|
94
|
+
### SBC API Key
|
|
95
|
+
1. Visit [https://dashboard.stablecoin.xyz](https://dashboard.stablecoin.xyz)
|
|
96
|
+
2. Sign up or log in
|
|
97
|
+
3. Create a new API key
|
|
98
|
+
4. Add it to your `.env` file as `VITE_SBC_API_KEY`
|
|
99
|
+
|
|
100
|
+
### Para API Key
|
|
101
|
+
1. Visit [https://para.xyz](https://para.xyz)
|
|
102
|
+
2. Create an account and register your application
|
|
103
|
+
3. Copy your API key
|
|
104
|
+
4. Add it to your `.env` file as `VITE_PARA_API_KEY`
|
|
105
|
+
|
|
106
|
+
## Customization
|
|
107
|
+
|
|
108
|
+
### Styling
|
|
109
|
+
- Edit `src/App.css` for component styles
|
|
110
|
+
- Edit `src/index.css` for global styles
|
|
111
|
+
- Customize Tailwind configuration in `tailwind.config.js`
|
|
112
|
+
|
|
113
|
+
### Chain Configuration
|
|
114
|
+
Change the chain in your `.env` file:
|
|
16
115
|
```env
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Optional
|
|
20
|
-
VITE_CHAIN=baseSepolia # or "base"
|
|
21
|
-
VITE_RPC_URL=
|
|
116
|
+
VITE_CHAIN=base # Base mainnet
|
|
117
|
+
VITE_CHAIN=baseSepolia # Base Sepolia testnet
|
|
22
118
|
```
|
|
23
119
|
|
|
120
|
+
**Note:** Radius Testnet is not currently supported by the Para wallet provider. Use the base `react` template for Radius Testnet support.
|
|
121
|
+
|
|
122
|
+
### Permit Configuration
|
|
123
|
+
Customize permit parameters in your application logic:
|
|
124
|
+
- Deadline (expiration time)
|
|
125
|
+
- Spender address
|
|
126
|
+
- Token amounts
|
|
127
|
+
|
|
128
|
+
## Building for Production
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pnpm build
|
|
132
|
+
# or
|
|
133
|
+
npm run build
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The built files will be in the `dist/` directory, ready for deployment to any static host (Vercel, Netlify, Cloudflare Pages, etc.).
|
|
137
|
+
|
|
138
|
+
## Use Cases
|
|
139
|
+
|
|
140
|
+
This template is ideal for:
|
|
141
|
+
- DeFi applications requiring token approvals
|
|
142
|
+
- DEX interfaces with optimized UX
|
|
143
|
+
- Staking platforms
|
|
144
|
+
- Lending/borrowing protocols
|
|
145
|
+
- Any application benefiting from gasless approvals
|
|
146
|
+
|
|
147
|
+
## Learn More
|
|
148
|
+
|
|
149
|
+
- [SBC Documentation](https://docs.stablecoin.xyz)
|
|
150
|
+
- [Para Documentation](https://docs.para.xyz)
|
|
151
|
+
- [EIP-2612 Specification](https://eips.ethereum.org/EIPS/eip-2612)
|
|
152
|
+
- [SBC App Kit GitHub](https://github.com/stablecoinxyz/app-kit)
|
|
153
|
+
- [React Documentation](https://react.dev)
|
|
154
|
+
- [Vite Documentation](https://vitejs.dev)
|
|
155
|
+
|
|
156
|
+
## Support
|
|
157
|
+
|
|
158
|
+
If you encounter any issues:
|
|
159
|
+
1. Check the [SBC Documentation](https://docs.stablecoin.xyz)
|
|
160
|
+
2. Review the [Para Documentation](https://docs.para.xyz)
|
|
161
|
+
3. Open an issue on the [SBC App Kit repository](https://github.com/stablecoinxyz/app-kit/issues)
|
|
162
|
+
|
|
163
|
+
## License
|
|
24
164
|
|
|
165
|
+
MIT
|
|
@@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
2
2
|
import { Environment, ParaProvider } from '@getpara/react-sdk';
|
|
3
3
|
import '@getpara/react-sdk/styles.css';
|
|
4
4
|
import { useSbcPara } from '@stablecoin.xyz/react';
|
|
5
|
+
import { radiusTestnet, TestSBC_CONTRACT_ADDRESS } from '@stablecoin.xyz/core';
|
|
5
6
|
import { useAccount, useWallet, useSignMessage } from '@getpara/react-sdk';
|
|
6
7
|
import { baseSepolia, base, type Chain } from 'viem/chains';
|
|
7
8
|
import { createPublicClient, http, getAddress, parseUnits, encodeFunctionData, erc20Abi } from 'viem';
|
|
@@ -12,15 +13,39 @@ import { buildPermitTypedData, hashPermitTypedData, hex32ToBase64, normalizeSign
|
|
|
12
13
|
import { usePara } from './hooks/usePara';
|
|
13
14
|
|
|
14
15
|
const queryClient = new QueryClient();
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
const getChain = () => {
|
|
18
|
+
const chainEnv = import.meta.env.VITE_CHAIN;
|
|
19
|
+
if (chainEnv === 'base') return base;
|
|
20
|
+
if (chainEnv === 'radiusTestnet') return radiusTestnet;
|
|
21
|
+
return baseSepolia;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const chain = getChain();
|
|
16
25
|
const rpcUrl = import.meta.env.VITE_RPC_URL;
|
|
17
26
|
|
|
27
|
+
// Radius testnet uses TestSBC (a test token for development)
|
|
28
|
+
const TEST_SBC_DECIMALS = 6;
|
|
29
|
+
|
|
18
30
|
const SBC_TOKEN_ADDRESS = (chain: Chain) => {
|
|
19
31
|
if (chain.id === baseSepolia.id) return '0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16';
|
|
20
32
|
if (chain.id === base.id) return '0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798';
|
|
33
|
+
if (chain.id === radiusTestnet.id) return TestSBC_CONTRACT_ADDRESS;
|
|
34
|
+
throw new Error('Unsupported chain');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const SBC_DECIMALS = (chain: Chain) => {
|
|
38
|
+
if (chain.id === baseSepolia.id) return 6;
|
|
39
|
+
if (chain.id === base.id) return 18;
|
|
40
|
+
if (chain.id === radiusTestnet.id) return TEST_SBC_DECIMALS;
|
|
21
41
|
throw new Error('Unsupported chain');
|
|
22
42
|
};
|
|
23
|
-
|
|
43
|
+
|
|
44
|
+
const getTokenSymbol = (chain: Chain) => {
|
|
45
|
+
if (chain.id === radiusTestnet.id) return 'TestSBC';
|
|
46
|
+
return 'SBC';
|
|
47
|
+
};
|
|
48
|
+
|
|
24
49
|
const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
|
|
25
50
|
|
|
26
51
|
// ERC20 + EIP-2612 nonces helper ABI
|
|
@@ -58,7 +83,12 @@ const permitAbi = [
|
|
|
58
83
|
}
|
|
59
84
|
] as const;
|
|
60
85
|
|
|
61
|
-
const chainExplorer = (c: Chain) =>
|
|
86
|
+
const chainExplorer = (c: Chain) => {
|
|
87
|
+
if (c.id === baseSepolia.id) return 'https://sepolia.basescan.org';
|
|
88
|
+
if (c.id === base.id) return 'https://basescan.org';
|
|
89
|
+
if (c.id === radiusTestnet.id) return 'https://testnet.radiustech.xyz/testnet/explorer';
|
|
90
|
+
return '';
|
|
91
|
+
};
|
|
62
92
|
|
|
63
93
|
function SmartAccountInfo({ account, refreshAccount, isLoadingAccount, accountError }: any) {
|
|
64
94
|
const [sbcBalance, setSbcBalance] = useState<string | null>(null);
|
|
@@ -73,7 +103,10 @@ function SmartAccountInfo({ account, refreshAccount, isLoadingAccount, accountEr
|
|
|
73
103
|
args: [account.address as `0x${string}`],
|
|
74
104
|
});
|
|
75
105
|
setSbcBalance((bal as bigint).toString());
|
|
76
|
-
} catch
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Failed to fetch smart account SBC balance:', error);
|
|
108
|
+
setSbcBalance('0');
|
|
109
|
+
}
|
|
77
110
|
})();
|
|
78
111
|
}, [account?.address]);
|
|
79
112
|
|
|
@@ -91,13 +124,8 @@ function SmartAccountInfo({ account, refreshAccount, isLoadingAccount, accountEr
|
|
|
91
124
|
<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>
|
|
92
125
|
<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>
|
|
93
126
|
<div className="flex justify-between"><span className="text-purple-700">Nonce:</span><span className="text-purple-600">{account.nonce}</span></div>
|
|
94
|
-
<div className="
|
|
95
|
-
|
|
96
|
-
<div className="space-y-1">
|
|
97
|
-
<div className="flex justify-between"><span className="text-purple-700">ETH:</span><span className="text-purple-600 font-mono text-xs">{fmtEth(account.balance)} ETH</span></div>
|
|
98
|
-
<div className="flex justify-between"><span className="text-purple-700">SBC:</span><span className="text-purple-600 font-mono text-xs">{fmtSbc(sbcBalance)} SBC</span></div>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
127
|
+
<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>
|
|
128
|
+
<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>
|
|
101
129
|
</div>
|
|
102
130
|
{accountError && <p className="mt-2 text-xs text-red-600">{String(accountError)}</p>}
|
|
103
131
|
</div>
|
|
@@ -186,29 +214,29 @@ function TransactionForm({ account, sbcAppKit }: { account: any; sbcAppKit: any
|
|
|
186
214
|
|
|
187
215
|
return (
|
|
188
216
|
<div className="p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
|
|
189
|
-
<h3 className="font-semibold text-gray-800 mb-4">💸 Send
|
|
217
|
+
<h3 className="font-semibold text-gray-800 mb-4">💸 Send {getTokenSymbol(chain)} Tokens</h3>
|
|
190
218
|
<div className="space-y-4">
|
|
191
219
|
<div>
|
|
192
220
|
<label className="block text-sm font-medium text-gray-700 mb-2">Recipient Address</label>
|
|
193
221
|
<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" />
|
|
194
222
|
</div>
|
|
195
223
|
<div>
|
|
196
|
-
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (
|
|
224
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">Amount ({getTokenSymbol(chain)})</label>
|
|
197
225
|
<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" />
|
|
198
226
|
</div>
|
|
199
227
|
<div className="p-3 bg-gray-50 rounded">
|
|
200
|
-
<div className="flex justify-between text-sm"><span>Amount:</span><span className="font-medium">{amount}
|
|
228
|
+
<div className="flex justify-between text-sm"><span>Amount:</span><span className="font-medium">{amount} {getTokenSymbol(chain)}</span></div>
|
|
201
229
|
<div className="flex justify-between text-xs text-gray-600"><span>Gas fees:</span><span>Covered by SBC Paymaster ✨</span></div>
|
|
202
230
|
<div className="flex justify-between text-xs text-gray-600"><span>Signing:</span><span>Your wallet will prompt to sign 🖊️</span></div>
|
|
203
231
|
</div>
|
|
204
232
|
<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">
|
|
205
|
-
{status==='loading' ? 'Waiting for signature...' : `Send ${amount}
|
|
233
|
+
{status==='loading' ? 'Waiting for signature...' : `Send ${amount} ${getTokenSymbol(chain)}`}
|
|
206
234
|
</button>
|
|
207
235
|
{status==='success' && result && (
|
|
208
236
|
<div className="p-3 bg-green-50 border border-green-200 rounded">
|
|
209
237
|
<p className="text-sm text-green-800 font-medium">✅ Transaction Submitted</p>
|
|
210
238
|
<p className="text-xs text-green-600 font-mono break-all mt-1">
|
|
211
|
-
<a href={`${chainExplorer(chain)}/tx/${result.transactionHash}`} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">View
|
|
239
|
+
<a href={`${chainExplorer(chain)}/tx/${result.transactionHash}`} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">View transaction: {result.transactionHash}</a>
|
|
212
240
|
</p>
|
|
213
241
|
</div>
|
|
214
242
|
)}
|