This commit is contained in:
2026-04-05 00:43:23 +05:30
commit 8be37d3e92
425 changed files with 101853 additions and 0 deletions

106
dmtp/client/lib/api.ts Normal file
View File

@@ -0,0 +1,106 @@
import axios from 'axios';
import { AuthService } from './auth';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
export const apiClient = axios.create({
baseURL: `${API_BASE_URL}/api/v1`,
headers: {
'Content-Type': 'application/json',
},
});
// Store for authentication callbacks
let authModalCallbacks: Array<() => void> = [];
export const triggerAuthModal = (callback?: () => void) => {
if (callback) {
authModalCallbacks.push(callback);
}
// Dispatch a custom event that components can listen to
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('auth-required'));
}
};
export const onAuthSuccess = () => {
authModalCallbacks.forEach(cb => cb());
authModalCallbacks = [];
};
// Add auth interceptor
if (typeof window !== 'undefined') {
apiClient.interceptors.request.use((config) => {
const walletAddress = localStorage.getItem('walletAddress');
const signature = localStorage.getItem('signature');
const message = localStorage.getItem('message');
const timestamp = localStorage.getItem('timestamp');
if (walletAddress && signature && message && timestamp) {
config.headers['X-Wallet-Address'] = walletAddress;
config.headers['X-Signature'] = signature;
// Base64 encode the message to handle newlines and special characters
config.headers['X-Message'] = btoa(encodeURIComponent(message));
config.headers['X-Timestamp'] = timestamp;
}
return config;
});
// Add response interceptor to handle auth errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Clear invalid auth data
AuthService.clearAuth();
// Trigger re-authentication
console.error('Authentication expired. Please reconnect your wallet.');
triggerAuthModal();
}
return Promise.reject(error);
}
);
}
export const api = {
tasks: {
list: async (params?: any) => {
const response = await apiClient.get('/tasks/list', { params });
return response.data;
},
getById: async (taskId: string) => {
const response = await apiClient.get(`/tasks/${taskId}`);
return response.data;
},
create: async (data: any) => {
const response = await apiClient.post('/tasks/create', data);
return response.data;
},
},
submissions: {
submit: async (data: any) => {
const response = await apiClient.post('/submissions/submit', data);
return response.data;
},
getStatus: async (submissionId: string) => {
const response = await apiClient.get(`/submissions/${submissionId}/status`);
return response.data;
},
mySubmissions: async () => {
const response = await apiClient.get('/submissions/my/submissions');
return response.data;
},
},
users: {
register: async (data: any) => {
const response = await apiClient.post('/users/register', data);
return response.data;
},
getProfile: async () => {
const response = await apiClient.get('/users/profile');
return response.data;
},
},
};

73
dmtp/client/lib/auth.ts Normal file
View File

@@ -0,0 +1,73 @@
/**
* Authentication utilities for wallet-based auth
*/
export class AuthService {
private static readonly AUTH_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes, matching server
/**
* Check if user is authenticated with valid credentials
*/
static isAuthenticated(): boolean {
if (typeof window === 'undefined') return false;
const walletAddress = localStorage.getItem('walletAddress');
const signature = localStorage.getItem('signature');
const timestamp = localStorage.getItem('timestamp');
if (!walletAddress || !signature || !timestamp) {
return false;
}
// Check if timestamp is still valid
const authTimestamp = parseInt(timestamp);
const now = Date.now();
const age = now - authTimestamp;
return age <= this.AUTH_EXPIRY_MS;
}
/**
* Clear authentication data
*/
static clearAuth(): void {
if (typeof window === 'undefined') return;
localStorage.removeItem('walletAddress');
localStorage.removeItem('signature');
localStorage.removeItem('message');
localStorage.removeItem('timestamp');
}
/**
* Get stored wallet address
*/
static getWalletAddress(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('walletAddress');
}
/**
* Store authentication data
*/
static storeAuth(
walletAddress: string,
signature: string,
message: string,
timestamp: number
): void {
if (typeof window === 'undefined') return;
localStorage.setItem('walletAddress', walletAddress);
localStorage.setItem('signature', signature);
localStorage.setItem('message', message);
localStorage.setItem('timestamp', timestamp.toString());
}
/**
* Generate authentication message for signing
*/
static generateAuthMessage(walletAddress: string, timestamp: number): string {
return `Sign this message to authenticate with Celo Task Marketplace.\n\nWallet: ${walletAddress}\nTimestamp: ${timestamp}\n\nThis request will not trigger a blockchain transaction or cost any gas fees.`;
}
}

66
dmtp/client/lib/celo.ts Normal file
View File

@@ -0,0 +1,66 @@
import { ethers } from 'ethers';
// Network configurations
export const CELO_NETWORKS = {
mainnet: {
chainId: 42220,
name: 'Celo Mainnet',
rpcUrl: 'https://forno.celo.org',
blockExplorer: 'https://celoscan.io',
cUSDAddress: '0x765DE816845861e75A25fCA122bb6898B8B1282a',
},
sepolia: {
chainId: 11142220,
name: 'Celo Sepolia Testnet',
rpcUrl: 'https://forno.celo-sepolia.celo-testnet.org',
blockExplorer: 'https://sepolia.celoscan.io',
cUSDAddress: '0x874069fa1eb16d44d622f2e0ca25eea172369bc1',
},
};
// Get current network config
export function getCurrentNetwork() {
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '11142220');
switch (chainId) {
case 42220:
return CELO_NETWORKS.mainnet;
case 11142220:
return CELO_NETWORKS.sepolia;
default:
return CELO_NETWORKS.sepolia;
}
}
// Get cUSD token address
export function getCUSDAddress(): string {
return getCurrentNetwork().cUSDAddress;
}
// Get block explorer URL
export function getExplorerUrl(txHash: string): string {
return `${getCurrentNetwork().blockExplorer}/tx/${txHash}`;
}
// Format address
export function formatAddress(address: string): string {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
// Check if address is valid
export function isValidAddress(address: string): boolean {
try {
return ethers.isAddress(address);
} catch {
return false;
}
}
// Parse error message
export function parseErrorMessage(error: any): string {
if (error.reason) return error.reason;
if (error.message) return error.message;
if (typeof error === 'string') return error;
return 'Transaction failed';
}

View File

@@ -0,0 +1,49 @@
import { ethers } from 'ethers';
import { getCUSDAddress, getCurrentNetwork } from './celo';
// Simplified cUSD ABI (ERC20)
export const CUSD_ABI = [
'function balanceOf(address) view returns (uint256)',
'function decimals() view returns (uint8)',
'function approve(address spender, uint256 amount) returns (bool)',
'function allowance(address owner, address spender) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
];
// TaskEscrow contract ABI
export const TASK_ESCROW_ABI = [
'function createTask(uint256 paymentAmount, uint256 durationInDays) returns (uint256)',
'function approveSubmission(uint256 taskId)',
'function rejectSubmission(uint256 taskId)',
'function getTask(uint256 taskId) view returns (tuple(uint256 taskId, address requester, address worker, uint256 paymentAmount, uint8 status, uint256 createdAt, uint256 expiresAt))',
'function taskCounter() view returns (uint256)',
'event TaskCreated(uint256 indexed taskId, address indexed requester, uint256 paymentAmount, uint256 expiresAt)',
'event PaymentReleased(uint256 indexed taskId, address indexed worker, uint256 workerAmount, uint256 platformFee)',
];
/**
* Get cUSD contract instance
*/
export function getCUSDContract(signerOrProvider: ethers.Signer | ethers.Provider) {
return new ethers.Contract(getCUSDAddress(), CUSD_ABI, signerOrProvider);
}
/**
* Get TaskEscrow contract instance
*/
export function getTaskEscrowContract(signerOrProvider: ethers.Signer | ethers.Provider) {
const contractAddress = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS;
if (!contractAddress) {
throw new Error('Contract address not configured');
}
return new ethers.Contract(contractAddress, TASK_ESCROW_ABI, signerOrProvider);
}
/**
* Get provider
*/
export function getProvider(): ethers.JsonRpcProvider {
return new ethers.JsonRpcProvider(getCurrentNetwork().rpcUrl);
}

View File

@@ -0,0 +1,57 @@
declare global {
interface Window {
ethereum?: any;
}
}
export interface MiniPayProvider {
isMiniPay: boolean;
isMetaMask?: boolean;
}
/**
* Check if running inside MiniPay app
*/
export function isMiniPay(): boolean {
if (typeof window === 'undefined') return false;
return Boolean(window.ethereum?.isMiniPay);
}
/**
* Check if MetaMask is available
*/
export function isMetaMask(): boolean {
if (typeof window === 'undefined') return false;
return Boolean(window.ethereum?.isMetaMask && !window.ethereum?.isMiniPay);
}
/**
* Get wallet provider name
*/
export function getWalletProvider(): string {
if (isMiniPay()) return 'MiniPay';
if (isMetaMask()) return 'MetaMask';
return 'Unknown';
}
/**
* Check if any wallet is available
*/
export function isWalletAvailable(): boolean {
if (typeof window === 'undefined') return false;
return Boolean(window.ethereum);
}
/**
* Get wallet installation URL
*/
export function getWalletInstallUrl(): string {
// Check if mobile
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
return 'https://minipay.opera.com/';
}
return 'https://metamask.io/download/';
}

35
dmtp/client/lib/utils.ts Normal file
View File

@@ -0,0 +1,35 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const formatAddress = (address: string): string => {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
export const formatCurrency = (amount: number): string => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
}).format(amount);
};
export const formatTimeRemaining = (expiresAt: string): string => {
const now = new Date().getTime();
const expiry = new Date(expiresAt).getTime();
const diff = expiry - now;
if (diff <= 0) return 'Expired';
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
if (days > 0) return `${days}d ${hours}h`;
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
};