abc-scaffold 1.0.0 → 1.0.2
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 +1 -1
- package/package.json +42 -42
- package/src/scaffold.js +346 -341
- package/templates/{.env.example → env.example} +4 -4
- package/templates/frontend/src/App.css +40 -52
- package/templates/frontend/src/App.jsx +40 -22
- package/templates/frontend/src/components/Navbar.css +0 -62
- package/templates/frontend/src/components/Navbar.jsx +0 -44
- package/templates/gitignore +11 -0
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "abc-scaffold",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Africa's Blockchain Club — project-agnostic blockchain scaffolding tool",
|
|
5
|
-
"author": "Africa's Blockchain Club",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"bin": {
|
|
9
|
-
"abc-scaffold": "bin/abc-scaffold.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"bin",
|
|
13
|
-
"src",
|
|
14
|
-
"templates",
|
|
15
|
-
"README.md",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"start": "node bin/abc-scaffold.js"
|
|
20
|
-
},
|
|
21
|
-
"keywords": [
|
|
22
|
-
"hardhat",
|
|
23
|
-
"blockchain",
|
|
24
|
-
"scaffold",
|
|
25
|
-
"ethereum",
|
|
26
|
-
"smart-contracts",
|
|
27
|
-
"africa",
|
|
28
|
-
"web3",
|
|
29
|
-
"erc4337",
|
|
30
|
-
"openzeppelin"
|
|
31
|
-
],
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"chalk": "^5.3.0",
|
|
34
|
-
"fs-extra": "^11.2.0"
|
|
35
|
-
},
|
|
36
|
-
"engines": {
|
|
37
|
-
"node": ">=18.0.0"
|
|
38
|
-
},
|
|
39
|
-
"publishConfig": {
|
|
40
|
-
"access": "public"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "abc-scaffold",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Africa's Blockchain Club — project-agnostic blockchain scaffolding tool",
|
|
5
|
+
"author": "Africa's Blockchain Club",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"abc-scaffold": "bin/abc-scaffold.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"templates",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node bin/abc-scaffold.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"hardhat",
|
|
23
|
+
"blockchain",
|
|
24
|
+
"scaffold",
|
|
25
|
+
"ethereum",
|
|
26
|
+
"smart-contracts",
|
|
27
|
+
"africa",
|
|
28
|
+
"web3",
|
|
29
|
+
"erc4337",
|
|
30
|
+
"openzeppelin"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"chalk": "^5.3.0",
|
|
34
|
+
"fs-extra": "^11.2.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/scaffold.js
CHANGED
|
@@ -1,341 +1,346 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { createInterface } from 'readline';
|
|
5
|
-
import fse from 'fs-extra';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
import * as logger from './logger.js';
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = path.dirname(__filename);
|
|
11
|
-
const TEMPLATES = path.join(__dirname, '..', 'templates');
|
|
12
|
-
|
|
13
|
-
function prompt(question) {
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
-
rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function isValidName(name) {
|
|
21
|
-
return /^[a-z0-9][a-z0-9\-_]*$/.test(name);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function copyTemplate(filename, targetDir) {
|
|
25
|
-
fse.copySync(path.join(TEMPLATES, filename), path.join(targetDir, filename));
|
|
26
|
-
logger.success(filename);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function buildReadme(projectName) {
|
|
30
|
-
return `# ${projectName}
|
|
31
|
-
|
|
32
|
-
> Scaffolded with [abc-scaffold](https://github.com/africas-blockchain-club/abc-scaffold) by **Africa's Blockchain Club**
|
|
33
|
-
|
|
34
|
-
A project-agnostic Hardhat workspace ready for smart contract development, testing, and frontend integration. Build anything — NFTs, DeFi protocols, DAOs, smart accounts, or any on-chain application.
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## Prerequisites
|
|
39
|
-
|
|
40
|
-
| Tool | Version |
|
|
41
|
-
|------|---------|
|
|
42
|
-
| Node.js | >= 18.0.0 |
|
|
43
|
-
| npm | >= 9.0.0 |
|
|
44
|
-
| MetaMask | Latest |
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## Getting Started
|
|
49
|
-
|
|
50
|
-
### 1. Install dependencies
|
|
51
|
-
|
|
52
|
-
\`\`\`bash
|
|
53
|
-
npm install
|
|
54
|
-
\`\`\`
|
|
55
|
-
|
|
56
|
-
### 2. Set up environment variables
|
|
57
|
-
|
|
58
|
-
\`\`\`bash
|
|
59
|
-
cp .env.example .env
|
|
60
|
-
\`\`\`
|
|
61
|
-
|
|
62
|
-
Fill in your values in \`.env\`:
|
|
63
|
-
|
|
64
|
-
- \`SEPOLIA_RPC_URL\` — get one free from [Infura](https://infura.io) or [Alchemy](https://alchemy.com)
|
|
65
|
-
- \`PRIVATE_KEY\` — your deployer wallet private key (**never commit this file**)
|
|
66
|
-
- \`ETHERSCAN_API_KEY\` — for contract verification on [Etherscan](https://etherscan.io)
|
|
67
|
-
|
|
68
|
-
### 3. Compile contracts
|
|
69
|
-
|
|
70
|
-
\`\`\`bash
|
|
71
|
-
|
|
72
|
-
\`\`\`
|
|
73
|
-
|
|
74
|
-
### 4. Run tests
|
|
75
|
-
|
|
76
|
-
\`\`\`bash
|
|
77
|
-
|
|
78
|
-
\`\`\`
|
|
79
|
-
|
|
80
|
-
### 5.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
\`\`\`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
npx hardhat run scripts/deploy.js --network localhost
|
|
90
|
-
\`\`\`
|
|
91
|
-
|
|
92
|
-
###
|
|
93
|
-
|
|
94
|
-
\`\`\`bash
|
|
95
|
-
cd frontend
|
|
96
|
-
npm install
|
|
97
|
-
npm run dev
|
|
98
|
-
# → http://localhost:5173
|
|
99
|
-
\`\`\`
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## Directory Structure
|
|
104
|
-
|
|
105
|
-
\`\`\`
|
|
106
|
-
${projectName}/
|
|
107
|
-
├── contracts/ # Solidity smart contracts
|
|
108
|
-
├── scripts/ # Deployment and utility scripts
|
|
109
|
-
├── test/ # Hardhat test files
|
|
110
|
-
├── frontend/ # React + Vite frontend
|
|
111
|
-
│ └── src/
|
|
112
|
-
│ ├── App.jsx
|
|
113
|
-
│ └── components/
|
|
114
|
-
│ └── Navbar.jsx ← Connect Wallet button lives here
|
|
115
|
-
├── hardhat.config.js # Hardhat configuration
|
|
116
|
-
├── .env.example # Environment variable template
|
|
117
|
-
└── README.md
|
|
118
|
-
\`\`\`
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## Writing Smart Contracts
|
|
123
|
-
|
|
124
|
-
Place your Solidity files in \`contracts/\`. The config targets Solidity \`0.8.26\` with the Cancun EVM and the optimizer enabled (200 runs).
|
|
125
|
-
|
|
126
|
-
**Example** — \`contracts/MyContract.sol\`:
|
|
127
|
-
|
|
128
|
-
\`\`\`solidity
|
|
129
|
-
// SPDX-License-Identifier: MIT
|
|
130
|
-
pragma solidity ^0.8.26;
|
|
131
|
-
|
|
132
|
-
contract MyContract {
|
|
133
|
-
// Your logic here
|
|
134
|
-
}
|
|
135
|
-
\`\`\`
|
|
136
|
-
|
|
137
|
-
### Included libraries
|
|
138
|
-
|
|
139
|
-
| Package | Use case |
|
|
140
|
-
|---------|----------|
|
|
141
|
-
| \`@openzeppelin/contracts\` | ERC-20, ERC-721, ERC-1155, access control, etc. |
|
|
142
|
-
| \`@openzeppelin/contracts-upgradeable\` | UUPS / Transparent proxy upgradeable variants |
|
|
143
|
-
| \`@openzeppelin/hardhat-upgrades\` | Deploy and upgrade helper tasks |
|
|
144
|
-
| \`@account-abstraction/contracts\` | ERC-4337 interfaces and EntryPoint |
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## Writing Tests
|
|
149
|
-
|
|
150
|
-
Place test files in \`test/\`. Hardhat ships with Mocha + Chai.
|
|
151
|
-
|
|
152
|
-
**Example** — \`test/MyContract.test.js\`:
|
|
153
|
-
|
|
154
|
-
\`\`\`javascript
|
|
155
|
-
const { expect } = require('chai');
|
|
156
|
-
const { ethers } = require('hardhat');
|
|
157
|
-
|
|
158
|
-
describe('MyContract', function () {
|
|
159
|
-
it('should deploy successfully', async function () {
|
|
160
|
-
const MyContract = await ethers.getContractFactory('MyContract');
|
|
161
|
-
const contract = await MyContract.deploy();
|
|
162
|
-
expect(await contract.getAddress()).to.not.equal(ethers.ZeroAddress);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
\`\`\`
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## Writing Deployment Scripts
|
|
170
|
-
|
|
171
|
-
**Example** — \`scripts/deploy.js\`:
|
|
172
|
-
|
|
173
|
-
\`\`\`javascript
|
|
174
|
-
const { ethers } = require('hardhat');
|
|
175
|
-
|
|
176
|
-
async function main() {
|
|
177
|
-
const [deployer] = await ethers.getSigners();
|
|
178
|
-
console.log('Deploying from:', deployer.address);
|
|
179
|
-
|
|
180
|
-
const MyContract = await ethers.getContractFactory('MyContract');
|
|
181
|
-
const contract = await MyContract.deploy();
|
|
182
|
-
await contract.waitForDeployment();
|
|
183
|
-
|
|
184
|
-
console.log('MyContract deployed to:', await contract.getAddress());
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
main().catch((err) => {
|
|
188
|
-
console.error(err);
|
|
189
|
-
process.exitCode = 1;
|
|
190
|
-
});
|
|
191
|
-
\`\`\`
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
## Networks
|
|
196
|
-
|
|
197
|
-
| Network | RPC | Chain ID |
|
|
198
|
-
|---------|-----|----------|
|
|
199
|
-
| Hardhat (local) | \`http://127.0.0.1:8545\` | 31337 |
|
|
200
|
-
| Sepolia (testnet) | \`SEPOLIA_RPC_URL\` in \`.env\` | 11155111 |
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## Verifying Contracts
|
|
205
|
-
|
|
206
|
-
\`\`\`bash
|
|
207
|
-
npx hardhat verify --network sepolia <CONTRACT_ADDRESS> <CONSTRUCTOR_ARGS>
|
|
208
|
-
\`\`\`
|
|
209
|
-
|
|
210
|
-
---
|
|
211
|
-
|
|
212
|
-
## Frontend
|
|
213
|
-
|
|
214
|
-
The \`frontend/\` folder is a Vite + React app with:
|
|
215
|
-
|
|
216
|
-
- A sticky navigation bar with a **Connect Wallet** button (MetaMask / EIP-1193)
|
|
217
|
-
- Ethers.js v6 already wired up
|
|
218
|
-
|
|
219
|
-
### Connecting to a deployed contract
|
|
220
|
-
|
|
221
|
-
Copy the ABI from \`artifacts/contracts/YourContract.sol/YourContract.json\` and import it:
|
|
222
|
-
|
|
223
|
-
\`\`\`javascript
|
|
224
|
-
import { BrowserProvider, Contract } from 'ethers';
|
|
225
|
-
import MyContractABI from '../../artifacts/contracts/MyContract.sol/MyContract.json';
|
|
226
|
-
|
|
227
|
-
const CONTRACT_ADDRESS = '0x...';
|
|
228
|
-
|
|
229
|
-
async function interact() {
|
|
230
|
-
const provider = new BrowserProvider(window.ethereum);
|
|
231
|
-
const signer = await provider.getSigner();
|
|
232
|
-
const contract = new Contract(CONTRACT_ADDRESS, MyContractABI.abi, signer);
|
|
233
|
-
// call contract methods here
|
|
234
|
-
}
|
|
235
|
-
\`\`\`
|
|
236
|
-
|
|
237
|
-
---
|
|
238
|
-
|
|
239
|
-
## Resources
|
|
240
|
-
|
|
241
|
-
- [Hardhat Docs](https://hardhat.org/docs)
|
|
242
|
-
- [Solidity Docs](https://docs.soliditylang.org)
|
|
243
|
-
- [OpenZeppelin Docs](https://docs.openzeppelin.com)
|
|
244
|
-
- [EIP-4337 (Account Abstraction)](https://eips.ethereum.org/EIPS/eip-4337)
|
|
245
|
-
- [Ethers.js v6 Docs](https://docs.ethers.org/v6)
|
|
246
|
-
- [Vite Docs](https://vitejs.dev)
|
|
247
|
-
|
|
248
|
-
---
|
|
249
|
-
|
|
250
|
-
*Built with Africa's Blockchain Club — scaffold your next project with \`npx abc-scaffold\`.*
|
|
251
|
-
`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function scaffoldFrontend(frontendDir, projectName) {
|
|
255
|
-
fse.ensureDirSync(path.join(frontendDir, 'src', 'components'));
|
|
256
|
-
fse.copySync(path.join(TEMPLATES, 'frontend'), frontendDir);
|
|
257
|
-
|
|
258
|
-
// Substitute project name in index.html
|
|
259
|
-
const htmlPath = path.join(frontendDir, 'index.html');
|
|
260
|
-
const html = fs.readFileSync(htmlPath, 'utf-8').replace(/{{PROJECT_NAME}}/g, projectName);
|
|
261
|
-
fs.writeFileSync(htmlPath, html);
|
|
262
|
-
|
|
263
|
-
logger.success('frontend/ (React + Vite + Connect Wallet)');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export async function scaffold(initialName) {
|
|
267
|
-
let projectName = initialName;
|
|
268
|
-
|
|
269
|
-
if (!projectName) {
|
|
270
|
-
projectName = await prompt(chalk.bold('? Project name: '));
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (!projectName) {
|
|
274
|
-
logger.error('Project name is required.');
|
|
275
|
-
process.exit(1);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Normalise: lowercase, spaces → hyphens
|
|
279
|
-
projectName = projectName.trim().toLowerCase().replace(/\s+/g, '-');
|
|
280
|
-
|
|
281
|
-
if (!isValidName(projectName)) {
|
|
282
|
-
logger.error(`"${projectName}" is not a valid package name. Use lowercase letters, numbers, hyphens, or underscores.`);
|
|
283
|
-
process.exit(1);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const targetDir = path.resolve(process.cwd(), projectName);
|
|
287
|
-
|
|
288
|
-
if (fs.existsSync(targetDir)) {
|
|
289
|
-
logger.error(`Directory "${projectName}" already exists. Choose a different name or delete it first.`);
|
|
290
|
-
process.exit(1);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// ── Create directory structure ──────────────────────────────────────────────
|
|
294
|
-
logger.step('Creating project structure');
|
|
295
|
-
|
|
296
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
297
|
-
|
|
298
|
-
for (const dir of ['contracts', 'scripts', 'test']) {
|
|
299
|
-
fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
|
|
300
|
-
logger.success(`${dir}/`);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ── Copy configuration files ────────────────────────────────────────────────
|
|
304
|
-
logger.step('Copying configuration files');
|
|
305
|
-
|
|
306
|
-
copyTemplate('hardhat.config.js', targetDir);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
logger.success('
|
|
322
|
-
|
|
323
|
-
// ──
|
|
324
|
-
logger.step('
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
console.log(
|
|
334
|
-
console.log(chalk.
|
|
335
|
-
console.log(
|
|
336
|
-
console.log(
|
|
337
|
-
console.log(chalk.cyan(
|
|
338
|
-
console.log('');
|
|
339
|
-
console.log(chalk.
|
|
340
|
-
console.log('');
|
|
341
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import fse from 'fs-extra';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import * as logger from './logger.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const TEMPLATES = path.join(__dirname, '..', 'templates');
|
|
12
|
+
|
|
13
|
+
function prompt(question) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isValidName(name) {
|
|
21
|
+
return /^[a-z0-9][a-z0-9\-_]*$/.test(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function copyTemplate(filename, targetDir) {
|
|
25
|
+
fse.copySync(path.join(TEMPLATES, filename), path.join(targetDir, filename));
|
|
26
|
+
logger.success(filename);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildReadme(projectName) {
|
|
30
|
+
return `# ${projectName}
|
|
31
|
+
|
|
32
|
+
> Scaffolded with [abc-scaffold](https://github.com/africas-blockchain-club/abc-scaffold) by **Africa's Blockchain Club**
|
|
33
|
+
|
|
34
|
+
A project-agnostic Hardhat workspace ready for smart contract development, testing, and frontend integration. Build anything — NFTs, DeFi protocols, DAOs, smart accounts, or any on-chain application.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Prerequisites
|
|
39
|
+
|
|
40
|
+
| Tool | Version |
|
|
41
|
+
|------|---------|
|
|
42
|
+
| Node.js | >= 18.0.0 |
|
|
43
|
+
| npm | >= 9.0.0 |
|
|
44
|
+
| MetaMask | Latest |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Getting Started
|
|
49
|
+
|
|
50
|
+
### 1. Install dependencies
|
|
51
|
+
|
|
52
|
+
\`\`\`bash
|
|
53
|
+
npm install
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
### 2. Set up environment variables
|
|
57
|
+
|
|
58
|
+
\`\`\`bash
|
|
59
|
+
cp .env.example .env
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
Fill in your values in \`.env\`:
|
|
63
|
+
|
|
64
|
+
- \`SEPOLIA_RPC_URL\` — get one free from [Infura](https://infura.io) or [Alchemy](https://alchemy.com)
|
|
65
|
+
- \`PRIVATE_KEY\` — your deployer wallet private key (**never commit this file**)
|
|
66
|
+
- \`ETHERSCAN_API_KEY\` — for contract verification on [Etherscan](https://etherscan.io)
|
|
67
|
+
|
|
68
|
+
### 3. Compile contracts
|
|
69
|
+
|
|
70
|
+
\`\`\`bash
|
|
71
|
+
npx hardhat compile
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
### 4. Run tests
|
|
75
|
+
|
|
76
|
+
\`\`\`bash
|
|
77
|
+
npx hardhat test
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
### 5. Deploy contracts locally
|
|
81
|
+
|
|
82
|
+
Open two terminals:
|
|
83
|
+
|
|
84
|
+
\`\`\`bash
|
|
85
|
+
# Terminal 1 — start a local node
|
|
86
|
+
npx hardhat node
|
|
87
|
+
|
|
88
|
+
# Terminal 2 — deploy your contracts
|
|
89
|
+
npx hardhat run scripts/deploy.js --network localhost
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
### 6. Start the frontend
|
|
93
|
+
|
|
94
|
+
\`\`\`bash
|
|
95
|
+
cd frontend
|
|
96
|
+
npm install
|
|
97
|
+
npm run dev
|
|
98
|
+
# → http://localhost:5173
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Directory Structure
|
|
104
|
+
|
|
105
|
+
\`\`\`
|
|
106
|
+
${projectName}/
|
|
107
|
+
├── contracts/ # Solidity smart contracts
|
|
108
|
+
├── scripts/ # Deployment and utility scripts
|
|
109
|
+
├── test/ # Hardhat test files
|
|
110
|
+
├── frontend/ # React + Vite frontend
|
|
111
|
+
│ └── src/
|
|
112
|
+
│ ├── App.jsx
|
|
113
|
+
│ └── components/
|
|
114
|
+
│ └── Navbar.jsx ← Connect Wallet button lives here
|
|
115
|
+
├── hardhat.config.js # Hardhat configuration
|
|
116
|
+
├── .env.example # Environment variable template
|
|
117
|
+
└── README.md
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Writing Smart Contracts
|
|
123
|
+
|
|
124
|
+
Place your Solidity files in \`contracts/\`. The config targets Solidity \`0.8.26\` with the Cancun EVM and the optimizer enabled (200 runs).
|
|
125
|
+
|
|
126
|
+
**Example** — \`contracts/MyContract.sol\`:
|
|
127
|
+
|
|
128
|
+
\`\`\`solidity
|
|
129
|
+
// SPDX-License-Identifier: MIT
|
|
130
|
+
pragma solidity ^0.8.26;
|
|
131
|
+
|
|
132
|
+
contract MyContract {
|
|
133
|
+
// Your logic here
|
|
134
|
+
}
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
### Included libraries
|
|
138
|
+
|
|
139
|
+
| Package | Use case |
|
|
140
|
+
|---------|----------|
|
|
141
|
+
| \`@openzeppelin/contracts\` | ERC-20, ERC-721, ERC-1155, access control, etc. |
|
|
142
|
+
| \`@openzeppelin/contracts-upgradeable\` | UUPS / Transparent proxy upgradeable variants |
|
|
143
|
+
| \`@openzeppelin/hardhat-upgrades\` | Deploy and upgrade helper tasks |
|
|
144
|
+
| \`@account-abstraction/contracts\` | ERC-4337 interfaces and EntryPoint |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Writing Tests
|
|
149
|
+
|
|
150
|
+
Place test files in \`test/\`. Hardhat ships with Mocha + Chai.
|
|
151
|
+
|
|
152
|
+
**Example** — \`test/MyContract.test.js\`:
|
|
153
|
+
|
|
154
|
+
\`\`\`javascript
|
|
155
|
+
const { expect } = require('chai');
|
|
156
|
+
const { ethers } = require('hardhat');
|
|
157
|
+
|
|
158
|
+
describe('MyContract', function () {
|
|
159
|
+
it('should deploy successfully', async function () {
|
|
160
|
+
const MyContract = await ethers.getContractFactory('MyContract');
|
|
161
|
+
const contract = await MyContract.deploy();
|
|
162
|
+
expect(await contract.getAddress()).to.not.equal(ethers.ZeroAddress);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Writing Deployment Scripts
|
|
170
|
+
|
|
171
|
+
**Example** — \`scripts/deploy.js\`:
|
|
172
|
+
|
|
173
|
+
\`\`\`javascript
|
|
174
|
+
const { ethers } = require('hardhat');
|
|
175
|
+
|
|
176
|
+
async function main() {
|
|
177
|
+
const [deployer] = await ethers.getSigners();
|
|
178
|
+
console.log('Deploying from:', deployer.address);
|
|
179
|
+
|
|
180
|
+
const MyContract = await ethers.getContractFactory('MyContract');
|
|
181
|
+
const contract = await MyContract.deploy();
|
|
182
|
+
await contract.waitForDeployment();
|
|
183
|
+
|
|
184
|
+
console.log('MyContract deployed to:', await contract.getAddress());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch((err) => {
|
|
188
|
+
console.error(err);
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
});
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Networks
|
|
196
|
+
|
|
197
|
+
| Network | RPC | Chain ID |
|
|
198
|
+
|---------|-----|----------|
|
|
199
|
+
| Hardhat (local) | \`http://127.0.0.1:8545\` | 31337 |
|
|
200
|
+
| Sepolia (testnet) | \`SEPOLIA_RPC_URL\` in \`.env\` | 11155111 |
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Verifying Contracts
|
|
205
|
+
|
|
206
|
+
\`\`\`bash
|
|
207
|
+
npx hardhat verify --network sepolia <CONTRACT_ADDRESS> <CONSTRUCTOR_ARGS>
|
|
208
|
+
\`\`\`
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Frontend
|
|
213
|
+
|
|
214
|
+
The \`frontend/\` folder is a Vite + React app with:
|
|
215
|
+
|
|
216
|
+
- A sticky navigation bar with a **Connect Wallet** button (MetaMask / EIP-1193)
|
|
217
|
+
- Ethers.js v6 already wired up
|
|
218
|
+
|
|
219
|
+
### Connecting to a deployed contract
|
|
220
|
+
|
|
221
|
+
Copy the ABI from \`artifacts/contracts/YourContract.sol/YourContract.json\` and import it:
|
|
222
|
+
|
|
223
|
+
\`\`\`javascript
|
|
224
|
+
import { BrowserProvider, Contract } from 'ethers';
|
|
225
|
+
import MyContractABI from '../../artifacts/contracts/MyContract.sol/MyContract.json';
|
|
226
|
+
|
|
227
|
+
const CONTRACT_ADDRESS = '0x...';
|
|
228
|
+
|
|
229
|
+
async function interact() {
|
|
230
|
+
const provider = new BrowserProvider(window.ethereum);
|
|
231
|
+
const signer = await provider.getSigner();
|
|
232
|
+
const contract = new Contract(CONTRACT_ADDRESS, MyContractABI.abi, signer);
|
|
233
|
+
// call contract methods here
|
|
234
|
+
}
|
|
235
|
+
\`\`\`
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Resources
|
|
240
|
+
|
|
241
|
+
- [Hardhat Docs](https://hardhat.org/docs)
|
|
242
|
+
- [Solidity Docs](https://docs.soliditylang.org)
|
|
243
|
+
- [OpenZeppelin Docs](https://docs.openzeppelin.com)
|
|
244
|
+
- [EIP-4337 (Account Abstraction)](https://eips.ethereum.org/EIPS/eip-4337)
|
|
245
|
+
- [Ethers.js v6 Docs](https://docs.ethers.org/v6)
|
|
246
|
+
- [Vite Docs](https://vitejs.dev)
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
*Built with Africa's Blockchain Club — scaffold your next project with \`npx abc-scaffold\`.*
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function scaffoldFrontend(frontendDir, projectName) {
|
|
255
|
+
fse.ensureDirSync(path.join(frontendDir, 'src', 'components'));
|
|
256
|
+
fse.copySync(path.join(TEMPLATES, 'frontend'), frontendDir);
|
|
257
|
+
|
|
258
|
+
// Substitute project name in index.html
|
|
259
|
+
const htmlPath = path.join(frontendDir, 'index.html');
|
|
260
|
+
const html = fs.readFileSync(htmlPath, 'utf-8').replace(/{{PROJECT_NAME}}/g, projectName);
|
|
261
|
+
fs.writeFileSync(htmlPath, html);
|
|
262
|
+
|
|
263
|
+
logger.success('frontend/ (React + Vite + Connect Wallet)');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function scaffold(initialName) {
|
|
267
|
+
let projectName = initialName;
|
|
268
|
+
|
|
269
|
+
if (!projectName) {
|
|
270
|
+
projectName = await prompt(chalk.bold('? Project name: '));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!projectName) {
|
|
274
|
+
logger.error('Project name is required.');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Normalise: lowercase, spaces → hyphens
|
|
279
|
+
projectName = projectName.trim().toLowerCase().replace(/\s+/g, '-');
|
|
280
|
+
|
|
281
|
+
if (!isValidName(projectName)) {
|
|
282
|
+
logger.error(`"${projectName}" is not a valid package name. Use lowercase letters, numbers, hyphens, or underscores.`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
287
|
+
|
|
288
|
+
if (fs.existsSync(targetDir)) {
|
|
289
|
+
logger.error(`Directory "${projectName}" already exists. Choose a different name or delete it first.`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Create directory structure ──────────────────────────────────────────────
|
|
294
|
+
logger.step('Creating project structure');
|
|
295
|
+
|
|
296
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
297
|
+
|
|
298
|
+
for (const dir of ['contracts', 'scripts', 'test']) {
|
|
299
|
+
fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
|
|
300
|
+
logger.success(`${dir}/`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Copy configuration files ────────────────────────────────────────────────
|
|
304
|
+
logger.step('Copying configuration files');
|
|
305
|
+
|
|
306
|
+
copyTemplate('hardhat.config.js', targetDir);
|
|
307
|
+
|
|
308
|
+
// These are stored without a leading dot in templates/ because npm strips
|
|
309
|
+
// dotfiles when publishing. We restore the dot on copy.
|
|
310
|
+
fse.copySync(path.join(TEMPLATES, 'env.example'), path.join(targetDir, '.env.example'));
|
|
311
|
+
logger.success('.env.example');
|
|
312
|
+
fse.copySync(path.join(TEMPLATES, 'gitignore'), path.join(targetDir, '.gitignore'));
|
|
313
|
+
logger.success('.gitignore');
|
|
314
|
+
|
|
315
|
+
// package.json — substitute project name
|
|
316
|
+
const pkgTemplate = fs.readFileSync(path.join(TEMPLATES, 'package.json'), 'utf-8');
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
path.join(targetDir, 'package.json'),
|
|
319
|
+
pkgTemplate.replace('"{{PROJECT_NAME}}"', `"${projectName}"`)
|
|
320
|
+
);
|
|
321
|
+
logger.success('package.json');
|
|
322
|
+
|
|
323
|
+
// ── Generate README ─────────────────────────────────────────────────────────
|
|
324
|
+
logger.step('Generating README.md');
|
|
325
|
+
fs.writeFileSync(path.join(targetDir, 'README.md'), buildReadme(projectName));
|
|
326
|
+
logger.success('README.md');
|
|
327
|
+
|
|
328
|
+
// ── Scaffold React frontend ─────────────────────────────────────────────────
|
|
329
|
+
logger.step('Scaffolding React frontend');
|
|
330
|
+
scaffoldFrontend(path.join(targetDir, 'frontend'), projectName);
|
|
331
|
+
|
|
332
|
+
// ── Done ────────────────────────────────────────────────────────────────────
|
|
333
|
+
console.log('');
|
|
334
|
+
console.log(chalk.bold.green('✨ Project scaffolded!'));
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log(' Next steps:');
|
|
337
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
338
|
+
console.log(chalk.cyan(' npm install') + chalk.gray(' # install root dependencies'));
|
|
339
|
+
console.log(chalk.cyan(' cp .env.example .env') + chalk.gray(' # fill in your keys'));
|
|
340
|
+
console.log(chalk.cyan(' npx hardhat compile') + chalk.gray(' # compile contracts'));
|
|
341
|
+
console.log(chalk.cyan(' cd frontend && npm install') + chalk.gray(' # install frontend dependencies'));
|
|
342
|
+
console.log(chalk.cyan(' npm run dev') + chalk.gray(' # start the UI'));
|
|
343
|
+
console.log('');
|
|
344
|
+
console.log(chalk.gray(' Docs → see README.md'));
|
|
345
|
+
console.log('');
|
|
346
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Example .env file - Duplicate this file to '.env' and fill in your values
|
|
2
|
-
SEPOLIA_RPC_URL="https://sepolia.infura.io/v3/YOUR_INFURA_KEY"
|
|
3
|
-
PRIVATE_KEY="YOUR_WALLET_PRIVATE_KEY"
|
|
4
|
-
ETHERSCAN_API_KEY="YOUR_ETHERSCAN_API_KEY"
|
|
1
|
+
# Example .env file - Duplicate this file to '.env' and fill in your values
|
|
2
|
+
SEPOLIA_RPC_URL="https://sepolia.infura.io/v3/YOUR_INFURA_KEY"
|
|
3
|
+
PRIVATE_KEY="YOUR_WALLET_PRIVATE_KEY"
|
|
4
|
+
ETHERSCAN_API_KEY="YOUR_ETHERSCAN_API_KEY"
|
|
@@ -19,75 +19,63 @@ body {
|
|
|
19
19
|
flex-direction: column;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/* ──
|
|
22
|
+
/* ── Centered content ── */
|
|
23
23
|
.main {
|
|
24
24
|
flex: 1;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
width: 100%;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* ── Hero ── */
|
|
32
|
-
.hero {
|
|
33
|
-
text-align: center;
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
34
28
|
}
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
/* ── Connect button ── */
|
|
31
|
+
.connect-btn {
|
|
32
|
+
background-color: #e94560;
|
|
39
33
|
color: #ffffff;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.subtitle {
|
|
34
|
+
border: none;
|
|
35
|
+
border-radius: 10px;
|
|
36
|
+
padding: 1rem 2.5rem;
|
|
45
37
|
font-size: 1.1rem;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
font-weight: 700;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
transition: background-color 0.18s ease, transform 0.12s ease;
|
|
41
|
+
letter-spacing: 0.3px;
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
55
|
-
gap: 1.5rem;
|
|
56
|
-
margin-top: 1rem;
|
|
44
|
+
.connect-btn:hover:not(:disabled) {
|
|
45
|
+
background-color: #c73652;
|
|
46
|
+
transform: translateY(-2px);
|
|
57
47
|
}
|
|
58
48
|
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
border: 1px solid #2a2a4a;
|
|
62
|
-
border-radius: 12px;
|
|
63
|
-
padding: 1.75rem 1.5rem;
|
|
64
|
-
text-align: left;
|
|
65
|
-
transition: border-color 0.2s ease, transform 0.2s ease;
|
|
49
|
+
.connect-btn:active:not(:disabled) {
|
|
50
|
+
transform: translateY(0);
|
|
66
51
|
}
|
|
67
52
|
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
.connect-btn:disabled {
|
|
54
|
+
background-color: #555566;
|
|
55
|
+
cursor: not-allowed;
|
|
71
56
|
}
|
|
72
57
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
/* ── Connected state ── */
|
|
59
|
+
.connected {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 0.6rem;
|
|
63
|
+
background-color: #1a1a2e;
|
|
64
|
+
border: 1px solid #e94560;
|
|
65
|
+
border-radius: 10px;
|
|
66
|
+
padding: 0.75rem 1.5rem;
|
|
78
67
|
}
|
|
79
68
|
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
.wallet-dot {
|
|
70
|
+
width: 10px;
|
|
71
|
+
height: 10px;
|
|
72
|
+
background-color: #4caf50;
|
|
73
|
+
border-radius: 50%;
|
|
74
|
+
flex-shrink: 0;
|
|
84
75
|
}
|
|
85
76
|
|
|
86
|
-
.
|
|
87
|
-
background-color: #0d0d1a;
|
|
88
|
-
color: #e94560;
|
|
89
|
-
padding: 0.1rem 0.4rem;
|
|
90
|
-
border-radius: 4px;
|
|
91
|
-
font-size: 0.85rem;
|
|
77
|
+
.wallet-address {
|
|
92
78
|
font-family: 'Fira Code', 'Cascadia Code', monospace;
|
|
79
|
+
font-size: 1rem;
|
|
80
|
+
color: #e2e2e2;
|
|
93
81
|
}
|
|
@@ -1,34 +1,52 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { BrowserProvider } from 'ethers';
|
|
1
3
|
import Navbar from './components/Navbar.jsx';
|
|
2
4
|
import './App.css';
|
|
3
5
|
|
|
4
6
|
function App() {
|
|
7
|
+
const [account, setAccount] = useState(null);
|
|
8
|
+
const [connecting, setConnecting] = useState(false);
|
|
9
|
+
|
|
10
|
+
async function connectWallet() {
|
|
11
|
+
if (!window.ethereum) {
|
|
12
|
+
alert('MetaMask is not installed. Please install it at https://metamask.io');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setConnecting(true);
|
|
17
|
+
try {
|
|
18
|
+
const provider = new BrowserProvider(window.ethereum);
|
|
19
|
+
const accounts = await provider.send('eth_requestAccounts', []);
|
|
20
|
+
setAccount(accounts[0]);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error('Wallet connection rejected:', err);
|
|
23
|
+
} finally {
|
|
24
|
+
setConnecting(false);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function truncate(addr) {
|
|
29
|
+
return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
5
32
|
return (
|
|
6
33
|
<div className="app">
|
|
7
34
|
<Navbar />
|
|
8
35
|
<main className="main">
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<div className="cards">
|
|
14
|
-
<div className="card">
|
|
15
|
-
<h3>📄 Write Contracts</h3>
|
|
16
|
-
<p>Add your Solidity files to <code>contracts/</code></p>
|
|
17
|
-
</div>
|
|
18
|
-
<div className="card">
|
|
19
|
-
<h3>⚙️ Compile</h3>
|
|
20
|
-
<p>Run <code>npm run compile</code> in the project root</p>
|
|
21
|
-
</div>
|
|
22
|
-
<div className="card">
|
|
23
|
-
<h3>🚀 Deploy</h3>
|
|
24
|
-
<p>Add a script to <code>scripts/</code> and run it with Hardhat</p>
|
|
25
|
-
</div>
|
|
26
|
-
<div className="card">
|
|
27
|
-
<h3>🔗 Connect</h3>
|
|
28
|
-
<p>Use the wallet button above to connect MetaMask, then interact with your contracts</p>
|
|
29
|
-
</div>
|
|
36
|
+
{account ? (
|
|
37
|
+
<div className="connected">
|
|
38
|
+
<span className="wallet-dot" />
|
|
39
|
+
<span className="wallet-address">{truncate(account)}</span>
|
|
30
40
|
</div>
|
|
31
|
-
|
|
41
|
+
) : (
|
|
42
|
+
<button
|
|
43
|
+
className="connect-btn"
|
|
44
|
+
onClick={connectWallet}
|
|
45
|
+
disabled={connecting}
|
|
46
|
+
>
|
|
47
|
+
{connecting ? 'Connecting…' : 'Connect Wallet'}
|
|
48
|
+
</button>
|
|
49
|
+
)}
|
|
32
50
|
</main>
|
|
33
51
|
</div>
|
|
34
52
|
);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
.navbar {
|
|
2
2
|
display: flex;
|
|
3
3
|
align-items: center;
|
|
4
|
-
justify-content: space-between;
|
|
5
4
|
padding: 1rem 2rem;
|
|
6
5
|
background-color: #1a1a2e;
|
|
7
6
|
border-bottom: 1px solid #2a2a4a;
|
|
@@ -10,12 +9,10 @@
|
|
|
10
9
|
z-index: 100;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
/* ── Brand ── */
|
|
14
12
|
.navbar-brand {
|
|
15
13
|
display: flex;
|
|
16
14
|
align-items: center;
|
|
17
15
|
gap: 0.6rem;
|
|
18
|
-
text-decoration: none;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
.navbar-logo {
|
|
@@ -29,62 +26,3 @@
|
|
|
29
26
|
color: #e94560;
|
|
30
27
|
letter-spacing: 0.3px;
|
|
31
28
|
}
|
|
32
|
-
|
|
33
|
-
/* ── Actions ── */
|
|
34
|
-
.navbar-actions {
|
|
35
|
-
display: flex;
|
|
36
|
-
align-items: center;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/* Connect button */
|
|
40
|
-
.connect-btn {
|
|
41
|
-
background-color: #e94560;
|
|
42
|
-
color: #ffffff;
|
|
43
|
-
border: none;
|
|
44
|
-
border-radius: 8px;
|
|
45
|
-
padding: 0.55rem 1.3rem;
|
|
46
|
-
font-size: 0.9rem;
|
|
47
|
-
font-weight: 600;
|
|
48
|
-
cursor: pointer;
|
|
49
|
-
transition: background-color 0.18s ease, transform 0.12s ease;
|
|
50
|
-
white-space: nowrap;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.connect-btn:hover:not(:disabled) {
|
|
54
|
-
background-color: #c73652;
|
|
55
|
-
transform: translateY(-1px);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.connect-btn:active:not(:disabled) {
|
|
59
|
-
transform: translateY(0);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.connect-btn:disabled {
|
|
63
|
-
background-color: #555566;
|
|
64
|
-
cursor: not-allowed;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/* Connected badge */
|
|
68
|
-
.wallet-badge {
|
|
69
|
-
display: flex;
|
|
70
|
-
align-items: center;
|
|
71
|
-
gap: 0.5rem;
|
|
72
|
-
background-color: #0f3460;
|
|
73
|
-
border: 1px solid #e94560;
|
|
74
|
-
border-radius: 8px;
|
|
75
|
-
padding: 0.45rem 1rem;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.wallet-dot {
|
|
79
|
-
width: 8px;
|
|
80
|
-
height: 8px;
|
|
81
|
-
background-color: #4caf50;
|
|
82
|
-
border-radius: 50%;
|
|
83
|
-
flex-shrink: 0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.wallet-address {
|
|
87
|
-
color: #e2e2e2;
|
|
88
|
-
font-family: 'Fira Code', 'Cascadia Code', monospace;
|
|
89
|
-
font-size: 0.875rem;
|
|
90
|
-
}
|
|
@@ -1,56 +1,12 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { BrowserProvider } from 'ethers';
|
|
3
1
|
import './Navbar.css';
|
|
4
2
|
|
|
5
3
|
export default function Navbar() {
|
|
6
|
-
const [account, setAccount] = useState(null);
|
|
7
|
-
const [connecting, setConnecting] = useState(false);
|
|
8
|
-
|
|
9
|
-
async function connectWallet() {
|
|
10
|
-
if (!window.ethereum) {
|
|
11
|
-
alert('MetaMask is not installed. Please install it at https://metamask.io');
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
setConnecting(true);
|
|
16
|
-
try {
|
|
17
|
-
const provider = new BrowserProvider(window.ethereum);
|
|
18
|
-
const accounts = await provider.send('eth_requestAccounts', []);
|
|
19
|
-
setAccount(accounts[0]);
|
|
20
|
-
} catch (err) {
|
|
21
|
-
console.error('Wallet connection rejected:', err);
|
|
22
|
-
} finally {
|
|
23
|
-
setConnecting(false);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function truncate(addr) {
|
|
28
|
-
return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
4
|
return (
|
|
32
5
|
<nav className="navbar">
|
|
33
6
|
<div className="navbar-brand">
|
|
34
7
|
<span className="navbar-logo">🔗</span>
|
|
35
8
|
<span className="navbar-title">Africa's Blockchain Club</span>
|
|
36
9
|
</div>
|
|
37
|
-
|
|
38
|
-
<div className="navbar-actions">
|
|
39
|
-
{account ? (
|
|
40
|
-
<div className="wallet-badge">
|
|
41
|
-
<span className="wallet-dot" />
|
|
42
|
-
<span className="wallet-address">{truncate(account)}</span>
|
|
43
|
-
</div>
|
|
44
|
-
) : (
|
|
45
|
-
<button
|
|
46
|
-
className="connect-btn"
|
|
47
|
-
onClick={connectWallet}
|
|
48
|
-
disabled={connecting}
|
|
49
|
-
>
|
|
50
|
-
{connecting ? 'Connecting…' : 'Connect Wallet'}
|
|
51
|
-
</button>
|
|
52
|
-
)}
|
|
53
|
-
</div>
|
|
54
10
|
</nav>
|
|
55
11
|
);
|
|
56
12
|
}
|