Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23,824 changes: 17,288 additions & 6,536 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,19 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@reown/appkit": "^1.8.8",
"@reown/appkit-adapter-ethers": "^1.8.8",
"@solana/wallet-adapter-base": "^0.9.26",
"@solana/wallet-adapter-react": "^0.15.26",
"@solana/wallet-adapter-react-ui": "^0.9.33",
"@solana/wallet-adapter-wallets": "^0.19.20",
"@solana/web3.js": "^1.98.0",
"@tanstack/react-query": "^5.85.5",
"@visx/wordcloud": "^3.10.0",
"@walletconnect/solana-adapter": "^0.0.8",
"@noble/hashes": "^1.8.0",
"bctsl-sdk": "git+https://git@github.com/bctsl/bctsl-sdk#v1.0.0",
"bignumber.js": "^9.0.1",
"bs58": "^6.0.0",
"buffer": "^6.0.3",
"camelcase-keys-deep": "^0.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -54,6 +63,7 @@
"react-router-dom": "^6.22.1",
"recharts": "^3.4.1",
"socket.io-client": "^4.8.1",
"solana-kite": "^1.7.4",
"tailwind-merge": "^3.3.1",
"tipping-contract": "git+https://git@github.com/aeternity/tipping-contract#v5.0.0-alpha2",
"uuid": "^13.0.0"
Expand Down
4 changes: 0 additions & 4 deletions src/api/__tests__/governance.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ describe('GovernanceApi Integration Tests', () => {
pollId = polls.data[0]?.poll;
if (!pollId) {
// Skip test if no polls available
console.log('No polls available for testing');
return;
}

Expand Down Expand Up @@ -209,7 +208,6 @@ describe('GovernanceApi Integration Tests', () => {
pollId = polls.data[0]?.poll;
if (!pollId) {
// Skip test if no polls available
console.log('No polls available for testing');
return;
}

Expand Down Expand Up @@ -321,7 +319,6 @@ describe('GovernanceApi Integration Tests', () => {
pollId = polls.data[0]?.poll;
if (!pollId) {
// Skip test if no polls available
console.log('No polls available for testing');
return;
}

Expand Down Expand Up @@ -369,7 +366,6 @@ describe('GovernanceApi Integration Tests', () => {
pollId = polls.data[0]?.poll;
if (!pollId) {
// Skip test if no polls available
console.log('No polls available for testing');
return;
}

Expand Down
31 changes: 0 additions & 31 deletions src/api/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ export const SuperheroApi = {
const base = (CONFIG.SUPERHERO_API_URL || '').replace(/\/$/, '');
if (!base) throw new Error('SUPERHERO_API_URL not configured');
const url = `${base}${path.startsWith('/') ? '' : '/'}${path}`;
if (process.env.NODE_ENV === 'development') {
console.log(`[SuperheroApi] Base URL: ${base}`);
console.log(`[SuperheroApi] Fetching: ${url}`);
}

// Create timeout controller if no signal provided
let timeoutController: AbortController | null = null;
let timeoutId: NodeJS.Timeout | null = null;
Expand Down Expand Up @@ -51,9 +46,6 @@ export const SuperheroApi = {
}

const error = new Error(`Superhero API error (${res.status}): ${errorMessage}`);
if (process.env.NODE_ENV === 'development') {
console.error(`[SuperheroApi] Error fetching ${url}:`, error);
}
throw error;
}

Expand All @@ -62,35 +54,18 @@ export const SuperheroApi = {
const contentLength = res.headers.get('content-length');

if (contentLength === '0' || (!contentType?.includes('application/json') && !contentType?.includes('text/json'))) {
if (process.env.NODE_ENV === 'development') {
console.warn(`[SuperheroApi] Unexpected response type for ${url}:`, {
contentType,
contentLength,
status: res.status,
statusText: res.statusText,
});
}
// Return null for empty responses instead of throwing
return null;
}

const text = await res.text();
if (!text || text.trim().length === 0) {
if (process.env.NODE_ENV === 'development') {
console.warn(`[SuperheroApi] Empty response body for ${url}`);
}
return null;
}

try {
return JSON.parse(text);
} catch (parseError) {
if (process.env.NODE_ENV === 'development') {
console.error(`[SuperheroApi] Failed to parse JSON response from ${url}:`, {
text: text.substring(0, 200),
error: parseError,
});
}
throw new Error(`Invalid JSON response from ${url}: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
}
} catch (err) {
Expand All @@ -103,16 +78,10 @@ export const SuperheroApi = {
if (err instanceof Error) {
if (err.name === 'AbortError' || err.message.includes('aborted')) {
const timeoutError = new Error('Request timeout: The API request took too long. Please try again.');
if (process.env.NODE_ENV === 'development') {
console.error(`[SuperheroApi] Request timeout for ${url}`);
}
throw timeoutError;
}
if (err instanceof TypeError && err.message.includes('fetch')) {
const networkError = new Error('Network error: Unable to connect to API. Please check your internet connection.');
if (process.env.NODE_ENV === 'development') {
console.error(`[SuperheroApi] Network error fetching ${url}:`, err);
}
throw networkError;
}
}
Expand Down
1 change: 0 additions & 1 deletion src/atoms/txQueueAtoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ function atomWithBroadcast<Value>(key: string, initialValue: Value) {
const channel = new BroadcastChannel(key);

channel.onmessage = (event) => {
console.log("[atomWithBroadcast] onmessage", event);
listeners.forEach((l) => l(event));
};

Expand Down
1 change: 1 addition & 0 deletions src/atoms/walletAtoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const tokenBalancesAtom = atomWithStorage<TokenBalance[]>('wallet:tokenBa
export const tokenPricesAtom = atomWithStorage<Record<string, string>>('wallet:tokenPrices', {});
export const cookiesConsentAtom = atomWithStorage<Record<string, boolean>>('wallet:cookiesConsent', {});
export const aex9BalancesAtom = atomWithStorage<Record<string, any[]>>('wallet:aex9Balances', {});
export const selectedChainAtom = atomWithStorage<'aeternity' | 'solana'>('wallet:selectedChain', 'aeternity');

// Non-persisted wallet state
export const profileAtom = atom<Record<string, any>>({});
Expand Down
5 changes: 0 additions & 5 deletions src/auth/deeplink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@
// These are simplified stubs - implement properly as needed

export const deeplinkLogin = () => {
console.log('deeplinkLogin - TODO: implement');
};

export const deeplinkPost = () => {
console.log('deeplinkPost - TODO: implement');
};

export const deeplinkTip = () => {
console.log('deeplinkTip - TODO: implement');
};

export const performAuthedCall = () => {
console.log('performAuthedCall - TODO: implement');
return Promise.resolve();
};

export const consumeAuthCallback = () => {
console.log('consumeAuthCallback - TODO: implement');
};
23 changes: 23 additions & 0 deletions src/chains/aeternity/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ChainAdapter } from '../types';
import { PostsService } from '@/api/generated';
import { TokensService } from '@/api/generated/services/TokensService';
import { SuperheroApi } from '@/api/backend';
import { resolvePostByKey } from '@/features/social/utils/resolvePost';

export const aeternityAdapter: ChainAdapter = {
id: 'aeternity',
listPosts: (params) => PostsService.listAll(params as any) as any,
listPopularPosts: (params) => SuperheroApi.listPopularPosts(params as any) as any,
listTokens: (params) => SuperheroApi.listTokens(params as any) as any,
listTokensPage: (params) => TokensService.listAll(params as any) as any,
findTokenByAddress: (address) => TokensService.findByAddress({ address: address.toUpperCase() } as any) as any,
listTokenRankings: (address, params) => SuperheroApi.listTokenRankings(address, params as any) as any,
getTokenPerformance: (address) => SuperheroApi.getTokenPerformance(address) as any,
listTokenTransactions: (address, params) => SuperheroApi.listTokenTransactions(address, params as any) as any,
listTokenHolders: (address, params) => SuperheroApi.listTokenHolders(address, params as any) as any,
listTrendingTags: (params) => SuperheroApi.listTrendingTags(params as any) as any,
getTopicByName: (name) => SuperheroApi.getTopicByName(name) as any,
getPostByKey: resolvePostByKey,
getPostById: (id) => PostsService.getById({ id } as any) as any,
listPostComments: (params) => PostsService.getComments(params as any) as any,
};
28 changes: 28 additions & 0 deletions src/chains/hooks/useChainLatestTransactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useActiveChain } from '@/hooks/useActiveChain';
import { useLatestTransactions } from '@/hooks/useLatestTransactions';
import { SolanaApi } from '@/chains/solana/backend';
import { mapSolanaTradeToTransaction } from '@/chains/solana/utils/tokenMapping';

export function useChainLatestTransactions() {
const { selectedChain } = useActiveChain();
const ae = useLatestTransactions();

const solanaQuery = useQuery({
queryKey: ['solana-trades', 'latest', 50],
queryFn: () => SolanaApi.listBclTrades({ limit: 50, page: 1, includes: 'token' }),
enabled: selectedChain === 'solana',
staleTime: 15_000,
});

const solanaTransactions = useMemo(() => {
const items = solanaQuery.data?.items || [];
return items.map(mapSolanaTradeToTransaction);
}, [solanaQuery.data]);

if (selectedChain === 'solana') {
return { latestTransactions: solanaTransactions };
}
return ae;
}
158 changes: 158 additions & 0 deletions src/chains/solana/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import type { Connection } from '@solana/web3.js';
import type { ChainAdapter } from '../types';
import { SolanaApi } from './backend';
import type { SocialPost } from './social/types';
import { mapSolanaTokenToAeToken } from './utils/tokenMapping';
import {
findPostSignatureByPostId,
getPostBySignature,
listPostsPage,
listRepliesByParentPostId,
} from './solanaPosting/rpc';

const mapSolanaPostToPostDto = (post: SocialPost) => {
const parent = post.parent ? String(post.parent) : '';
const topics = parent ? [`comment:${parent}`] : [];
return {
id: post.post_id || post.id,
tx_hash: post.id,
tx_args: [
{ post_id: post.post_id },
{ signature: post.id },
...(parent ? [{ parent }] : []),
],
sender_address: post.sender_address,
contract_address: '',
type: post.type === 'comment' ? 'COMMENT' : 'POST',
content: post.content,
topics,
media: post.media || [],
total_comments: post.total_comments ?? 0,
created_at: post.created_at,
slug: post.slug,
} as any;
};

export const createSolanaAdapter = (connection: Connection | null): ChainAdapter => ({
id: 'solana',
listPosts: async (params) => {
if (!connection) throw new Error('Solana connection not available');
const { items } = await listPostsPage(connection, { limit: params.limit });
return {
items: items.map(mapSolanaPostToPostDto),
meta: { currentPage: 1, totalPages: 1, totalItems: items.length },
};
},
listPopularPosts: (params) => SolanaApi.listPopularPosts(params as any) as any,
listTokens: async (params) => {
const resp = await SolanaApi.listBclTokens({
orderBy: params.orderBy,
orderDirection: params.orderDirection,
limit: params.limit,
page: params.page,
search: params.search,
ownerAddress: params.ownerAddress,
creatorAddress: params.creatorAddress,
includes: 'performance',
});
const items = (resp?.items || []).map(mapSolanaTokenToAeToken);
return { ...resp, items };
},
listTokensPage: async (params) => {
const resp = await SolanaApi.listBclTokens({
orderBy: params.orderBy,
orderDirection: params.orderDirection,
limit: params.limit,
page: params.page,
search: params.search,
ownerAddress: params.ownerAddress,
creatorAddress: params.creatorAddress,
includes: 'performance',
});
const items = (resp?.items || []).map(mapSolanaTokenToAeToken);
return { ...resp, items };
},
findTokenByAddress: async (address) => {
const token = await SolanaApi.getBclToken(address);
return token ? mapSolanaTokenToAeToken(token as any) : null;
},
listTokenRankings: async (address, params) => {
const resp = await SolanaApi.listBclTokenRankings(address, params as any);
const items = (resp?.items || []).map(mapSolanaTokenToAeToken);
return { ...resp, items };
},
getTokenPerformance: (address) => SolanaApi.getBclTokenPerformance(address) as any,
listTokenTransactions: (address, params) => SolanaApi.listBclTokenTrades(address, { ...params, includes: 'token' }) as any,
listTokenHolders: async (address, params) => {
const resp = await SolanaApi.listBclTokenHolders(address, params as any);
const items = Array.isArray(resp?.items)
? resp.items.map((holder: any) => ({
...holder,
address: holder.address || holder.owner_address,
}))
: [];
return { ...resp, items };
},
listTrendingTags: async (params) => {
const resp = await SolanaApi.listTrendingTags(params as any);
const items = Array.isArray(resp?.items) ? resp.items : [];
if (items.length > 0) return resp;

const orderByFallback = params?.orderBy === 'score'
? 'market_cap'
: params?.orderBy === 'source'
? 'market_cap'
: params?.orderBy;

const fallback = await SolanaApi.listBclTokens({
orderBy: orderByFallback,
orderDirection: params?.orderDirection,
limit: params?.limit ?? 20,
page: params?.page ?? 1,
search: params?.search,
});
const fallbackItems = Array.isArray(fallback?.items) ? fallback.items : [];
return {
...fallback,
items: fallbackItems.map((token: any) => ({
tag: token?.name ?? token?.symbol ?? '',
score: Number(token?.trending_score ?? token?.market_cap?.usd ?? token?.holders_count ?? 0),
source: token?.collection_address ? 'solana' : undefined,
token_sale_address: token?.token_sale_address ?? token?.sale_address,
sale_address: token?.token_sale_address ?? token?.sale_address,
token_address: token?.token_sale_address ?? token?.sale_address,
})).filter((t: any) => t.tag),
};
},
getTopicByName: (name) => SolanaApi.getTopicByName(name) as any,
getPostByKey: async (key: string) => {
if (!connection) throw new Error('Solana connection not available');
const normalized = String(key || '').trim();
if (!normalized) throw new Error('Missing post identifier');
try {
const bySig = await getPostBySignature(connection, normalized);
return mapSolanaPostToPostDto(bySig);
} catch {
// fall through
}
const found = await findPostSignatureByPostId(connection, normalized, { limit: 500 });
if (!found) throw new Error('Post not found');
const post = await getPostBySignature(connection, found.signature);
return mapSolanaPostToPostDto(post);
},
getPostById: async (id: string) => {
if (!connection) throw new Error('Solana connection not available');
const found = await findPostSignatureByPostId(connection, id, { limit: 500 });
if (!found) throw new Error('Post not found');
const post = await getPostBySignature(connection, found.signature);
return mapSolanaPostToPostDto(post);
},
listPostComments: async ({ id, limit }) => {
if (!connection) throw new Error('Solana connection not available');
const resp = await listRepliesByParentPostId(connection, id, { limit });
return {
items: resp.items.map(mapSolanaPostToPostDto),
meta: { currentPage: 1, totalPages: 1, totalItems: resp.items.length },
};
},
});
Loading