diff --git a/api-schema.graphql b/api-schema.graphql index e8c2aa6..0473771 100644 --- a/api-schema.graphql +++ b/api-schema.graphql @@ -363,6 +363,9 @@ type Query { anonRequestIdentityChallenge(input: RequestIdentityChallengeInput!): IdentityChallenge appConfig: AppConfig! me: User + solanaGetBalance(account: String!): String + solanaGetTokenAccounts(account: String!): JSON + solanaGetTransactions(account: String!): JSON uptime: Float! userFindManyCommunity(input: UserFindManyCommunityInput!): CommunityPaging! userFindManyCommunityMember(input: UserFindManyCommunityMemberInput!): CommunityMemberPaging! @@ -507,6 +510,7 @@ input VerifyIdentityChallengeInput { } type Wallet { + communityId: String createdAt: DateTime id: String! name: String! diff --git a/libs/api/community/data-access/src/lib/api-community-data-admin.service.ts b/libs/api/community/data-access/src/lib/api-community-data-admin.service.ts index e21184e..daaff8a 100644 --- a/libs/api/community/data-access/src/lib/api-community-data-admin.service.ts +++ b/libs/api/community/data-access/src/lib/api-community-data-admin.service.ts @@ -15,7 +15,7 @@ export class ApiCommunityDataAdminService { async findManyCommunity(input: AdminFindManyCommunityInput): Promise { return this.data.findMany({ - orderBy: { createdAt: 'desc' }, + orderBy: { name: 'asc' }, where: getCommunityWhereAdminInput(input), limit: input.limit, page: input.page, diff --git a/libs/api/community/data-access/src/lib/api-community-data-anon.service.ts b/libs/api/community/data-access/src/lib/api-community-data-anon.service.ts index 25e10bf..870e9e4 100644 --- a/libs/api/community/data-access/src/lib/api-community-data-anon.service.ts +++ b/libs/api/community/data-access/src/lib/api-community-data-anon.service.ts @@ -10,7 +10,7 @@ export class ApiCommunityDataAnonService { async findManyCommunity(input: AnonFindManyCommunityInput): Promise { return this.data.findMany({ - orderBy: { createdAt: 'desc' }, + orderBy: { name: 'asc' }, where: getCommunityWhereAnonInput(input), limit: input.limit, page: input.page, diff --git a/libs/api/community/data-access/src/lib/api-community-data-user.service.ts b/libs/api/community/data-access/src/lib/api-community-data-user.service.ts index 3fc334e..c7f6983 100644 --- a/libs/api/community/data-access/src/lib/api-community-data-user.service.ts +++ b/libs/api/community/data-access/src/lib/api-community-data-user.service.ts @@ -20,7 +20,7 @@ export class ApiCommunityDataUserService { async findManyCommunity(input: UserFindManyCommunityInput): Promise { return this.data.findMany({ - orderBy: { createdAt: 'desc' }, + orderBy: { name: 'asc' }, where: getCommunityWhereUserInput(input), limit: input.limit, page: input.page, diff --git a/libs/api/community/data-access/src/lib/api-community-data.service.ts b/libs/api/community/data-access/src/lib/api-community-data.service.ts index 70dc3f6..3525337 100644 --- a/libs/api/community/data-access/src/lib/api-community-data.service.ts +++ b/libs/api/community/data-access/src/lib/api-community-data.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common' import { CommunityMemberRole, Prisma } from '@prisma/client' +import { Keypair } from '@solana/web3.js' import { ApiCoreService, PagingInputFields, slugifyId } from '@tokengator-mint/api-core-data-access' import { CommunityPaging } from './entity/community.entity' @@ -7,11 +8,21 @@ import { CommunityPaging } from './entity/community.entity' export class ApiCommunityDataService { constructor(private readonly core: ApiCoreService) {} async create(data: Omit, userId?: string) { + const kp = Keypair.generate() return this.core.data.community.create({ data: { ...data, slug: slugifyId(data.name).toLowerCase(), members: userId ? { create: [{ userId, role: CommunityMemberRole.Admin }] } : data.members, + wallets: data.wallets + ? data.wallets + : { + create: { + name: 'Fee Payer', + publicKey: kp.publicKey.toBase58(), + secretKey: JSON.stringify(Array.from(kp.secretKey)), + }, + }, }, }) } diff --git a/libs/api/community/data-access/src/lib/api-community-provision.service.ts b/libs/api/community/data-access/src/lib/api-community-provision.service.ts index bc632df..1d89b2d 100644 --- a/libs/api/community/data-access/src/lib/api-community-provision.service.ts +++ b/libs/api/community/data-access/src/lib/api-community-provision.service.ts @@ -129,6 +129,21 @@ export const provisionCommunities: ProvisionCommunityInput[] = [ ], }, }, + { + name: 'COLOSSEUM', + description: `Powering online Solana hackathons, accelerating winners & investing in breakout crypto startups.🏟️`, + imageUrl: 'https://pbs.twimg.com/profile_images/1684566200662233092/BZzDPr5q_400x400.jpg', + wallets: { + create: [ + { + name: 'Fee Payer', + publicKey: 'CLSMpWGGvmjh9qFoLtANu2gts3frcrNr2J4XEPoFvQi2', + secretKey: + '[172,236,240,133,3,153,43,108,116,65,138,10,92,241,146,193,31,113,94,213,116,167,89,137,204,233,119,92,232,8,216,9,168,107,67,216,45,228,124,94,213,148,190,81,30,168,9,180,158,112,154,224,189,227,6,7,19,25,18,103,56,132,85,111]', + }, + ], + }, + }, ] @Injectable() @@ -146,7 +161,7 @@ export class ApiCommunityProvisionService { } private async provisionCommunities() { - await Promise.all(provisionCommunities.map((user) => this.provisionCommunity(user))) + await Promise.all(provisionCommunities.map((item) => this.provisionCommunity(item))) } private async provisionCommunity(input: ProvisionCommunityInput) { diff --git a/libs/api/solana/data-access/src/lib/api-solana.service.ts b/libs/api/solana/data-access/src/lib/api-solana.service.ts index f2719b1..c6b6f91 100644 --- a/libs/api/solana/data-access/src/lib/api-solana.service.ts +++ b/libs/api/solana/data-access/src/lib/api-solana.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common' import { OnEvent } from '@nestjs/event-emitter' import { Cron, CronExpression } from '@nestjs/schedule' -import { getMint, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token' +import { getMint, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' import { TokenMetadata } from '@solana/spl-token-metadata' import { AccountInfo, Connection, LAMPORTS_PER_SOL, ParsedAccountData, PublicKey } from '@solana/web3.js' import { ApiCoreService, CORE_APP_STARTED } from '@tokengator-mint/api-core-data-access' @@ -59,4 +59,20 @@ export class ApiSolanaService { return metadata.state } + + async getBalance(account: string) { + return this.connection.getBalance(new PublicKey(account)) + } + + async getTokenAccounts(account: string) { + const [tokenAccounts, token2022Accounts] = await Promise.all([ + this.connection.getParsedTokenAccountsByOwner(new PublicKey(account), { programId: TOKEN_PROGRAM_ID }), + this.connection.getParsedTokenAccountsByOwner(new PublicKey(account), { programId: TOKEN_2022_PROGRAM_ID }), + ]) + return [...tokenAccounts.value, ...token2022Accounts.value] + } + + async getTransactions(account: string) { + return this.connection.getConfirmedSignaturesForAddress2(new PublicKey(account), { limit: 50 }) + } } diff --git a/libs/api/solana/feature/src/lib/api-solana.resolver.ts b/libs/api/solana/feature/src/lib/api-solana.resolver.ts index 9189060..b423cb6 100644 --- a/libs/api/solana/feature/src/lib/api-solana.resolver.ts +++ b/libs/api/solana/feature/src/lib/api-solana.resolver.ts @@ -1,7 +1,26 @@ -import { Resolver } from '@nestjs/graphql' +import { UseGuards } from '@nestjs/common' +import { Args, Query, Resolver } from '@nestjs/graphql' +import { ApiAuthGraphQLUserGuard } from '@tokengator-mint/api-auth-data-access' import { ApiSolanaService } from '@tokengator-mint/api-solana-data-access' +import { GraphQLJSON } from 'graphql-scalars' @Resolver() +@UseGuards(ApiAuthGraphQLUserGuard) export class ApiSolanaResolver { constructor(private readonly service: ApiSolanaService) {} + + @Query(() => String, { nullable: true }) + solanaGetBalance(@Args('account') account: string) { + return this.service.getBalance(account) + } + + @Query(() => GraphQLJSON, { nullable: true }) + solanaGetTokenAccounts(@Args('account') account: string) { + return this.service.getTokenAccounts(account) + } + + @Query(() => GraphQLJSON, { nullable: true }) + solanaGetTransactions(@Args('account') account: string) { + return this.service.getTransactions(account) + } } diff --git a/libs/api/wallet/data-access/src/lib/api-wallet-data.service.ts b/libs/api/wallet/data-access/src/lib/api-wallet-data.service.ts index eb14ca3..b8d008f 100644 --- a/libs/api/wallet/data-access/src/lib/api-wallet-data.service.ts +++ b/libs/api/wallet/data-access/src/lib/api-wallet-data.service.ts @@ -21,6 +21,7 @@ export class ApiWalletDataService { secretKey: JSON.stringify(Array.from(kp.secretKey)), name, publicKey, + communityId: input.communityId, }, }) } diff --git a/libs/api/wallet/data-access/src/lib/entity/wallet.entity.ts b/libs/api/wallet/data-access/src/lib/entity/wallet.entity.ts index f430193..38f497f 100644 --- a/libs/api/wallet/data-access/src/lib/entity/wallet.entity.ts +++ b/libs/api/wallet/data-access/src/lib/entity/wallet.entity.ts @@ -13,6 +13,8 @@ export class Wallet { name!: string @Field() publicKey!: string + @Field({ nullable: true }) + communityId?: string | null } @ObjectType() diff --git a/libs/sdk/src/generated/graphql-sdk.ts b/libs/sdk/src/generated/graphql-sdk.ts index b698f8c..251301a 100644 --- a/libs/sdk/src/generated/graphql-sdk.ts +++ b/libs/sdk/src/generated/graphql-sdk.ts @@ -567,6 +567,9 @@ export type Query = { anonRequestIdentityChallenge?: Maybe appConfig: AppConfig me?: Maybe + solanaGetBalance?: Maybe + solanaGetTokenAccounts?: Maybe + solanaGetTransactions?: Maybe uptime: Scalars['Float']['output'] userFindManyCommunity: CommunityPaging userFindManyCommunityMember: CommunityMemberPaging @@ -658,6 +661,18 @@ export type QueryAnonRequestIdentityChallengeArgs = { input: RequestIdentityChallengeInput } +export type QuerySolanaGetBalanceArgs = { + account: Scalars['String']['input'] +} + +export type QuerySolanaGetTokenAccountsArgs = { + account: Scalars['String']['input'] +} + +export type QuerySolanaGetTransactionsArgs = { + account: Scalars['String']['input'] +} + export type QueryUserFindManyCommunityArgs = { input: UserFindManyCommunityInput } @@ -850,6 +865,7 @@ export type VerifyIdentityChallengeInput = { export type Wallet = { __typename?: 'Wallet' + communityId?: Maybe createdAt?: Maybe id: Scalars['String']['output'] name: Scalars['String']['output'] @@ -2263,6 +2279,24 @@ export type UserFindManyPriceQuery = { }> } +export type SolanaGetBalanceQueryVariables = Exact<{ + account: Scalars['String']['input'] +}> + +export type SolanaGetBalanceQuery = { __typename?: 'Query'; balance?: string | null } + +export type SolanaGetTokenAccountsQueryVariables = Exact<{ + account: Scalars['String']['input'] +}> + +export type SolanaGetTokenAccountsQuery = { __typename?: 'Query'; items?: any | null } + +export type SolanaGetTransactionsQueryVariables = Exact<{ + account: Scalars['String']['input'] +}> + +export type SolanaGetTransactionsQuery = { __typename?: 'Query'; items?: any | null } + export type UserDetailsFragment = { __typename?: 'User' avatarUrl?: string | null @@ -2477,6 +2511,7 @@ export type WalletDetailsFragment = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null } export type AdminFindManyWalletQueryVariables = Exact<{ @@ -2494,6 +2529,7 @@ export type AdminFindManyWalletQuery = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null }> meta: { __typename?: 'PagingMeta' @@ -2521,6 +2557,7 @@ export type AdminFindOneWalletQuery = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null } | null } @@ -2537,6 +2574,7 @@ export type AdminCreateWalletMutation = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null } | null } @@ -2554,6 +2592,7 @@ export type AdminUpdateWalletMutation = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null } | null } @@ -2578,6 +2617,7 @@ export type UserFindManyWalletQuery = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null }> meta: { __typename?: 'PagingMeta' @@ -2605,6 +2645,7 @@ export type UserFindOneWalletQuery = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null } | null } @@ -2621,6 +2662,7 @@ export type UserCreateWalletMutation = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null } | null } @@ -2638,6 +2680,7 @@ export type UserUpdateWalletMutation = { name: string publicKey: string updatedAt?: Date | null + communityId?: string | null } | null } @@ -2790,6 +2833,7 @@ export const WalletDetailsFragmentDoc = gql` name publicKey updatedAt + communityId } ` export const LoginDocument = gql` @@ -3309,6 +3353,21 @@ export const UserFindManyPriceDocument = gql` } ${PriceDetailsFragmentDoc} ` +export const SolanaGetBalanceDocument = gql` + query solanaGetBalance($account: String!) { + balance: solanaGetBalance(account: $account) + } +` +export const SolanaGetTokenAccountsDocument = gql` + query solanaGetTokenAccounts($account: String!) { + items: solanaGetTokenAccounts(account: $account) + } +` +export const SolanaGetTransactionsDocument = gql` + query solanaGetTransactions($account: String!) { + items: solanaGetTransactions(account: $account) + } +` export const AdminCreateUserDocument = gql` mutation adminCreateUser($input: AdminCreateUserInput!) { created: adminCreateUser(input: $input) { @@ -3542,6 +3601,9 @@ const AdminCreatePriceDocumentString = print(AdminCreatePriceDocument) const AdminUpdatePriceDocumentString = print(AdminUpdatePriceDocument) const AdminDeletePriceDocumentString = print(AdminDeletePriceDocument) const UserFindManyPriceDocumentString = print(UserFindManyPriceDocument) +const SolanaGetBalanceDocumentString = print(SolanaGetBalanceDocument) +const SolanaGetTokenAccountsDocumentString = print(SolanaGetTokenAccountsDocument) +const SolanaGetTransactionsDocumentString = print(SolanaGetTransactionsDocument) const AdminCreateUserDocumentString = print(AdminCreateUserDocument) const AdminDeleteUserDocumentString = print(AdminDeleteUserDocument) const AdminFindManyUserDocumentString = print(AdminFindManyUserDocument) @@ -4810,6 +4872,69 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = variables, ) }, + solanaGetBalance( + variables: SolanaGetBalanceQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise<{ + data: SolanaGetBalanceQuery + errors?: GraphQLError[] + extensions?: any + headers: Headers + status: number + }> { + return withWrapper( + (wrappedRequestHeaders) => + client.rawRequest(SolanaGetBalanceDocumentString, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + 'solanaGetBalance', + 'query', + variables, + ) + }, + solanaGetTokenAccounts( + variables: SolanaGetTokenAccountsQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise<{ + data: SolanaGetTokenAccountsQuery + errors?: GraphQLError[] + extensions?: any + headers: Headers + status: number + }> { + return withWrapper( + (wrappedRequestHeaders) => + client.rawRequest(SolanaGetTokenAccountsDocumentString, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + 'solanaGetTokenAccounts', + 'query', + variables, + ) + }, + solanaGetTransactions( + variables: SolanaGetTransactionsQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise<{ + data: SolanaGetTransactionsQuery + errors?: GraphQLError[] + extensions?: any + headers: Headers + status: number + }> { + return withWrapper( + (wrappedRequestHeaders) => + client.rawRequest(SolanaGetTransactionsDocumentString, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + 'solanaGetTransactions', + 'query', + variables, + ) + }, adminCreateUser( variables: AdminCreateUserMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, diff --git a/libs/sdk/src/graphql/feature-solana.graphql b/libs/sdk/src/graphql/feature-solana.graphql new file mode 100644 index 0000000..6886c03 --- /dev/null +++ b/libs/sdk/src/graphql/feature-solana.graphql @@ -0,0 +1,10 @@ +query solanaGetBalance($account: String!) { + balance: solanaGetBalance(account: $account) +} +query solanaGetTokenAccounts($account: String!) { + items: solanaGetTokenAccounts(account: $account) +} + +query solanaGetTransactions($account: String!) { + items: solanaGetTransactions(account: $account) +} diff --git a/libs/sdk/src/graphql/feature-wallet.graphql b/libs/sdk/src/graphql/feature-wallet.graphql index 82918ca..a9c5345 100644 --- a/libs/sdk/src/graphql/feature-wallet.graphql +++ b/libs/sdk/src/graphql/feature-wallet.graphql @@ -4,6 +4,7 @@ fragment WalletDetails on Wallet { name publicKey updatedAt + communityId } query adminFindManyWallet($input: WalletAdminFindManyInput!) { diff --git a/libs/sdk/src/index.ts b/libs/sdk/src/index.ts index 32e0c97..0d7848b 100644 --- a/libs/sdk/src/index.ts +++ b/libs/sdk/src/index.ts @@ -1,6 +1,7 @@ export * from './generated/graphql-sdk' export * from './lib/constants' export * from './lib/ellipsify' +export * from './lib/format-price' export * from './lib/get-graphql-client' export * from './lib/get-graphql-sdk' export * from './lib/response-middleware' diff --git a/libs/sdk/src/lib/format-price.ts b/libs/sdk/src/lib/format-price.ts new file mode 100644 index 0000000..24f2fa3 --- /dev/null +++ b/libs/sdk/src/lib/format-price.ts @@ -0,0 +1,9 @@ +import { LAMPORTS_PER_SOL } from '@solana/web3.js' + +export function formatSol(amount: string) { + return formatPrice((parseInt(amount) / LAMPORTS_PER_SOL).toString(), 6) + ' SOL' +} + +export function formatPrice(amount: string, decimals = 2) { + return Intl.NumberFormat('en-US', { maximumFractionDigits: decimals }).format(parseFloat(amount)) +} diff --git a/libs/web/community/ui/src/lib/community-ui-grid-item.tsx b/libs/web/community/ui/src/lib/community-ui-grid-item.tsx index 0f00dcf..91034d4 100644 --- a/libs/web/community/ui/src/lib/community-ui-grid-item.tsx +++ b/libs/web/community/ui/src/lib/community-ui-grid-item.tsx @@ -1,6 +1,6 @@ import { Paper } from '@mantine/core' +import { UiGroup } from '@pubkey-ui/core' import { Community } from '@tokengator-mint/sdk' -import { UiDebugModal, UiGroup } from '@pubkey-ui/core' import { CommunityUiItem } from './community-ui-item' export function CommunityUiGridItem({ community, to }: { community: Community; to?: string }) { @@ -8,7 +8,6 @@ export function CommunityUiGridItem({ community, to }: { community: Community; t - ) diff --git a/libs/web/community/ui/src/lib/community-ui-item.tsx b/libs/web/community/ui/src/lib/community-ui-item.tsx index 91b2664..bd55f07 100644 --- a/libs/web/community/ui/src/lib/community-ui-item.tsx +++ b/libs/web/community/ui/src/lib/community-ui-item.tsx @@ -1,5 +1,5 @@ import { AvatarProps, Group, type GroupProps, Stack, Text } from '@mantine/core' -import { UiAnchor, type UiAnchorProps } from '@pubkey-ui/core' +import { UiAnchor, type UiAnchorProps, UiStack } from '@pubkey-ui/core' import { Community } from '@tokengator-mint/sdk' import { CommunityUiAvatar } from './community-ui-avatar' @@ -19,18 +19,20 @@ export function CommunityUiItem({ if (!community) return null return ( - - - - - - {community?.name} - - - {community?.description} - - - - + + + + + + + {community?.name} + + + + + + {community?.description} + + ) } diff --git a/libs/web/solana/data-access/src/index.ts b/libs/web/solana/data-access/src/index.ts index 944a989..0e56a48 100644 --- a/libs/web/solana/data-access/src/index.ts +++ b/libs/web/solana/data-access/src/index.ts @@ -3,3 +3,4 @@ export * from './lib/create-transaction' export * from './lib/solana-provider' export * from './lib/ui-toast-link' export * from './lib/use-account' +export * from './lib/use-solana-get-balance' diff --git a/libs/web/solana/data-access/src/lib/cluster-provider.tsx b/libs/web/solana/data-access/src/lib/cluster-provider.tsx index a0a0e21..0c00a19 100644 --- a/libs/web/solana/data-access/src/lib/cluster-provider.tsx +++ b/libs/web/solana/data-access/src/lib/cluster-provider.tsx @@ -89,7 +89,7 @@ export function ClusterProvider({ children }: { children: ReactNode }) { setClusters(clusters.filter((item) => item.name !== cluster.name)) }, setCluster: (cluster: Cluster) => setCluster(cluster), - getExplorerUrl: (path: string) => `https://explorer.solana.com/${path}${getClusterUrlParam(cluster)}`, + getExplorerUrl: (path: string) => `https://solana.fm/${path}${getClusterUrlParam(cluster)}`, } return {children} } @@ -102,13 +102,13 @@ function getClusterUrlParam(cluster: Cluster): string { let suffix = '' switch (cluster.network) { case ClusterNetwork.Devnet: - suffix = 'devnet' + suffix = 'devnet-solana' break case ClusterNetwork.Mainnet: suffix = '' break case ClusterNetwork.Testnet: - suffix = 'testnet' + suffix = 'testnet-solana' break default: suffix = `custom&customUrl=${encodeURIComponent(cluster.endpoint)}` diff --git a/libs/web/solana/data-access/src/lib/use-solana-get-balance.ts b/libs/web/solana/data-access/src/lib/use-solana-get-balance.ts new file mode 100644 index 0000000..28e3ec0 --- /dev/null +++ b/libs/web/solana/data-access/src/lib/use-solana-get-balance.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@tanstack/react-query' +import { useSdk } from '@tokengator-mint/web-core-data-access' + +export function useSolanaGetBalance({ account }: { account: string }) { + const sdk = useSdk() + return useQuery({ + queryKey: ['solanaGetBalance', account], + queryFn: () => sdk.solanaGetBalance({ account }).then((res) => res.data.balance ?? '0'), + }) +} + +export function useSolanaGetTokenAccounts({ account }: { account: string }) { + const sdk = useSdk() + return useQuery({ + queryKey: ['solanaGetTokenAccounts', account], + queryFn: () => sdk.solanaGetTokenAccounts({ account }).then((res) => res.data.items ?? []), + }) +} + +export function useSolanaGetTransactions({ account }: { account: string }) { + const sdk = useSdk() + return useQuery({ + queryKey: ['solanaGetTransactions', account], + queryFn: () => sdk.solanaGetTransactions({ account }).then((res) => res.data.items ?? []), + }) +} diff --git a/libs/web/solana/ui/src/lib/solana-ui-account-tokens.tsx b/libs/web/solana/ui/src/lib/solana-ui-account-tokens.tsx index 1b9d1a1..7097257 100644 --- a/libs/web/solana/ui/src/lib/solana-ui-account-tokens.tsx +++ b/libs/web/solana/ui/src/lib/solana-ui-account-tokens.tsx @@ -1,10 +1,10 @@ import { ActionIcon, Button, Group, Loader, Table, Text } from '@mantine/core' -import { ellipsify } from '@tokengator-mint/sdk' -import { useGetTokenAccounts } from '@tokengator-mint/web-solana-data-access' import { UiError, UiInfo, UiStack } from '@pubkey-ui/core' import { PublicKey } from '@solana/web3.js' import { IconRefresh } from '@tabler/icons-react' import { useQueryClient } from '@tanstack/react-query' +import { ellipsify } from '@tokengator-mint/sdk' +import { useGetTokenAccounts } from '@tokengator-mint/web-solana-data-access' import { useMemo, useState } from 'react' import { SolanaUiAccountTokenBalance } from './solana-ui-account-token-balance' @@ -33,7 +33,7 @@ export function SolanaUiAccountTokens({ address }: { address: PublicKey }) { onClick={async () => { await query.refetch() await client.invalidateQueries({ - queryKey: ['getTokenAccountBalance'], + queryKey: ['solanaGetBalance'], }) }} > diff --git a/libs/web/solana/ui/src/lib/solana-ui-explorer-link.tsx b/libs/web/solana/ui/src/lib/solana-ui-explorer-link.tsx index 5202cba..4887978 100644 --- a/libs/web/solana/ui/src/lib/solana-ui-explorer-link.tsx +++ b/libs/web/solana/ui/src/lib/solana-ui-explorer-link.tsx @@ -1,6 +1,7 @@ -import { Anchor, AnchorProps, Group } from '@mantine/core' -import { useCluster } from '@tokengator-mint/web-solana-data-access' +import { ActionIcon, ActionIconProps, Anchor, AnchorProps, Group, Tooltip } from '@mantine/core' import { UiCopy } from '@pubkey-ui/core' +import { IconWorld } from '@tabler/icons-react' +import { useCluster } from '@tokengator-mint/web-solana-data-access' export function SolanaUiExplorerLink({ label, @@ -23,6 +24,7 @@ export function SolanaUiExplorerLink({ ) } + export function SolanaExplorerAnchor({ label = 'View on Solana Explorer', path, @@ -38,3 +40,30 @@ export function SolanaExplorerAnchor({ ) } + +export function SolanaExplorerIcon({ + tooltip = 'View on Solana Explorer', + path, + ...props +}: ActionIconProps & { + tooltip?: string + path: string +}) { + const { getExplorerUrl } = useCluster() + return ( + + + + + + ) +} diff --git a/libs/web/wallet/feature/src/lib/user-wallet-detail.feature.tsx b/libs/web/wallet/feature/src/lib/user-wallet-detail.feature.tsx index d49bd68..1983cc9 100644 --- a/libs/web/wallet/feature/src/lib/user-wallet-detail.feature.tsx +++ b/libs/web/wallet/feature/src/lib/user-wallet-detail.feature.tsx @@ -1,8 +1,9 @@ import { Group } from '@mantine/core' import { UiBack, UiDebugModal, UiError, UiGroup, UiLoader, UiStack, UiTabRoute, UiTabRoutes } from '@pubkey-ui/core' import { useUserFindOneWallet } from '@tokengator-mint/web-wallet-data-access' -import { WalletUiItem } from '@tokengator-mint/web-wallet-ui' +import { WalletUiItem, WalletUiSolTokenAccounts, WalletUiSolTransactions } from '@tokengator-mint/web-wallet-ui' import { useParams } from 'react-router-dom' + import { UserWalletDetailInfoTab } from './user-wallet-detail-info.tab' import { UserWalletDetailSettingsTab } from './user-wallet-detail-settings.tab' @@ -23,6 +24,16 @@ export default function UserWalletDetailFeature() { label: 'Info', element: , }, + { + path: 'transactions', + label: 'Transactions', + element: , + }, + { + path: 'token-accounts', + label: 'Token Accounts', + element: , + }, { path: 'settings', label: 'Settings', diff --git a/libs/web/wallet/feature/src/lib/user-wallet-list.feature.tsx b/libs/web/wallet/feature/src/lib/user-wallet-list.feature.tsx index 903718a..3543b9a 100644 --- a/libs/web/wallet/feature/src/lib/user-wallet-list.feature.tsx +++ b/libs/web/wallet/feature/src/lib/user-wallet-list.feature.tsx @@ -1,4 +1,4 @@ -import { Button, Group } from '@mantine/core' +import { Anchor, Button, Group, Text } from '@mantine/core' import { UiDebugModal, UiInfo, UiLoader, UiStack } from '@pubkey-ui/core' import { UiSearchField } from '@tokengator-mint/web-core-ui' import { useUserFindManyWallet } from '@tokengator-mint/web-wallet-data-access' @@ -20,6 +20,24 @@ export default function UserWalletListFeature({ communityId }: { communityId: st Create + + + Wallets are used to pay fees for storage and transactions and receive payments. Wallets are associated + with a community. + + + Go to{' '} + + faucet.solana.com + {' '} + to get some SOL for your wallet. + + + } + /> {query.isLoading ? ( diff --git a/libs/web/wallet/ui/src/index.ts b/libs/web/wallet/ui/src/index.ts index d8de7d6..1c9f9b5 100644 --- a/libs/web/wallet/ui/src/index.ts +++ b/libs/web/wallet/ui/src/index.ts @@ -1,11 +1,14 @@ export * from './lib/admin-wallet-ui-create-form' export * from './lib/admin-wallet-ui-table' export * from './lib/admin-wallet-ui-update-form' +export * from './lib/user-wallet-ui-create-form' +export * from './lib/user-wallet-ui-table' +export * from './lib/user-wallet-ui-update-form' export * from './lib/wallet-ui-avatar' export * from './lib/wallet-ui-grid' export * from './lib/wallet-ui-grid-item' export * from './lib/wallet-ui-info' export * from './lib/wallet-ui-item' -export * from './lib/user-wallet-ui-create-form' -export * from './lib/user-wallet-ui-table' -export * from './lib/user-wallet-ui-update-form' +export * from './lib/wallet-ui-sol-balance' +export * from './lib/wallet-ui-sol-token-accounts' +export * from './lib/wallet-ui-sol-transactions' diff --git a/libs/web/wallet/ui/src/lib/user-wallet-ui-create-form.tsx b/libs/web/wallet/ui/src/lib/user-wallet-ui-create-form.tsx index 9c02e0c..3b2c236 100644 --- a/libs/web/wallet/ui/src/lib/user-wallet-ui-create-form.tsx +++ b/libs/web/wallet/ui/src/lib/user-wallet-ui-create-form.tsx @@ -14,7 +14,12 @@ export function UserWalletUiCreateForm({ submit }: { submit: (res: WalletUserCre return (
submit(values))}> - + diff --git a/libs/web/wallet/ui/src/lib/wallet-ui-grid-item.tsx b/libs/web/wallet/ui/src/lib/wallet-ui-grid-item.tsx index f46c3cf..c7c2da5 100644 --- a/libs/web/wallet/ui/src/lib/wallet-ui-grid-item.tsx +++ b/libs/web/wallet/ui/src/lib/wallet-ui-grid-item.tsx @@ -1,14 +1,22 @@ -import { Paper } from '@mantine/core' +import { Group, Paper, Stack } from '@mantine/core' +import { UiCopy, UiGroup } from '@pubkey-ui/core' import { Wallet } from '@tokengator-mint/sdk' -import { UiDebugModal, UiGroup } from '@pubkey-ui/core' +import { SolanaExplorerIcon } from '@tokengator-mint/web-solana-ui' import { WalletUiItem } from './wallet-ui-item' +import { WalletUiSolBalance } from './wallet-ui-sol-balance' export function WalletUiGridItem({ wallet, to }: { wallet: Wallet; to?: string }) { return ( - + + + + + + + ) diff --git a/libs/web/wallet/ui/src/lib/wallet-ui-sol-balance.tsx b/libs/web/wallet/ui/src/lib/wallet-ui-sol-balance.tsx new file mode 100644 index 0000000..5618318 --- /dev/null +++ b/libs/web/wallet/ui/src/lib/wallet-ui-sol-balance.tsx @@ -0,0 +1,26 @@ +import { Group, Text } from '@mantine/core' +import { UiLoader } from '@pubkey-ui/core' +import { formatSol, Wallet } from '@tokengator-mint/sdk' +import { useSolanaGetBalance } from '@tokengator-mint/web-solana-data-access' + +export function WalletUiSolBalance({ wallet }: { wallet: Wallet }) { + const query = useSolanaGetBalance({ account: wallet.publicKey }) + + return query.isLoading ? ( + + + + ) : ( + query.refetch()} + style={{ + cursor: 'pointer', + }} + > + {formatSol(query.data ?? '0')} + + ) +} diff --git a/libs/web/wallet/ui/src/lib/wallet-ui-sol-token-accounts.tsx b/libs/web/wallet/ui/src/lib/wallet-ui-sol-token-accounts.tsx new file mode 100644 index 0000000..43c8913 --- /dev/null +++ b/libs/web/wallet/ui/src/lib/wallet-ui-sol-token-accounts.tsx @@ -0,0 +1,104 @@ +import { ActionIcon, Button, Group, Loader, Table, Text } from '@mantine/core' +import { UiError, UiInfo, UiStack } from '@pubkey-ui/core' +import { AccountInfo, ParsedAccountData, PublicKey } from '@solana/web3.js' +import { IconRefresh } from '@tabler/icons-react' +import { useQueryClient } from '@tanstack/react-query' +import { ellipsify, Wallet } from '@tokengator-mint/sdk' +import { useSolanaGetTokenAccounts } from '@tokengator-mint/web-solana-data-access' +import { SolanaUiExplorerLink } from '@tokengator-mint/web-solana-ui' +import { useMemo, useState } from 'react' + +export function WalletUiSolTokenAccounts({ wallet }: { wallet: Wallet }) { + const query = useSolanaGetTokenAccounts({ account: wallet.publicKey }) + + const [showAll, setShowAll] = useState(false) + const client = useQueryClient() + const items: { account: AccountInfo; pubkey: PublicKey }[] = useMemo(() => { + if (showAll) return query.data + return query.data?.slice(0, 5) + }, [query.data, showAll]) + + return ( +
+ + + Token Accounts + + {query.isLoading ? ( + + ) : ( + { + await query.refetch() + await client.invalidateQueries({ + queryKey: ['getTokenAccountBalance'], + }) + }} + > + + + )} + + + {query.isError && } + + {query.isSuccess && ( +
+ {query.data.length === 0 ? ( + + ) : ( + + + + Public Key + Mint + Balance + + + + {items + ?.sort( + (a, b) => + b.account.data.parsed.info.tokenAmount.uiAmount - + a.account.data.parsed.info.tokenAmount.uiAmount, + ) + .map(({ account, pubkey }) => ( + + + + + + + + {account.data.parsed.info.tokenAmount.uiAmount} + + ))} + + {(query.data?.length ?? 0) > 5 && ( + + + + + + )} + +
+ )} +
+ )} +
+
+ ) +} diff --git a/libs/web/wallet/ui/src/lib/wallet-ui-sol-transactions.tsx b/libs/web/wallet/ui/src/lib/wallet-ui-sol-transactions.tsx new file mode 100644 index 0000000..c4ea526 --- /dev/null +++ b/libs/web/wallet/ui/src/lib/wallet-ui-sol-transactions.tsx @@ -0,0 +1,86 @@ +import { ActionIcon, Badge, Button, Group, Loader, Table, Text } from '@mantine/core' +import { UiError, UiInfo, UiStack, UiTime } from '@pubkey-ui/core' +import { ConfirmedSignatureInfo } from '@solana/web3.js' +import { IconRefresh } from '@tabler/icons-react' +import { ellipsify, Wallet } from '@tokengator-mint/sdk' +import { useSolanaGetTransactions } from '@tokengator-mint/web-solana-data-access' +import { SolanaUiExplorerLink } from '@tokengator-mint/web-solana-ui' +import { useMemo, useState } from 'react' + +export function WalletUiSolTransactions({ wallet }: { wallet: Wallet }) { + const query = useSolanaGetTransactions({ account: wallet.publicKey }) + const [showAll, setShowAll] = useState(false) + + const items: ConfirmedSignatureInfo[] = useMemo(() => { + if (showAll) return query.data + return query.data?.slice(0, 5) + }, [query.data, showAll]) + + return ( + + + Transactions + {query.isLoading ? ( + + ) : ( + query.refetch()}> + + + )} + + {query.isError && } + {query.isSuccess && query.data.length === 0 ? ( + + ) : ( + + + + Signature + Slot + Block Time + Status + + + + {items?.map((item) => ( + + + + + + + + + + + + {item.err ? ( + + Failed + + ) : ( + Success + )} + + + ))} + {(query.data?.length ?? 0) > 5 && ( + + + + + + )} + +
+ )} +
+ ) +}