mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
init
This commit is contained in:
106
dmtp/client/lib/api.ts
Normal file
106
dmtp/client/lib/api.ts
Normal 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
73
dmtp/client/lib/auth.ts
Normal 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
66
dmtp/client/lib/celo.ts
Normal 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';
|
||||
}
|
||||
49
dmtp/client/lib/contracts.ts
Normal file
49
dmtp/client/lib/contracts.ts
Normal 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);
|
||||
}
|
||||
57
dmtp/client/lib/minipay.ts
Normal file
57
dmtp/client/lib/minipay.ts
Normal 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
35
dmtp/client/lib/utils.ts
Normal 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`;
|
||||
};
|
||||
Reference in New Issue
Block a user