create-sbc-app 0.1.5 → 0.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 +9 -6
- package/bin/cli.js +30 -7
- package/package.json +1 -2
- package/templates/README.md +0 -2
- package/templates/react/package.json.template +3 -5
- package/templates/react-dynamic/.env.template +7 -0
- package/templates/react-dynamic/README.md.template +24 -0
- package/templates/react-dynamic/eslint.config.js.template +34 -0
- package/templates/react-dynamic/index.html.template +14 -0
- package/templates/react-dynamic/package.json.template +33 -0
- package/templates/react-dynamic/postcss.config.js.template +8 -0
- package/templates/react-dynamic/public/sbc-logo.png +0 -0
- package/templates/react-dynamic/src/App.css.template +5 -0
- package/templates/react-dynamic/src/App.tsx.template +322 -0
- package/templates/react-dynamic/src/env.d.ts.template +14 -0
- package/templates/react-dynamic/src/index.css.template +15 -0
- package/templates/react-dynamic/src/main.tsx.template +12 -0
- package/templates/react-dynamic/tailwind.config.js.template +13 -0
- package/templates/react-dynamic/tsconfig.json.template +18 -0
- package/templates/react-dynamic/vite.config.ts.template +11 -0
- package/templates/react-para/.env.template +7 -0
- package/templates/react-para/README.md.template +24 -0
- package/templates/react-para/eslint.config.js.template +34 -0
- package/templates/react-para/index.html.template +14 -0
- package/templates/react-para/package.json.template +35 -0
- package/templates/react-para/postcss.config.js.template +8 -0
- package/templates/react-para/public/sbc-logo.png +0 -0
- package/templates/react-para/src/App.tsx.template +333 -0
- package/templates/react-para/src/components/ConnectButton.tsx.template +99 -0
- package/templates/react-para/src/env.d.ts.template +14 -0
- package/templates/react-para/src/hooks/usePara.ts.template +34 -0
- package/templates/react-para/src/hooks/useParaViem.ts.template +61 -0
- package/templates/react-para/src/index.css.template +5 -0
- package/templates/react-para/src/main.tsx.template +12 -0
- package/templates/react-para/src/providers.tsx.template +39 -0
- package/templates/react-para/src/utils/permit.ts.template +217 -0
- package/templates/react-para/tailwind.config.js.template +13 -0
- package/templates/react-para/tsconfig.json.template +18 -0
- package/templates/react-para/vite.config.ts.template +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stable Coin Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Arguments:
|
|
|
33
33
|
|
|
34
34
|
Options:
|
|
35
35
|
-V, --version output the version number
|
|
36
|
-
-t, --template <type> Template to use: react,
|
|
36
|
+
-t, --template <type> Template to use: react, react-dynamic, or react-para
|
|
37
37
|
--api-key <apiKey> Your SBC API key for immediate configuration
|
|
38
38
|
--wallet <wallet> Wallet integration (not yet implemented)
|
|
39
39
|
-h, --help display help for command
|
|
@@ -41,11 +41,14 @@ Options:
|
|
|
41
41
|
Examples:
|
|
42
42
|
$ create-sbc-app my-app
|
|
43
43
|
$ create-sbc-app my-app --template react
|
|
44
|
-
$ 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
|
|
45
47
|
|
|
46
48
|
Available Templates:
|
|
47
|
-
- react
|
|
48
|
-
-
|
|
49
|
+
- react React + Vite template with SBC integration
|
|
50
|
+
- react-dynamic React + Vite with Dynamic wallet integration
|
|
51
|
+
- react-para React + Vite with Para wallet integration
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
## ✨ Features
|
|
@@ -166,8 +169,8 @@ npm run dev
|
|
|
166
169
|
|
|
167
170
|
## 📚 Documentation
|
|
168
171
|
|
|
169
|
-
- **[SBC
|
|
170
|
-
- **[GitHub Repository](https://github.com/stablecoinxyz/app-kit)** - Source code and examples
|
|
172
|
+
- **[SBC Documentation](https://docs.stablecoin.xyz)** - Official docs
|
|
173
|
+
- **[GitHub Repository](https://github.com/stablecoinxyz/app-kit)** - AppKit API Reference. Source code and examples
|
|
171
174
|
|
|
172
175
|
## 📄 License
|
|
173
176
|
|
package/bin/cli.js
CHANGED
|
@@ -10,9 +10,9 @@ const program = new Command();
|
|
|
10
10
|
program
|
|
11
11
|
.name('create-sbc-app')
|
|
12
12
|
.description('Create a new SBC App Kit project with an opinionated template')
|
|
13
|
-
.version('0.
|
|
13
|
+
.version('0.2.0')
|
|
14
14
|
.argument('[project-directory]', 'Directory to create the new app in')
|
|
15
|
-
.option('-t, --template <template>', 'Template to use: react,
|
|
15
|
+
.option('-t, --template <template>', 'Template to use: react, react-dynamic, or react-para')
|
|
16
16
|
.option('--api-key <apiKey>', 'Your SBC API key for immediate configuration')
|
|
17
17
|
.option('--wallet <wallet>', 'Wallet integration (not yet implemented)')
|
|
18
18
|
.addHelpText('after', `
|
|
@@ -22,8 +22,9 @@ Examples:
|
|
|
22
22
|
$ create-sbc-app my-app --template react --api-key your-api-key
|
|
23
23
|
|
|
24
24
|
Available Templates:
|
|
25
|
-
- react
|
|
26
|
-
-
|
|
25
|
+
- react React + Vite template with SBC integration
|
|
26
|
+
- react-dynamic React + Vite with Dynamic wallet integration
|
|
27
|
+
- react-para React + Vite with Para wallet integration
|
|
27
28
|
`)
|
|
28
29
|
.action(async (dir, options) => {
|
|
29
30
|
if (options.wallet) {
|
|
@@ -32,7 +33,8 @@ Available Templates:
|
|
|
32
33
|
}
|
|
33
34
|
const templateChoices = [
|
|
34
35
|
{ title: 'React', value: 'react' },
|
|
35
|
-
{ title: '
|
|
36
|
+
{ title: 'React (Dynamic wallet)', value: 'react-dynamic' },
|
|
37
|
+
{ title: 'React (Para wallet)', value: 'react-para' }
|
|
36
38
|
];
|
|
37
39
|
// Use provided argument or prompt for project directory
|
|
38
40
|
let projectDir = dir && dir.trim() ? dir.trim() : '';
|
|
@@ -49,7 +51,7 @@ Available Templates:
|
|
|
49
51
|
projectDir = res.dir.trim();
|
|
50
52
|
}
|
|
51
53
|
// Use provided option or prompt for template
|
|
52
|
-
let template = options.template && ['react', '
|
|
54
|
+
let template = options.template && ['react', 'react-dynamic', 'react-para'].includes(options.template) ? options.template : '';
|
|
53
55
|
if (!template) {
|
|
54
56
|
const res = await prompts({
|
|
55
57
|
type: 'select',
|
|
@@ -62,7 +64,7 @@ Available Templates:
|
|
|
62
64
|
process.exit(1);
|
|
63
65
|
}
|
|
64
66
|
template = res.template; // The value is already what we want from the choices
|
|
65
|
-
if (!template || !['react', '
|
|
67
|
+
if (!template || !['react', 'react-dynamic', 'react-para'].includes(template)) {
|
|
66
68
|
console.log('Template selection is required.');
|
|
67
69
|
process.exit(1);
|
|
68
70
|
}
|
|
@@ -97,6 +99,27 @@ Available Templates:
|
|
|
97
99
|
chain: 'baseSepolia',
|
|
98
100
|
apiKey: apiKey
|
|
99
101
|
});
|
|
102
|
+
// Ensure SBC logo exists in public/ for all templates
|
|
103
|
+
try {
|
|
104
|
+
const publicDir = path.join(targetDir, 'public');
|
|
105
|
+
await fs.ensureDir(publicDir);
|
|
106
|
+
const sourceLogo = path.resolve(__dirname, '../templates/react/public/sbc-logo.png');
|
|
107
|
+
const destLogo = path.join(publicDir, 'sbc-logo.png');
|
|
108
|
+
if (!(await fs.pathExists(destLogo)) && (await fs.pathExists(sourceLogo))) {
|
|
109
|
+
await fs.copy(sourceLogo, destLogo);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
// If .env.template exists, ensure apiKey placeholder is applied (copyTemplate already replaces)
|
|
114
|
+
// Also, create a default .env if none exists to make onboarding faster
|
|
115
|
+
try {
|
|
116
|
+
const envTemplatePath = path.join(targetDir, '.env.template');
|
|
117
|
+
const envPath = path.join(targetDir, '.env');
|
|
118
|
+
if (await fs.pathExists(envTemplatePath) && !(await fs.pathExists(envPath))) {
|
|
119
|
+
await fs.copy(envTemplatePath, envPath);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
100
123
|
console.log(`\nSuccess! Created ${projectDir} using the ${template} template.`);
|
|
101
124
|
console.log(`\nNext steps:`);
|
|
102
125
|
console.log(` cd ${projectDir}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-sbc-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scaffold a new SBC App Kit project with one command.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-sbc-app": "bin/cli.js"
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"create-app",
|
|
48
48
|
"template",
|
|
49
49
|
"react",
|
|
50
|
-
"nextjs",
|
|
51
50
|
"vite"
|
|
52
51
|
],
|
|
53
52
|
"author": "SBC Team",
|
package/templates/README.md
CHANGED
|
@@ -11,13 +11,11 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@stablecoin.xyz/core": "latest",
|
|
13
13
|
"@stablecoin.xyz/react": "latest",
|
|
14
|
-
"react": "^
|
|
15
|
-
"react-dom": "^
|
|
16
|
-
"viem": "^2.
|
|
14
|
+
"react": "^19.1.0",
|
|
15
|
+
"react-dom": "^19.1.0",
|
|
16
|
+
"viem": "^2.33.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@types/react": "^18.2.0",
|
|
20
|
-
"@types/react-dom": "^18.2.0",
|
|
21
19
|
"@vitejs/plugin-react": "^4.0.0",
|
|
22
20
|
"typescript": "^5.0.0",
|
|
23
21
|
"vite": "^5.0.0"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# React + Dynamic + SBC App Kit
|
|
2
|
+
|
|
3
|
+
Gasless transactions on Base using Dynamic SDK with 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` and fill in values:
|
|
15
|
+
|
|
16
|
+
```env
|
|
17
|
+
VITE_SBC_API_KEY={{apiKey}}
|
|
18
|
+
VITE_DYNAMIC_ENVIRONMENT_ID=your_dynamic_env_id
|
|
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 + Dynamic</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,33 @@
|
|
|
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
|
+
"@dynamic-labs/ethereum": "^4.25.7",
|
|
13
|
+
"@dynamic-labs/ethereum-aa": "^4.25.7",
|
|
14
|
+
"@dynamic-labs/sdk-react-core": "^4.25.7",
|
|
15
|
+
"@stablecoin.xyz/core": "latest",
|
|
16
|
+
"@stablecoin.xyz/react": "latest",
|
|
17
|
+
"react": "^19.1.0",
|
|
18
|
+
"react-dom": "^19.1.0",
|
|
19
|
+
"viem": "^2.33.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/react": "^18.2.0",
|
|
23
|
+
"@types/react-dom": "^18.2.0",
|
|
24
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
25
|
+
"autoprefixer": "^10.4.0",
|
|
26
|
+
"postcss": "^8.4.0",
|
|
27
|
+
"tailwindcss": "^3.4.17",
|
|
28
|
+
"typescript": "^5.0.0",
|
|
29
|
+
"vite": "^5.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
Binary file
|
|
@@ -0,0 +1,322 @@
|
|
|
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 { baseSepolia, base, type Chain } from 'viem/chains';
|
|
6
|
+
import { createPublicClient, http, getAddress, parseUnits, encodeFunctionData, erc20Abi } from 'viem';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import './App.css';
|
|
9
|
+
|
|
10
|
+
const chain = (import.meta.env.VITE_CHAIN === 'base') ? base : baseSepolia;
|
|
11
|
+
const rpcUrl = import.meta.env.VITE_RPC_URL;
|
|
12
|
+
|
|
13
|
+
const SBC_TOKEN_ADDRESS = (chain: Chain) => {
|
|
14
|
+
if (chain.id === baseSepolia.id) return '0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16';
|
|
15
|
+
if (chain.id === base.id) return '0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798';
|
|
16
|
+
throw new Error('Unsupported chain');
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const SBC_DECIMALS = (chain: Chain) => chain.id === baseSepolia.id ? 6 : 18;
|
|
20
|
+
|
|
21
|
+
const chainExplorer = (chain: Chain) => {
|
|
22
|
+
if (chain.id === baseSepolia.id) return 'https://sepolia.basescan.org';
|
|
23
|
+
if (chain.id === base.id) return 'https://basescan.org';
|
|
24
|
+
return '';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
|
|
28
|
+
|
|
29
|
+
// ERC20 + EIP-2612 nonces helper ABI
|
|
30
|
+
const erc20PermitAbi = [
|
|
31
|
+
...erc20Abi,
|
|
32
|
+
{
|
|
33
|
+
"inputs": [
|
|
34
|
+
{ "internalType": "address", "name": "owner", "type": "address" }
|
|
35
|
+
],
|
|
36
|
+
"name": "nonces",
|
|
37
|
+
"outputs": [
|
|
38
|
+
{ "internalType": "uint256", "name": "", "type": "uint256" }
|
|
39
|
+
],
|
|
40
|
+
"stateMutability": "view",
|
|
41
|
+
"type": "function"
|
|
42
|
+
}
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
// Wrapper to render DynamicWidget only after SDK is ready
|
|
46
|
+
const DynamicWidgetWrapper = () => {
|
|
47
|
+
const { sdkHasLoaded } = useDynamicContext();
|
|
48
|
+
if (!sdkHasLoaded) return null;
|
|
49
|
+
return <DynamicWidget />;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Wrapper to render DynamicUserProfile only after SDK is ready
|
|
53
|
+
const DynamicUserProfileWrapper = () => {
|
|
54
|
+
const { sdkHasLoaded } = useDynamicContext();
|
|
55
|
+
if (!sdkHasLoaded) return null;
|
|
56
|
+
return <DynamicUserProfile />;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function WalletStatus() {
|
|
60
|
+
const { primaryWallet } = useDynamicContext();
|
|
61
|
+
const [balances, setBalances] = useState<{ eth: string | null; sbc: string | null }>({ eth: null, sbc: null });
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!primaryWallet?.address) return;
|
|
65
|
+
(async () => {
|
|
66
|
+
try {
|
|
67
|
+
const [ethBalance, sbcBalance] = await Promise.all([
|
|
68
|
+
publicClient.getBalance({ address: primaryWallet.address as `0x${string}` }),
|
|
69
|
+
publicClient.readContract({
|
|
70
|
+
address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
|
|
71
|
+
abi: erc20Abi,
|
|
72
|
+
functionName: 'balanceOf',
|
|
73
|
+
args: [primaryWallet.address as `0x${string}`],
|
|
74
|
+
})
|
|
75
|
+
]);
|
|
76
|
+
setBalances({ eth: ethBalance.toString(), sbc: (sbcBalance as bigint).toString() });
|
|
77
|
+
} catch {
|
|
78
|
+
setBalances({ eth: null, sbc: null });
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
}, [primaryWallet?.address]);
|
|
82
|
+
|
|
83
|
+
if (!primaryWallet) return null;
|
|
84
|
+
const fmtEth = (v: string | null) => v ? (Number(v) / 1e18).toFixed(4) : '0.0000';
|
|
85
|
+
const fmtSbc = (v: string | null) => v ? (Number(v) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(4) : '0.0000';
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
89
|
+
<div className="flex justify-between items-start mb-3">
|
|
90
|
+
<div className="flex-1">
|
|
91
|
+
<h3 className="font-semibold text-green-800 mb-1">✅ Dynamic Wallet Connected</h3>
|
|
92
|
+
<p className="text-xs text-green-600 font-mono break-all mb-2">EOA: {primaryWallet.address}</p>
|
|
93
|
+
<p className="text-xs text-green-600 mb-2">Connected via Dynamic SDK</p>
|
|
94
|
+
<p className="text-xs text-green-600 mb-2"><strong>Chain:</strong> {chain.name} (ID: {chain.id})</p>
|
|
95
|
+
<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>
|
|
97
|
+
<div className="flex gap-4">
|
|
98
|
+
<span className="text-xs text-green-600"><strong>ETH:</strong> {fmtEth(balances.eth)}</span>
|
|
99
|
+
<span className="text-xs text-green-600"><strong>SBC:</strong> {fmtSbc(balances.sbc)}</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function SmartAccountInfo({ account, refreshAccount, isLoadingAccount, accountError }: any) {
|
|
109
|
+
const [sbcBalance, setSbcBalance] = useState<string | null>(null);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!account?.address) return;
|
|
112
|
+
(async () => {
|
|
113
|
+
try {
|
|
114
|
+
const bal = await publicClient.readContract({
|
|
115
|
+
address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
|
|
116
|
+
abi: erc20Abi,
|
|
117
|
+
functionName: 'balanceOf',
|
|
118
|
+
args: [account.address as `0x${string}`],
|
|
119
|
+
});
|
|
120
|
+
setSbcBalance((bal as bigint).toString());
|
|
121
|
+
} catch { setSbcBalance('0'); }
|
|
122
|
+
})();
|
|
123
|
+
}, [account?.address]);
|
|
124
|
+
|
|
125
|
+
if (!account) return null;
|
|
126
|
+
const fmtEth = (v: string | null) => v ? (Number(v) / 1e18).toFixed(6) : '0.000000';
|
|
127
|
+
const fmtSbc = (v: string | null) => v ? (Number(v) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(2) : '0.00';
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="mb-6 p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
|
131
|
+
<div className="flex justify-between items-center mb-2">
|
|
132
|
+
<h3 className="font-semibold text-purple-800">🔐 Smart Account Status</h3>
|
|
133
|
+
<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>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="space-y-2 text-sm">
|
|
136
|
+
<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
|
+
<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
|
+
<div className="flex justify-between"><span className="text-purple-700">Nonce:</span><span className="text-purple-600">{account.nonce}</span></div>
|
|
139
|
+
<div className="pt-2 border-t border-purple-200">
|
|
140
|
+
<p className="text-xs font-medium text-purple-700 mb-2">Smart Account Balances:</p>
|
|
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>
|
|
146
|
+
</div>
|
|
147
|
+
{accountError && <p className="mt-2 text-xs text-red-600">{String(accountError)}</p>}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function TransactionForm({ account, sbcAppKit }: { account: any; sbcAppKit: any }) {
|
|
153
|
+
const [recipient, setRecipient] = useState('');
|
|
154
|
+
const [amount, setAmount] = useState('1');
|
|
155
|
+
const [status, setStatus] = useState<'idle'|'loading'|'success'|'error'>('idle');
|
|
156
|
+
const [error, setError] = useState<string | null>(null);
|
|
157
|
+
const [result, setResult] = useState<any>(null);
|
|
158
|
+
const isValid = recipient && /^0x[a-fA-F0-9]{40}$/.test(recipient) && parseFloat(amount) > 0;
|
|
159
|
+
|
|
160
|
+
const sendTx = async () => {
|
|
161
|
+
if (!isValid || !account || !sbcAppKit) return;
|
|
162
|
+
try {
|
|
163
|
+
setStatus('loading'); setError(null);
|
|
164
|
+
const owner = sbcAppKit.getOwnerAddress();
|
|
165
|
+
const value = parseUnits(amount, SBC_DECIMALS(chain));
|
|
166
|
+
const deadline = Math.floor(Date.now() / 1000) + 60 * 30;
|
|
167
|
+
const { publicClient: pc, walletClient: wc } = (sbcAppKit as any);
|
|
168
|
+
const [nonce, tokenName] = await Promise.all([
|
|
169
|
+
pc.readContract({ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, abi: erc20PermitAbi, functionName: 'nonces', args: [owner] }),
|
|
170
|
+
pc.readContract({ address: SBC_TOKEN_ADDRESS(chain), abi: erc20Abi, functionName: 'name' })
|
|
171
|
+
]);
|
|
172
|
+
const domain = { name: tokenName as string, version: '1', chainId: chain.id, verifyingContract: SBC_TOKEN_ADDRESS(chain) };
|
|
173
|
+
const types = { Permit: [
|
|
174
|
+
{ name: 'owner', type: 'address' },
|
|
175
|
+
{ name: 'spender', type: 'address' },
|
|
176
|
+
{ name: 'value', type: 'uint256' },
|
|
177
|
+
{ name: 'nonce', type: 'uint256' },
|
|
178
|
+
{ name: 'deadline', type: 'uint256' },
|
|
179
|
+
] } as const;
|
|
180
|
+
const message = { owner, spender: account.address, value, nonce, deadline: BigInt(deadline) } as const;
|
|
181
|
+
const signature = await wc.signTypedData({ domain, types, primaryType: 'Permit', message });
|
|
182
|
+
const { r, s, v } = (await import('viem')).parseSignature(signature);
|
|
183
|
+
const permitData = encodeFunctionData({
|
|
184
|
+
abi: [{ name: 'permit', type: 'function', inputs: [
|
|
185
|
+
{ name: 'owner', type: 'address' },
|
|
186
|
+
{ name: 'spender', type: 'address' },
|
|
187
|
+
{ name: 'value', type: 'uint256' },
|
|
188
|
+
{ name: 'deadline', type: 'uint256' },
|
|
189
|
+
{ name: 'v', type: 'uint8' },
|
|
190
|
+
{ name: 'r', type: 'bytes32' },
|
|
191
|
+
{ name: 's', type: 'bytes32' }
|
|
192
|
+
] }],
|
|
193
|
+
functionName: 'permit',
|
|
194
|
+
args: [owner, account.address, value, BigInt(deadline), v, r, s]
|
|
195
|
+
});
|
|
196
|
+
const transferFromData = encodeFunctionData({
|
|
197
|
+
abi: erc20Abi,
|
|
198
|
+
functionName: 'transferFrom',
|
|
199
|
+
args: [owner, recipient as `0x${string}`, value]
|
|
200
|
+
});
|
|
201
|
+
const res = await sbcAppKit.sendUserOperation({
|
|
202
|
+
calls: [
|
|
203
|
+
{ to: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, data: permitData },
|
|
204
|
+
{ to: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, data: transferFromData }
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
setResult(res);
|
|
208
|
+
setStatus('success');
|
|
209
|
+
} catch (e: any) {
|
|
210
|
+
setError(e?.message || 'Transaction failed');
|
|
211
|
+
setStatus('error');
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (!account) return null;
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<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 SBC Tokens</h3>
|
|
220
|
+
<div className="space-y-4">
|
|
221
|
+
<div>
|
|
222
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">Recipient Address</label>
|
|
223
|
+
<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
|
+
</div>
|
|
225
|
+
<div>
|
|
226
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (SBC)</label>
|
|
227
|
+
<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
|
+
</div>
|
|
229
|
+
<div className="p-3 bg-gray-50 rounded">
|
|
230
|
+
<div className="flex justify-between text-sm"><span>Amount:</span><span className="font-medium">{amount} SBC</span></div>
|
|
231
|
+
<div className="flex justify-between text-xs text-gray-600"><span>Gas fees:</span><span>Covered by SBC Paymaster ✨</span></div>
|
|
232
|
+
<div className="flex justify-between text-xs text-gray-600"><span>Signing:</span><span>Your Dynamic wallet will prompt to sign 🖊️</span></div>
|
|
233
|
+
</div>
|
|
234
|
+
<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} SBC`}
|
|
236
|
+
</button>
|
|
237
|
+
{status==='success' && result && (
|
|
238
|
+
<div className="p-3 bg-green-50 border border-green-200 rounded">
|
|
239
|
+
<p className="text-sm text-green-800 font-medium">✅ Transaction Submitted</p>
|
|
240
|
+
<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 on BaseScan: {result.transactionHash}</a>
|
|
242
|
+
</p>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
{status==='error' && error && (
|
|
246
|
+
<div className="p-3 bg-red-50 border border-red-200 rounded">
|
|
247
|
+
<p className="text-sm text-red-800 font-medium">❌ Transaction Failed</p>
|
|
248
|
+
<p className="text-xs text-red-600 mt-1">{error}</p>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function DynamicApp() {
|
|
257
|
+
const { primaryWallet } = useDynamicContext();
|
|
258
|
+
const { sbcAppKit, isInitialized, error, account, isLoadingAccount, accountError, refreshAccount } = useSbcDynamic({
|
|
259
|
+
apiKey: import.meta.env.VITE_SBC_API_KEY,
|
|
260
|
+
chain,
|
|
261
|
+
primaryWallet,
|
|
262
|
+
rpcUrl,
|
|
263
|
+
debug: true
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<>
|
|
268
|
+
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
269
|
+
<h3 className="font-semibold text-blue-800 mb-2">🔗 Connect to Dynamic</h3>
|
|
270
|
+
<p className="text-sm text-blue-600 mb-3">
|
|
271
|
+
Connect your wallet or sign in with email to create a smart account with Dynamic
|
|
272
|
+
</p>
|
|
273
|
+
<DynamicWidgetWrapper />
|
|
274
|
+
</div>
|
|
275
|
+
{primaryWallet && isInitialized && (
|
|
276
|
+
<>
|
|
277
|
+
<WalletStatus />
|
|
278
|
+
<SmartAccountInfo account={account} refreshAccount={refreshAccount} isLoadingAccount={isLoadingAccount} accountError={accountError} />
|
|
279
|
+
<TransactionForm account={account} sbcAppKit={sbcAppKit} />
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
{error && <div className="error">{error.message}</div>}
|
|
283
|
+
</>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export default function App() {
|
|
288
|
+
return (
|
|
289
|
+
<DynamicContextProvider
|
|
290
|
+
settings={{
|
|
291
|
+
environmentId: import.meta.env.VITE_DYNAMIC_ENVIRONMENT_ID || '',
|
|
292
|
+
walletConnectors: [EthereumWalletConnectors, ZeroDevSmartWalletConnectors],
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<div className="min-h-screen bg-gray-50 py-8">
|
|
296
|
+
<div className="max-w-2xl mx-auto px-4">
|
|
297
|
+
<div className="text-center mb-8">
|
|
298
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-2 flex items-center justify-center gap-3">
|
|
299
|
+
<img src="/sbc-logo.png" alt="SBC Logo" width={36} height={36} />
|
|
300
|
+
SBC (Dynamic) Integration
|
|
301
|
+
</h1>
|
|
302
|
+
<p className="text-gray-600">Gasless transactions with Dynamic SDK integration</p>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<DynamicApp />
|
|
306
|
+
|
|
307
|
+
<DynamicUserProfileWrapper />
|
|
308
|
+
|
|
309
|
+
<div className="mt-8 text-center text-xs text-gray-500">
|
|
310
|
+
<p>
|
|
311
|
+
Powered by{' '}
|
|
312
|
+
<a href="https://github.com/stablecoinxyz/app-kit" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">SBC AppKit</a>
|
|
313
|
+
{' '}• Dynamic SDK integration
|
|
314
|
+
</p>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</DynamicContextProvider>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
@@ -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
|
+
|