Created
June 24, 2025 15:07
-
-
Save MatiFalcone/6112a52a6c4c1dcbf60af18d4ee8c078 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { AirService } from '@mocanetwork/airkit'; | |
| export const API_BASE_URL = 'https://mv-hub-backend-dev-0358dc390a17.herokuapp.com/api/v1'; | |
| // Types for better type safety | |
| export interface UserProfile { | |
| id: string; | |
| email?: string; | |
| name?: string; | |
| avatarUrl?: string; | |
| createdAt: string; | |
| updatedAt: string; | |
| } | |
| export interface Wallet { | |
| id: string; | |
| address: string; | |
| chainId: number; | |
| isDefault: boolean; | |
| walletType: string; | |
| createdAt: string; | |
| } | |
| export interface Transaction { | |
| id: string; | |
| hash: string; | |
| from: string; | |
| to: string; | |
| value: string; | |
| chainId: number; | |
| timestamp: string; | |
| status: 'pending' | 'confirmed' | 'failed'; | |
| } | |
| // Singleton instance | |
| let airServiceInstance: AirService | null = null; | |
| /** | |
| * Get or create the singleton AirService instance | |
| */ | |
| export function getAirService(): AirService { | |
| if (!airServiceInstance) { | |
| airServiceInstance = new AirService({ | |
| partnerId: "76ec4ea7-c80c-4f40-99e9-540132d1ba2c", | |
| }); | |
| } | |
| return airServiceInstance; | |
| } | |
| export class AirKitApi { | |
| private apiBaseUrl: string; | |
| private service: AirService; | |
| constructor(apiBaseUrl: string = API_BASE_URL) { | |
| this.service = getAirService(); | |
| this.apiBaseUrl = apiBaseUrl; | |
| } | |
| /** | |
| * Add a new user to the Motorverse Hub | |
| * This should be called first after successful Airkit authentication | |
| * @returns The created user data | |
| */ | |
| async addUser(): Promise<any> { | |
| try { | |
| // First get the Airkit JWT token | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch(`${this.apiBaseUrl}/users`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${token}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to add user: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to add user to Motorverse Hub:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get the Airkit JWT token | |
| * @private | |
| */ | |
| private async getAirkitToken(): Promise<string> { | |
| try { | |
| // @ts-ignore - Using type assertion as the method might not be in the types | |
| const tokenResponse = await this.service.getAccessToken() as { token?: string; access_token?: string }; | |
| const token = tokenResponse?.access_token || tokenResponse?.token; | |
| if (!token) { | |
| throw new Error('No access token available'); | |
| } | |
| return token; | |
| } catch (error) { | |
| console.error('Failed to get Airkit token:', error); | |
| throw new Error('Authentication required. Please ensure you are logged in with Airkit.'); | |
| } | |
| } | |
| /** | |
| * Add an email to the current user's profile | |
| * @param email The email address to add | |
| * @returns The updated user data | |
| */ | |
| async addEmail(email: string): Promise<any> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch(`${this.apiBaseUrl}/users/email`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${token}` | |
| }, | |
| body: JSON.stringify({ email }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to add email: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to add email to user profile:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Add a wallet to the current user's profile | |
| * @param address The wallet address to add | |
| * @param signature The signature of the message | |
| * @param message The message that was signed | |
| * @param walletType The type of wallet (e.g., 'metamask') | |
| * @returns The updated user data | |
| */ | |
| async addWallet(address: string, signature: string, message: string, walletType: string = 'metamask'): Promise<any> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch(`${this.apiBaseUrl}/wallets`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${token}` | |
| }, | |
| body: JSON.stringify({ | |
| address, | |
| signature, | |
| message, | |
| walletType | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to add wallet: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to add wallet to user profile:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Remove a wallet from the current user's profile | |
| * @param walletId The ID of the wallet to remove | |
| * @returns True if the wallet was successfully removed | |
| */ | |
| async deleteWallet(walletId: string): Promise<boolean> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch(`${this.apiBaseUrl}/wallets/${encodeURIComponent(walletId)}`, { | |
| method: 'DELETE', | |
| headers: { | |
| 'Authorization': `Bearer ${token}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to delete wallet: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return true; | |
| } catch (error) { | |
| console.error('Error deleting wallet:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Set a wallet as the default wallet for the user | |
| * @param walletId The ID of the wallet to set as default | |
| * @returns True if the operation was successful | |
| */ | |
| async setDefaultWallet(walletId: string): Promise<boolean> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch(`${this.apiBaseUrl}/wallets/${walletId}/set-default`, { | |
| method: 'PATCH', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${token}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to set default wallet: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return true; | |
| } catch (error) { | |
| console.error('Error setting default wallet:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get the current user's information | |
| * @returns The user's data | |
| */ | |
| async getUser(): Promise<any> { | |
| try { | |
| console.log('🔑 Attempting to get Airkit token...'); | |
| const token = await this.getAirkitToken(); | |
| if (!token) { | |
| console.error('❌ No token available for getUser request'); | |
| throw new Error('Authentication required. No token available.'); | |
| } | |
| console.log('🌐 Fetching user data from:', `${this.apiBaseUrl}/users`); | |
| const response = await fetch(`${this.apiBaseUrl}/users`, { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| console.log('this is a response:', response); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| let errorData; | |
| try { | |
| errorData = JSON.parse(errorText); | |
| } catch (e) { | |
| errorData = { message: errorText }; | |
| } | |
| console.error('❌ Failed to get user:', { | |
| status: response.status, | |
| statusText: response.statusText, | |
| error: errorData, | |
| headers: Object.fromEntries(response.headers.entries()) | |
| }); | |
| throw new Error(`Failed to get user: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| const userData = await response.json(); | |
| console.log('✅ Successfully retrieved user data:', userData); | |
| return userData; | |
| } catch (error) { | |
| console.error('❌ Error in getUser:', { | |
| error: error instanceof Error ? { | |
| message: error.message, | |
| name: error.name, | |
| stack: error.stack, | |
| cause: error.cause | |
| } : error, | |
| timestamp: new Date().toISOString() | |
| }); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get the current user's wallets with enhanced details | |
| */ | |
| async getUserWallets(): Promise<Wallet[]> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch(`${this.apiBaseUrl}/wallets`, { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to get user wallets: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to get user wallets:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get wallet details by address | |
| * @param address The wallet address to get details for | |
| */ | |
| async getWalletDetails(address: string): Promise<Wallet> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch(`${this.apiBaseUrl}/wallets/${encodeURIComponent(address)}`, { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to get wallet details: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to get wallet details:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get wallet balance | |
| * @param address The wallet address to get balance for | |
| * @param chainId The chain ID to get balance for | |
| */ | |
| async getWalletBalance(address: string, chainId: number): Promise<{ | |
| address: string; | |
| chainId: number; | |
| balance: string; | |
| tokenBalance?: string; | |
| tokenAddress?: string; | |
| }> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch( | |
| `${this.apiBaseUrl}/wallets/${encodeURIComponent(address)}/balance?chainId=${chainId}`, { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to get wallet balance: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to get wallet balance:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get transaction history for a wallet | |
| * @param address The wallet address | |
| * @param chainId The chain ID | |
| * @param limit Number of transactions to return (default: 10) | |
| * @param offset Pagination offset (default: 0) | |
| */ | |
| async getTransactionHistory( | |
| address: string, | |
| chainId: number, | |
| limit: number = 10, | |
| offset: number = 0 | |
| ): Promise<{ transactions: Transaction[]; total: number }> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch( | |
| `${this.apiBaseUrl}/wallets/${encodeURIComponent(address)}/transactions?` + | |
| `chainId=${chainId}&limit=${limit}&offset=${offset}`, { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to get transaction history: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to get transaction history:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Update user profile | |
| * @param updates Object containing the fields to update | |
| */ | |
| async updateProfile(updates: Partial<{ | |
| name?: string; | |
| email?: string; | |
| avatarUrl?: string; | |
| preferences?: Record<string, any>; | |
| }>): Promise<UserProfile> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| const response = await fetch(`${this.apiBaseUrl}/users`, { | |
| method: 'PATCH', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${token}` | |
| }, | |
| body: JSON.stringify(updates) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to update profile: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to update user profile:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Refresh the authentication token | |
| */ | |
| async refreshToken(): Promise<{ token: string; expiresIn: number }> { | |
| try { | |
| const response = await fetch(`${this.apiBaseUrl}/auth/refresh`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| credentials: 'include' // For httpOnly cookies | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(`Failed to refresh token: ${response.status} ${response.statusText}`, { | |
| cause: errorData | |
| }); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Failed to refresh token:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Logout the current user | |
| */ | |
| async logout(): Promise<void> { | |
| try { | |
| const token = await this.getAirkitToken(); | |
| await fetch(`${this.apiBaseUrl}/auth/logout`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| // Clear any local tokens or state | |
| // @ts-ignore - Using type assertion as the method might not be in the types | |
| if (this.service.logout) { | |
| // @ts-ignore | |
| await this.service.logout(); | |
| } | |
| } catch (error) { | |
| console.error('Failed to logout:', error); | |
| // Even if logout fails on the server, we should clear local state | |
| throw error; | |
| } | |
| } | |
| } | |
| // Create a singleton instance | |
| export const airkitApi = new AirKitApi(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment