abc-scaffold 1.0.0 → 1.0.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/LICENSE +1 -1
- package/package.json +42 -42
- package/src/scaffold.js +346 -341
- package/templates/{.env.example → env.example} +4 -4
- 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.1",
|
|
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
|
-
npm run compile
|
|
72
|
-
\`\`\`
|
|
73
|
-
|
|
74
|
-
### 4. Run tests
|
|
75
|
-
|
|
76
|
-
\`\`\`bash
|
|
77
|
-
npm test
|
|
78
|
-
\`\`\`
|
|
79
|
-
|
|
80
|
-
### 5. Start a local Hardhat node
|
|
81
|
-
|
|
82
|
-
\`\`\`bash
|
|
83
|
-
npx hardhat node
|
|
84
|
-
\`\`\`
|
|
85
|
-
|
|
86
|
-
### 6. Deploy contracts locally
|
|
87
|
-
|
|
88
|
-
\`\`\`bash
|
|
89
|
-
npx hardhat run scripts/deploy.js --network localhost
|
|
90
|
-
\`\`\`
|
|
91
|
-
|
|
92
|
-
### 7. 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
|
-
|
|
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
|
+
npm run compile
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
### 4. Run tests
|
|
75
|
+
|
|
76
|
+
\`\`\`bash
|
|
77
|
+
npm test
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
### 5. Start a local Hardhat node
|
|
81
|
+
|
|
82
|
+
\`\`\`bash
|
|
83
|
+
npx hardhat node
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
### 6. Deploy contracts locally
|
|
87
|
+
|
|
88
|
+
\`\`\`bash
|
|
89
|
+
npx hardhat run scripts/deploy.js --network localhost
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
### 7. 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(' npm run 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"
|