Skip to content

Instantly share code, notes, and snippets.

@MatiFalcone
Created June 24, 2025 15:07
Show Gist options
  • Select an option

  • Save MatiFalcone/6112a52a6c4c1dcbf60af18d4ee8c078 to your computer and use it in GitHub Desktop.

Select an option

Save MatiFalcone/6112a52a6c4c1dcbf60af18d4ee8c078 to your computer and use it in GitHub Desktop.
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