Initial commit
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# Root-level environment configuration (if needed)
|
||||
# This file is for any root-level configuration
|
||||
# Most configuration should go in apps/smoke-tests/.env
|
||||
|
||||
# Optional: Chain 138 RPC endpoint
|
||||
# CHAIN_138_RPC_URL=https://138.rpc.thirdweb.com
|
||||
|
||||
# Optional: API keys (if using thirdweb services)
|
||||
# THIRDWEB_API_KEY=your_api_key_here
|
||||
21
.eslintrc.json
Normal file
21
.eslintrc.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2022,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2022": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }]
|
||||
}
|
||||
}
|
||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
*.tsbuildinfo
|
||||
coverage
|
||||
.vercel
|
||||
.turbo
|
||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
92
CHANGELOG.md
Normal file
92
CHANGELOG.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.0] - 2024-01-01
|
||||
|
||||
### Added
|
||||
|
||||
#### Chain Package (`@dbis-thirdweb/chain`)
|
||||
- Chain 138 canonical definition (ChainID 138, CAIP-2: eip155:138)
|
||||
- RPC endpoint configuration
|
||||
- Native currency metadata
|
||||
- Type-safe chain exports
|
||||
|
||||
#### Wallets Package (`@dbis-thirdweb/wallets`)
|
||||
- Wallet configuration defaults for Chain 138
|
||||
- Gas strategy configuration
|
||||
- RPC failover support
|
||||
- Chain switching utilities (`switchToChain138`, `ensureChain138`)
|
||||
- Automatic chain addition if missing
|
||||
|
||||
#### x402 Package (`@dbis-thirdweb/x402`)
|
||||
- Payment request creation and validation
|
||||
- Pay-to-access flow implementation
|
||||
- Replay protection via request ID tracking
|
||||
- Receipt verification on-chain
|
||||
- Expiration handling
|
||||
|
||||
#### Bridge Package (`@dbis-thirdweb/bridge`)
|
||||
- Canonical chain mapping for Chain 138
|
||||
- Supported routes configuration
|
||||
- Token lists (native, wrapped, stablecoins)
|
||||
- Quote generation with slippage protection
|
||||
- Bridge execution helpers
|
||||
- Status tracking and finality checks
|
||||
|
||||
#### Tokens Package (`@dbis-thirdweb/tokens`)
|
||||
- ERC20 token deployment, minting, transfers
|
||||
- ERC721 NFT collection deployment and management
|
||||
- ERC1155 edition deployment and batch operations
|
||||
- Metadata hosting strategy (IPFS/thirdweb storage)
|
||||
- Token factory utilities
|
||||
|
||||
#### AI Package (`@dbis-thirdweb/ai`)
|
||||
- Chain-aware prompt templates with Chain 138 guardrails
|
||||
- Read action templates (balance, block height, token info)
|
||||
- Write action templates (transfers, contract interactions)
|
||||
- Chain ID routing validation
|
||||
- Error handling for unsupported operations
|
||||
|
||||
#### HTTP API Package (`@dbis-thirdweb/http-api`)
|
||||
- Standardized HTTP API client wrapper
|
||||
- Retry logic with exponential backoff
|
||||
- Configurable timeouts
|
||||
- Chain 138 base URL configuration
|
||||
- Type-safe request/response interfaces
|
||||
|
||||
#### Smoke Tests (`apps/smoke-tests`)
|
||||
- End-to-end test suite for all 6 offerings
|
||||
- Wallet connection and transaction tests
|
||||
- x402 payment flow tests
|
||||
- Bridge quote and execution tests
|
||||
- Token deployment and transfer tests
|
||||
- AI read/write operation tests
|
||||
- HTTP API client tests
|
||||
- Unified test runner with Vitest
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- pnpm monorepo workspace setup
|
||||
- TypeScript 5.x configuration
|
||||
- ESLint and Prettier configuration
|
||||
- tsup build system for all packages
|
||||
- Comprehensive documentation
|
||||
|
||||
### Dependencies
|
||||
|
||||
- @thirdweb-dev/sdk: ^4.0.0
|
||||
- @thirdweb-dev/chains: ^0.1.0
|
||||
- ethers: ^5.7.0
|
||||
- vitest: ^1.0.0 (for testing)
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Additional bridge providers integration
|
||||
- [ ] Enhanced metadata hosting options
|
||||
- [ ] More comprehensive test coverage
|
||||
- [ ] Performance optimizations
|
||||
- [ ] Additional token standards support
|
||||
- [ ] Extended AI prompt templates
|
||||
92
PROJECT_STATUS.md
Normal file
92
PROJECT_STATUS.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Project Status
|
||||
|
||||
## ✅ Implementation Complete
|
||||
|
||||
All planned features have been implemented and built successfully.
|
||||
|
||||
### Package Status
|
||||
|
||||
| Package | Status | Build | Tests | Documentation |
|
||||
|---------|--------|-------|-------|---------------|
|
||||
| `@dbis-thirdweb/chain` | ✅ Complete | ✅ Built | 📝 README | ✅ |
|
||||
| `@dbis-thirdweb/wallets` | ✅ Complete | ✅ Built | 📝 README | ✅ |
|
||||
| `@dbis-thirdweb/x402` | ✅ Complete | ✅ Built | 📝 README | ✅ |
|
||||
| `@dbis-thirdweb/bridge` | ✅ Complete | ✅ Built | 📝 README | ✅ |
|
||||
| `@dbis-thirdweb/tokens` | ✅ Complete | ✅ Built | 📝 README | ✅ |
|
||||
| `@dbis-thirdweb/ai` | ✅ Complete | ✅ Built | 📝 README | ✅ |
|
||||
| `@dbis-thirdweb/http-api` | ✅ Complete | ✅ Built | 📝 README | ✅ |
|
||||
|
||||
### Test Suite Status
|
||||
|
||||
| Test Suite | Status | Location |
|
||||
|------------|--------|----------|
|
||||
| Wallets Tests | ✅ Created | `apps/smoke-tests/src/wallets/` |
|
||||
| x402 Tests | ✅ Created | `apps/smoke-tests/src/x402/` |
|
||||
| Bridge Tests | ✅ Created | `apps/smoke-tests/src/bridge/` |
|
||||
| Tokens Tests | ✅ Created | `apps/smoke-tests/src/tokens/` |
|
||||
| AI Tests | ✅ Created | `apps/smoke-tests/src/ai/` |
|
||||
| HTTP API Tests | ✅ Created | `apps/smoke-tests/src/http-api/` |
|
||||
|
||||
### Build Configuration
|
||||
|
||||
- ✅ pnpm workspace configured
|
||||
- ✅ TypeScript 5.x setup
|
||||
- ✅ tsup build system
|
||||
- ✅ ESLint configuration
|
||||
- ✅ Prettier configuration
|
||||
- ✅ All packages build successfully
|
||||
|
||||
### Documentation
|
||||
|
||||
- ✅ Root README with overview
|
||||
- ✅ Individual package READMEs
|
||||
- ✅ Smoke tests README
|
||||
- ✅ Setup guide (SETUP.md)
|
||||
- ✅ Changelog (CHANGELOG.md)
|
||||
|
||||
### Next Steps for Users
|
||||
|
||||
1. **Setup Test Environment**
|
||||
```bash
|
||||
cd apps/smoke-tests
|
||||
cp .env.example .env
|
||||
# Edit .env with TEST_PRIVATE_KEY
|
||||
```
|
||||
|
||||
2. **Run Smoke Tests**
|
||||
```bash
|
||||
pnpm smoke-tests
|
||||
```
|
||||
|
||||
3. **Integrate Packages**
|
||||
```typescript
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import { getWalletConfig } from '@dbis-thirdweb/wallets';
|
||||
// ... use packages
|
||||
```
|
||||
|
||||
4. **Deploy (when ready)**
|
||||
- Publish packages to npm registry
|
||||
- Or use workspace packages directly
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- Bridge implementation uses simplified quote logic (should integrate with actual bridge provider APIs)
|
||||
- x402 payment flow is a reference implementation (may need adjustments for production)
|
||||
- Token metadata hosting uses placeholder IPFS URIs (configure actual storage)
|
||||
- HTTP API endpoints use placeholder URLs (configure actual thirdweb API endpoints)
|
||||
- Some thirdweb SDK APIs may require additional configuration for Chain 138
|
||||
|
||||
### Dependencies
|
||||
|
||||
- @thirdweb-dev/sdk: ^4.0.0
|
||||
- @thirdweb-dev/chains: ^0.1.0
|
||||
- ethers: ^5.7.0
|
||||
- TypeScript: ^5.0.0
|
||||
- pnpm: >= 8.0.0
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Ready for testing and integration
|
||||
|
||||
**Last Updated**: 2024-01-01
|
||||
57
QUICK_START.md
Normal file
57
QUICK_START.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Quick Start Guide
|
||||
|
||||
Get started with Chain 138 enablement in 5 minutes.
|
||||
|
||||
## 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## 2. Build Packages
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 3. (Optional) Run Tests
|
||||
|
||||
```bash
|
||||
cd apps/smoke-tests
|
||||
cp .env.example .env
|
||||
# Edit .env with your TEST_PRIVATE_KEY
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## 4. Use in Your Code
|
||||
|
||||
```typescript
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, privateKey);
|
||||
// Use SDK as normal - Chain 138 is fully configured!
|
||||
```
|
||||
|
||||
## Package Quick Links
|
||||
|
||||
- **Chain Definition**: `@dbis-thirdweb/chain`
|
||||
- **Wallets**: `@dbis-thirdweb/wallets`
|
||||
- **Payments (x402)**: `@dbis-thirdweb/x402`
|
||||
- **Bridge**: `@dbis-thirdweb/bridge`
|
||||
- **Tokens**: `@dbis-thirdweb/tokens`
|
||||
- **AI**: `@dbis-thirdweb/ai`
|
||||
- **HTTP API**: `@dbis-thirdweb/http-api`
|
||||
|
||||
See individual package READMEs for detailed usage examples.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [README.md](README.md) - Project overview
|
||||
- [SETUP.md](SETUP.md) - Detailed setup guide
|
||||
- [PROJECT_STATUS.md](PROJECT_STATUS.md) - Current project status
|
||||
- [CHANGELOG.md](CHANGELOG.md) - Version history
|
||||
|
||||
## Need Help?
|
||||
|
||||
Check package-specific READMEs in `packages/*/README.md` or the main [SETUP.md](SETUP.md) guide.
|
||||
153
README.md
Normal file
153
README.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# dbis-thirdweb
|
||||
|
||||
Chain 138 full enablement for thirdweb offerings: **Wallets**, **x402**, **Bridge**, **Tokens**, **AI**, and **HTTP API**.
|
||||
|
||||
## Overview
|
||||
|
||||
This monorepo provides complete enablement of Chain 138 (ChainID 138) across six core thirdweb offerings. Each offering is implemented as an independent package with its own API, tests, and documentation.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
packages/
|
||||
chain/ - Chain 138 canonical definition (CAIP-2: eip155:138)
|
||||
wallets/ - Wallet connectors & configuration
|
||||
x402/ - Payment primitives & pay-to-access flows
|
||||
bridge/ - Bridge routes & execution helpers
|
||||
tokens/ - ERC20/721/1155 deployment & management
|
||||
ai/ - Chain-aware AI prompts & actions
|
||||
http-api/ - HTTP API client wrapper with retries
|
||||
|
||||
apps/
|
||||
smoke-tests/ - End-to-end tests for all 6 offerings
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js >= 18.0.0
|
||||
- pnpm >= 8.0.0
|
||||
- Access to Chain 138 RPC endpoint (default: https://138.rpc.thirdweb.com)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. Build all packages:
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
3. Run smoke tests (optional - requires test private key):
|
||||
```bash
|
||||
cd apps/smoke-tests
|
||||
cp .env.example .env
|
||||
# Edit .env with your TEST_PRIVATE_KEY
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Each package can be imported independently:
|
||||
|
||||
```typescript
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import { getWalletConfig, switchToChain138 } from '@dbis-thirdweb/wallets';
|
||||
import { PayToAccessFlow } from '@dbis-thirdweb/x402';
|
||||
import { generateBridgeQuote } from '@dbis-thirdweb/bridge';
|
||||
import { createTokenFactory } from '@dbis-thirdweb/tokens';
|
||||
import { createReadPrompt } from '@dbis-thirdweb/ai';
|
||||
import { createChain138API } from '@dbis-thirdweb/http-api';
|
||||
```
|
||||
|
||||
See individual package READMEs for detailed usage:
|
||||
- [`packages/chain/README.md`](packages/chain/README.md)
|
||||
- [`packages/wallets/README.md`](packages/wallets/README.md)
|
||||
- [`packages/x402/README.md`](packages/x402/README.md)
|
||||
- [`packages/bridge/README.md`](packages/bridge/README.md)
|
||||
- [`packages/tokens/README.md`](packages/tokens/README.md)
|
||||
- [`packages/ai/README.md`](packages/ai/README.md)
|
||||
- [`packages/http-api/README.md`](packages/http-api/README.md)
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
Build all packages:
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Build specific package:
|
||||
```bash
|
||||
pnpm --filter @dbis-thirdweb/chain build
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Run all smoke tests:
|
||||
```bash
|
||||
pnpm smoke-tests
|
||||
```
|
||||
|
||||
Run specific test suite:
|
||||
```bash
|
||||
cd apps/smoke-tests
|
||||
pnpm test:wallets
|
||||
pnpm test:x402
|
||||
pnpm test:bridge
|
||||
pnpm test:tokens
|
||||
pnpm test:ai
|
||||
pnpm test:http-api
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
Lint all packages:
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## Package Details
|
||||
|
||||
### @dbis-thirdweb/chain
|
||||
Canonical Chain 138 definition with chain ID, RPC endpoints, native currency, and explorer configuration.
|
||||
|
||||
### @dbis-thirdweb/wallets
|
||||
Wallet configuration defaults, chain switching utilities, and RPC failover for Chain 138.
|
||||
|
||||
### @dbis-thirdweb/x402
|
||||
Payment primitives, pay-to-access flows, replay protection, and receipt verification for Chain 138.
|
||||
|
||||
### @dbis-thirdweb/bridge
|
||||
Bridge route configuration, token lists, quote generation, and execution helpers for bridging to Chain 138.
|
||||
|
||||
### @dbis-thirdweb/tokens
|
||||
ERC20/721/1155 token deployment, minting, transfers, and metadata hosting for Chain 138.
|
||||
|
||||
### @dbis-thirdweb/ai
|
||||
Chain-aware AI prompt templates, read/write action templates, and chain ID validation for Chain 138.
|
||||
|
||||
### @dbis-thirdweb/http-api
|
||||
HTTP API client wrapper with retry logic, exponential backoff, timeouts, and Chain 138-specific endpoints.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build errors**: Ensure all dependencies are installed with `pnpm install`
|
||||
2. **Test failures**: Check that `TEST_PRIVATE_KEY` is set in `apps/smoke-tests/.env`
|
||||
3. **RPC connection errors**: Verify Chain 138 RPC endpoint is accessible
|
||||
4. **Chain mismatch errors**: Ensure you're using Chain 138 (chainId: 138) in your configuration
|
||||
|
||||
### Getting Help
|
||||
|
||||
- Check individual package READMEs for package-specific issues
|
||||
- Review smoke test output for detailed error messages
|
||||
- Verify Chain 138 is properly configured in thirdweb dashboard
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
156
SETUP.md
Normal file
156
SETUP.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Setup Guide
|
||||
|
||||
Complete setup instructions for Chain 138 full enablement project.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js >= 18.0.0
|
||||
- pnpm >= 8.0.0 (or use corepack: `corepack enable`)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository (if applicable)
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Build all packages:
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Build a specific package:
|
||||
```bash
|
||||
pnpm --filter @dbis-thirdweb/chain build
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Setup Test Environment
|
||||
|
||||
1. Create test environment file:
|
||||
```bash
|
||||
cd apps/smoke-tests
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Edit `.env` and add your test private key:
|
||||
```bash
|
||||
TEST_PRIVATE_KEY=your_private_key_here
|
||||
TEST_RPC_URL=https://138.rpc.thirdweb.com # Optional
|
||||
TEST_RECIPIENT=0x0000000000000000000000000000000000000001 # Optional
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
Run all smoke tests:
|
||||
```bash
|
||||
pnpm smoke-tests
|
||||
```
|
||||
|
||||
Or from the smoke-tests directory:
|
||||
```bash
|
||||
cd apps/smoke-tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Run specific test suite:
|
||||
```bash
|
||||
cd apps/smoke-tests
|
||||
pnpm test:wallets
|
||||
pnpm test:x402
|
||||
pnpm test:bridge
|
||||
pnpm test:tokens
|
||||
pnpm test:ai
|
||||
pnpm test:http-api
|
||||
```
|
||||
|
||||
## Using the Packages
|
||||
|
||||
### In Your Project
|
||||
|
||||
Install packages locally (if published):
|
||||
```bash
|
||||
pnpm add @dbis-thirdweb/chain @dbis-thirdweb/wallets
|
||||
```
|
||||
|
||||
Or use workspace packages directly (in this monorepo):
|
||||
```typescript
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import { getWalletConfig } from '@dbis-thirdweb/wallets';
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
|
||||
```typescript
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import { getWalletConfig } from '@dbis-thirdweb/wallets';
|
||||
|
||||
// Initialize SDK with Chain 138
|
||||
const sdk = new ThirdwebSDK(chain138, privateKey);
|
||||
|
||||
// Get wallet configuration
|
||||
const config = getWalletConfig({
|
||||
confirmationBlocks: 2,
|
||||
});
|
||||
```
|
||||
|
||||
See individual package READMEs for detailed usage examples.
|
||||
|
||||
## Development
|
||||
|
||||
### Linting
|
||||
|
||||
Lint all packages:
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
### Package Structure
|
||||
|
||||
```
|
||||
packages/
|
||||
chain/ - Chain 138 definition (CAIP-2: eip155:138)
|
||||
wallets/ - Wallet config & chain switching
|
||||
x402/ - Payment primitives & pay-to-access
|
||||
bridge/ - Bridge routes & execution
|
||||
tokens/ - ERC20/721/1155 token management
|
||||
ai/ - Chain-aware AI prompts & actions
|
||||
http-api/ - HTTP API client wrapper
|
||||
|
||||
apps/
|
||||
smoke-tests/ - End-to-end tests for all offerings
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Errors
|
||||
|
||||
- Ensure all dependencies are installed: `pnpm install`
|
||||
- Clear build cache and rebuild: `rm -rf packages/*/dist && pnpm build`
|
||||
|
||||
### Test Failures
|
||||
|
||||
- Verify `TEST_PRIVATE_KEY` is set in `apps/smoke-tests/.env`
|
||||
- Check RPC endpoint is accessible: `curl https://138.rpc.thirdweb.com`
|
||||
- Ensure test account has sufficient balance for transactions
|
||||
|
||||
### Type Errors
|
||||
|
||||
- Rebuild all packages: `pnpm build`
|
||||
- Check TypeScript version compatibility
|
||||
- Verify workspace dependencies are linked: `pnpm list --depth=0`
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Configure your test environment (`.env` file)
|
||||
2. Run smoke tests to verify all offerings work
|
||||
3. Integrate packages into your application
|
||||
4. Deploy to production when ready
|
||||
|
||||
For detailed package documentation, see individual README files in each package directory.
|
||||
14
apps/smoke-tests/.env.example
Normal file
14
apps/smoke-tests/.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
# Chain 138 Smoke Tests Configuration
|
||||
# Copy this file to .env and fill in your actual values
|
||||
|
||||
# Private key for test wallet (required for write operations)
|
||||
# IMPORTANT: Never commit .env file with real private keys!
|
||||
TEST_PRIVATE_KEY=your_private_key_here
|
||||
|
||||
# Optional: RPC endpoint override
|
||||
# Default: https://138.rpc.thirdweb.com
|
||||
TEST_RPC_URL=https://138.rpc.thirdweb.com
|
||||
|
||||
# Optional: Test recipient address for transfers
|
||||
# Default: 0x0000000000000000000000000000000000000001
|
||||
TEST_RECIPIENT=0x0000000000000000000000000000000000000001
|
||||
49
apps/smoke-tests/README.md
Normal file
49
apps/smoke-tests/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Smoke Tests
|
||||
|
||||
End-to-end smoke tests for all Chain 138 offerings.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create `.env` file in this directory:
|
||||
```bash
|
||||
TEST_PRIVATE_KEY=your_private_key_here
|
||||
TEST_RPC_URL=https://138.rpc.thirdweb.com # Optional
|
||||
TEST_RECIPIENT=0x... # Optional, default test recipient
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
Run all tests:
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Run specific test suite:
|
||||
```bash
|
||||
pnpm test:wallets
|
||||
pnpm test:x402
|
||||
pnpm test:bridge
|
||||
pnpm test:tokens
|
||||
pnpm test:ai
|
||||
pnpm test:http-api
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- **Wallets**: Connect, sign message, native transfer, chain switching
|
||||
- **x402**: Payment request creation, fulfillment, receipt verification, replay protection
|
||||
- **Bridge**: Route validation, quote generation, token lists
|
||||
- **Tokens**: ERC20 deploy, mint, transfer, balance queries
|
||||
- **AI**: Read actions (balance, block height), write actions (transfer), prompt validation
|
||||
- **HTTP API**: Client creation, configuration validation
|
||||
|
||||
## Notes
|
||||
|
||||
- Tests requiring write operations need `TEST_PRIVATE_KEY` set
|
||||
- Some tests may skip if private key is not provided
|
||||
- Tests use Chain 138 testnet/mainnet as configured
|
||||
41
apps/smoke-tests/package.json
Normal file
41
apps/smoke-tests/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "smoke-tests",
|
||||
"version": "0.1.0",
|
||||
"description": "Smoke tests for Chain 138 offerings",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:wallets": "vitest run src/wallets",
|
||||
"test:x402": "vitest run src/x402",
|
||||
"test:bridge": "vitest run src/bridge",
|
||||
"test:tokens": "vitest run src/tokens",
|
||||
"test:ai": "vitest run src/ai",
|
||||
"test:http-api": "vitest run src/http-api",
|
||||
"test:all": "vitest run"
|
||||
},
|
||||
"keywords": [
|
||||
"testing",
|
||||
"smoke-tests",
|
||||
"chain-138"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dbis-thirdweb/chain": "workspace:*",
|
||||
"@dbis-thirdweb/wallets": "workspace:*",
|
||||
"@dbis-thirdweb/x402": "workspace:*",
|
||||
"@dbis-thirdweb/bridge": "workspace:*",
|
||||
"@dbis-thirdweb/tokens": "workspace:*",
|
||||
"@dbis-thirdweb/ai": "workspace:*",
|
||||
"@dbis-thirdweb/http-api": "workspace:*",
|
||||
"@thirdweb-dev/sdk": "^4.0.0",
|
||||
"ethers": "^5.7.0",
|
||||
"dotenv": "^16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^1.0.0"
|
||||
}
|
||||
}
|
||||
90
apps/smoke-tests/src/ai/index.test.ts
Normal file
90
apps/smoke-tests/src/ai/index.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { ethers } from 'ethers';
|
||||
import {
|
||||
createReadBalanceAction,
|
||||
createReadBlockHeightAction,
|
||||
createTransferAction,
|
||||
executeReadAction,
|
||||
createReadPrompt,
|
||||
validatePromptForChain138,
|
||||
validateChainId,
|
||||
} from '@dbis-thirdweb/ai';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import { testConfig } from '../config';
|
||||
|
||||
describe('AI Smoke Tests', () => {
|
||||
it('should create read balance action', () => {
|
||||
const action = createReadBalanceAction('0x1234567890123456789012345678901234567890');
|
||||
expect(action.type).toBe('read');
|
||||
expect(action.chainId).toBe(138);
|
||||
expect(action.operation).toBe('getBalance');
|
||||
});
|
||||
|
||||
it('should create read block height action', () => {
|
||||
const action = createReadBlockHeightAction();
|
||||
expect(action.type).toBe('read');
|
||||
expect(action.chainId).toBe(138);
|
||||
expect(action.operation).toBe('getBlockNumber');
|
||||
});
|
||||
|
||||
it('should create transfer action', () => {
|
||||
const action = createTransferAction(
|
||||
'0x1234567890123456789012345678901234567890',
|
||||
'1000000000000000000'
|
||||
);
|
||||
expect(action.type).toBe('write');
|
||||
expect(action.chainId).toBe(138);
|
||||
expect(action.operation).toBe('transferNative');
|
||||
});
|
||||
|
||||
it('should execute read balance action', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping read balance test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const provider = sdk.getProvider();
|
||||
const signer = await sdk.getSigner();
|
||||
const address = await signer.getAddress();
|
||||
|
||||
const action = createReadBalanceAction(address);
|
||||
const result = await executeReadAction(action, sdk, provider);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof (result as { balance: string }).balance).toBe('string');
|
||||
});
|
||||
|
||||
it('should execute read block height action', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping block height test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const provider = sdk.getProvider();
|
||||
|
||||
const action = createReadBlockHeightAction();
|
||||
const result = await executeReadAction(action, sdk, provider);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect((result as { blockNumber: number }).blockNumber).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should create chain-aware prompts', () => {
|
||||
const prompt = createReadPrompt('getBalance', { address: '0x...' });
|
||||
expect(prompt).toContain('Chain 138');
|
||||
expect(prompt).toContain('138');
|
||||
});
|
||||
|
||||
it('should validate prompts for Chain 138', () => {
|
||||
const validPrompt = 'Perform operation on Chain 138';
|
||||
expect(() => validatePromptForChain138(validPrompt)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate chain ID', () => {
|
||||
expect(() => validateChainId(138)).not.toThrow();
|
||||
expect(() => validateChainId(1)).toThrow();
|
||||
});
|
||||
});
|
||||
69
apps/smoke-tests/src/bridge/index.test.ts
Normal file
69
apps/smoke-tests/src/bridge/index.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ethers } from 'ethers';
|
||||
import {
|
||||
getSupportedRoutes,
|
||||
isRouteSupported,
|
||||
generateBridgeQuote,
|
||||
getAllBridgeableTokens,
|
||||
} from '@dbis-thirdweb/bridge';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
describe('Bridge Smoke Tests', () => {
|
||||
it('should get supported routes to Chain 138', () => {
|
||||
const routes = getSupportedRoutes();
|
||||
expect(routes.length).toBeGreaterThan(0);
|
||||
expect(routes.every((r) => r.toChainId === chain138.chainId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should check if route is supported', () => {
|
||||
expect(isRouteSupported(1, 138)).toBe(true); // Ethereum to Chain 138
|
||||
expect(isRouteSupported(138, 1)).toBe(false); // Chain 138 to Ethereum (not supported)
|
||||
});
|
||||
|
||||
it('should get bridgeable tokens', () => {
|
||||
const tokens = getAllBridgeableTokens();
|
||||
expect(tokens.length).toBeGreaterThan(0);
|
||||
const nativeToken = tokens.find((t) => t.isNative);
|
||||
expect(nativeToken).toBeTruthy();
|
||||
expect(nativeToken?.symbol).toBe('ETH');
|
||||
});
|
||||
|
||||
it('should generate bridge quote', async () => {
|
||||
const tokens = getAllBridgeableTokens();
|
||||
const nativeToken = tokens.find((t) => t.isNative);
|
||||
expect(nativeToken).toBeTruthy();
|
||||
|
||||
const quote = await generateBridgeQuote({
|
||||
fromChainId: 1,
|
||||
toChainId: 138,
|
||||
token: nativeToken!,
|
||||
amount: ethers.utils.parseEther('0.1'),
|
||||
slippageBps: 50,
|
||||
});
|
||||
|
||||
expect(quote.fromChainId).toBe(1);
|
||||
expect(quote.toChainId).toBe(138);
|
||||
expect(quote.token).toBe(nativeToken);
|
||||
expect(quote.amount).toBe(ethers.utils.parseEther('0.1'));
|
||||
expect(quote.estimatedOutput).toBeGreaterThan(0n);
|
||||
expect(quote.fee).toBeGreaterThanOrEqual(0n);
|
||||
expect(quote.minimumOutput).toBeLessThanOrEqual(quote.estimatedOutput);
|
||||
});
|
||||
|
||||
it('should validate quote', async () => {
|
||||
const tokens = getAllBridgeableTokens();
|
||||
const nativeToken = tokens.find((t) => t.isNative);
|
||||
expect(nativeToken).toBeTruthy();
|
||||
|
||||
const quote = await generateBridgeQuote({
|
||||
fromChainId: 1,
|
||||
toChainId: 138,
|
||||
token: nativeToken!,
|
||||
amount: ethers.utils.parseEther('0.1'),
|
||||
});
|
||||
|
||||
// Quote should be valid
|
||||
expect(quote.amount).toBeGreaterThan(0n);
|
||||
expect(quote.estimatedOutput).toBeGreaterThan(0n);
|
||||
});
|
||||
});
|
||||
26
apps/smoke-tests/src/config.ts
Normal file
26
apps/smoke-tests/src/config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
/**
|
||||
* Test configuration
|
||||
*/
|
||||
export const testConfig = {
|
||||
/**
|
||||
* Private key for test wallet (required for write operations)
|
||||
* Set via TEST_PRIVATE_KEY environment variable
|
||||
*/
|
||||
privateKey: process.env.TEST_PRIVATE_KEY || '',
|
||||
|
||||
/**
|
||||
* RPC endpoint override (optional)
|
||||
*/
|
||||
rpcUrl: process.env.TEST_RPC_URL,
|
||||
|
||||
/**
|
||||
* Test recipient address (optional)
|
||||
*/
|
||||
testRecipient: process.env.TEST_RECIPIENT || '0x0000000000000000000000000000000000000001',
|
||||
};
|
||||
|
||||
if (!testConfig.privateKey) {
|
||||
console.warn('WARNING: TEST_PRIVATE_KEY not set. Write operations will fail.');
|
||||
}
|
||||
48
apps/smoke-tests/src/http-api/index.test.ts
Normal file
48
apps/smoke-tests/src/http-api/index.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createAPIClient, createChain138API } from '@dbis-thirdweb/http-api';
|
||||
|
||||
describe('HTTP API Smoke Tests', () => {
|
||||
it('should create API client', () => {
|
||||
const client = createAPIClient({
|
||||
timeout: 30000,
|
||||
retries: 3,
|
||||
});
|
||||
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should create Chain 138 API client', () => {
|
||||
const api = createChain138API({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
expect(api).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have chain 138 configuration', () => {
|
||||
const api = createChain138API();
|
||||
// API client should be configured for Chain 138
|
||||
expect(api).toBeTruthy();
|
||||
});
|
||||
|
||||
// Note: Actual API endpoint tests would require:
|
||||
// 1. Valid API key
|
||||
// 2. Actual thirdweb API endpoints for Chain 138
|
||||
// 3. Network access
|
||||
// These are placeholders for when API is fully configured
|
||||
|
||||
it('should handle timeout configuration', () => {
|
||||
const client = createAPIClient({
|
||||
timeout: 5000,
|
||||
});
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle retry configuration', () => {
|
||||
const client = createAPIClient({
|
||||
retries: 5,
|
||||
retryDelay: 2000,
|
||||
});
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
});
|
||||
11
apps/smoke-tests/src/index.ts
Normal file
11
apps/smoke-tests/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Unified smoke test runner for all Chain 138 offerings
|
||||
* Run with: pnpm test
|
||||
*/
|
||||
|
||||
export * from './wallets/index.test';
|
||||
export * from './x402/index.test';
|
||||
export * from './bridge/index.test';
|
||||
export * from './tokens/index.test';
|
||||
export * from './ai/index.test';
|
||||
export * from './http-api/index.test';
|
||||
106
apps/smoke-tests/src/tokens/index.test.ts
Normal file
106
apps/smoke-tests/src/tokens/index.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { ethers } from 'ethers';
|
||||
import {
|
||||
createTokenFactory,
|
||||
deployERC20,
|
||||
mintERC20,
|
||||
transferERC20,
|
||||
getERC20Balance,
|
||||
} from '@dbis-thirdweb/tokens';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import { testConfig } from '../config';
|
||||
|
||||
describe('Tokens Smoke Tests', () => {
|
||||
it('should create token factory', () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping factory test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const factory = createTokenFactory(sdk);
|
||||
expect(factory).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should deploy ERC20 token', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping ERC20 deploy test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const contractAddress = await deployERC20(sdk, {
|
||||
name: 'Test Token',
|
||||
symbol: 'TEST',
|
||||
description: 'Test token for smoke tests',
|
||||
initialSupply: ethers.utils.parseEther('1000000'),
|
||||
});
|
||||
|
||||
expect(contractAddress).toBeTruthy();
|
||||
expect(ethers.isAddress(contractAddress)).toBe(true);
|
||||
|
||||
// Verify contract exists
|
||||
const contract = await sdk.getContract(contractAddress, 'token');
|
||||
const name = await contract.call('name');
|
||||
expect(name).toBe('Test Token');
|
||||
});
|
||||
|
||||
it('should mint ERC20 tokens', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping mint test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const signer = await sdk.getSigner();
|
||||
const recipient = await signer.getAddress();
|
||||
|
||||
// Deploy token first
|
||||
const contractAddress = await deployERC20(sdk, {
|
||||
name: 'Mint Test Token',
|
||||
symbol: 'MTT',
|
||||
});
|
||||
|
||||
// Mint tokens
|
||||
const amount = ethers.utils.parseEther('1000');
|
||||
await mintERC20(sdk, contractAddress, amount, recipient);
|
||||
|
||||
// Check balance
|
||||
const balance = await getERC20Balance(sdk, contractAddress, recipient);
|
||||
expect(balance).toBeGreaterThanOrEqual(amount);
|
||||
});
|
||||
|
||||
it('should transfer ERC20 tokens', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping transfer test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const signer = await sdk.getSigner();
|
||||
const fromAddress = await signer.getAddress();
|
||||
|
||||
// Deploy and mint tokens
|
||||
const contractAddress = await deployERC20(sdk, {
|
||||
name: 'Transfer Test Token',
|
||||
symbol: 'TTT',
|
||||
initialSupply: ethers.utils.parseEther('10000'),
|
||||
});
|
||||
|
||||
// Transfer tokens
|
||||
const transferAmount = ethers.utils.parseEther('100');
|
||||
await transferERC20(sdk, contractAddress, testConfig.testRecipient, transferAmount);
|
||||
|
||||
// Check recipient balance
|
||||
const recipientBalance = await getERC20Balance(sdk, contractAddress, testConfig.testRecipient);
|
||||
expect(recipientBalance).toBeGreaterThanOrEqual(transferAmount);
|
||||
});
|
||||
|
||||
it('should handle token metadata', () => {
|
||||
// Test metadata utilities exist
|
||||
const { defaultMetadataConfig } = require('@dbis-thirdweb/tokens');
|
||||
expect(defaultMetadataConfig).toBeTruthy();
|
||||
expect(defaultMetadataConfig.ipfsGateway).toBeTruthy();
|
||||
});
|
||||
});
|
||||
86
apps/smoke-tests/src/wallets/index.test.ts
Normal file
86
apps/smoke-tests/src/wallets/index.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { chain138, getWalletConfig } from '@dbis-thirdweb/wallets';
|
||||
import { testConfig } from '../config';
|
||||
|
||||
describe('Wallets Smoke Tests', () => {
|
||||
it('should get wallet config for Chain 138', () => {
|
||||
const config = getWalletConfig();
|
||||
expect(config.confirmationBlocks).toBeGreaterThan(0);
|
||||
expect(config.rpcFailover.primary).toContain('138');
|
||||
});
|
||||
|
||||
it('should create SDK instance with Chain 138', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping SDK test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const chainId = await sdk.getChainId();
|
||||
expect(chainId).toBe(138);
|
||||
});
|
||||
|
||||
it('should connect and get chain ID', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping connection test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const address = await sdk.wallet.getAddress();
|
||||
expect(address).toBeTruthy();
|
||||
|
||||
const chainId = await sdk.getChainId();
|
||||
expect(chainId).toBe(138);
|
||||
});
|
||||
|
||||
it('should sign a message', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping sign message test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const message = 'Test message for Chain 138';
|
||||
const signature = await sdk.wallet.sign(message);
|
||||
expect(signature).toBeTruthy();
|
||||
expect(signature.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should send native token transfer', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping native transfer test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const provider = sdk.getProvider();
|
||||
const signer = await sdk.getSigner();
|
||||
|
||||
// Get initial balance
|
||||
const fromAddress = await signer.getAddress();
|
||||
const initialBalance = await provider.getBalance(fromAddress);
|
||||
|
||||
// Send small amount (0.001 ETH)
|
||||
const amount = BigInt('1000000000000000'); // 0.001 ETH
|
||||
const tx = await signer.sendTransaction({
|
||||
to: testConfig.testRecipient,
|
||||
value: amount,
|
||||
});
|
||||
|
||||
expect(tx.hash).toBeTruthy();
|
||||
|
||||
// Wait for confirmation
|
||||
const receipt = await provider.waitForTransaction(tx.hash, 1);
|
||||
expect(receipt).toBeTruthy();
|
||||
expect(receipt.status).toBe(1);
|
||||
});
|
||||
|
||||
it('should verify chain switching utilities', () => {
|
||||
// Verify chain138 export
|
||||
expect(chain138).toBeTruthy();
|
||||
expect(chain138.chainId).toBe(138);
|
||||
expect(chain138.rpc.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
116
apps/smoke-tests/src/x402/index.test.ts
Normal file
116
apps/smoke-tests/src/x402/index.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { ethers } from 'ethers';
|
||||
import {
|
||||
PayToAccessFlow,
|
||||
InMemoryReplayProtectionStore,
|
||||
type PaymentRequest,
|
||||
} from '@dbis-thirdweb/x402';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import { testConfig } from '../config';
|
||||
|
||||
describe('x402 Smoke Tests', () => {
|
||||
it('should create payment request', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping x402 test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const provider = sdk.getProvider();
|
||||
const replayStore = new InMemoryReplayProtectionStore();
|
||||
const flow = new PayToAccessFlow(provider, replayStore);
|
||||
|
||||
const request = await flow.createRequest({
|
||||
amount: ethers.utils.parseEther('0.001'),
|
||||
recipient: testConfig.testRecipient,
|
||||
expiresInSeconds: 3600,
|
||||
});
|
||||
|
||||
expect(request.requestId).toBeTruthy();
|
||||
expect(request.amount).toBe(ethers.utils.parseEther('0.001'));
|
||||
expect(request.recipient).toBe(testConfig.testRecipient);
|
||||
});
|
||||
|
||||
it('should generate challenge from request', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping challenge test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const provider = sdk.getProvider();
|
||||
const replayStore = new InMemoryReplayProtectionStore();
|
||||
const flow = new PayToAccessFlow(provider, replayStore);
|
||||
|
||||
const request = await flow.createRequest({
|
||||
amount: ethers.utils.parseEther('0.001'),
|
||||
recipient: testConfig.testRecipient,
|
||||
expiresInSeconds: 3600,
|
||||
});
|
||||
|
||||
const challenge = flow.generateChallenge(request);
|
||||
expect(challenge.request.requestId).toBe(request.requestId);
|
||||
expect(challenge.nonce).toBeTruthy();
|
||||
expect(challenge.message).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should fulfill payment and verify receipt', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping fulfillment test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const provider = sdk.getProvider();
|
||||
const signer = await sdk.getSigner();
|
||||
const replayStore = new InMemoryReplayProtectionStore();
|
||||
const flow = new PayToAccessFlow(provider, replayStore);
|
||||
|
||||
// Create request
|
||||
const request = await flow.createRequest({
|
||||
amount: ethers.utils.parseEther('0.001'),
|
||||
recipient: testConfig.testRecipient,
|
||||
expiresInSeconds: 3600,
|
||||
});
|
||||
|
||||
// Generate challenge
|
||||
const challenge = flow.generateChallenge(request);
|
||||
|
||||
// Fulfill payment
|
||||
const receipt = await flow.fulfillPayment(challenge, signer);
|
||||
expect(receipt.txHash).toBeTruthy();
|
||||
expect(receipt.requestId).toBe(request.requestId);
|
||||
expect(receipt.blockNumber).toBeGreaterThan(0);
|
||||
|
||||
// Verify payment
|
||||
const isValid = await flow.verifyPayment(receipt, request);
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('should prevent replay attacks', async () => {
|
||||
if (!testConfig.privateKey) {
|
||||
console.log('Skipping replay protection test - no private key');
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, testConfig.privateKey);
|
||||
const provider = sdk.getProvider();
|
||||
const replayStore = new InMemoryReplayProtectionStore();
|
||||
const flow = new PayToAccessFlow(provider, replayStore);
|
||||
|
||||
const request = await flow.createRequest({
|
||||
amount: ethers.utils.parseEther('0.001'),
|
||||
recipient: testConfig.testRecipient,
|
||||
expiresInSeconds: 3600,
|
||||
});
|
||||
|
||||
// First request should succeed
|
||||
await expect(flow.createRequest({
|
||||
amount: ethers.utils.parseEther('0.001'),
|
||||
recipient: testConfig.testRecipient,
|
||||
expiresInSeconds: 3600,
|
||||
metadata: request.requestId, // Use same request ID to test replay protection
|
||||
})).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
18
apps/smoke-tests/tsconfig.json
Normal file
18
apps/smoke-tests/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../../packages/chain" },
|
||||
{ "path": "../../packages/wallets" },
|
||||
{ "path": "../../packages/x402" },
|
||||
{ "path": "../../packages/bridge" },
|
||||
{ "path": "../../packages/tokens" },
|
||||
{ "path": "../../packages/ai" },
|
||||
{ "path": "../../packages/http-api" }
|
||||
]
|
||||
}
|
||||
10
apps/smoke-tests/vitest.config.ts
Normal file
10
apps/smoke-tests/vitest.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
testTimeout: 60000,
|
||||
hookTimeout: 60000,
|
||||
},
|
||||
});
|
||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "dbis-thirdweb",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Chain 138 full enablement for thirdweb offerings",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"test": "pnpm -r test",
|
||||
"lint": "pnpm -r lint",
|
||||
"smoke-tests": "pnpm --filter smoke-tests run test"
|
||||
},
|
||||
"keywords": [
|
||||
"thirdweb",
|
||||
"chain-138",
|
||||
"blockchain",
|
||||
"ethereum"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@9.0.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
66
packages/ai/README.md
Normal file
66
packages/ai/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# @dbis-thirdweb/ai
|
||||
|
||||
Chain-aware AI prompts and actions for Chain 138.
|
||||
|
||||
## Usage
|
||||
|
||||
### Prompt Generation
|
||||
|
||||
```typescript
|
||||
import { createReadPrompt, createWritePrompt, validatePromptForChain138 } from '@dbis-thirdweb/ai';
|
||||
|
||||
// Create read prompt
|
||||
const readPrompt = createReadPrompt('getBalance', { address: '0x...' });
|
||||
|
||||
// Create write prompt
|
||||
const writePrompt = createWritePrompt('transferNative', {
|
||||
to: '0x...',
|
||||
amount: '1000000000000000000', // 1 ETH in wei
|
||||
});
|
||||
|
||||
// Validate prompt targets Chain 138
|
||||
validatePromptForChain138(userPrompt);
|
||||
```
|
||||
|
||||
### Read Actions
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createReadBalanceAction,
|
||||
createReadBlockHeightAction,
|
||||
executeReadAction,
|
||||
} from '@dbis-thirdweb/ai';
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138);
|
||||
const provider = sdk.getProvider();
|
||||
|
||||
// Create action
|
||||
const action = createReadBalanceAction('0x...');
|
||||
|
||||
// Execute
|
||||
const result = await executeReadAction(action, sdk, provider);
|
||||
console.log(result); // { address, balance, balanceFormatted }
|
||||
```
|
||||
|
||||
### Write Actions
|
||||
|
||||
```typescript
|
||||
import { createTransferAction, executeWriteAction } from '@dbis-thirdweb/ai';
|
||||
|
||||
// Create transfer action
|
||||
const action = createTransferAction('0x...', '1000000000000000000');
|
||||
|
||||
// Execute (requires signer)
|
||||
const result = await executeWriteAction(action, sdk);
|
||||
console.log(result); // { txHash, chainId }
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Chain-aware prompt templates with Chain 138 guardrails
|
||||
- Read action templates (balance, block height, token info)
|
||||
- Write action templates (transfers, contract interactions)
|
||||
- Chain ID routing validation
|
||||
- Error handling for unsupported operations
|
||||
38
packages/ai/package.json
Normal file
38
packages/ai/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@dbis-thirdweb/ai",
|
||||
"version": "0.1.0",
|
||||
"description": "Chain-aware AI prompts and actions for Chain 138",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"lint": "eslint src",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"keywords": [
|
||||
"thirdweb",
|
||||
"ai",
|
||||
"chain-138",
|
||||
"prompts"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dbis-thirdweb/chain": "workspace:*",
|
||||
"@thirdweb-dev/sdk": "^4.0.0",
|
||||
"ethers": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
179
packages/ai/src/actions.ts
Normal file
179
packages/ai/src/actions.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import type { providers } from 'ethers';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import type { ReadAction, WriteAction } from './types';
|
||||
import { validateChainId } from './types';
|
||||
|
||||
/**
|
||||
* Read action templates
|
||||
*/
|
||||
|
||||
/**
|
||||
* Read balance action template
|
||||
*/
|
||||
export function createReadBalanceAction(address: string): ReadAction {
|
||||
return {
|
||||
type: 'read',
|
||||
description: `Read native token balance for address ${address}`,
|
||||
chainId: chain138.chainId,
|
||||
operation: 'getBalance',
|
||||
params: { address },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read block height action template
|
||||
*/
|
||||
export function createReadBlockHeightAction(): ReadAction {
|
||||
return {
|
||||
type: 'read',
|
||||
description: 'Read current block height',
|
||||
chainId: chain138.chainId,
|
||||
operation: 'getBlockNumber',
|
||||
params: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read token info action template
|
||||
*/
|
||||
export function createReadTokenInfoAction(contractAddress: string): ReadAction {
|
||||
return {
|
||||
type: 'read',
|
||||
description: `Read token information for contract ${contractAddress}`,
|
||||
chainId: chain138.chainId,
|
||||
operation: 'getTokenInfo',
|
||||
params: { contractAddress },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute read action
|
||||
*/
|
||||
export async function executeReadAction(
|
||||
action: ReadAction,
|
||||
sdk: ThirdwebSDK,
|
||||
provider: providers.Provider
|
||||
): Promise<unknown> {
|
||||
validateChainId(action.chainId);
|
||||
|
||||
switch (action.operation) {
|
||||
case 'getBalance': {
|
||||
const address = action.params.address as string;
|
||||
const balance = await provider.getBalance(address);
|
||||
return {
|
||||
address,
|
||||
balance: balance.toString(),
|
||||
balanceFormatted: `${(Number(balance) / 1e18).toFixed(4)} ${chain138.nativeCurrency.symbol}`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'getBlockNumber': {
|
||||
const blockNumber = await provider.getBlockNumber();
|
||||
return {
|
||||
blockNumber,
|
||||
chainId: chain138.chainId,
|
||||
};
|
||||
}
|
||||
|
||||
case 'getTokenInfo': {
|
||||
const contractAddress = action.params.contractAddress as string;
|
||||
try {
|
||||
const contract = await sdk.getContract(contractAddress);
|
||||
// Attempt to read token info if it's a token contract
|
||||
const [name, symbol, decimals] = await Promise.all([
|
||||
contract.call('name').catch(() => null),
|
||||
contract.call('symbol').catch(() => null),
|
||||
contract.call('decimals').catch(() => null),
|
||||
]);
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
name: name || 'Unknown',
|
||||
symbol: symbol || 'Unknown',
|
||||
decimals: decimals || 18,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get token info: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown read operation: ${action.operation}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write action templates
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create transfer action template
|
||||
*/
|
||||
export function createTransferAction(
|
||||
to: string,
|
||||
amount: string,
|
||||
tokenAddress?: string
|
||||
): WriteAction {
|
||||
return {
|
||||
type: 'write',
|
||||
description: tokenAddress
|
||||
? `Transfer ${amount} tokens from ${tokenAddress} to ${to}`
|
||||
: `Transfer ${amount} ${chain138.nativeCurrency.symbol} to ${to}`,
|
||||
chainId: chain138.chainId,
|
||||
operation: tokenAddress ? 'transferToken' : 'transferNative',
|
||||
params: {
|
||||
to,
|
||||
amount,
|
||||
tokenAddress,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute write action
|
||||
*/
|
||||
export async function executeWriteAction(
|
||||
action: WriteAction,
|
||||
sdk: ThirdwebSDK
|
||||
): Promise<{ txHash: string; chainId: number }> {
|
||||
validateChainId(action.chainId);
|
||||
|
||||
const signer = await sdk.getSigner();
|
||||
if (!signer) {
|
||||
throw new Error('No signer available for write operations');
|
||||
}
|
||||
|
||||
switch (action.operation) {
|
||||
case 'transferNative': {
|
||||
const to = action.params.to as string;
|
||||
const amount = action.params.amount as string;
|
||||
const tx = await signer.sendTransaction({
|
||||
to,
|
||||
value: BigInt(amount),
|
||||
});
|
||||
|
||||
return {
|
||||
txHash: tx.hash,
|
||||
chainId: chain138.chainId,
|
||||
};
|
||||
}
|
||||
|
||||
case 'transferToken': {
|
||||
const to = action.params.to as string;
|
||||
const amount = action.params.amount as string;
|
||||
const tokenAddress = action.params.tokenAddress as string;
|
||||
|
||||
const contract = await sdk.getContract(tokenAddress, 'token');
|
||||
const tx = await contract.erc20.transfer(to, amount);
|
||||
|
||||
return {
|
||||
txHash: tx.receipt.transactionHash,
|
||||
chainId: chain138.chainId,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown write operation: ${action.operation}`);
|
||||
}
|
||||
}
|
||||
3
packages/ai/src/index.ts
Normal file
3
packages/ai/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './prompts';
|
||||
export * from './actions';
|
||||
88
packages/ai/src/prompts.ts
Normal file
88
packages/ai/src/prompts.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import type { AIAction } from './types';
|
||||
import { validateChainId } from './types';
|
||||
|
||||
/**
|
||||
* Chain-aware prompt templates with Chain 138 guardrails
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base prompt prefix for Chain 138 operations
|
||||
*/
|
||||
const CHAIN_138_PREFIX = `You are operating on Chain 138 (ChainID: ${chain138.chainId}).
|
||||
All operations must be performed on Chain 138 only.
|
||||
RPC endpoint: ${chain138.rpc[0] || 'https://138.rpc.thirdweb.com'}
|
||||
Native currency: ${chain138.nativeCurrency.symbol}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Create chain-aware prompt for read operations
|
||||
*/
|
||||
export function createReadPrompt(operation: string, params: Record<string, unknown>): string {
|
||||
return `${CHAIN_138_PREFIX}
|
||||
|
||||
Perform a read operation on Chain 138:
|
||||
Operation: ${operation}
|
||||
Parameters: ${JSON.stringify(params, null, 2)}
|
||||
|
||||
Return the result in a structured format.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create chain-aware prompt for write operations
|
||||
*/
|
||||
export function createWritePrompt(operation: string, params: Record<string, unknown>): string {
|
||||
return `${CHAIN_138_PREFIX}
|
||||
|
||||
Perform a write operation on Chain 138:
|
||||
Operation: ${operation}
|
||||
Parameters: ${JSON.stringify(params, null, 2)}
|
||||
|
||||
IMPORTANT: Verify the chain ID is ${chain138.chainId} before executing.
|
||||
Return the transaction hash upon success.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create prompt from AI action
|
||||
*/
|
||||
export function createPromptFromAction(action: AIAction): string {
|
||||
// Validate chain ID
|
||||
validateChainId(action.chainId);
|
||||
|
||||
if (action.type === 'read') {
|
||||
return createReadPrompt(action.operation, action.params);
|
||||
} else {
|
||||
return createWritePrompt(action.operation, action.params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract chain ID from natural language prompt (safety check)
|
||||
*/
|
||||
export function extractChainIdFromPrompt(prompt: string): number | null {
|
||||
// Look for explicit chain ID mentions
|
||||
const chainIdMatch = prompt.match(/chain[_\s]?id[:\s]+(\d+)/i);
|
||||
if (chainIdMatch) {
|
||||
return parseInt(chainIdMatch[1], 10);
|
||||
}
|
||||
|
||||
// Look for "Chain 138" mention
|
||||
if (prompt.match(/chain[_\s]?138/i)) {
|
||||
return chain138.chainId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardrail: Ensure prompt targets Chain 138
|
||||
*/
|
||||
export function validatePromptForChain138(prompt: string): void {
|
||||
const extractedChainId = extractChainIdFromPrompt(prompt);
|
||||
|
||||
if (extractedChainId && extractedChainId !== chain138.chainId) {
|
||||
throw new Error(
|
||||
`Prompt targets chain ${extractedChainId}, but this module is configured for Chain 138 (${chain138.chainId})`
|
||||
);
|
||||
}
|
||||
}
|
||||
76
packages/ai/src/types.ts
Normal file
76
packages/ai/src/types.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
/**
|
||||
* AI action types
|
||||
*/
|
||||
export type AIActionType = 'read' | 'write';
|
||||
|
||||
/**
|
||||
* Base AI action
|
||||
*/
|
||||
export interface BaseAIAction {
|
||||
/**
|
||||
* Action type (read or write)
|
||||
*/
|
||||
type: AIActionType;
|
||||
|
||||
/**
|
||||
* Description of the action
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Chain ID (should be Chain 138)
|
||||
*/
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read action (query blockchain data)
|
||||
*/
|
||||
export interface ReadAction extends BaseAIAction {
|
||||
type: 'read';
|
||||
/**
|
||||
* What to read (e.g., "balance", "block height", "token info")
|
||||
*/
|
||||
operation: string;
|
||||
|
||||
/**
|
||||
* Parameters for the read operation
|
||||
*/
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write action (send transaction)
|
||||
*/
|
||||
export interface WriteAction extends BaseAIAction {
|
||||
type: 'write';
|
||||
/**
|
||||
* Transaction description
|
||||
*/
|
||||
operation: string;
|
||||
|
||||
/**
|
||||
* Transaction parameters
|
||||
*/
|
||||
params: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Estimated gas cost (optional)
|
||||
*/
|
||||
estimatedGas?: bigint;
|
||||
}
|
||||
|
||||
export type AIAction = ReadAction | WriteAction;
|
||||
|
||||
/**
|
||||
* Validate chain ID is Chain 138
|
||||
*/
|
||||
export function validateChainId(chainId: number): void {
|
||||
if (chainId !== chain138.chainId) {
|
||||
throw new Error(
|
||||
`Invalid chain ID: ${chainId}. Expected Chain 138 (${chain138.chainId})`
|
||||
);
|
||||
}
|
||||
}
|
||||
12
packages/ai/tsconfig.json
Normal file
12
packages/ai/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../chain" }
|
||||
]
|
||||
}
|
||||
58
packages/bridge/README.md
Normal file
58
packages/bridge/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# @dbis-thirdweb/bridge
|
||||
|
||||
Bridge routes and execution for Chain 138.
|
||||
|
||||
## Usage
|
||||
|
||||
### Get Supported Routes
|
||||
|
||||
```typescript
|
||||
import { getSupportedRoutes, isRouteSupported } from '@dbis-thirdweb/bridge';
|
||||
|
||||
const routes = getSupportedRoutes();
|
||||
const isSupported = isRouteSupported(1, 138); // Ethereum to Chain 138
|
||||
```
|
||||
|
||||
### Generate Bridge Quote
|
||||
|
||||
```typescript
|
||||
import { generateBridgeQuote, getAllBridgeableTokens } from '@dbis-thirdweb/bridge';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
const tokens = getAllBridgeableTokens();
|
||||
const nativeToken = tokens.find(t => t.isNative);
|
||||
|
||||
const quote = await generateBridgeQuote({
|
||||
fromChainId: 1,
|
||||
toChainId: 138,
|
||||
token: nativeToken!,
|
||||
amount: ethers.utils.parseEther('0.1'),
|
||||
slippageBps: 50, // 0.5%
|
||||
});
|
||||
```
|
||||
|
||||
### Execute Bridge
|
||||
|
||||
```typescript
|
||||
import { executeBridge, getBridgeStatus } from '@dbis-thirdweb/bridge';
|
||||
|
||||
const status = await executeBridge(quote, signer, provider);
|
||||
|
||||
// Poll for status
|
||||
const finalStatus = await getBridgeStatus(status.sourceTxHash, provider);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Canonical chain mapping for Chain 138
|
||||
- Supported routes configuration
|
||||
- Token lists (native, wrapped, stablecoins)
|
||||
- Quote generation with slippage protection
|
||||
- Bridge execution helpers
|
||||
- Status tracking and finality checks
|
||||
|
||||
## Notes
|
||||
|
||||
- This implementation uses simplified bridge logic
|
||||
- In production, integrate with thirdweb Bridge SDK or bridge provider APIs
|
||||
- Token addresses need to be configured for Chain 138's actual deployed tokens
|
||||
38
packages/bridge/package.json
Normal file
38
packages/bridge/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@dbis-thirdweb/bridge",
|
||||
"version": "0.1.0",
|
||||
"description": "Bridge routes and execution for Chain 138",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"lint": "eslint src",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"keywords": [
|
||||
"thirdweb",
|
||||
"bridge",
|
||||
"chain-138",
|
||||
"cross-chain"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dbis-thirdweb/chain": "workspace:*",
|
||||
"@thirdweb-dev/sdk": "^4.0.0",
|
||||
"ethers": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
90
packages/bridge/src/execution.ts
Normal file
90
packages/bridge/src/execution.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Signer, providers } from 'ethers';
|
||||
import type { BridgeQuote, BridgeStatus } from './types';
|
||||
import { validateQuote } from './quote';
|
||||
|
||||
/**
|
||||
* Execute bridge transaction
|
||||
* Note: This is a simplified implementation. In production, you'd use thirdweb Bridge SDK
|
||||
* or a bridge provider's SDK/API
|
||||
*/
|
||||
export async function executeBridge(
|
||||
quote: BridgeQuote,
|
||||
signer: Signer,
|
||||
provider: providers.Provider
|
||||
): Promise<BridgeStatus> {
|
||||
// Validate quote
|
||||
if (!validateQuote(quote)) {
|
||||
throw new Error('Invalid bridge quote');
|
||||
}
|
||||
|
||||
// In production, this would:
|
||||
// 1. Use thirdweb Bridge SDK: await bridge.bridge(quote)
|
||||
// 2. Or call bridge provider API
|
||||
// 3. Or interact with bridge contract directly
|
||||
|
||||
// Simplified implementation - send native transfer as placeholder
|
||||
// This should be replaced with actual bridge contract interaction
|
||||
const tx = await signer.sendTransaction({
|
||||
to: quote.token.address,
|
||||
value: quote.amount,
|
||||
// Bridge contract would have specific data/contract interaction here
|
||||
});
|
||||
|
||||
// Wait for transaction
|
||||
const receipt = await provider.waitForTransaction(tx.hash, 1);
|
||||
|
||||
if (!receipt || receipt.status !== 1) {
|
||||
throw new Error('Bridge transaction failed');
|
||||
}
|
||||
|
||||
return {
|
||||
sourceTxHash: receipt.transactionHash,
|
||||
status: 'processing', // Would track through bridge provider to get final status
|
||||
step: 'Transaction confirmed on source chain',
|
||||
sourceBlockNumber: receipt.blockNumber,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bridge status (polling for destination chain confirmation)
|
||||
*/
|
||||
export async function getBridgeStatus(
|
||||
sourceTxHash: string,
|
||||
provider: providers.Provider
|
||||
): Promise<BridgeStatus> {
|
||||
// In production, this would query the bridge provider API or monitor on-chain events
|
||||
const receipt = await provider.getTransactionReceipt(sourceTxHash);
|
||||
|
||||
if (!receipt) {
|
||||
return {
|
||||
sourceTxHash,
|
||||
status: 'pending',
|
||||
step: 'Waiting for source chain confirmation',
|
||||
};
|
||||
}
|
||||
|
||||
// Simplified - in production, you'd check bridge provider for destination chain status
|
||||
return {
|
||||
sourceTxHash,
|
||||
status: receipt.status === 1 ? 'processing' : 'failed',
|
||||
step: receipt.status === 1
|
||||
? 'Processing bridge - waiting for destination chain'
|
||||
: 'Bridge transaction failed',
|
||||
sourceBlockNumber: receipt.blockNumber,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check finality threshold for Chain 138
|
||||
*/
|
||||
export const FINALITY_THRESHOLD_BLOCKS = 1; // Adjust based on Chain 138's finality rules
|
||||
|
||||
/**
|
||||
* Check if bridge transaction has reached finality
|
||||
*/
|
||||
export async function checkFinality(
|
||||
blockNumber: number,
|
||||
currentBlockNumber: number
|
||||
): Promise<boolean> {
|
||||
return currentBlockNumber >= blockNumber + FINALITY_THRESHOLD_BLOCKS;
|
||||
}
|
||||
5
packages/bridge/src/index.ts
Normal file
5
packages/bridge/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './types';
|
||||
export * from './routes';
|
||||
export * from './tokenLists';
|
||||
export * from './quote';
|
||||
export * from './execution';
|
||||
75
packages/bridge/src/quote.ts
Normal file
75
packages/bridge/src/quote.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { providers } from 'ethers';
|
||||
import { ethers } from 'ethers';
|
||||
import type { BridgeQuote, BridgeableToken } from './types';
|
||||
import { isRouteSupported } from './routes';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
/**
|
||||
* Generate bridge quote
|
||||
* Note: This is a simplified implementation. In production, you'd call a bridge provider API
|
||||
*/
|
||||
export async function generateBridgeQuote(params: {
|
||||
fromChainId: number;
|
||||
toChainId: number;
|
||||
token: BridgeableToken;
|
||||
amount: bigint;
|
||||
provider?: providers.Provider;
|
||||
slippageBps?: number; // Basis points (e.g., 100 = 1%)
|
||||
}): Promise<BridgeQuote> {
|
||||
const { fromChainId, toChainId, token, amount, slippageBps = 50 } = params;
|
||||
|
||||
// Validate route
|
||||
if (!isRouteSupported(fromChainId, toChainId)) {
|
||||
throw new Error(`Bridge route from ${fromChainId} to ${toChainId} is not supported`);
|
||||
}
|
||||
|
||||
// Simplified quote calculation
|
||||
// In production, this would call a bridge provider API (e.g., thirdweb Bridge API, Socket, etc.)
|
||||
const bridgeFeeBps = 10; // 0.1% fee (simplified)
|
||||
const fee = (amount * BigInt(bridgeFeeBps)) / BigInt(10000);
|
||||
const estimatedOutput = amount - fee;
|
||||
|
||||
// Apply slippage protection
|
||||
const minimumOutput = (estimatedOutput * BigInt(10000 - slippageBps)) / BigInt(10000);
|
||||
|
||||
// Estimated time (simplified - actual time depends on bridge provider and chains)
|
||||
const estimatedTimeSeconds = 300; // 5 minutes default
|
||||
|
||||
return {
|
||||
fromChainId,
|
||||
toChainId,
|
||||
token,
|
||||
amount,
|
||||
estimatedOutput,
|
||||
fee,
|
||||
estimatedTimeSeconds,
|
||||
minimumOutput,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate bridge quote
|
||||
*/
|
||||
export function validateQuote(quote: BridgeQuote): boolean {
|
||||
const amount = typeof quote.amount === 'bigint' ? quote.amount : BigInt(quote.amount.toString());
|
||||
const estimatedOutput = typeof quote.estimatedOutput === 'bigint' ? quote.estimatedOutput : BigInt(quote.estimatedOutput.toString());
|
||||
const minimumOutput = typeof quote.minimumOutput === 'bigint' ? quote.minimumOutput : BigInt(quote.minimumOutput.toString());
|
||||
|
||||
if (amount <= 0n) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (estimatedOutput <= 0n) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (minimumOutput > estimatedOutput) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isRouteSupported(quote.fromChainId, quote.toChainId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
59
packages/bridge/src/routes.ts
Normal file
59
packages/bridge/src/routes.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import type { BridgeRoute, BridgeableToken } from './types';
|
||||
|
||||
/**
|
||||
* Native token for Chain 138
|
||||
*/
|
||||
export const chain138NativeToken: BridgeableToken = {
|
||||
address: '0x0000000000000000000000000000000000000000',
|
||||
symbol: 'ETH',
|
||||
name: 'Ether',
|
||||
decimals: 18,
|
||||
isNative: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Canonical chain mapping: supported source chains for bridging to Chain 138
|
||||
* Add chain IDs that you want to support bridging FROM
|
||||
*/
|
||||
export const SUPPORTED_SOURCE_CHAINS = [
|
||||
1, // Ethereum Mainnet
|
||||
5, // Goerli
|
||||
137, // Polygon
|
||||
80001, // Mumbai
|
||||
// Add more chains as needed
|
||||
];
|
||||
|
||||
/**
|
||||
* Get supported bridge routes TO Chain 138
|
||||
*/
|
||||
export function getSupportedRoutes(): BridgeRoute[] {
|
||||
return SUPPORTED_SOURCE_CHAINS.map((fromChainId) => ({
|
||||
fromChainId,
|
||||
toChainId: chain138.chainId,
|
||||
tokens: [chain138NativeToken], // Add more tokens as needed
|
||||
active: true,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route is supported
|
||||
*/
|
||||
export function isRouteSupported(fromChainId: number, toChainId: number): boolean {
|
||||
return (
|
||||
toChainId === chain138.chainId &&
|
||||
SUPPORTED_SOURCE_CHAINS.includes(fromChainId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route for specific chain pair
|
||||
*/
|
||||
export function getRoute(fromChainId: number, toChainId: number): BridgeRoute | null {
|
||||
if (!isRouteSupported(fromChainId, toChainId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const routes = getSupportedRoutes();
|
||||
return routes.find((r) => r.fromChainId === fromChainId) || null;
|
||||
}
|
||||
50
packages/bridge/src/tokenLists.ts
Normal file
50
packages/bridge/src/tokenLists.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { BridgeableToken } from './types';
|
||||
import { chain138NativeToken } from './routes';
|
||||
|
||||
/**
|
||||
* Token lists for Chain 138 bridging
|
||||
*/
|
||||
|
||||
/**
|
||||
* Native and wrapped tokens
|
||||
*/
|
||||
export const nativeTokens: BridgeableToken[] = [
|
||||
chain138NativeToken,
|
||||
// Add WETH if it exists on Chain 138
|
||||
// {
|
||||
// address: '0x...',
|
||||
// symbol: 'WETH',
|
||||
// name: 'Wrapped Ether',
|
||||
// decimals: 18,
|
||||
// isNative: false,
|
||||
// },
|
||||
];
|
||||
|
||||
/**
|
||||
* Stablecoins (add actual addresses when available)
|
||||
*/
|
||||
export const stablecoins: BridgeableToken[] = [
|
||||
// Example structure - fill in with actual Chain 138 token addresses
|
||||
// {
|
||||
// address: '0x...',
|
||||
// symbol: 'USDC',
|
||||
// name: 'USD Coin',
|
||||
// decimals: 6,
|
||||
// isNative: false,
|
||||
// },
|
||||
];
|
||||
|
||||
/**
|
||||
* All bridgeable tokens for Chain 138
|
||||
*/
|
||||
export function getAllBridgeableTokens(): BridgeableToken[] {
|
||||
return [...nativeTokens, ...stablecoins];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find token by address
|
||||
*/
|
||||
export function findToken(address: string): BridgeableToken | undefined {
|
||||
const tokens = getAllBridgeableTokens();
|
||||
return tokens.find((token) => token.address.toLowerCase() === address.toLowerCase());
|
||||
}
|
||||
141
packages/bridge/src/types.ts
Normal file
141
packages/bridge/src/types.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { BigNumberish } from 'ethers';
|
||||
|
||||
/**
|
||||
* Supported token for bridging
|
||||
*/
|
||||
export interface BridgeableToken {
|
||||
/**
|
||||
* Token address (native token uses zero address)
|
||||
*/
|
||||
address: string;
|
||||
|
||||
/**
|
||||
* Token symbol
|
||||
*/
|
||||
symbol: string;
|
||||
|
||||
/**
|
||||
* Token name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Token decimals
|
||||
*/
|
||||
decimals: number;
|
||||
|
||||
/**
|
||||
* Token logo URL (optional)
|
||||
*/
|
||||
logoURI?: string;
|
||||
|
||||
/**
|
||||
* Whether this is the native token
|
||||
*/
|
||||
isNative: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge route between two chains
|
||||
*/
|
||||
export interface BridgeRoute {
|
||||
/**
|
||||
* Source chain ID
|
||||
*/
|
||||
fromChainId: number;
|
||||
|
||||
/**
|
||||
* Destination chain ID (should be 138 for Chain 138)
|
||||
*/
|
||||
toChainId: number;
|
||||
|
||||
/**
|
||||
* Supported tokens on this route
|
||||
*/
|
||||
tokens: BridgeableToken[];
|
||||
|
||||
/**
|
||||
* Whether route is currently active
|
||||
*/
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge quote
|
||||
*/
|
||||
export interface BridgeQuote {
|
||||
/**
|
||||
* Source chain ID
|
||||
*/
|
||||
fromChainId: number;
|
||||
|
||||
/**
|
||||
* Destination chain ID
|
||||
*/
|
||||
toChainId: number;
|
||||
|
||||
/**
|
||||
* Token being bridged
|
||||
*/
|
||||
token: BridgeableToken;
|
||||
|
||||
/**
|
||||
* Amount to bridge (in token units)
|
||||
*/
|
||||
amount: BigNumberish;
|
||||
|
||||
/**
|
||||
* Estimated output amount (in token units)
|
||||
*/
|
||||
estimatedOutput: BigNumberish;
|
||||
|
||||
/**
|
||||
* Bridge fee (in token units)
|
||||
*/
|
||||
fee: BigNumberish;
|
||||
|
||||
/**
|
||||
* Estimated time in seconds
|
||||
*/
|
||||
estimatedTimeSeconds: number;
|
||||
|
||||
/**
|
||||
* Minimum output amount (slippage protection)
|
||||
*/
|
||||
minimumOutput: BigNumberish;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge transaction status
|
||||
*/
|
||||
export interface BridgeStatus {
|
||||
/**
|
||||
* Transaction hash on source chain
|
||||
*/
|
||||
sourceTxHash: string;
|
||||
|
||||
/**
|
||||
* Transaction hash on destination chain (when available)
|
||||
*/
|
||||
destinationTxHash?: string;
|
||||
|
||||
/**
|
||||
* Current status
|
||||
*/
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
|
||||
/**
|
||||
* Current step description
|
||||
*/
|
||||
step: string;
|
||||
|
||||
/**
|
||||
* Block number on source chain
|
||||
*/
|
||||
sourceBlockNumber?: number;
|
||||
|
||||
/**
|
||||
* Block number on destination chain (when available)
|
||||
*/
|
||||
destinationBlockNumber?: number;
|
||||
}
|
||||
12
packages/bridge/tsconfig.json
Normal file
12
packages/bridge/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../chain" }
|
||||
]
|
||||
}
|
||||
19
packages/chain/README.md
Normal file
19
packages/chain/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# @dbis-thirdweb/chain
|
||||
|
||||
Canonical Chain 138 definition for thirdweb integrations.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
// Use with thirdweb SDK
|
||||
const sdk = new ThirdwebSDK(chain138);
|
||||
```
|
||||
|
||||
## Chain Details
|
||||
|
||||
- **ChainID**: 138
|
||||
- **CAIP-2**: `eip155:138`
|
||||
- **RPC**: `https://138.rpc.thirdweb.com`
|
||||
- **Native Currency**: ETH (18 decimals)
|
||||
36
packages/chain/package.json
Normal file
36
packages/chain/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@dbis-thirdweb/chain",
|
||||
"version": "0.1.0",
|
||||
"description": "Chain 138 canonical definition for thirdweb",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"lint": "eslint src",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"keywords": [
|
||||
"thirdweb",
|
||||
"chain-138",
|
||||
"blockchain",
|
||||
"eip155:138"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@thirdweb-dev/chains": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
27
packages/chain/src/index.ts
Normal file
27
packages/chain/src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Chain } from '@thirdweb-dev/chains';
|
||||
|
||||
/**
|
||||
* Chain 138 canonical definition
|
||||
* CAIP-2: eip155:138
|
||||
*/
|
||||
export const chain138: Chain = {
|
||||
chain: 'Chain 138',
|
||||
chainId: 138,
|
||||
rpc: ['https://138.rpc.thirdweb.com'],
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
shortName: 'chain138',
|
||||
slug: 'chain-138',
|
||||
testnet: false,
|
||||
name: 'Chain 138',
|
||||
// Explorer will be inferred from thirdweb or added if available
|
||||
explorers: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Export as default for convenience
|
||||
*/
|
||||
export default chain138;
|
||||
10
packages/chain/tsconfig.json
Normal file
10
packages/chain/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": []
|
||||
}
|
||||
63
packages/http-api/README.md
Normal file
63
packages/http-api/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# @dbis-thirdweb/http-api
|
||||
|
||||
HTTP API client wrapper for Chain 138 with retries, timeouts, and type-safe endpoints.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Client
|
||||
|
||||
```typescript
|
||||
import { createAPIClient } from '@dbis-thirdweb/http-api';
|
||||
|
||||
const client = createAPIClient({
|
||||
apiKey: 'your-api-key',
|
||||
timeout: 30000,
|
||||
retries: 3,
|
||||
});
|
||||
|
||||
// GET request
|
||||
const data = await client.get('/endpoint');
|
||||
|
||||
// POST request
|
||||
const result = await client.post('/endpoint', { data: 'value' });
|
||||
```
|
||||
|
||||
### Chain 138 API Endpoints
|
||||
|
||||
```typescript
|
||||
import { createChain138API } from '@dbis-thirdweb/http-api';
|
||||
|
||||
const api = createChain138API({
|
||||
apiKey: 'your-api-key',
|
||||
});
|
||||
|
||||
// Get chain metadata
|
||||
const metadata = await api.getChainMetadata();
|
||||
|
||||
// Get transaction receipt
|
||||
const receipt = await api.getTransactionReceipt('0x...');
|
||||
|
||||
// Get transaction logs
|
||||
const logs = await api.getTransactionLogs('0x...');
|
||||
|
||||
// Get block
|
||||
const block = await api.getBlock(12345);
|
||||
|
||||
// Get latest block
|
||||
const latestBlock = await api.getLatestBlock();
|
||||
|
||||
// Send transaction (if supported)
|
||||
const txResult = await api.sendTransaction({
|
||||
to: '0x...',
|
||||
value: '1000000000000000000',
|
||||
});
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Standardized client wrapper with retry logic
|
||||
- Exponential backoff for retries
|
||||
- Configurable timeouts
|
||||
- Chain 138 base URL configuration
|
||||
- Type-safe request/response interfaces
|
||||
- Automatic chain ID header injection
|
||||
36
packages/http-api/package.json
Normal file
36
packages/http-api/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@dbis-thirdweb/http-api",
|
||||
"version": "0.1.0",
|
||||
"description": "HTTP API client wrapper for Chain 138",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"lint": "eslint src",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"keywords": [
|
||||
"thirdweb",
|
||||
"http-api",
|
||||
"chain-138",
|
||||
"api-client"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dbis-thirdweb/chain": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
175
packages/http-api/src/client.ts
Normal file
175
packages/http-api/src/client.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
/**
|
||||
* HTTP API client configuration
|
||||
*/
|
||||
export interface APIClientConfig {
|
||||
/**
|
||||
* Base URL for API requests
|
||||
*/
|
||||
baseURL: string;
|
||||
|
||||
/**
|
||||
* API key (if required)
|
||||
*/
|
||||
apiKey?: string;
|
||||
|
||||
/**
|
||||
* Request timeout in milliseconds
|
||||
* @default 30000
|
||||
*/
|
||||
timeout?: number;
|
||||
|
||||
/**
|
||||
* Number of retries for failed requests
|
||||
* @default 3
|
||||
*/
|
||||
retries?: number;
|
||||
|
||||
/**
|
||||
* Retry delay in milliseconds (exponential backoff base)
|
||||
* @default 1000
|
||||
*/
|
||||
retryDelay?: number;
|
||||
|
||||
/**
|
||||
* Chain ID (should be Chain 138)
|
||||
*/
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration for Chain 138
|
||||
*/
|
||||
export const defaultConfig: APIClientConfig = {
|
||||
baseURL: 'https://api.thirdweb.com',
|
||||
timeout: 30000,
|
||||
retries: 3,
|
||||
retryDelay: 1000,
|
||||
chainId: chain138.chainId,
|
||||
};
|
||||
|
||||
/**
|
||||
* Sleep utility for retry delays
|
||||
*/
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP API client with retry logic and timeout handling
|
||||
*/
|
||||
export class APIClient {
|
||||
private config: APIClientConfig;
|
||||
|
||||
constructor(config: Partial<APIClientConfig> = {}) {
|
||||
this.config = { ...defaultConfig, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request with retries and timeout
|
||||
*/
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${this.config.baseURL}${endpoint}`;
|
||||
const timeout = this.config.timeout || 30000;
|
||||
|
||||
// Add API key to headers if provided
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
if (this.config.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${this.config.apiKey}`;
|
||||
}
|
||||
|
||||
// Add chain ID to headers if needed
|
||||
headers['x-chain-id'] = this.config.chainId.toString();
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
let lastError: Error | null = null;
|
||||
const maxRetries = this.config.retries || 3;
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as T;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
lastError = error as Error;
|
||||
|
||||
// Don't retry on abort (timeout)
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error(`Request timeout after ${timeout}ms`);
|
||||
}
|
||||
|
||||
// Don't retry on last attempt
|
||||
if (attempt < maxRetries) {
|
||||
const delay = this.config.retryDelay! * Math.pow(2, attempt); // Exponential backoff
|
||||
await sleep(delay);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Request failed after retries');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
async get<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'GET' });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*/
|
||||
async post<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request
|
||||
*/
|
||||
async put<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*/
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create API client for Chain 138
|
||||
*/
|
||||
export function createAPIClient(config?: Partial<APIClientConfig>): APIClient {
|
||||
return new APIClient(config);
|
||||
}
|
||||
119
packages/http-api/src/endpoints.ts
Normal file
119
packages/http-api/src/endpoints.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { APIClient, createAPIClient } from './client';
|
||||
|
||||
/**
|
||||
* Chain metadata response
|
||||
*/
|
||||
export interface ChainMetadata {
|
||||
chainId: number;
|
||||
name: string;
|
||||
nativeCurrency: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
rpc: string[];
|
||||
explorers?: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction receipt response
|
||||
*/
|
||||
export interface TransactionReceipt {
|
||||
blockHash: string;
|
||||
blockNumber: number;
|
||||
contractAddress: string | null;
|
||||
cumulativeGasUsed: string;
|
||||
effectiveGasPrice: string;
|
||||
from: string;
|
||||
gasUsed: string;
|
||||
logs: Array<{
|
||||
address: string;
|
||||
topics: string[];
|
||||
data: string;
|
||||
}>;
|
||||
logsBloom: string;
|
||||
status: number;
|
||||
to: string | null;
|
||||
transactionHash: string;
|
||||
transactionIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Block response
|
||||
*/
|
||||
export interface BlockResponse {
|
||||
number: number;
|
||||
hash: string;
|
||||
parentHash: string;
|
||||
timestamp: number;
|
||||
transactions: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain 138 API endpoints wrapper
|
||||
*/
|
||||
export class Chain138API {
|
||||
constructor(private client: APIClient) {}
|
||||
|
||||
/**
|
||||
* Fetch chain metadata for Chain 138
|
||||
*/
|
||||
async getChainMetadata(): Promise<ChainMetadata> {
|
||||
return this.client.get<ChainMetadata>('/v1/chains/138');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction receipt
|
||||
*/
|
||||
async getTransactionReceipt(txHash: string): Promise<TransactionReceipt> {
|
||||
return this.client.get<TransactionReceipt>(`/v1/chains/138/transactions/${txHash}/receipt`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction logs
|
||||
*/
|
||||
async getTransactionLogs(txHash: string): Promise<TransactionReceipt['logs']> {
|
||||
const receipt = await this.getTransactionReceipt(txHash);
|
||||
return receipt.logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block by number
|
||||
*/
|
||||
async getBlock(blockNumber: number): Promise<BlockResponse> {
|
||||
return this.client.get<BlockResponse>(`/v1/chains/138/blocks/${blockNumber}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest block
|
||||
*/
|
||||
async getLatestBlock(): Promise<BlockResponse> {
|
||||
return this.client.get<BlockResponse>('/v1/chains/138/blocks/latest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send transaction via API (if supported)
|
||||
*/
|
||||
async sendTransaction(txData: {
|
||||
to: string;
|
||||
value?: string;
|
||||
data?: string;
|
||||
gasLimit?: string;
|
||||
gasPrice?: string;
|
||||
}): Promise<{ txHash: string }> {
|
||||
return this.client.post<{ txHash: string }>('/v1/chains/138/transactions', txData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Chain 138 API client
|
||||
*/
|
||||
export function createChain138API(
|
||||
config?: Parameters<typeof createAPIClient>[0]
|
||||
): Chain138API {
|
||||
const client = createAPIClient(config);
|
||||
return new Chain138API(client);
|
||||
}
|
||||
2
packages/http-api/src/index.ts
Normal file
2
packages/http-api/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './client';
|
||||
export * from './endpoints';
|
||||
12
packages/http-api/tsconfig.json
Normal file
12
packages/http-api/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../chain" }
|
||||
]
|
||||
}
|
||||
85
packages/tokens/README.md
Normal file
85
packages/tokens/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# @dbis-thirdweb/tokens
|
||||
|
||||
ERC20/721/1155 token deployment and management for Chain 138.
|
||||
|
||||
## Usage
|
||||
|
||||
### Token Factory
|
||||
|
||||
```typescript
|
||||
import { createTokenFactory } from '@dbis-thirdweb/tokens';
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
const sdk = new ThirdwebSDK(chain138, privateKey);
|
||||
const factory = createTokenFactory(sdk);
|
||||
|
||||
// Deploy ERC20
|
||||
const erc20Address = await factory.deployERC20({
|
||||
name: 'My Token',
|
||||
symbol: 'MTK',
|
||||
initialSupply: '1000000',
|
||||
});
|
||||
|
||||
// Deploy ERC721
|
||||
const erc721Address = await factory.deployERC721({
|
||||
name: 'My NFT',
|
||||
symbol: 'MNFT',
|
||||
});
|
||||
|
||||
// Deploy ERC1155
|
||||
const erc1155Address = await factory.deployERC1155({
|
||||
name: 'My Edition',
|
||||
});
|
||||
```
|
||||
|
||||
### ERC20 Operations
|
||||
|
||||
```typescript
|
||||
import { mintERC20, transferERC20, getERC20Balance } from '@dbis-thirdweb/tokens';
|
||||
|
||||
await mintERC20(sdk, erc20Address, '1000', recipientAddress);
|
||||
await transferERC20(sdk, erc20Address, recipientAddress, '100');
|
||||
const balance = await getERC20Balance(sdk, erc20Address, address);
|
||||
```
|
||||
|
||||
### ERC721 Operations
|
||||
|
||||
```typescript
|
||||
import { mintERC721, transferERC721, getERC721Metadata } from '@dbis-thirdweb/tokens';
|
||||
|
||||
const tokenId = await mintERC721(sdk, erc721Address, {
|
||||
name: 'My NFT #1',
|
||||
description: 'Description',
|
||||
image: 'ipfs://...',
|
||||
});
|
||||
|
||||
await transferERC721(sdk, erc721Address, tokenId, recipientAddress);
|
||||
const metadata = await getERC721Metadata(sdk, erc721Address, tokenId);
|
||||
```
|
||||
|
||||
### ERC1155 Operations
|
||||
|
||||
```typescript
|
||||
import { mintERC1155, batchMintERC1155, getERC1155Balance } from '@dbis-thirdweb/tokens';
|
||||
|
||||
await mintERC1155(sdk, erc1155Address, 0n, '100', {
|
||||
name: 'Edition #1',
|
||||
image: 'ipfs://...',
|
||||
});
|
||||
|
||||
await batchMintERC1155(sdk, erc1155Address, [
|
||||
{ tokenId: 1n, amount: '50', metadata: {...} },
|
||||
{ tokenId: 2n, amount: '25', metadata: {...} },
|
||||
]);
|
||||
|
||||
const balance = await getERC1155Balance(sdk, erc1155Address, address, 0n);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- ERC20 deploy/mint/transfer/balance
|
||||
- ERC721 deploy/mint/transfer/metadata
|
||||
- ERC1155 deploy/batch mint/transfer
|
||||
- Metadata hosting strategy (IPFS/thirdweb storage)
|
||||
- Token factory utilities
|
||||
40
packages/tokens/package.json
Normal file
40
packages/tokens/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@dbis-thirdweb/tokens",
|
||||
"version": "0.1.0",
|
||||
"description": "ERC20/721/1155 token deployment and management for Chain 138",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"lint": "eslint src",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"keywords": [
|
||||
"thirdweb",
|
||||
"tokens",
|
||||
"ERC20",
|
||||
"ERC721",
|
||||
"ERC1155",
|
||||
"chain-138"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dbis-thirdweb/chain": "workspace:*",
|
||||
"@thirdweb-dev/sdk": "^4.0.0",
|
||||
"ethers": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
116
packages/tokens/src/erc1155.ts
Normal file
116
packages/tokens/src/erc1155.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import type { TokenMetadata } from './metadata';
|
||||
import type { BigNumberish } from 'ethers';
|
||||
|
||||
/**
|
||||
* ERC1155 token deployment parameters
|
||||
*/
|
||||
export interface ERC1155DeployParams {
|
||||
name: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
royaltyRecipient?: string;
|
||||
royaltyBps?: number; // Basis points
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy ERC1155 token contract
|
||||
*/
|
||||
export async function deployERC1155(
|
||||
sdk: ThirdwebSDK,
|
||||
params: ERC1155DeployParams
|
||||
): Promise<string> {
|
||||
const contractAddress = await sdk.deployer.deployEdition({
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
image: params.image,
|
||||
primary_sale_recipient: await sdk.getSigner()?.getAddress(),
|
||||
});
|
||||
|
||||
return contractAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mint ERC1155 tokens (batch mint)
|
||||
*/
|
||||
export async function mintERC1155(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
tokenId: bigint,
|
||||
amount: BigNumberish,
|
||||
metadata: TokenMetadata,
|
||||
to?: string
|
||||
): Promise<void> {
|
||||
const contract = await sdk.getContract(contractAddress, 'edition');
|
||||
const recipient = to || (await sdk.getSigner()?.getAddress()) || '';
|
||||
|
||||
await contract.erc1155.mintTo(recipient, {
|
||||
metadata,
|
||||
supply: amount.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch mint ERC1155 tokens
|
||||
*/
|
||||
export async function batchMintERC1155(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
mints: Array<{
|
||||
tokenId: bigint;
|
||||
amount: BigNumberish;
|
||||
metadata: TokenMetadata;
|
||||
}>,
|
||||
to?: string
|
||||
): Promise<void> {
|
||||
const contract = await sdk.getContract(contractAddress, 'edition');
|
||||
const recipient = to || (await sdk.getSigner()?.getAddress()) || '';
|
||||
|
||||
const payloads = mints.map((mint) => ({
|
||||
metadata: mint.metadata,
|
||||
supply: mint.amount.toString(),
|
||||
}));
|
||||
|
||||
await contract.erc1155.mintBatchTo(recipient, payloads);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer ERC1155 tokens
|
||||
*/
|
||||
export async function transferERC1155(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
tokenId: bigint,
|
||||
amount: BigNumberish,
|
||||
to: string
|
||||
): Promise<void> {
|
||||
const contract = await sdk.getContract(contractAddress, 'edition');
|
||||
await contract.erc1155.transfer(to, tokenId.toString(), amount.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ERC1155 token balance
|
||||
*/
|
||||
export async function getERC1155Balance(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
address: string,
|
||||
tokenId: bigint
|
||||
): Promise<bigint> {
|
||||
const contract = await sdk.getContract(contractAddress, 'edition');
|
||||
const balance = await contract.erc1155.balanceOf(address, tokenId);
|
||||
return BigInt(balance.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ERC1155 token metadata
|
||||
*/
|
||||
export async function getERC1155Metadata(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
tokenId: bigint
|
||||
): Promise<TokenMetadata> {
|
||||
const contract = await sdk.getContract(contractAddress, 'edition');
|
||||
const nft = await contract.erc1155.get(tokenId.toString());
|
||||
return nft.metadata as TokenMetadata;
|
||||
}
|
||||
81
packages/tokens/src/erc20.ts
Normal file
81
packages/tokens/src/erc20.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { Signer } from 'ethers';
|
||||
import type { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import type { BigNumberish } from 'ethers';
|
||||
|
||||
/**
|
||||
* ERC20 token deployment parameters
|
||||
*/
|
||||
export interface ERC20DeployParams {
|
||||
name: string;
|
||||
symbol: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
initialSupply?: BigNumberish;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy ERC20 token
|
||||
*/
|
||||
export async function deployERC20(
|
||||
sdk: ThirdwebSDK,
|
||||
params: ERC20DeployParams
|
||||
): Promise<string> {
|
||||
// Use thirdweb SDK to deploy ERC20
|
||||
// This uses thirdweb's prebuilt ERC20 contract
|
||||
const contractAddress = await sdk.deployer.deployToken({
|
||||
name: params.name,
|
||||
symbol: params.symbol,
|
||||
description: params.description,
|
||||
image: params.image,
|
||||
primary_sale_recipient: await sdk.getSigner()?.getAddress(),
|
||||
});
|
||||
|
||||
// If initial supply is specified, mint it
|
||||
if (params.initialSupply) {
|
||||
const contract = await sdk.getContract(contractAddress, 'token');
|
||||
await contract.erc20.mint(params.initialSupply.toString());
|
||||
}
|
||||
|
||||
return contractAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mint ERC20 tokens
|
||||
*/
|
||||
export async function mintERC20(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
amount: BigNumberish,
|
||||
to?: string
|
||||
): Promise<void> {
|
||||
const contract = await sdk.getContract(contractAddress, 'token');
|
||||
const recipient = to || (await sdk.getSigner()?.getAddress()) || '';
|
||||
|
||||
await contract.erc20.mintTo(recipient, amount.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer ERC20 tokens
|
||||
*/
|
||||
export async function transferERC20(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
to: string,
|
||||
amount: BigNumberish
|
||||
): Promise<void> {
|
||||
const contract = await sdk.getContract(contractAddress, 'token');
|
||||
await contract.erc20.transfer(to, amount.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ERC20 balance
|
||||
*/
|
||||
export async function getERC20Balance(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
address: string
|
||||
): Promise<bigint> {
|
||||
const contract = await sdk.getContract(contractAddress, 'token');
|
||||
const balance = await contract.erc20.balanceOf(address);
|
||||
return BigInt(balance.value.toString());
|
||||
}
|
||||
87
packages/tokens/src/erc721.ts
Normal file
87
packages/tokens/src/erc721.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import type { TokenMetadata } from './metadata';
|
||||
|
||||
/**
|
||||
* ERC721 token deployment parameters
|
||||
*/
|
||||
export interface ERC721DeployParams {
|
||||
name: string;
|
||||
symbol: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
royaltyRecipient?: string;
|
||||
royaltyBps?: number; // Basis points (e.g., 500 = 5%)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy ERC721 token contract
|
||||
*/
|
||||
export async function deployERC721(
|
||||
sdk: ThirdwebSDK,
|
||||
params: ERC721DeployParams
|
||||
): Promise<string> {
|
||||
const contractAddress = await sdk.deployer.deployNFTCollection({
|
||||
name: params.name,
|
||||
symbol: params.symbol,
|
||||
description: params.description,
|
||||
image: params.image,
|
||||
primary_sale_recipient: await sdk.getSigner()?.getAddress(),
|
||||
});
|
||||
|
||||
return contractAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mint ERC721 token
|
||||
*/
|
||||
export async function mintERC721(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
metadata: TokenMetadata,
|
||||
to?: string
|
||||
): Promise<bigint> {
|
||||
const contract = await sdk.getContract(contractAddress, 'nft-collection');
|
||||
const recipient = to || (await sdk.getSigner()?.getAddress()) || '';
|
||||
|
||||
const result = await contract.mintTo(recipient, metadata);
|
||||
return BigInt(result.id.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer ERC721 token
|
||||
*/
|
||||
export async function transferERC721(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
tokenId: bigint,
|
||||
to: string
|
||||
): Promise<void> {
|
||||
const contract = await sdk.getContract(contractAddress, 'nft-collection');
|
||||
await contract.transfer(tokenId.toString(), to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ERC721 token metadata
|
||||
*/
|
||||
export async function getERC721Metadata(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
tokenId: bigint
|
||||
): Promise<TokenMetadata> {
|
||||
const contract = await sdk.getContract(contractAddress, 'nft-collection');
|
||||
const nft = await contract.erc721.get(tokenId);
|
||||
return nft.metadata as TokenMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ERC721 balance (number of tokens owned)
|
||||
*/
|
||||
export async function getERC721Balance(
|
||||
sdk: ThirdwebSDK,
|
||||
contractAddress: string,
|
||||
address: string
|
||||
): Promise<bigint> {
|
||||
const contract = await sdk.getContract(contractAddress, 'nft-collection');
|
||||
const balance = await contract.erc721.balanceOf(address);
|
||||
return BigInt(balance.toString());
|
||||
}
|
||||
39
packages/tokens/src/factory.ts
Normal file
39
packages/tokens/src/factory.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { deployERC20, type ERC20DeployParams } from './erc20';
|
||||
import { deployERC721, type ERC721DeployParams } from './erc721';
|
||||
import { deployERC1155, type ERC1155DeployParams } from './erc1155';
|
||||
|
||||
/**
|
||||
* Token factory for deploying different token types
|
||||
*/
|
||||
export class TokenFactory {
|
||||
constructor(private sdk: ThirdwebSDK) {}
|
||||
|
||||
/**
|
||||
* Deploy ERC20 token
|
||||
*/
|
||||
async deployERC20(params: ERC20DeployParams): Promise<string> {
|
||||
return deployERC20(this.sdk, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy ERC721 token
|
||||
*/
|
||||
async deployERC721(params: ERC721DeployParams): Promise<string> {
|
||||
return deployERC721(this.sdk, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy ERC1155 token
|
||||
*/
|
||||
async deployERC1155(params: ERC1155DeployParams): Promise<string> {
|
||||
return deployERC1155(this.sdk, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create token factory instance
|
||||
*/
|
||||
export function createTokenFactory(sdk: ThirdwebSDK): TokenFactory {
|
||||
return new TokenFactory(sdk);
|
||||
}
|
||||
5
packages/tokens/src/index.ts
Normal file
5
packages/tokens/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './metadata';
|
||||
export * from './erc20';
|
||||
export * from './erc721';
|
||||
export * from './erc1155';
|
||||
export * from './factory';
|
||||
74
packages/tokens/src/metadata.ts
Normal file
74
packages/tokens/src/metadata.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Metadata hosting strategy configuration
|
||||
*/
|
||||
export interface MetadataConfig {
|
||||
/**
|
||||
* IPFS gateway URL for uploading/fetching metadata
|
||||
*/
|
||||
ipfsGateway: string;
|
||||
|
||||
/**
|
||||
* Whether to pin metadata to IPFS
|
||||
*/
|
||||
pinToIpfs: boolean;
|
||||
|
||||
/**
|
||||
* Alternative metadata storage options
|
||||
*/
|
||||
storage?: {
|
||||
/**
|
||||
* Use thirdweb storage
|
||||
*/
|
||||
useThirdwebStorage?: boolean;
|
||||
|
||||
/**
|
||||
* Custom storage endpoint
|
||||
*/
|
||||
customEndpoint?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Default metadata configuration
|
||||
*/
|
||||
export const defaultMetadataConfig: MetadataConfig = {
|
||||
ipfsGateway: 'https://ipfs.io/ipfs/',
|
||||
pinToIpfs: true,
|
||||
storage: {
|
||||
useThirdwebStorage: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Token metadata structure
|
||||
*/
|
||||
export interface TokenMetadata {
|
||||
name: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
external_url?: string;
|
||||
attributes?: Array<{
|
||||
trait_type: string;
|
||||
value: string | number;
|
||||
}>;
|
||||
// Additional properties can be added as needed
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate token metadata URI
|
||||
*/
|
||||
export function generateMetadataURI(
|
||||
metadata: TokenMetadata,
|
||||
config: MetadataConfig = defaultMetadataConfig
|
||||
): string {
|
||||
// In production, this would upload to IPFS/thirdweb storage
|
||||
// For now, return a placeholder
|
||||
if (config.storage?.useThirdwebStorage) {
|
||||
// Would use thirdweb storage SDK here
|
||||
return 'ipfs://...';
|
||||
}
|
||||
|
||||
// Fallback to direct IPFS gateway
|
||||
return 'ipfs://...';
|
||||
}
|
||||
12
packages/tokens/tsconfig.json
Normal file
12
packages/tokens/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../chain" }
|
||||
]
|
||||
}
|
||||
46
packages/wallets/README.md
Normal file
46
packages/wallets/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# @dbis-thirdweb/wallets
|
||||
|
||||
Wallet connectors and configuration for Chain 138.
|
||||
|
||||
## Usage
|
||||
|
||||
### Wallet Configuration
|
||||
|
||||
```typescript
|
||||
import { getWalletConfig } from '@dbis-thirdweb/wallets';
|
||||
|
||||
const config = getWalletConfig({
|
||||
confirmationBlocks: 2,
|
||||
gasStrategy: {
|
||||
multiplier: 1.5,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Chain Switching
|
||||
|
||||
```typescript
|
||||
import { switchToChain138, ensureChain138 } from '@dbis-thirdweb/wallets';
|
||||
import { useWallet } from '@thirdweb-dev/react';
|
||||
|
||||
function MyComponent() {
|
||||
const wallet = useWallet();
|
||||
|
||||
const handleSwitch = async () => {
|
||||
await switchToChain138(wallet);
|
||||
};
|
||||
|
||||
// Or use ensure (switches only if not already on Chain 138)
|
||||
const handleEnsure = async () => {
|
||||
await ensureChain138(wallet);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Chain 138 wallet configuration defaults
|
||||
- Gas strategy configuration
|
||||
- RPC failover support
|
||||
- Chain switching utilities
|
||||
- Automatic chain addition if missing
|
||||
40
packages/wallets/package.json
Normal file
40
packages/wallets/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@dbis-thirdweb/wallets",
|
||||
"version": "0.1.0",
|
||||
"description": "Wallet connectors and configuration for Chain 138",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"lint": "eslint src",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"keywords": [
|
||||
"thirdweb",
|
||||
"wallet",
|
||||
"chain-138",
|
||||
"connector"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dbis-thirdweb/chain": "workspace:*",
|
||||
"@thirdweb-dev/chains": "^0.1.0",
|
||||
"@thirdweb-dev/react": "^4.0.0",
|
||||
"@thirdweb-dev/sdk": "^4.0.0",
|
||||
"ethers": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
71
packages/wallets/src/chainSwitching.ts
Normal file
71
packages/wallets/src/chainSwitching.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
import type { Chain } from '@thirdweb-dev/chains';
|
||||
|
||||
/**
|
||||
* Wallet interface for chain switching operations
|
||||
* Compatible with thirdweb wallet connectors
|
||||
*/
|
||||
export interface WalletLike {
|
||||
getChainId(): number;
|
||||
switchChain(chainId: number): Promise<void>;
|
||||
addChain(chain: Chain): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch wallet to Chain 138
|
||||
* @param wallet - Connected wallet instance
|
||||
* @returns Promise that resolves when chain switch is complete
|
||||
*/
|
||||
export async function switchToChain138(wallet: WalletLike): Promise<void> {
|
||||
try {
|
||||
// Check if wallet is already on Chain 138
|
||||
if (wallet.getChainId() === chain138.chainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to switch chain
|
||||
await wallet.switchChain(chain138.chainId);
|
||||
} catch (error: unknown) {
|
||||
// If chain doesn't exist in wallet, try to add it
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
(error.code === 4902 || error.code === -32603)
|
||||
) {
|
||||
await addChain138(wallet);
|
||||
await wallet.switchChain(chain138.chainId);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Chain 138 to wallet if it doesn't exist
|
||||
* @param wallet - Connected wallet instance
|
||||
*/
|
||||
export async function addChain138(wallet: WalletLike): Promise<void> {
|
||||
const chain: Chain = chain138;
|
||||
await wallet.addChain(chain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if wallet is connected to Chain 138
|
||||
* @param wallet - Connected wallet instance
|
||||
* @returns true if wallet is on Chain 138
|
||||
*/
|
||||
export function isOnChain138(wallet: WalletLike): boolean {
|
||||
return wallet.getChainId() === chain138.chainId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure wallet is on Chain 138, switching if necessary
|
||||
* @param wallet - Connected wallet instance
|
||||
* @returns Promise that resolves when wallet is on Chain 138
|
||||
*/
|
||||
export async function ensureChain138(wallet: WalletLike): Promise<void> {
|
||||
if (!isOnChain138(wallet)) {
|
||||
await switchToChain138(wallet);
|
||||
}
|
||||
}
|
||||
3
packages/wallets/src/index.ts
Normal file
3
packages/wallets/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './walletConfig';
|
||||
export * from './chainSwitching';
|
||||
export { chain138 } from '@dbis-thirdweb/chain';
|
||||
92
packages/wallets/src/walletConfig.ts
Normal file
92
packages/wallets/src/walletConfig.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
/**
|
||||
* Wallet configuration defaults for Chain 138
|
||||
*/
|
||||
export interface WalletConfig {
|
||||
/**
|
||||
* Number of confirmation blocks required before considering a transaction final
|
||||
* @default 1
|
||||
*/
|
||||
confirmationBlocks: number;
|
||||
|
||||
/**
|
||||
* Gas strategy configuration
|
||||
*/
|
||||
gasStrategy: {
|
||||
/**
|
||||
* Gas price multiplier (1.0 = current gas price, 1.2 = 20% more)
|
||||
* @default 1.2
|
||||
*/
|
||||
multiplier: number;
|
||||
|
||||
/**
|
||||
* Maximum gas price in gwei (0 = no limit)
|
||||
* @default 0
|
||||
*/
|
||||
maxGasPriceGwei: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* RPC failover configuration
|
||||
*/
|
||||
rpcFailover: {
|
||||
/**
|
||||
* Primary RPC endpoint
|
||||
*/
|
||||
primary: string;
|
||||
|
||||
/**
|
||||
* Fallback RPC endpoints (used if primary fails)
|
||||
*/
|
||||
fallbacks: string[];
|
||||
|
||||
/**
|
||||
* Timeout in milliseconds for RPC requests
|
||||
* @default 10000
|
||||
*/
|
||||
timeout: number;
|
||||
|
||||
/**
|
||||
* Number of retries before switching to fallback
|
||||
* @default 2
|
||||
*/
|
||||
retries: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Default wallet configuration for Chain 138
|
||||
*/
|
||||
export const defaultWalletConfig: WalletConfig = {
|
||||
confirmationBlocks: 1,
|
||||
gasStrategy: {
|
||||
multiplier: 1.2,
|
||||
maxGasPriceGwei: 0,
|
||||
},
|
||||
rpcFailover: {
|
||||
primary: chain138.rpc[0] || 'https://138.rpc.thirdweb.com',
|
||||
fallbacks: [],
|
||||
timeout: 10000,
|
||||
retries: 2,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get wallet configuration for Chain 138 with optional overrides
|
||||
*/
|
||||
export function getWalletConfig(overrides?: Partial<WalletConfig>): WalletConfig {
|
||||
return {
|
||||
...defaultWalletConfig,
|
||||
...overrides,
|
||||
gasStrategy: {
|
||||
...defaultWalletConfig.gasStrategy,
|
||||
...overrides?.gasStrategy,
|
||||
},
|
||||
rpcFailover: {
|
||||
...defaultWalletConfig.rpcFailover,
|
||||
...overrides?.rpcFailover,
|
||||
fallbacks: overrides?.rpcFailover?.fallbacks ?? defaultWalletConfig.rpcFailover.fallbacks,
|
||||
},
|
||||
};
|
||||
}
|
||||
12
packages/wallets/tsconfig.json
Normal file
12
packages/wallets/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../chain" }
|
||||
]
|
||||
}
|
||||
43
packages/x402/README.md
Normal file
43
packages/x402/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# @dbis-thirdweb/x402
|
||||
|
||||
x402 payment primitives and pay-to-access flows for Chain 138.
|
||||
|
||||
## Usage
|
||||
|
||||
### Pay-to-Access Flow
|
||||
|
||||
```typescript
|
||||
import { PayToAccessFlow, InMemoryReplayProtectionStore } from '@dbis-thirdweb/x402';
|
||||
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
// Server-side: Create request
|
||||
const sdk = new ThirdwebSDK(chain138);
|
||||
const provider = sdk.getProvider();
|
||||
const replayStore = new InMemoryReplayProtectionStore();
|
||||
|
||||
const flow = new PayToAccessFlow(provider, replayStore);
|
||||
|
||||
const request = await flow.createRequest({
|
||||
amount: ethers.utils.parseEther('0.01'),
|
||||
recipient: '0x...',
|
||||
expiresInSeconds: 3600,
|
||||
});
|
||||
|
||||
const challenge = flow.generateChallenge(request);
|
||||
|
||||
// Client-side: Fulfill payment
|
||||
const signer = await wallet.getSigner();
|
||||
const receipt = await flow.fulfillPayment(challenge, signer);
|
||||
|
||||
// Server-side: Verify payment
|
||||
const isValid = await flow.verifyPayment(receipt, request);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Payment request creation and validation
|
||||
- Replay protection via request ID tracking
|
||||
- Receipt verification on-chain
|
||||
- Pay-to-access flow implementation
|
||||
- Expiration handling
|
||||
38
packages/x402/package.json
Normal file
38
packages/x402/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@dbis-thirdweb/x402",
|
||||
"version": "0.1.0",
|
||||
"description": "x402 payment primitives and pay-to-access flows for Chain 138",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"lint": "eslint src",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"keywords": [
|
||||
"thirdweb",
|
||||
"x402",
|
||||
"payment",
|
||||
"chain-138"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dbis-thirdweb/chain": "workspace:*",
|
||||
"@thirdweb-dev/sdk": "^4.0.0",
|
||||
"ethers": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
118
packages/x402/src/flows/payToAccess.ts
Normal file
118
packages/x402/src/flows/payToAccess.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { Signer, providers } from 'ethers';
|
||||
import { ethers } from 'ethers';
|
||||
import type { PaymentRequest, PaymentChallenge, PaymentReceipt } from '../types';
|
||||
import { validateRequest, type ReplayProtectionStore } from '../replayProtection';
|
||||
import { verifyReceipt, waitForReceipt } from '../receiptVerification';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
/**
|
||||
* Generate payment challenge for x402 pay-to-access flow
|
||||
*/
|
||||
export function generateChallenge(request: PaymentRequest): PaymentChallenge {
|
||||
const nonce = ethers.utils.hexlify(ethers.utils.randomBytes(32));
|
||||
|
||||
// Create message to sign (includes request details + nonce for replay protection)
|
||||
const message = JSON.stringify({
|
||||
requestId: request.requestId,
|
||||
amount: request.amount.toString(),
|
||||
recipient: request.recipient,
|
||||
expiresAt: request.expiresAt,
|
||||
nonce,
|
||||
chainId: chain138.chainId,
|
||||
});
|
||||
|
||||
return {
|
||||
request,
|
||||
nonce,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* x402 Pay-to-Access Flow
|
||||
* HTTP request → x402 challenge → settlement on-chain
|
||||
*/
|
||||
export class PayToAccessFlow {
|
||||
constructor(
|
||||
private provider: providers.Provider,
|
||||
private replayStore: ReplayProtectionStore
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a payment request (server-side)
|
||||
*/
|
||||
async createRequest(params: {
|
||||
amount: bigint;
|
||||
recipient: string;
|
||||
expiresInSeconds?: number;
|
||||
metadata?: string;
|
||||
}): Promise<PaymentRequest> {
|
||||
const requestId = ethers.utils.hexlify(ethers.utils.randomBytes(32));
|
||||
const expiresAt = Math.floor(Date.now() / 1000) + (params.expiresInSeconds || 3600);
|
||||
|
||||
const request: PaymentRequest = {
|
||||
requestId,
|
||||
amount: params.amount,
|
||||
recipient: params.recipient,
|
||||
expiresAt,
|
||||
metadata: params.metadata,
|
||||
};
|
||||
|
||||
// Validate request (check for expiration and replay)
|
||||
await validateRequest(request, this.replayStore);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate challenge from request (server-side)
|
||||
*/
|
||||
generateChallenge(request: PaymentRequest): PaymentChallenge {
|
||||
return generateChallenge(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fulfill payment (client-side with signer)
|
||||
*/
|
||||
async fulfillPayment(
|
||||
challenge: PaymentChallenge,
|
||||
signer: Signer
|
||||
): Promise<PaymentReceipt> {
|
||||
// Validate request before processing
|
||||
await validateRequest(challenge.request, this.replayStore);
|
||||
|
||||
// Create and send transaction
|
||||
const tx = await signer.sendTransaction({
|
||||
to: challenge.request.recipient,
|
||||
value: challenge.request.amount,
|
||||
});
|
||||
|
||||
// Wait for confirmation
|
||||
const receipt = await waitForReceipt(tx.hash, this.provider);
|
||||
|
||||
// Mark request as used to prevent replay
|
||||
await this.replayStore.markAsUsed(challenge.request.requestId);
|
||||
|
||||
return {
|
||||
...receipt,
|
||||
requestId: challenge.request.requestId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify payment receipt (server-side)
|
||||
*/
|
||||
async verifyPayment(
|
||||
receipt: PaymentReceipt,
|
||||
originalRequest: PaymentRequest
|
||||
): Promise<boolean> {
|
||||
const isValid = await verifyReceipt(receipt, originalRequest, this.provider);
|
||||
|
||||
if (isValid) {
|
||||
// Mark as used if verification succeeds
|
||||
await this.replayStore.markAsUsed(originalRequest.requestId);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
4
packages/x402/src/index.ts
Normal file
4
packages/x402/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './types';
|
||||
export * from './replayProtection';
|
||||
export * from './receiptVerification';
|
||||
export * from './flows/payToAccess';
|
||||
72
packages/x402/src/receiptVerification.ts
Normal file
72
packages/x402/src/receiptVerification.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { providers } from 'ethers';
|
||||
import type { PaymentReceipt, PaymentRequest } from './types';
|
||||
import { chain138 } from '@dbis-thirdweb/chain';
|
||||
|
||||
/**
|
||||
* Verify payment receipt on-chain
|
||||
*/
|
||||
export async function verifyReceipt(
|
||||
receipt: PaymentReceipt,
|
||||
request: PaymentRequest,
|
||||
provider: providers.Provider
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Get transaction receipt
|
||||
const txReceipt = await provider.getTransactionReceipt(receipt.txHash);
|
||||
|
||||
if (!txReceipt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify transaction is on correct chain
|
||||
// Note: Provider should already be configured for Chain 138
|
||||
|
||||
// Verify transaction succeeded
|
||||
if (txReceipt.status !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify block number matches
|
||||
if (txReceipt.blockNumber !== receipt.blockNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify amount was transferred (check logs or transaction value)
|
||||
// This is a simplified check - in production, you'd verify specific logs/events
|
||||
const tx = await provider.getTransaction(receipt.txHash);
|
||||
if (tx && tx.value.toString() !== request.amount.toString()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error verifying receipt:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for payment receipt confirmation
|
||||
*/
|
||||
export async function waitForReceipt(
|
||||
txHash: string,
|
||||
provider: providers.Provider,
|
||||
confirmations: number = 1
|
||||
): Promise<PaymentReceipt> {
|
||||
const receipt = await provider.waitForTransaction(txHash, confirmations);
|
||||
|
||||
if (!receipt) {
|
||||
throw new Error(`Transaction ${txHash} not found`);
|
||||
}
|
||||
|
||||
if (receipt.status !== 1) {
|
||||
throw new Error(`Transaction ${txHash} failed`);
|
||||
}
|
||||
|
||||
return {
|
||||
txHash: receipt.transactionHash,
|
||||
blockNumber: receipt.blockNumber,
|
||||
requestId: '', // Should be extracted from transaction data/logs
|
||||
confirmedAt: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
}
|
||||
50
packages/x402/src/replayProtection.ts
Normal file
50
packages/x402/src/replayProtection.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { PaymentRequest } from './types';
|
||||
|
||||
/**
|
||||
* Storage interface for replay protection
|
||||
*/
|
||||
export interface ReplayProtectionStore {
|
||||
hasBeenUsed(requestId: string): Promise<boolean>;
|
||||
markAsUsed(requestId: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory replay protection store (for testing/single-instance use)
|
||||
*/
|
||||
export class InMemoryReplayProtectionStore implements ReplayProtectionStore {
|
||||
private usedRequests = new Set<string>();
|
||||
|
||||
async hasBeenUsed(requestId: string): Promise<boolean> {
|
||||
return this.usedRequests.has(requestId);
|
||||
}
|
||||
|
||||
async markAsUsed(requestId: string): Promise<void> {
|
||||
this.usedRequests.add(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if payment request has expired
|
||||
*/
|
||||
export function isRequestExpired(request: PaymentRequest): boolean {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return request.expiresAt < now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate payment request for replay protection
|
||||
*/
|
||||
export async function validateRequest(
|
||||
request: PaymentRequest,
|
||||
store: ReplayProtectionStore
|
||||
): Promise<void> {
|
||||
// Check expiration
|
||||
if (isRequestExpired(request)) {
|
||||
throw new Error(`Payment request ${request.requestId} has expired`);
|
||||
}
|
||||
|
||||
// Check if already used
|
||||
if (await store.hasBeenUsed(request.requestId)) {
|
||||
throw new Error(`Payment request ${request.requestId} has already been used`);
|
||||
}
|
||||
}
|
||||
76
packages/x402/src/types.ts
Normal file
76
packages/x402/src/types.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { BigNumberish } from 'ethers';
|
||||
|
||||
/**
|
||||
* x402 Payment Request
|
||||
*/
|
||||
export interface PaymentRequest {
|
||||
/**
|
||||
* Unique request ID (prevents replay attacks)
|
||||
*/
|
||||
requestId: string;
|
||||
|
||||
/**
|
||||
* Payment amount in native currency (wei)
|
||||
*/
|
||||
amount: BigNumberish;
|
||||
|
||||
/**
|
||||
* Recipient address
|
||||
*/
|
||||
recipient: string;
|
||||
|
||||
/**
|
||||
* Timestamp when request expires (Unix timestamp in seconds)
|
||||
*/
|
||||
expiresAt: number;
|
||||
|
||||
/**
|
||||
* Optional metadata or description
|
||||
*/
|
||||
metadata?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* x402 Payment Challenge
|
||||
*/
|
||||
export interface PaymentChallenge {
|
||||
/**
|
||||
* Original request
|
||||
*/
|
||||
request: PaymentRequest;
|
||||
|
||||
/**
|
||||
* Nonce for replay protection
|
||||
*/
|
||||
nonce: string;
|
||||
|
||||
/**
|
||||
* Challenge message to sign
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* x402 Payment Receipt
|
||||
*/
|
||||
export interface PaymentReceipt {
|
||||
/**
|
||||
* Transaction hash
|
||||
*/
|
||||
txHash: string;
|
||||
|
||||
/**
|
||||
* Block number where transaction was confirmed
|
||||
*/
|
||||
blockNumber: number;
|
||||
|
||||
/**
|
||||
* Request ID that was fulfilled
|
||||
*/
|
||||
requestId: string;
|
||||
|
||||
/**
|
||||
* Timestamp of confirmation
|
||||
*/
|
||||
confirmedAt: number;
|
||||
}
|
||||
12
packages/x402/tsconfig.json
Normal file
12
packages/x402/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../chain" }
|
||||
]
|
||||
}
|
||||
11046
pnpm-lock.yaml
generated
Normal file
11046
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
- 'apps/*'
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./"
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user