z0gcode 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/.env.example +41 -0
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/z0g.mjs +1052 -0
- package/contracts/Z0gSession.json +424 -0
- package/contracts/Z0gSession.sol +106 -0
- package/package.json +60 -0
- package/skills/0g/CHAIN.md +229 -0
- package/skills/0g/COMPUTE.md +296 -0
- package/skills/0g/NETWORK_CONFIG.md +163 -0
- package/skills/0g/SECURITY.md +174 -0
- package/skills/0g/STORAGE.md +167 -0
- package/skills/0g/TESTING.md +263 -0
- package/src/agent.mjs +434 -0
- package/src/anchor.mjs +65 -0
- package/src/checkpoints.mjs +64 -0
- package/src/client.mjs +134 -0
- package/src/commands.mjs +93 -0
- package/src/config.mjs +74 -0
- package/src/context.mjs +55 -0
- package/src/crypto.mjs +47 -0
- package/src/env.mjs +47 -0
- package/src/goal.mjs +45 -0
- package/src/inft.mjs +79 -0
- package/src/mcp-server.mjs +38 -0
- package/src/mcp.mjs +91 -0
- package/src/media.mjs +52 -0
- package/src/models-info.mjs +97 -0
- package/src/plan.mjs +21 -0
- package/src/prompt.mjs +149 -0
- package/src/provenance.mjs +46 -0
- package/src/sessions.mjs +168 -0
- package/src/settings.mjs +37 -0
- package/src/skills.mjs +43 -0
- package/src/tools.mjs +481 -0
- package/src/ui.mjs +798 -0
- package/src/user-skills.mjs +111 -0
- package/src/worktree.mjs +61 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Security Patterns
|
|
2
|
+
|
|
3
|
+
Cross-cutting security patterns for 0G development: key management, data integrity, contract
|
|
4
|
+
security, and TEE verification.
|
|
5
|
+
|
|
6
|
+
## Key Management
|
|
7
|
+
|
|
8
|
+
### Environment Variables Pattern
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# .env — NEVER commit this file
|
|
12
|
+
PRIVATE_KEY=your_private_key_here
|
|
13
|
+
RPC_URL=https://evmrpc-testnet.0g.ai
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import 'dotenv/config';
|
|
18
|
+
|
|
19
|
+
// CORRECT — load from environment
|
|
20
|
+
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
|
|
21
|
+
|
|
22
|
+
// WRONG — hardcoded key (NEVER do this)
|
|
23
|
+
// const wallet = new ethers.Wallet("0xabc123...", provider);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### .gitignore Requirements
|
|
27
|
+
|
|
28
|
+
```gitignore
|
|
29
|
+
# MUST be in .gitignore
|
|
30
|
+
.env
|
|
31
|
+
.env.local
|
|
32
|
+
.env.*.local
|
|
33
|
+
*.key
|
|
34
|
+
*.pem
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Validation Pattern
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
function validateEnv() {
|
|
41
|
+
const required = ['PRIVATE_KEY', 'RPC_URL'];
|
|
42
|
+
const missing = required.filter((key) => !process.env[key]);
|
|
43
|
+
if (missing.length > 0) {
|
|
44
|
+
throw new Error(`Missing environment variables: ${missing.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Data Integrity (Storage)
|
|
50
|
+
|
|
51
|
+
### Verified Downloads
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// ALWAYS use verified=true in production
|
|
55
|
+
await indexer.download(rootHash, outputPath, true);
|
|
56
|
+
// Throws if Merkle proof verification fails
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Hash Verification
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { ZgFile } from '@0glabs/0g-ts-sdk';
|
|
63
|
+
|
|
64
|
+
async function verifyFile(filePath: string, expectedHash: string): Promise<boolean> {
|
|
65
|
+
const file = await ZgFile.fromFilePath(filePath);
|
|
66
|
+
try {
|
|
67
|
+
const [tree, err] = await file.merkleTree();
|
|
68
|
+
if (err) return false;
|
|
69
|
+
return tree.rootHash() === expectedHash;
|
|
70
|
+
} finally {
|
|
71
|
+
await file.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## TEE Verification (Compute)
|
|
77
|
+
|
|
78
|
+
Always verify provider TEE attestation before sensitive workloads:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const services = await broker.inference.listService();
|
|
82
|
+
const verifiedProviders = services.filter((s) => s.teeVerified === true);
|
|
83
|
+
|
|
84
|
+
if (verifiedProviders.length === 0) {
|
|
85
|
+
throw new Error('No TEE-verified providers available');
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Smart Contract Security
|
|
90
|
+
|
|
91
|
+
### Access Control
|
|
92
|
+
|
|
93
|
+
```solidity
|
|
94
|
+
// SPDX-License-Identifier: MIT
|
|
95
|
+
pragma solidity ^0.8.24;
|
|
96
|
+
|
|
97
|
+
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
98
|
+
|
|
99
|
+
contract SecureStorage is Ownable {
|
|
100
|
+
mapping(bytes32 => address) public fileOwners;
|
|
101
|
+
|
|
102
|
+
function registerFile(bytes32 rootHash) external {
|
|
103
|
+
require(fileOwners[rootHash] == address(0), "Already registered");
|
|
104
|
+
fileOwners[rootHash] = msg.sender;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function verifyOwner(bytes32 rootHash, address owner) external view returns (bool) {
|
|
108
|
+
return fileOwners[rootHash] == owner;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Input Validation
|
|
114
|
+
|
|
115
|
+
```solidity
|
|
116
|
+
function store(bytes32 rootHash, string calldata metadata) external {
|
|
117
|
+
require(rootHash != bytes32(0), "Invalid root hash");
|
|
118
|
+
require(bytes(metadata).length > 0, "Empty metadata");
|
|
119
|
+
require(bytes(metadata).length <= 1024, "Metadata too long");
|
|
120
|
+
// ... storage logic
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## API Security
|
|
125
|
+
|
|
126
|
+
### Rate Limiting Pattern
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const requestTimes: number[] = [];
|
|
130
|
+
const MAX_REQUESTS = 10;
|
|
131
|
+
const WINDOW_MS = 60_000;
|
|
132
|
+
|
|
133
|
+
function checkRateLimit(): boolean {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const windowStart = now - WINDOW_MS;
|
|
136
|
+
// Remove old entries
|
|
137
|
+
while (requestTimes.length > 0 && requestTimes[0] < windowStart) {
|
|
138
|
+
requestTimes.shift();
|
|
139
|
+
}
|
|
140
|
+
if (requestTimes.length >= MAX_REQUESTS) return false;
|
|
141
|
+
requestTimes.push(now);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Balance Check Before Operations
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
async function ensureSufficientBalance(broker: any, minBalance: number) {
|
|
150
|
+
// getLedger() returns tuple: [0]=address, [1]=totalBalance, [2]=availableBalance
|
|
151
|
+
const account = await broker.ledger.getLedger();
|
|
152
|
+
const available = parseFloat(ethers.formatEther(account[2]));
|
|
153
|
+
if (available < minBalance) {
|
|
154
|
+
throw new Error(`Insufficient balance: ${available} 0G available, ${minBalance} 0G required`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Checklist
|
|
160
|
+
|
|
161
|
+
- [ ] Private keys loaded from `.env`, never hardcoded
|
|
162
|
+
- [ ] `.env` is in `.gitignore`
|
|
163
|
+
- [ ] All downloads use verified mode (`true`)
|
|
164
|
+
- [ ] TEE verification checked for compute providers
|
|
165
|
+
- [ ] Smart contracts use access control
|
|
166
|
+
- [ ] Input validation on all public functions
|
|
167
|
+
- [ ] Balance checked before operations
|
|
168
|
+
- [ ] No secrets in logs or error messages
|
|
169
|
+
|
|
170
|
+
## References
|
|
171
|
+
|
|
172
|
+
- [OWASP Smart Contract Top 10](https://owasp.org/www-project-smart-contract-top-10/)
|
|
173
|
+
- [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts)
|
|
174
|
+
- See also: [NETWORK_CONFIG.md](./NETWORK_CONFIG.md)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Storage Patterns
|
|
2
|
+
|
|
3
|
+
Architecture, SDK reference, and best practices for 0G decentralized storage.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────┐
|
|
9
|
+
│ Log Layer │
|
|
10
|
+
│ (Raw file / blob storage) │
|
|
11
|
+
├─────────────────────────────────┤
|
|
12
|
+
│ 0G Consensus │
|
|
13
|
+
│ (Data availability proofs) │
|
|
14
|
+
└─────────────────────────────────┘
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Log Layer (File Storage)
|
|
18
|
+
|
|
19
|
+
### Core Concepts
|
|
20
|
+
|
|
21
|
+
- **ZgFile**: Represents a file in the 0G storage system
|
|
22
|
+
- **Merkle Tree**: Every file is split into chunks and organized as a Merkle tree
|
|
23
|
+
- **Root Hash**: The Merkle root — unique identifier for the file content
|
|
24
|
+
- **Indexer**: Service that locates data across storage nodes
|
|
25
|
+
|
|
26
|
+
### File Upload Lifecycle
|
|
27
|
+
|
|
28
|
+
1. Create a `ZgFile` from local file path or buffer
|
|
29
|
+
2. Generate the Merkle tree (computes root hash)
|
|
30
|
+
3. Upload via the Indexer (distributes chunks to storage nodes)
|
|
31
|
+
4. Store the root hash for later retrieval
|
|
32
|
+
5. **Close the file handle** (critical — prevents memory leaks)
|
|
33
|
+
|
|
34
|
+
### Upload Pattern
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { ZgFile, Indexer } from '@0glabs/0g-ts-sdk';
|
|
38
|
+
import { ethers } from 'ethers';
|
|
39
|
+
import 'dotenv/config';
|
|
40
|
+
|
|
41
|
+
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
|
|
42
|
+
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
|
|
43
|
+
|
|
44
|
+
const indexer = new Indexer(process.env.STORAGE_INDEXER!);
|
|
45
|
+
const file = await ZgFile.fromFilePath(filePath);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const [tree, err] = await file.merkleTree();
|
|
49
|
+
if (err) throw err;
|
|
50
|
+
|
|
51
|
+
const rootHash = tree!.rootHash();
|
|
52
|
+
console.log('Root hash:', rootHash);
|
|
53
|
+
|
|
54
|
+
const [tx, uploadErr] = await indexer.upload(file, process.env.RPC_URL!, wallet);
|
|
55
|
+
if (uploadErr) throw new Error(`Upload failed: ${uploadErr.message}`);
|
|
56
|
+
console.log('Upload tx:', tx);
|
|
57
|
+
|
|
58
|
+
return rootHash;
|
|
59
|
+
} finally {
|
|
60
|
+
await file.close();
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Download Pattern
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { Indexer } from '@0glabs/0g-ts-sdk';
|
|
68
|
+
|
|
69
|
+
const indexer = new Indexer(process.env.STORAGE_INDEXER!);
|
|
70
|
+
|
|
71
|
+
// download() can throw OR return an error — always use try/catch
|
|
72
|
+
try {
|
|
73
|
+
const err = await indexer.download(rootHash, outputPath, /* verified */ true);
|
|
74
|
+
if (err) throw err;
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
throw new Error(`Download failed: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Upload from Buffer
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { ZgFile } from '@0glabs/0g-ts-sdk';
|
|
84
|
+
import * as fs from 'fs';
|
|
85
|
+
import * as os from 'os';
|
|
86
|
+
import * as path from 'path';
|
|
87
|
+
|
|
88
|
+
// Write buffer to temp file (SDK requires file path)
|
|
89
|
+
const tempPath = path.join(os.tmpdir(), `0g-upload-${Date.now()}`);
|
|
90
|
+
fs.writeFileSync(tempPath, buffer);
|
|
91
|
+
|
|
92
|
+
const file = await ZgFile.fromFilePath(tempPath);
|
|
93
|
+
try {
|
|
94
|
+
const [tree, err] = await file.merkleTree();
|
|
95
|
+
if (err) throw err;
|
|
96
|
+
const rootHash = tree!.rootHash();
|
|
97
|
+
const [, uploadErr] = await indexer.upload(file, process.env.RPC_URL!, wallet);
|
|
98
|
+
if (uploadErr) throw new Error(`Upload failed: ${uploadErr.message}`);
|
|
99
|
+
return rootHash;
|
|
100
|
+
} finally {
|
|
101
|
+
await file.close();
|
|
102
|
+
fs.unlinkSync(tempPath); // Clean up temp file
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Merkle Verification
|
|
107
|
+
|
|
108
|
+
### Computing Root Hash
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { ZgFile } from '@0glabs/0g-ts-sdk';
|
|
112
|
+
|
|
113
|
+
const file = await ZgFile.fromFilePath(filePath);
|
|
114
|
+
try {
|
|
115
|
+
const [tree, err] = await file.merkleTree();
|
|
116
|
+
if (err) throw err;
|
|
117
|
+
console.log('Root hash:', tree.rootHash());
|
|
118
|
+
} finally {
|
|
119
|
+
await file.close();
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Verified Download
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// The third parameter enables Merkle proof verification
|
|
127
|
+
// Note: download() can throw or return errors — always use try/catch
|
|
128
|
+
try {
|
|
129
|
+
const err = await indexer.download(rootHash, outputPath, true);
|
|
130
|
+
if (err) throw err;
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
throw new Error(`Download failed: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Critical Rules
|
|
137
|
+
|
|
138
|
+
### ALWAYS
|
|
139
|
+
|
|
140
|
+
- Generate the Merkle tree before uploading
|
|
141
|
+
- Close file handles after upload (`file.close()`)
|
|
142
|
+
- Store root hashes — they are the ONLY way to retrieve files
|
|
143
|
+
- Use verified downloads in production (third param = `true`)
|
|
144
|
+
- Clean up temp files when uploading from buffers
|
|
145
|
+
|
|
146
|
+
### NEVER
|
|
147
|
+
|
|
148
|
+
- Skip Merkle tree generation before upload
|
|
149
|
+
- Forget to close `ZgFile` handles (causes memory leaks)
|
|
150
|
+
- Lose the root hash (data becomes irretrievable)
|
|
151
|
+
- Upload without a funded wallet (transaction will fail)
|
|
152
|
+
|
|
153
|
+
## Common Errors
|
|
154
|
+
|
|
155
|
+
| Error | Cause | Fix |
|
|
156
|
+
| ----------------------- | -------------------------- | ----------------------------------- |
|
|
157
|
+
| `merkle tree error` | File is empty or corrupted | Verify file exists and has content |
|
|
158
|
+
| `insufficient funds` | Wallet has no 0G | Fund wallet from faucet (testnet) |
|
|
159
|
+
| `indexer not available` | Wrong indexer URL | Check STORAGE_INDEXER in .env |
|
|
160
|
+
| `file not found` | Root hash doesn't exist | Verify root hash is correct |
|
|
161
|
+
| `connection refused` | RPC endpoint down | Try alternative RPC or check status |
|
|
162
|
+
|
|
163
|
+
## References
|
|
164
|
+
|
|
165
|
+
- [0G Storage SDK Docs](https://docs.0g.ai/build-with-0g/storage-network/sdk)
|
|
166
|
+
- [0G Storage Architecture](https://docs.0g.ai/learn/0g-storage)
|
|
167
|
+
- See also: [NETWORK_CONFIG.md](./NETWORK_CONFIG.md), [SECURITY.md](./SECURITY.md)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
Testing strategies, mock patterns, and testnet workflows for 0G applications.
|
|
4
|
+
|
|
5
|
+
## Testing Strategy
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────┐
|
|
9
|
+
│ Integration Tests │
|
|
10
|
+
│ (Testnet, real transactions) │
|
|
11
|
+
├─────────────────────────────────────┤
|
|
12
|
+
│ Unit Tests │
|
|
13
|
+
│ (Mocked SDK, local only) │
|
|
14
|
+
├─────────────────────────────────────┤
|
|
15
|
+
│ Static Analysis │
|
|
16
|
+
│ (TypeScript, Solhint, Slither) │
|
|
17
|
+
└─────────────────────────────────────┘
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Unit Testing
|
|
21
|
+
|
|
22
|
+
### Mock Storage SDK
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
26
|
+
|
|
27
|
+
// Mock the 0G SDK
|
|
28
|
+
vi.mock('@0glabs/0g-ts-sdk', () => ({
|
|
29
|
+
ZgFile: {
|
|
30
|
+
fromFilePath: vi.fn().mockResolvedValue({
|
|
31
|
+
merkleTree: vi.fn().mockResolvedValue([{ rootHash: () => '0xabc123' }, null]),
|
|
32
|
+
close: vi.fn(),
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
Indexer: vi.fn().mockImplementation(() => ({
|
|
36
|
+
upload: vi.fn().mockResolvedValue('0xtxhash'),
|
|
37
|
+
download: vi.fn().mockResolvedValue(undefined),
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe('Storage Upload', () => {
|
|
42
|
+
it('should return root hash after upload', async () => {
|
|
43
|
+
const { ZgFile, Indexer } = await import('@0glabs/0g-ts-sdk');
|
|
44
|
+
const file = await ZgFile.fromFilePath('test.txt');
|
|
45
|
+
const [tree] = await file.merkleTree();
|
|
46
|
+
expect(tree.rootHash()).toBe('0xabc123');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Mock Compute Broker
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
vi.mock('@0glabs/0g-serving-broker', () => ({
|
|
55
|
+
createZGComputeNetworkBroker: vi.fn().mockResolvedValue({
|
|
56
|
+
inference: {
|
|
57
|
+
listService: vi
|
|
58
|
+
.fn()
|
|
59
|
+
.mockResolvedValue([
|
|
60
|
+
{ providerAddress: '0x123', serviceType: 'chatbot', model: 'test-model' },
|
|
61
|
+
]),
|
|
62
|
+
getServiceMetadata: vi.fn().mockResolvedValue({
|
|
63
|
+
endpoint: 'http://localhost:3000',
|
|
64
|
+
model: 'test-model',
|
|
65
|
+
}),
|
|
66
|
+
getRequestHeaders: vi.fn().mockResolvedValue({ Authorization: 'Bearer test' }),
|
|
67
|
+
processResponse: vi.fn().mockResolvedValue(true),
|
|
68
|
+
acknowledgeProviderSigner: vi.fn().mockResolvedValue(undefined),
|
|
69
|
+
},
|
|
70
|
+
ledger: {
|
|
71
|
+
getLedger: vi.fn().mockResolvedValue({
|
|
72
|
+
totalBalance: 10000000000000000000n,
|
|
73
|
+
availableBalance: 5000000000000000000n,
|
|
74
|
+
}),
|
|
75
|
+
depositFund: vi.fn(),
|
|
76
|
+
transferFund: vi.fn(),
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
}));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Mock ethers
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
vi.mock('ethers', async () => {
|
|
86
|
+
const actual = await vi.importActual('ethers');
|
|
87
|
+
return {
|
|
88
|
+
...actual,
|
|
89
|
+
JsonRpcProvider: vi.fn().mockImplementation(() => ({
|
|
90
|
+
getNetwork: vi.fn().mockResolvedValue({ chainId: 16602n }),
|
|
91
|
+
})),
|
|
92
|
+
Wallet: vi.fn().mockImplementation(() => ({
|
|
93
|
+
address: '0xTestAddress',
|
|
94
|
+
provider: {},
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Contract Testing
|
|
101
|
+
|
|
102
|
+
### Hardhat Tests
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { expect } from 'chai';
|
|
106
|
+
import { ethers } from 'hardhat';
|
|
107
|
+
|
|
108
|
+
describe('MyContract', function () {
|
|
109
|
+
it('should deploy and set initial value', async function () {
|
|
110
|
+
const Contract = await ethers.getContractFactory('MyContract');
|
|
111
|
+
const contract = await Contract.deploy(42);
|
|
112
|
+
await contract.waitForDeployment();
|
|
113
|
+
|
|
114
|
+
expect(await contract.getValue()).to.equal(42);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should update value', async function () {
|
|
118
|
+
const Contract = await ethers.getContractFactory('MyContract');
|
|
119
|
+
const contract = await Contract.deploy(0);
|
|
120
|
+
await contract.waitForDeployment();
|
|
121
|
+
|
|
122
|
+
const tx = await contract.setValue(100);
|
|
123
|
+
await tx.wait();
|
|
124
|
+
|
|
125
|
+
expect(await contract.getValue()).to.equal(100);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx hardhat test
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Foundry Tests
|
|
135
|
+
|
|
136
|
+
```solidity
|
|
137
|
+
// test/MyContract.t.sol
|
|
138
|
+
// SPDX-License-Identifier: MIT
|
|
139
|
+
pragma solidity ^0.8.24;
|
|
140
|
+
|
|
141
|
+
import "forge-std/Test.sol";
|
|
142
|
+
import "../src/MyContract.sol";
|
|
143
|
+
|
|
144
|
+
contract MyContractTest is Test {
|
|
145
|
+
MyContract public myContract;
|
|
146
|
+
|
|
147
|
+
function setUp() public {
|
|
148
|
+
myContract = new MyContract(42);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function testGetValue() public view {
|
|
152
|
+
assertEq(myContract.getValue(), 42);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function testSetValue() public {
|
|
156
|
+
myContract.setValue(100);
|
|
157
|
+
assertEq(myContract.getValue(), 100);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
forge test
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Testnet Integration Testing
|
|
167
|
+
|
|
168
|
+
### Test Workflow
|
|
169
|
+
|
|
170
|
+
1. Use testnet endpoints (never mainnet for tests)
|
|
171
|
+
2. Fund test wallet from faucet
|
|
172
|
+
3. Run operations against real infrastructure
|
|
173
|
+
4. Verify results on block explorer
|
|
174
|
+
|
|
175
|
+
### Integration Test Example
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { describe, it, expect } from 'vitest';
|
|
179
|
+
import { ethers } from 'ethers';
|
|
180
|
+
import { ZgFile, Indexer } from '@0glabs/0g-ts-sdk';
|
|
181
|
+
|
|
182
|
+
// Only run in integration test mode
|
|
183
|
+
describe.skipIf(!process.env.RUN_INTEGRATION)('Storage Integration', () => {
|
|
184
|
+
const provider = new ethers.JsonRpcProvider('https://evmrpc-testnet.0g.ai');
|
|
185
|
+
const wallet = new ethers.Wallet(process.env.TEST_PRIVATE_KEY!, provider);
|
|
186
|
+
const indexer = new Indexer('https://indexer-storage-testnet-turbo.0g.ai');
|
|
187
|
+
|
|
188
|
+
it('should upload and download a file', async () => {
|
|
189
|
+
// Upload
|
|
190
|
+
const file = await ZgFile.fromFilePath('./test-fixtures/sample.txt');
|
|
191
|
+
try {
|
|
192
|
+
const [tree, err] = await file.merkleTree();
|
|
193
|
+
if (err) throw err;
|
|
194
|
+
const rootHash = tree!.rootHash();
|
|
195
|
+
const [, uploadErr] = await indexer.upload(file, process.env.RPC_URL!, wallet);
|
|
196
|
+
if (uploadErr) throw uploadErr;
|
|
197
|
+
|
|
198
|
+
// Download (can throw — use try/catch)
|
|
199
|
+
const outputPath = './test-output/downloaded.txt';
|
|
200
|
+
const dlErr = await indexer.download(rootHash, outputPath, true);
|
|
201
|
+
if (dlErr) throw dlErr;
|
|
202
|
+
|
|
203
|
+
// Verify
|
|
204
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
205
|
+
} finally {
|
|
206
|
+
await file.close();
|
|
207
|
+
}
|
|
208
|
+
}, 120_000); // Long timeout for network operations
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Running Integration Tests
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# Run unit tests only
|
|
216
|
+
npm test
|
|
217
|
+
|
|
218
|
+
# Run with integration tests
|
|
219
|
+
RUN_INTEGRATION=1 TEST_PRIVATE_KEY=0x... npm test
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Project Setup (vitest)
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// vitest.config.ts
|
|
226
|
+
import { defineConfig } from 'vitest/config';
|
|
227
|
+
|
|
228
|
+
export default defineConfig({
|
|
229
|
+
test: {
|
|
230
|
+
globals: true,
|
|
231
|
+
environment: 'node',
|
|
232
|
+
testTimeout: 30_000,
|
|
233
|
+
include: ['test/**/*.test.ts'],
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
// package.json scripts
|
|
240
|
+
{
|
|
241
|
+
"scripts": {
|
|
242
|
+
"test": "vitest run",
|
|
243
|
+
"test:watch": "vitest",
|
|
244
|
+
"test:integration": "RUN_INTEGRATION=1 vitest run"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Best Practices
|
|
250
|
+
|
|
251
|
+
1. **Mock external calls** in unit tests — never hit real networks
|
|
252
|
+
2. **Use testnet** for integration tests — never mainnet
|
|
253
|
+
3. **Set long timeouts** for network operations (60-120s)
|
|
254
|
+
4. **Fund test wallets** with small amounts only
|
|
255
|
+
5. **Clean up** test artifacts (uploaded files, deployed contracts)
|
|
256
|
+
6. **Use separate keys** for testing and production
|
|
257
|
+
|
|
258
|
+
## References
|
|
259
|
+
|
|
260
|
+
- [Vitest Documentation](https://vitest.dev)
|
|
261
|
+
- [Hardhat Testing](https://hardhat.org/tutorial/testing-contracts)
|
|
262
|
+
- [Foundry Testing](https://book.getfoundry.sh/forge/tests)
|
|
263
|
+
- See also: [NETWORK_CONFIG.md](./NETWORK_CONFIG.md)
|