Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- services/api.ts +152 -0
- services/cryptoService.ts +161 -0
- services/geminiService.ts +182 -0
- services/nftService.ts +81 -0
services/api.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
InternalAccount,
|
| 3 |
+
CitiTransaction,
|
| 4 |
+
CustomerProfileResponse,
|
| 5 |
+
AuthResponse,
|
| 6 |
+
UserSession,
|
| 7 |
+
AccountsGroupDetailsList
|
| 8 |
+
} from '../types/index';
|
| 9 |
+
|
| 10 |
+
// Unified mock account data consistent with Citi OpenAPI structures
|
| 11 |
+
const MOCK_ACCOUNTS_LIST: InternalAccount[] = [
|
| 12 |
+
{
|
| 13 |
+
id: 'citi_acc_99201',
|
| 14 |
+
productName: 'Corporate Mastery Checking',
|
| 15 |
+
accountNickname: 'Main Ops Node',
|
| 16 |
+
displayAccountNumber: 'XXXXXX9594',
|
| 17 |
+
currency: 'USD',
|
| 18 |
+
status: 'ACTIVE',
|
| 19 |
+
currentBalance: 1245000.50,
|
| 20 |
+
availableBalance: 1240000.00,
|
| 21 |
+
institutionName: 'Citi US',
|
| 22 |
+
connectionId: 'CITI-G-001'
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
id: 'citi_acc_44102',
|
| 26 |
+
productName: 'Elite Treasury Savings',
|
| 27 |
+
displayAccountNumber: 'XXXXXX1022',
|
| 28 |
+
currency: 'USD',
|
| 29 |
+
status: 'ACTIVE',
|
| 30 |
+
currentBalance: 5200450.00,
|
| 31 |
+
availableBalance: 5200450.00,
|
| 32 |
+
institutionName: 'Citi US',
|
| 33 |
+
connectionId: 'CITI-G-002'
|
| 34 |
+
}
|
| 35 |
+
];
|
| 36 |
+
|
| 37 |
+
export const apiClient = {
|
| 38 |
+
auth: {
|
| 39 |
+
async me(): Promise<AuthResponse> {
|
| 40 |
+
const user = localStorage.getItem('lumina_user');
|
| 41 |
+
return user ? { isAuthenticated: true, user: JSON.parse(user) } : { isAuthenticated: false, user: null };
|
| 42 |
+
},
|
| 43 |
+
async register(username: string, password: string) {
|
| 44 |
+
const users = JSON.parse(localStorage.getItem('lumina_registry') || '{}');
|
| 45 |
+
if (users[username]) return { success: false, error: 'Identity already exists.' };
|
| 46 |
+
users[username] = { password, role: 'Root Admin' };
|
| 47 |
+
localStorage.setItem('lumina_registry', JSON.stringify(users));
|
| 48 |
+
return { success: true };
|
| 49 |
+
},
|
| 50 |
+
async login(username: string, password?: string) {
|
| 51 |
+
const users = JSON.parse(localStorage.getItem('lumina_registry') || '{"alex":{"password":"password123","role":"Root Admin"}}');
|
| 52 |
+
if (users[username] && users[username].password === password) {
|
| 53 |
+
const user = { id: 'USR-1', name: username, role: users[username].role, lastLogin: new Date().toISOString() };
|
| 54 |
+
localStorage.setItem('lumina_user', JSON.stringify(user));
|
| 55 |
+
return { success: true, user };
|
| 56 |
+
}
|
| 57 |
+
return { success: false, error: 'Identity rejected credentials.' };
|
| 58 |
+
},
|
| 59 |
+
async logout() {
|
| 60 |
+
localStorage.removeItem('lumina_user');
|
| 61 |
+
return { success: true };
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
chat: {
|
| 65 |
+
async getHistory() {
|
| 66 |
+
return JSON.parse(localStorage.getItem('lumina_chat_history') || '[]');
|
| 67 |
+
},
|
| 68 |
+
async saveMessage(role: string, content: string) {
|
| 69 |
+
const history = JSON.parse(localStorage.getItem('lumina_chat_history') || '[]');
|
| 70 |
+
history.push({ id: Date.now(), role, content, timestamp: new Date().toISOString() });
|
| 71 |
+
localStorage.setItem('lumina_chat_history', JSON.stringify(history));
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
async getRegistryNodes(): Promise<InternalAccount[]> {
|
| 75 |
+
return MOCK_ACCOUNTS_LIST;
|
| 76 |
+
},
|
| 77 |
+
async getRegistryDetails(): Promise<AccountsGroupDetailsList> {
|
| 78 |
+
return {
|
| 79 |
+
accountGroupDetails: [
|
| 80 |
+
{
|
| 81 |
+
accountGroup: "CHECKING",
|
| 82 |
+
checkingAccountsDetails: MOCK_ACCOUNTS_LIST.filter(a => a.productName.includes('Checking')).map(a => ({
|
| 83 |
+
accountId: a.id,
|
| 84 |
+
productName: a.productName,
|
| 85 |
+
displayAccountNumber: a.displayAccountNumber,
|
| 86 |
+
currencyCode: a.currency,
|
| 87 |
+
accountStatus: a.status as 'ACTIVE',
|
| 88 |
+
currentBalance: a.currentBalance,
|
| 89 |
+
availableBalance: a.availableBalance,
|
| 90 |
+
accountDescription: a.productName,
|
| 91 |
+
balanceType: 'ASSET'
|
| 92 |
+
}))
|
| 93 |
+
}
|
| 94 |
+
]
|
| 95 |
+
};
|
| 96 |
+
},
|
| 97 |
+
async getTransactions(accountId: string): Promise<CitiTransaction[]> {
|
| 98 |
+
return [
|
| 99 |
+
{
|
| 100 |
+
accountId,
|
| 101 |
+
currencyCode: 'USD',
|
| 102 |
+
transactionAmount: -25000.00,
|
| 103 |
+
transactionDate: new Date().toISOString().split('T')[0],
|
| 104 |
+
transactionDescription: 'QUANTUM_COMPUTE_Q3_ALLOCATION',
|
| 105 |
+
transactionId: 'TXN_C_' + Math.random().toString(36).substring(7).toUpperCase(),
|
| 106 |
+
transactionStatus: 'POSTED',
|
| 107 |
+
transactionType: 'PAYMENT',
|
| 108 |
+
displayAccountNumber: 'XXXXXX9594'
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
accountId,
|
| 112 |
+
currencyCode: 'USD',
|
| 113 |
+
transactionAmount: 14200.50,
|
| 114 |
+
transactionDate: new Date().toISOString().split('T')[0],
|
| 115 |
+
transactionDescription: 'NODE_ALPHA_INCENTIVE_PAYOUT',
|
| 116 |
+
transactionId: 'TXN_C_' + Math.random().toString(36).substring(7).toUpperCase(),
|
| 117 |
+
transactionStatus: 'POSTED',
|
| 118 |
+
transactionType: 'CREDIT',
|
| 119 |
+
displayAccountNumber: 'XXXXXX9594'
|
| 120 |
+
}
|
| 121 |
+
];
|
| 122 |
+
},
|
| 123 |
+
async getStatements(): Promise<any> {
|
| 124 |
+
return {
|
| 125 |
+
statements: [
|
| 126 |
+
{ statementId: 'STMT-001', statementDate: '2024-03-01', productFamily: 'Checking', accountId: 'citi_acc_99201' },
|
| 127 |
+
{ statementId: 'STMT-002', statementDate: '2024-02-01', productFamily: 'Checking', accountId: 'citi_acc_99201' }
|
| 128 |
+
]
|
| 129 |
+
};
|
| 130 |
+
},
|
| 131 |
+
async getStatementDetails(id: string): Promise<any> {
|
| 132 |
+
return {
|
| 133 |
+
dataPayload: JSON.stringify({
|
| 134 |
+
encryptedPayload: {
|
| 135 |
+
header: { alg: 'RSA-OAEP-4096', typ: 'JWT' },
|
| 136 |
+
iv: 'q7x2...m9l0',
|
| 137 |
+
data: 'base64_payload_artifact'
|
| 138 |
+
}
|
| 139 |
+
})
|
| 140 |
+
};
|
| 141 |
+
},
|
| 142 |
+
async getCustomerProfile(accountId: string): Promise<CustomerProfileResponse> {
|
| 143 |
+
return {
|
| 144 |
+
customer: { firstName: 'Alex', lastName: 'Rivera', title: 'Mx.', companyName: 'Lumina Quantum Systems' },
|
| 145 |
+
contacts: {
|
| 146 |
+
emails: ['a.rivera@luminaquantum.io'],
|
| 147 |
+
addresses: [{ addressLine1: '401 Quantum Drive', city: 'Palo Alto', region: 'CA', postalCode: '94304', country: 'US', type: 'BUSINESS' }],
|
| 148 |
+
phones: [{ type: 'CELL', country: '1', number: '9542312002' }]
|
| 149 |
+
}
|
| 150 |
+
};
|
| 151 |
+
}
|
| 152 |
+
};
|
services/cryptoService.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { CoinMarketData, CoinDetail, GlobalData, TrendingCoin, SearchResult } from '../types/index.ts';
|
| 3 |
+
|
| 4 |
+
const COINGECKO_BASE = 'https://api.coingecko.com/api/v3';
|
| 5 |
+
|
| 6 |
+
export interface MarketChartData {
|
| 7 |
+
prices: [number, number][];
|
| 8 |
+
market_caps: [number, number][];
|
| 9 |
+
total_volumes: [number, number][];
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const MOCK_MARKETS: CoinMarketData[] = [
|
| 13 |
+
{ id: 'bitcoin', symbol: 'btc', name: 'Bitcoin', image: 'https://assets.coingecko.com/coins/images/1/large/bitcoin.png', current_price: 64231.50, market_cap: 1260000000000, market_cap_rank: 1, fully_diluted_valuation: 1350000000000, total_volume: 35000000000, high_24h: 65000, low_24h: 63000, price_change_24h: 1231, price_change_percentage_24h: 1.95, market_cap_change_24h: 24000000000, market_cap_change_percentage_24h: 1.9, circulating_supply: 19600000, total_supply: 21000000, max_supply: 21000000, ath: 73000, ath_change_percentage: -12, ath_date: '2024-03-14', atl: 67, atl_change_percentage: 95000, atl_date: '2013-07-06', roi: null, last_updated: new Date().toISOString(), sparkline_in_7d: { price: [63000, 63500, 64000, 63800, 64500, 65000, 64231] } },
|
| 14 |
+
{ id: 'ethereum', symbol: 'eth', name: 'Ethereum', image: 'https://assets.coingecko.com/coins/images/279/large/ethereum.png', current_price: 3450.20, market_cap: 415000000000, market_cap_rank: 2, fully_diluted_valuation: 415000000000, total_volume: 15000000000, high_24h: 3550, low_24h: 3350, price_change_24h: 50, price_change_percentage_24h: 1.45, market_cap_change_24h: 5000000000, market_cap_change_percentage_24h: 1.4, circulating_supply: 120000000, total_supply: 120000000, max_supply: null, ath: 4878, ath_change_percentage: -29, ath_date: '2021-11-10', atl: 0.42, atl_change_percentage: 820000, atl_date: '2015-10-20', roi: null, last_updated: new Date().toISOString(), sparkline_in_7d: { price: [3300, 3350, 3400, 3380, 3450, 3500, 3450] } },
|
| 15 |
+
{ id: 'solana', symbol: 'sol', name: 'Solana', image: 'https://assets.coingecko.com/coins/images/4128/large/solana.png', current_price: 145.50, market_cap: 65000000000, market_cap_rank: 5, fully_diluted_valuation: 82000000000, total_volume: 4000000000, high_24h: 155, low_24h: 140, price_change_24h: -5, price_change_percentage_24h: -3.2, market_cap_change_24h: -2000000000, market_cap_change_percentage_24h: -3.1, circulating_supply: 445000000, total_supply: 570000000, max_supply: null, ath: 259, ath_change_percentage: -44, ath_date: '2021-11-06', atl: 0.50, atl_change_percentage: 29000, atl_date: '2020-05-11', roi: null, last_updated: new Date().toISOString(), sparkline_in_7d: { price: [150, 155, 152, 148, 145, 146, 145] } }
|
| 16 |
+
];
|
| 17 |
+
|
| 18 |
+
export const cryptoService = {
|
| 19 |
+
ping: async () => {
|
| 20 |
+
try {
|
| 21 |
+
const res = await fetch(`${COINGECKO_BASE}/ping`, { mode: 'cors' });
|
| 22 |
+
return res.ok;
|
| 23 |
+
} catch {
|
| 24 |
+
return false;
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
|
| 28 |
+
getMarkets: async (vsCurrency: string = 'usd', perPage: number = 10, page: number = 1): Promise<CoinMarketData[]> => {
|
| 29 |
+
try {
|
| 30 |
+
const res = await fetch(
|
| 31 |
+
`${COINGECKO_BASE}/coins/markets?vs_currency=${vsCurrency}&order=market_cap_desc&per_page=${perPage}&page=${page}&sparkline=true&price_change_percentage=24h,7d`,
|
| 32 |
+
{ mode: 'cors' }
|
| 33 |
+
);
|
| 34 |
+
if (!res.ok) throw new Error('Market data fetch failed');
|
| 35 |
+
return await res.json();
|
| 36 |
+
} catch (error) {
|
| 37 |
+
console.warn("Crypto API unavailable, using simulated node data", error);
|
| 38 |
+
return MOCK_MARKETS.slice(0, perPage);
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
|
| 42 |
+
getCoinById: async (id: string): Promise<CoinDetail | null> => {
|
| 43 |
+
try {
|
| 44 |
+
const res = await fetch(`${COINGECKO_BASE}/coins/${id}?localization=false&tickers=true&market_data=true&community_data=true&developer_data=true&sparkline=true`, { mode: 'cors' });
|
| 45 |
+
if (!res.ok) throw new Error('Coin details fetch failed');
|
| 46 |
+
return await res.json();
|
| 47 |
+
} catch (error) {
|
| 48 |
+
console.warn(`Details for ${id} unavailable`, error);
|
| 49 |
+
const mock = MOCK_MARKETS.find(m => m.id === id) as any;
|
| 50 |
+
if (mock) {
|
| 51 |
+
return {
|
| 52 |
+
...mock,
|
| 53 |
+
description: { en: `Institutional-grade ledger summary for ${mock.name}. Handshake verified.` },
|
| 54 |
+
links: { homepage: ['#'], blockchain_site: ['#'], official_forum_url: [], chat_url: [], announcement_url: [], twitter_screen_name: '', facebook_username: '', bitcointalk_thread_identifier: null, telegram_channel_identifier: '', subreddit_url: '', repos_url: { github: [], bitbucket: [] } },
|
| 55 |
+
genesis_date: '2009-01-03',
|
| 56 |
+
sentiment_votes_up_percentage: 85,
|
| 57 |
+
sentiment_votes_down_percentage: 15
|
| 58 |
+
};
|
| 59 |
+
}
|
| 60 |
+
return null;
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
|
| 64 |
+
getMarketChart: async (id: string, days: string = '7', vsCurrency: string = 'usd'): Promise<MarketChartData | null> => {
|
| 65 |
+
try {
|
| 66 |
+
const res = await fetch(`${COINGECKO_BASE}/coins/${id}/market_chart?vs_currency=${vsCurrency}&days=${days}`, { mode: 'cors' });
|
| 67 |
+
if (!res.ok) throw new Error('Market chart fetch failed');
|
| 68 |
+
return await res.json();
|
| 69 |
+
} catch (error) {
|
| 70 |
+
console.warn(`Chart data for ${id} unavailable`, error);
|
| 71 |
+
const mock = MOCK_MARKETS.find(m => m.id === id);
|
| 72 |
+
if (mock) {
|
| 73 |
+
const now = Date.now();
|
| 74 |
+
const prices: [number, number][] = mock.sparkline_in_7d!.price.map((p, i) => [now - (7 - i) * 86400000, p]);
|
| 75 |
+
return { prices, market_caps: [], total_volumes: [] };
|
| 76 |
+
}
|
| 77 |
+
return null;
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
|
| 81 |
+
getGlobal: async (): Promise<GlobalData | null> => {
|
| 82 |
+
try {
|
| 83 |
+
const res = await fetch(`${COINGECKO_BASE}/global`, { mode: 'cors' });
|
| 84 |
+
if (!res.ok) throw new Error('Global data fetch failed');
|
| 85 |
+
const data = await res.json();
|
| 86 |
+
return data.data;
|
| 87 |
+
} catch (error) {
|
| 88 |
+
console.warn("Global market metrics unavailable", error);
|
| 89 |
+
return {
|
| 90 |
+
active_cryptocurrencies: 12400,
|
| 91 |
+
upcoming_icos: 0,
|
| 92 |
+
ongoing_icos: 42,
|
| 93 |
+
ended_icos: 3401,
|
| 94 |
+
markets: 900,
|
| 95 |
+
total_market_cap: { usd: 2400000000000 },
|
| 96 |
+
total_volume: { usd: 85000000000 },
|
| 97 |
+
market_cap_percentage: { btc: 52.1, eth: 17.2 },
|
| 98 |
+
market_cap_change_percentage_24h_usd: 1.2,
|
| 99 |
+
updated_at: Date.now() / 1000
|
| 100 |
+
};
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
|
| 104 |
+
getTrending: async (): Promise<TrendingCoin[]> => {
|
| 105 |
+
try {
|
| 106 |
+
const res = await fetch(`${COINGECKO_BASE}/search/trending`, { mode: 'cors' });
|
| 107 |
+
if (!res.ok) throw new Error('Trending fetch failed');
|
| 108 |
+
const data = await res.json();
|
| 109 |
+
return data.coins;
|
| 110 |
+
} catch (error) {
|
| 111 |
+
console.warn("Trending data unavailable", error);
|
| 112 |
+
return MOCK_MARKETS.map((m, i) => ({
|
| 113 |
+
item: {
|
| 114 |
+
id: m.id,
|
| 115 |
+
coin_id: i,
|
| 116 |
+
name: m.name,
|
| 117 |
+
symbol: m.symbol,
|
| 118 |
+
market_cap_rank: m.market_cap_rank,
|
| 119 |
+
thumb: m.image,
|
| 120 |
+
small: m.image,
|
| 121 |
+
large: m.image,
|
| 122 |
+
slug: m.id,
|
| 123 |
+
price_btc: 1,
|
| 124 |
+
score: i,
|
| 125 |
+
data: {
|
| 126 |
+
price: m.current_price,
|
| 127 |
+
price_btc: '1',
|
| 128 |
+
price_change_percentage_24h: { usd: m.price_change_percentage_24h },
|
| 129 |
+
market_cap: m.market_cap.toString(),
|
| 130 |
+
total_volume: m.total_volume.toString(),
|
| 131 |
+
sparkline: ''
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
}));
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
|
| 138 |
+
search: async (query: string): Promise<SearchResult | null> => {
|
| 139 |
+
try {
|
| 140 |
+
const res = await fetch(`${COINGECKO_BASE}/search?query=${query}`, { mode: 'cors' });
|
| 141 |
+
if (!res.ok) throw new Error('Search failed');
|
| 142 |
+
return await res.json();
|
| 143 |
+
} catch (error) {
|
| 144 |
+
console.warn(`Search for ${query} unavailable`, error);
|
| 145 |
+
return {
|
| 146 |
+
coins: MOCK_MARKETS.filter(m => m.name.toLowerCase().includes(query.toLowerCase())).map(m => ({
|
| 147 |
+
id: m.id,
|
| 148 |
+
name: m.name,
|
| 149 |
+
api_symbol: m.symbol,
|
| 150 |
+
symbol: m.symbol,
|
| 151 |
+
market_cap_rank: m.market_cap_rank,
|
| 152 |
+
thumb: m.image,
|
| 153 |
+
large: m.image
|
| 154 |
+
})),
|
| 155 |
+
exchanges: [],
|
| 156 |
+
nfts: [],
|
| 157 |
+
categories: []
|
| 158 |
+
};
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
};
|
services/geminiService.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { GoogleGenAI, Type, Modality } from "@google/genai";
|
| 3 |
+
import { SimulationResult, AIInsight } from "../types/index";
|
| 4 |
+
|
| 5 |
+
// Direct initialization as per instructions
|
| 6 |
+
const getAI = () => new GoogleGenAI({ apiKey: process.env.API_KEY as string });
|
| 7 |
+
|
| 8 |
+
export { Type, Modality };
|
| 9 |
+
|
| 10 |
+
export const TTS_LANGUAGES = [
|
| 11 |
+
{ name: 'English', code: 'en' }, { name: 'French', code: 'fr' }, { name: 'German', code: 'de' },
|
| 12 |
+
{ name: 'Spanish', code: 'es' }, { name: 'Portuguese', code: 'pt' }, { name: 'Chinese', code: 'zh' },
|
| 13 |
+
{ name: 'Japanese', code: 'ja' }, { name: 'Korean', code: 'ko' }, { name: 'Hindi', code: 'hi' },
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
export const TTS_VOICES = [
|
| 17 |
+
{ name: 'Zephyr', style: 'Bright' }, { name: 'Puck', style: 'Upbeat' }, { name: 'Charon', style: 'Informative' },
|
| 18 |
+
{ name: 'Kore', style: 'Firm' }, { name: 'Fenrir', style: 'Excitable' }, { name: 'Leda', style: 'Youthful' }
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
function decodeBase64(base64: string) {
|
| 22 |
+
const binaryString = atob(base64);
|
| 23 |
+
const bytes = new Uint8Array(binaryString.length);
|
| 24 |
+
for (let i = 0; i < binaryString.length; i++) {
|
| 25 |
+
bytes[i] = binaryString.charCodeAt(i);
|
| 26 |
+
}
|
| 27 |
+
return bytes;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
async function decodeAudioData(data: Uint8Array, ctx: AudioContext, sampleRate: number, numChannels: number): Promise<AudioBuffer> {
|
| 31 |
+
const byteLen = data.byteLength - (data.byteLength % 2);
|
| 32 |
+
const dataInt16 = new Int16Array(data.buffer, 0, byteLen / 2);
|
| 33 |
+
const frameCount = dataInt16.length / numChannels;
|
| 34 |
+
const buffer = ctx.createBuffer(numChannels, frameCount, sampleRate);
|
| 35 |
+
for (let channel = 0; channel < numChannels; channel++) {
|
| 36 |
+
const channelData = buffer.getChannelData(channel);
|
| 37 |
+
for (let i = 0; i < frameCount; i++) {
|
| 38 |
+
channelData[i] = dataInt16[i * numChannels + channel] / 32768.0;
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
return buffer;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
let audioContext: AudioContext | null = null;
|
| 45 |
+
export const getAudioContext = () => {
|
| 46 |
+
if (!audioContext) {
|
| 47 |
+
audioContext = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: 24000 });
|
| 48 |
+
}
|
| 49 |
+
return audioContext;
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
// fix: Added support for multi-speaker synthesis and updated config type signature
|
| 53 |
+
export const synthesizeSpeech = async (config: {
|
| 54 |
+
text: string,
|
| 55 |
+
voiceName: string,
|
| 56 |
+
directorNotes?: string,
|
| 57 |
+
multiSpeaker?: { speaker1: string, voice1: string, speaker2: string, voice2: string }
|
| 58 |
+
}) => {
|
| 59 |
+
try {
|
| 60 |
+
const ai = getAI();
|
| 61 |
+
const promptText = config.directorNotes ? `${config.directorNotes} ${config.text}` : config.text;
|
| 62 |
+
|
| 63 |
+
// fix: Define speechConfig based on presence of multi-speaker configuration
|
| 64 |
+
const speechConfig: any = config.multiSpeaker ? {
|
| 65 |
+
multiSpeakerVoiceConfig: {
|
| 66 |
+
speakerVoiceConfigs: [
|
| 67 |
+
{
|
| 68 |
+
speaker: config.multiSpeaker.speaker1,
|
| 69 |
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: config.multiSpeaker.voice1 } }
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
speaker: config.multiSpeaker.speaker2,
|
| 73 |
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: config.multiSpeaker.voice2 } }
|
| 74 |
+
}
|
| 75 |
+
]
|
| 76 |
+
}
|
| 77 |
+
} : {
|
| 78 |
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: config.voiceName } }
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const response = await ai.models.generateContent({
|
| 82 |
+
model: "gemini-2.5-flash-preview-tts",
|
| 83 |
+
contents: [{ parts: [{ text: promptText }] }],
|
| 84 |
+
config: {
|
| 85 |
+
responseModalities: [Modality.AUDIO],
|
| 86 |
+
speechConfig
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
const base64Audio = response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
|
| 90 |
+
if (base64Audio) {
|
| 91 |
+
const ctx = getAudioContext();
|
| 92 |
+
if (ctx.state === 'suspended') await ctx.resume();
|
| 93 |
+
const audioBuffer = await decodeAudioData(decodeBase64(base64Audio), ctx, 24000, 1);
|
| 94 |
+
const source = ctx.createBufferSource();
|
| 95 |
+
source.buffer = audioBuffer;
|
| 96 |
+
source.connect(ctx.destination);
|
| 97 |
+
source.start();
|
| 98 |
+
return true;
|
| 99 |
+
}
|
| 100 |
+
} catch (error) {
|
| 101 |
+
console.error("Advanced Synthesis failure:", error);
|
| 102 |
+
}
|
| 103 |
+
return false;
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
export const speakText = async (text: string) => synthesizeSpeech({ text, voiceName: 'Zephyr' });
|
| 107 |
+
|
| 108 |
+
export const callGemini = async (model: string, contents: any, config: any = {}) => {
|
| 109 |
+
const ai = getAI();
|
| 110 |
+
const normalizedContents = typeof contents === 'string' ? [{ parts: [{ text: contents }] }] :
|
| 111 |
+
(Array.isArray(contents) ? contents : [contents]);
|
| 112 |
+
return await ai.models.generateContent({
|
| 113 |
+
model: model || 'gemini-3-flash-preview',
|
| 114 |
+
contents: normalizedContents,
|
| 115 |
+
config
|
| 116 |
+
});
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
export const processVoiceCommand = async (command: string) => {
|
| 120 |
+
try {
|
| 121 |
+
const prompt = `You are the Lumina Neural Parser. Analyze: "${command}". Extract amount, recipient, category. Return ONLY JSON: { "action": "SEND_MONEY", "amount": number, "recipient": string, "category": string, "narration": "Confirming dispatch..." }`;
|
| 122 |
+
const response = await callGemini('gemini-3-flash-preview', prompt, { responseMimeType: "application/json" });
|
| 123 |
+
return JSON.parse(response.text || '{}');
|
| 124 |
+
} catch (error) {
|
| 125 |
+
return { action: "ERROR", narration: "Communication link unstable." };
|
| 126 |
+
}
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
export const getFinancialAdviceStream = async (query: string, context: any) => {
|
| 130 |
+
const ai = getAI();
|
| 131 |
+
return await ai.models.generateContentStream({
|
| 132 |
+
model: 'gemini-3-flash-preview',
|
| 133 |
+
contents: [{ parts: [{ text: `Context: ${JSON.stringify(context)}. User Query: ${query}` }] }],
|
| 134 |
+
config: { systemInstruction: "You are the Lumina Quantum Financial Advisor. Be professional, concise, and technically accurate." }
|
| 135 |
+
});
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
// fix: Implemented getSystemIntelligenceFeed missing in services/geminiService.ts
|
| 139 |
+
export const getSystemIntelligenceFeed = async (): Promise<AIInsight[]> => {
|
| 140 |
+
try {
|
| 141 |
+
const ai = getAI();
|
| 142 |
+
const response = await ai.models.generateContent({
|
| 143 |
+
model: 'gemini-3-flash-preview',
|
| 144 |
+
contents: [{ parts: [{ text: "Generate 4 brief institutional financial intelligence alerts for a quantum ledger. Format as JSON array: [{title, description, severity: 'INFO'|'CRITICAL'}]" }] }],
|
| 145 |
+
config: { responseMimeType: "application/json" }
|
| 146 |
+
});
|
| 147 |
+
return JSON.parse(response.text || '[]');
|
| 148 |
+
} catch (error) {
|
| 149 |
+
console.error("Intelligence feed failure:", error);
|
| 150 |
+
return [
|
| 151 |
+
{ id: '1', title: "Node Sync Active", description: "All global registry nodes reporting stable parity.", severity: "INFO" }
|
| 152 |
+
];
|
| 153 |
+
}
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
export const runSimulationForecast = async (prompt: string): Promise<SimulationResult> => {
|
| 157 |
+
try {
|
| 158 |
+
const ai = getAI();
|
| 159 |
+
const response = await ai.models.generateContent({
|
| 160 |
+
model: 'gemini-3-flash-preview',
|
| 161 |
+
contents: [{ parts: [{ text: `Perform financial simulation for: ${prompt}. Return JSON.` }] }],
|
| 162 |
+
config: { responseMimeType: "application/json" }
|
| 163 |
+
});
|
| 164 |
+
return JSON.parse(response.text || '{}');
|
| 165 |
+
} catch (error) {
|
| 166 |
+
return { outcomeNarrative: "Simulation failed.", projectedValue: 0, confidenceScore: 0, status: "ERROR", simulationId: "ERR_A1" };
|
| 167 |
+
}
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
+
export const getPortfolioSuggestions = async (context: any) => {
|
| 171 |
+
try {
|
| 172 |
+
const ai = getAI();
|
| 173 |
+
const response = await ai.models.generateContent({
|
| 174 |
+
model: 'gemini-3-flash-preview',
|
| 175 |
+
contents: [{ parts: [{ text: `Strategize for: ${JSON.stringify(context)}. Return 3 strategies as JSON array.` }] }],
|
| 176 |
+
config: { responseMimeType: "application/json" }
|
| 177 |
+
});
|
| 178 |
+
return JSON.parse(response.text || '[]');
|
| 179 |
+
} catch {
|
| 180 |
+
return [];
|
| 181 |
+
}
|
| 182 |
+
};
|
services/nftService.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { callGemini } from "./geminiService";
|
| 3 |
+
|
| 4 |
+
export interface GeneratedNFT {
|
| 5 |
+
name: string;
|
| 6 |
+
description: string;
|
| 7 |
+
imageUrl: string;
|
| 8 |
+
traits: Array<{ trait_type: string; value: string | number }>;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export const nftService = {
|
| 12 |
+
/**
|
| 13 |
+
* Synthesizes an NFT image using Gemini 2.5 Flash Image via Backend
|
| 14 |
+
*/
|
| 15 |
+
generateImage: async (prompt: string): Promise<string | null> => {
|
| 16 |
+
try {
|
| 17 |
+
const response = await callGemini(
|
| 18 |
+
'gemini-2.5-flash-image',
|
| 19 |
+
{
|
| 20 |
+
parts: [{ text: `High-fidelity digital art NFT, institutional futuristic style, 4k, cinematic lighting: ${prompt}` }],
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
imageConfig: { aspectRatio: "1:1" }
|
| 24 |
+
}
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
// Extract the image from candidates
|
| 28 |
+
if (response.candidates && response.candidates[0]?.content?.parts) {
|
| 29 |
+
for (const part of response.candidates[0].content.parts) {
|
| 30 |
+
if (part.inlineData) {
|
| 31 |
+
return `data:image/png;base64,${part.inlineData.data}`;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
return null;
|
| 36 |
+
} catch (error) {
|
| 37 |
+
console.error("Image Synthesis Failed:", error);
|
| 38 |
+
return null;
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Generates high-lore metadata using Gemini 3 Flash
|
| 44 |
+
*/
|
| 45 |
+
generateMetadata: async (imagePrompt: string): Promise<Partial<GeneratedNFT>> => {
|
| 46 |
+
try {
|
| 47 |
+
const response = await callGemini(
|
| 48 |
+
'gemini-3-flash-preview',
|
| 49 |
+
`Generate a name, institutional description, and 3-4 rarity traits for an NFT based on this theme: ${imagePrompt}. Return ONLY JSON with keys: name, description, traits (array of {trait_type, value}).`,
|
| 50 |
+
{ responseMimeType: "application/json" }
|
| 51 |
+
);
|
| 52 |
+
|
| 53 |
+
const text = response.text || '{}';
|
| 54 |
+
return JSON.parse(text);
|
| 55 |
+
} catch (error) {
|
| 56 |
+
console.error("Metadata Generation Failed:", error);
|
| 57 |
+
return {
|
| 58 |
+
name: "Quantum Relic",
|
| 59 |
+
description: "An encrypted digital artifact from the Lumina Ledger.",
|
| 60 |
+
traits: [{ trait_type: "Rarity", value: "Classified" }]
|
| 61 |
+
};
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
|
| 65 |
+
/**
|
| 66 |
+
* Simulated OpenSea Minting sequence
|
| 67 |
+
*/
|
| 68 |
+
mintToOpenSea: async (nft: GeneratedNFT, openSeaKey: string, walletAddress: string) => {
|
| 69 |
+
const steps = [
|
| 70 |
+
"Initializing secure tunnel to OpenSea Indexer...",
|
| 71 |
+
`Authenticating via API Key: ${openSeaKey.substring(0, 4)}****`,
|
| 72 |
+
"Pinning assets to IPFS (InterPlanetary File System)...",
|
| 73 |
+
"Requesting wallet signature for contract 0x7892...B002",
|
| 74 |
+
"Broadcasting transaction to Ethereum Mainnet...",
|
| 75 |
+
"Waiting for block confirmation...",
|
| 76 |
+
`Asset Indexed Successfully. TokenID: ${Math.floor(Math.random() * 100000)}`
|
| 77 |
+
];
|
| 78 |
+
|
| 79 |
+
return steps;
|
| 80 |
+
}
|
| 81 |
+
};
|