From ab0cd0433e7d8dcd74a134c0c8f9e521de8d0d72 Mon Sep 17 00:00:00 2001 From: Bram Borggreve Date: Mon, 8 Apr 2024 07:18:42 +0100 Subject: [PATCH] feat: create asset activity --- api-schema.graphql | 18 ++- .../src/lib/api-asset.data-access.module.ts | 3 +- .../data-access/src/lib/api-asset.service.ts | 72 +++++++-- .../src/lib/entity/asset-activity.entity.ts | 14 +- .../src/lib/entity/asset.entity.ts | 6 +- .../feature/src/lib/api-asset.resolver.ts | 17 ++- libs/api/preset/data-access/src/index.ts | 4 +- .../src/lib/api-preset-minter.service.ts | 143 +++++++++++++++++- .../src/lib/api-preset-provision-data.ts | 5 + .../src/lib/entity/preset-activity.enum.ts | 5 + .../src/lib/entity/preset.entity.ts | 3 + .../lib/entity/token-gator-activity.entity.ts | 7 + libs/sdk/src/generated/graphql-sdk.ts | 110 +++++++++++--- libs/sdk/src/graphql/feature-asset.graphql | 10 +- libs/sdk/src/graphql/feature-preset.graphql | 1 + .../src/lib/use-get-asset-activity.ts | 15 +- .../src/lib/user-asset-detail-feature.tsx | 37 +++-- .../ui/src/lib/asset-activity-ui-points.tsx | 4 +- libs/web/asset/ui/src/lib/asset-ui-item.tsx | 3 +- prisma/schema.prisma | 12 +- 20 files changed, 404 insertions(+), 85 deletions(-) create mode 100644 libs/api/preset/data-access/src/lib/entity/preset-activity.enum.ts create mode 100644 libs/api/preset/data-access/src/lib/entity/token-gator-activity.entity.ts diff --git a/api-schema.graphql b/api-schema.graphql index fe18507..0620adc 100644 --- a/api-schema.graphql +++ b/api-schema.graphql @@ -84,10 +84,10 @@ type AppConfig { type Asset { account: String! + activities: [PresetActivity!]! attributes: [[String!]!]! description: String! image: String! - lists: [AssetActivityType!]! name: String! } @@ -99,7 +99,7 @@ type AssetActivity { pointsLabel: String! pointsTotal: Float! startDate: DateTime! - type: AssetActivityType! + type: PresetActivity! } type AssetActivityEntry { @@ -109,11 +109,6 @@ type AssetActivityEntry { url: String } -enum AssetActivityType { - Payouts - Points -} - type Claim { account: String! amount: String! @@ -316,6 +311,7 @@ type Mutation { adminUpdateUser(input: AdminUpdateUserInput!, userId: String!): User adminUpdateWallet(input: WalletAdminUpdateInput!, walletId: String!): Wallet anonVerifyIdentityChallenge(input: VerifyIdentityChallengeInput!): IdentityChallenge + createAssetActivity(account: String!, type: PresetActivity!): AssetActivity login(input: LoginInput!): User logout: Boolean register(input: RegisterInput!): User @@ -355,6 +351,7 @@ type PagingMeta { } type Preset { + activities: [PresetActivity!] color: String! config: JSON createdAt: DateTime @@ -365,6 +362,11 @@ type Preset { updatedAt: DateTime } +enum PresetActivity { + Payouts + Points +} + input PresetAdminCreateInput { description: String name: String! @@ -464,7 +466,7 @@ type Query { appConfig: AppConfig! currencies: [Currency!]! getAsset(account: String!): Asset! - getAssetActivity(account: String!, type: AssetActivityType!): AssetActivity! + getAssetActivity(account: String!, type: PresetActivity!): AssetActivity me: User metadataAll(account: String!): JSON solanaGetBalance(account: String!): String diff --git a/libs/api/asset/data-access/src/lib/api-asset.data-access.module.ts b/libs/api/asset/data-access/src/lib/api-asset.data-access.module.ts index b80c906..074fcea 100644 --- a/libs/api/asset/data-access/src/lib/api-asset.data-access.module.ts +++ b/libs/api/asset/data-access/src/lib/api-asset.data-access.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common' import { ApiCoreDataAccessModule } from '@tokengator/api-core-data-access' import { ApiMetadataDataAccessModule } from '@tokengator/api-metadata-data-access' +import { ApiPresetDataAccessModule } from '@tokengator/api-preset-data-access' import { ApiAssetService } from './api-asset.service' @Module({ - imports: [ApiCoreDataAccessModule, ApiMetadataDataAccessModule], + imports: [ApiCoreDataAccessModule, ApiMetadataDataAccessModule, ApiPresetDataAccessModule], providers: [ApiAssetService], exports: [ApiAssetService], }) diff --git a/libs/api/asset/data-access/src/lib/api-asset.service.ts b/libs/api/asset/data-access/src/lib/api-asset.service.ts index 62cc3f4..303c4d3 100644 --- a/libs/api/asset/data-access/src/lib/api-asset.service.ts +++ b/libs/api/asset/data-access/src/lib/api-asset.service.ts @@ -1,37 +1,76 @@ import { Injectable } from '@nestjs/common' +import { PublicKey } from '@solana/web3.js' import { ApiMetadataService } from '@tokengator/api-metadata-data-access' -import { AssetActivity, AssetActivityType } from './entity/asset-activity.entity' +import { ApiPresetService, PresetActivity } from '@tokengator/api-preset-data-access' +import { AssetActivity } from './entity/asset-activity.entity' import { Asset } from './entity/asset.entity' @Injectable() export class ApiAssetService { - constructor(private readonly metadata: ApiMetadataService) {} + constructor(private readonly metadata: ApiMetadataService, private readonly preset: ApiPresetService) {} - async getAsset(account: string): Promise { - const { json } = await this.metadata.getAll(account) + async getAsset(account: string): Promise { + const { json, accountMetadata } = await this.metadata.getAll(account) if (!json) { throw new Error('Asset metadata not found') } + const mint = accountMetadata?.state.updateAuthority + if (!mint) { + throw new Error('Asset minter not found') + } + + const presetId = json.attributes.find((attr) => attr.trait_type === 'preset' && attr.value)?.value + if (!presetId) { + throw new Error('Asset preset not found') + } + + const preset = await this.preset.data.findOne(presetId) + return { account, name: json.name, description: json.description, image: json.image, - lists: this.getLists('business-visa'), + activities: preset.activities ?? [], attributes: json.attributes.map((attr) => [attr.trait_type, attr.value]), + mint, } } - async getAssetActivity(account: string, type: AssetActivityType): Promise { + async getAssetActivity(account: string, type: PresetActivity): Promise { + const { accountMetadata } = await this.metadata.getAll(account) + + const mint = accountMetadata?.state.updateAuthority + if (!mint) { + throw new Error('Asset minter not found') + } + + const activityPda = this.preset.minter.getActivityPda({ + mint: new PublicKey(mint), + label: type.toLowerCase(), + }) + + const activity = await this.preset.minter.getActivity({ account: activityPda }) + + if (typeof activity === 'boolean') { + // + // console.log('Creating activity...') + // await this.createAssetActivity(account, type) + // throw new Error('Activity not found') + return null + } + // this.preset.minter.getCommunityPda() + console.log(`Account, type: ${account}, ${type}`, activity) + const listAccount = `pda-${account}-${type}` const found = { label: `${type}`, startDate: new Date(), endDate: new Date().getTime() + 30 * 24 * 60 * 60 * 1000, entries: - type === AssetActivityType.Payouts + type === PresetActivity.Payouts ? [ { timestamp: new Date(2024, 0, 1), message: 'Payout for December 2023', points: 100 }, { timestamp: new Date(2024, 1, 1), message: 'Payout for January 2024', points: 100 }, @@ -50,7 +89,7 @@ export class ApiAssetService { } const pointsTotal = found.entries.reduce((acc, entry) => acc + (entry.points ?? 0), 0) - const pointsLabel = type === AssetActivityType.Payouts ? 'USD' : 'Points' + const pointsLabel = type === PresetActivity.Payouts ? 'USD' : 'Points' return { account: listAccount, @@ -64,11 +103,16 @@ export class ApiAssetService { } } - private getLists(preset: string): AssetActivityType[] { - // TODO: Specify per preset what activity lists are available - if (preset === 'business-visa') { - return [AssetActivityType.Payouts, AssetActivityType.Points] - } - return [AssetActivityType.Points] + async createAssetActivity(account: string, activity: PresetActivity) { + const asset = await this.getAsset(account) + const minter = await this.preset.minter.getMinter(asset.mint.toString()) + console.log('minter', minter) + // const activity = await this.getAssetActivity(account, type) + // + // if (activity) { + // return activity + // } + + return this.preset.minter.createActivity({ minter, asset: account, activity }) } } diff --git a/libs/api/asset/data-access/src/lib/entity/asset-activity.entity.ts b/libs/api/asset/data-access/src/lib/entity/asset-activity.entity.ts index 78e0715..09cc21f 100644 --- a/libs/api/asset/data-access/src/lib/entity/asset-activity.entity.ts +++ b/libs/api/asset/data-access/src/lib/entity/asset-activity.entity.ts @@ -1,18 +1,12 @@ -import { Field, ObjectType, registerEnumType } from '@nestjs/graphql' - -export enum AssetActivityType { - Payouts = 'Payouts', - Points = 'Points', -} - -registerEnumType(AssetActivityType, { name: 'AssetActivityType' }) +import { Field, ObjectType } from '@nestjs/graphql' +import { PresetActivity } from '@tokengator/api-preset-data-access' @ObjectType() export class AssetActivity { @Field() account!: string - @Field(() => AssetActivityType) - type!: AssetActivityType + @Field(() => PresetActivity) + type!: PresetActivity @Field() label!: string @Field() diff --git a/libs/api/asset/data-access/src/lib/entity/asset.entity.ts b/libs/api/asset/data-access/src/lib/entity/asset.entity.ts index 8f50f2a..b34a1b4 100644 --- a/libs/api/asset/data-access/src/lib/entity/asset.entity.ts +++ b/libs/api/asset/data-access/src/lib/entity/asset.entity.ts @@ -1,5 +1,5 @@ import { Field, ObjectType } from '@nestjs/graphql' -import { AssetActivityType } from './asset-activity.entity' +import { PresetActivity } from '@tokengator/api-preset-data-access' @ObjectType() export class Asset { @@ -11,8 +11,8 @@ export class Asset { description!: string @Field() image!: string - @Field(() => [AssetActivityType]) - lists!: AssetActivityType[] + @Field(() => [PresetActivity]) + activities!: PresetActivity[] @Field(() => [[String]]) attributes!: [string, string][] } diff --git a/libs/api/asset/feature/src/lib/api-asset.resolver.ts b/libs/api/asset/feature/src/lib/api-asset.resolver.ts index 3529b44..a47660b 100644 --- a/libs/api/asset/feature/src/lib/api-asset.resolver.ts +++ b/libs/api/asset/feature/src/lib/api-asset.resolver.ts @@ -1,7 +1,8 @@ import { UseGuards } from '@nestjs/common' -import { Args, Query, Resolver } from '@nestjs/graphql' -import { ApiAssetService, Asset, AssetActivity, AssetActivityType } from '@tokengator/api-asset-data-access' +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' +import { ApiAssetService, Asset, AssetActivity } from '@tokengator/api-asset-data-access' import { ApiAuthGraphQLUserGuard } from '@tokengator/api-auth-data-access' +import { PresetActivity } from '@tokengator/api-preset-data-access' @Resolver(() => Asset) @UseGuards(ApiAuthGraphQLUserGuard) @@ -13,11 +14,19 @@ export class ApiAssetResolver { return this.service.getAsset(account) } - @Query(() => AssetActivity) + @Query(() => AssetActivity, { nullable: true }) getAssetActivity( @Args('account') account: string, - @Args({ name: 'type', type: () => AssetActivityType }) type: AssetActivityType, + @Args({ name: 'type', type: () => PresetActivity }) type: PresetActivity, ) { return this.service.getAssetActivity(account, type) } + + @Mutation(() => AssetActivity, { nullable: true }) + createAssetActivity( + @Args('account') account: string, + @Args({ name: 'type', type: () => PresetActivity }) type: PresetActivity, + ) { + return this.service.createAssetActivity(account, type) + } } diff --git a/libs/api/preset/data-access/src/index.ts b/libs/api/preset/data-access/src/index.ts index 48b3c21..407c858 100644 --- a/libs/api/preset/data-access/src/index.ts +++ b/libs/api/preset/data-access/src/index.ts @@ -3,10 +3,12 @@ export * from './lib/api-preset.service' export * from './lib/dto/preset-admin-create.input' export * from './lib/dto/preset-admin-find-many.input' export * from './lib/dto/preset-admin-update.input' +export * from './lib/dto/preset-user-find-many.input' export * from './lib/dto/preset-user-mint-from-minter' export * from './lib/dto/preset-user-mint-from-preset' -export * from './lib/dto/preset-user-find-many.input' +export * from './lib/entity/preset-activity.enum' export * from './lib/entity/preset.entity' +export * from './lib/entity/token-gator-activity.entity' export * from './lib/entity/token-gator-minter-application-config.entity' export * from './lib/entity/token-gator-minter-config.entity' export * from './lib/entity/token-gator-minter-metadata-config.entity' diff --git a/libs/api/preset/data-access/src/lib/api-preset-minter.service.ts b/libs/api/preset/data-access/src/lib/api-preset-minter.service.ts index d1a54b0..1a2af0b 100644 --- a/libs/api/preset/data-access/src/lib/api-preset-minter.service.ts +++ b/libs/api/preset/data-access/src/lib/api-preset-minter.service.ts @@ -1,7 +1,7 @@ import * as anchor from '@coral-xyz/anchor' import { AnchorProvider, Program } from '@coral-xyz/anchor' import { Injectable, Logger } from '@nestjs/common' -import { Preset } from '@prisma/client' +import { Preset, PresetActivity } from '@prisma/client' import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token' import { @@ -21,6 +21,7 @@ import { import { ApiCoreService } from '@tokengator/api-core-data-access' import { ApiSolanaService } from '@tokengator/api-solana-data-access' import { + getActivityPda, getCommunityPda, getIdentityProviders, getMinterPda, @@ -34,10 +35,12 @@ import { WEN_NEW_STANDARD_PROGRAM_ID, WenNewStandardIDL, } from '@tokengator/api-solana-util' +import { Buffer } from 'buffer' import { LRUCache } from 'lru-cache' import { ApiPresetDataService } from './api-preset-data.service' import { PresetUserMintFromMinter } from './dto/preset-user-mint-from-minter' import { PresetUserMintFromPreset } from './dto/preset-user-mint-from-preset' +import { TokenGatorActivity } from './entity/token-gator-activity.entity' import { TokenGatorMinter } from './entity/token-gator-minter.entity' import { formatTokenGatorMinter } from './helpers/format-token-gator-minter' @@ -60,6 +63,20 @@ export class ApiPresetMinterService { return this.solana.connection.getSlot(commitment as Commitment) }, }) + private readonly cacheActivity = new LRUCache({ + max: 1000, + ttl: 30_000, + fetchMethod: async (activityPda: string) => { + this.logger.verbose(`Caching slot`) + return this.getProgramTokenMinter() + .account.activity.fetch(activityPda) + .then((res) => { + console.log(`Activity: ${activityPda}`, res) + return res ? (res as unknown as TokenGatorActivity) : false + }) + .catch(() => false) + }, + }) private readonly feePayer: Keypair private readonly programId = getTokengatorMinterProgramId('devnet') @@ -258,7 +275,7 @@ export class ApiPresetMinterService { const metadata: [string, string][] = [...collectionMetadata, ...metadataMint] as [string, string][] const feePayer = this.feePayer - const groupMintPublicKey = found.minterConfig.mint + const mintPublicKey = found.minterConfig.mint const memberMintKeypair = Keypair.generate() const { name, symbol, uri } = { uri: this.getMetadataUrl(memberMintKeypair.publicKey.toString()), @@ -273,7 +290,7 @@ export class ApiPresetMinterService { ) // ---- THIS WILL BE MOVED TO THE SDK AT SOME POINT ---- - const [group] = getWNSGroupPda(groupMintPublicKey, WEN_NEW_STANDARD_PROGRAM_ID) + const [group] = getWNSGroupPda(mintPublicKey, WEN_NEW_STANDARD_PROGRAM_ID) const [member] = getWNSMemberPda(memberMintKeypair.publicKey, WEN_NEW_STANDARD_PROGRAM_ID) const [manager] = getWNSManagerPda(WEN_NEW_STANDARD_PROGRAM_ID) @@ -382,6 +399,112 @@ export class ApiPresetMinterService { return signature } + async createActivity({ + minter, + asset, + activity, + }: { + minter: TokenGatorMinter + asset: string + activity: PresetActivity + }) { + // const memberPublicKey = new PublicKey(member) + const [activityPda] = getActivityPda({ + mint: new PublicKey(asset), + label: activity.toLowerCase(), + programId: this.programId, + }) + + const mintPublicKey = new PublicKey(minter.minterConfig.mint) + const [group] = getWNSGroupPda(mintPublicKey, WEN_NEW_STANDARD_PROGRAM_ID) + + const assetPublicKey = new PublicKey(asset) + const [member] = getWNSMemberPda(new PublicKey(asset), WEN_NEW_STANDARD_PROGRAM_ID) + + const [slot, { blockhash, lastValidBlockHeight }] = await Promise.all([ + this.getCachedSlot(), + this.getCachedBlockhash(), + ]) + + const [createLookupTableIx, lookupTableAccount] = AddressLookupTableProgram.createLookupTable({ + authority: this.feePayer.publicKey, + payer: this.feePayer.publicKey, + recentSlot: slot - 1, + }) + + const extendLookupTableIx = AddressLookupTableProgram.extendLookupTable({ + addresses: [ + group, + activityPda, + new PublicKey(minter.publicKey), + member, + this.feePayer.publicKey, + SYSVAR_RENT_PUBKEY, + WEN_NEW_STANDARD_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + SystemProgram.programId, + ], + authority: this.feePayer.publicKey, + lookupTable: lookupTableAccount, + payer: this.feePayer.publicKey, + }) + + const txMessage = new TransactionMessage({ + instructions: [createLookupTableIx, extendLookupTableIx], + payerKey: this.feePayer.publicKey, + recentBlockhash: blockhash, + }).compileToV0Message() + + const tx = new VersionedTransaction(txMessage) + tx.sign([this.feePayer]) + + await this.sendAndConfirmTransaction({ transaction: tx, blockhash, lastValidBlockHeight }) + + // Wait for 500 ms + await new Promise((resolve) => setTimeout(resolve, 750)) + + this.logger.debug(`Creating activity: ${activity} for minter: ${minter.publicKey}`) + const createActivityIx = await this.getProgramTokenMinter(this.solana.getAnchorProvider(this.feePayer)) + .methods.createActivity({ + label: activity.toLowerCase(), + endDate: null, + startDate: null, + }) + .accounts({ + activity: activityPda, + minter: minter.publicKey, + mint: assetPublicKey, + group, + member, + systemProgram: SystemProgram.programId, + }) + .instruction() + + const { value: lookupTableStore } = await this.solana.connection.getAddressLookupTable(lookupTableAccount) + + if (!lookupTableStore) { + throw new Error('Lookup table not found') + } + this.logger.debug(`Lookup table: ${lookupTableStore}`) + const transactionMessage = new TransactionMessage({ + instructions: [ComputeBudgetProgram.setComputeUnitLimit({ units: 30_000 }), createActivityIx], + payerKey: this.feePayer.publicKey, + recentBlockhash: blockhash, + }).compileToV0Message([lookupTableStore]) + + const transaction = new VersionedTransaction(transactionMessage) + transaction.sign([this.feePayer]) + + this.logger.debug(`Sending Transaction: ${transactionMessage}`) + + await this.sendAndConfirmTransaction({ transaction, blockhash, lastValidBlockHeight }) + + this.logger.verbose(`Activity created: ${activity} for minter: ${minter.publicKey}`) + + return + } + async getMinters(): Promise { return this.getProgramTokenMinter() .account.minter.all() @@ -399,6 +522,19 @@ export class ApiPresetMinterService { ) } + getActivityPda({ mint, label }: { mint: PublicKey; label: string }) { + const [account] = getActivityPda({ + mint, + label: label.toLowerCase(), + programId: this.programId, + }) + return account + } + + async getActivity({ account }: { account: PublicKey }) { + return this.cacheActivity.fetch(account.toString()) + } + getCommunityPda(communitySlug: string): PublicKey { const [account] = getCommunityPda(communitySlug, this.programId) return account @@ -481,6 +617,7 @@ export class ApiPresetMinterService { blockhash: string lastValidBlockHeight: number }): Promise { + console.log(Buffer.from(transaction.serialize()).toString('base64')) const signature = await this.solana.connection.sendTransaction(transaction, { skipPreflight: true }) this.logger.debug(`Signature: ${signature}`) await this.solana.connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature }, 'confirmed') diff --git a/libs/api/preset/data-access/src/lib/api-preset-provision-data.ts b/libs/api/preset/data-access/src/lib/api-preset-provision-data.ts index 866416a..ff0a9f0 100644 --- a/libs/api/preset/data-access/src/lib/api-preset-provision-data.ts +++ b/libs/api/preset/data-access/src/lib/api-preset-provision-data.ts @@ -1,4 +1,5 @@ import { Prisma } from '@prisma/client' +import { PresetActivity } from './entity/preset-activity.enum' export const provisionPresets: Prisma.PresetCreateInput[] = [ { @@ -7,6 +8,7 @@ export const provisionPresets: Prisma.PresetCreateInput[] = [ imageUrl: 'https://raw.githubusercontent.com/pubkeyapp/tokengator-assets/main/presets/preset-business-visa.png', color: 'indigo', config: {}, + activities: [PresetActivity.Payouts], prices: { create: [ // @@ -21,6 +23,7 @@ export const provisionPresets: Prisma.PresetCreateInput[] = [ imageUrl: 'https://raw.githubusercontent.com/pubkeyapp/tokengator-assets/main/presets/preset-visitor-pass.png', color: 'lime', config: {}, + activities: [PresetActivity.Points], prices: { create: [ // @@ -35,6 +38,7 @@ export const provisionPresets: Prisma.PresetCreateInput[] = [ imageUrl: 'https://raw.githubusercontent.com/pubkeyapp/tokengator-assets/main/presets/preset-citizenship.png', color: 'grape', config: {}, + activities: [], prices: { create: [ // @@ -49,6 +53,7 @@ export const provisionPresets: Prisma.PresetCreateInput[] = [ imageUrl: 'https://raw.githubusercontent.com/pubkeyapp/tokengator-assets/main/presets/preset-residence.png', color: 'teal', config: {}, + activities: [PresetActivity.Payouts], prices: { create: [ // diff --git a/libs/api/preset/data-access/src/lib/entity/preset-activity.enum.ts b/libs/api/preset/data-access/src/lib/entity/preset-activity.enum.ts new file mode 100644 index 0000000..801aa2e --- /dev/null +++ b/libs/api/preset/data-access/src/lib/entity/preset-activity.enum.ts @@ -0,0 +1,5 @@ +import { registerEnumType } from '@nestjs/graphql' +import { PresetActivity } from '@prisma/client' +export { PresetActivity } + +registerEnumType(PresetActivity, { name: 'PresetActivity' }) \ No newline at end of file diff --git a/libs/api/preset/data-access/src/lib/entity/preset.entity.ts b/libs/api/preset/data-access/src/lib/entity/preset.entity.ts index 8486878..11d86be 100644 --- a/libs/api/preset/data-access/src/lib/entity/preset.entity.ts +++ b/libs/api/preset/data-access/src/lib/entity/preset.entity.ts @@ -2,6 +2,7 @@ import { Field, ObjectType } from '@nestjs/graphql' import { Prisma } from '@prisma/client' import { PagingResponse } from '@tokengator/api-core-data-access' import { GraphQLJSON } from 'graphql-scalars' +import { PresetActivity } from './preset-activity.enum' @ObjectType() export class Preset { @@ -21,6 +22,8 @@ export class Preset { imageUrl?: string | null @Field(() => GraphQLJSON, { nullable: true }) config!: Prisma.JsonValue | null + @Field(() => [PresetActivity], { nullable: true }) + activities?: PresetActivity[] } @ObjectType() diff --git a/libs/api/preset/data-access/src/lib/entity/token-gator-activity.entity.ts b/libs/api/preset/data-access/src/lib/entity/token-gator-activity.entity.ts new file mode 100644 index 0000000..7033e09 --- /dev/null +++ b/libs/api/preset/data-access/src/lib/entity/token-gator-activity.entity.ts @@ -0,0 +1,7 @@ +import { Field, ObjectType } from '@nestjs/graphql' + +@ObjectType() +export class TokenGatorActivity { + @Field(() => String) + account!: string +} diff --git a/libs/sdk/src/generated/graphql-sdk.ts b/libs/sdk/src/generated/graphql-sdk.ts index b5201cf..0d72a78 100644 --- a/libs/sdk/src/generated/graphql-sdk.ts +++ b/libs/sdk/src/generated/graphql-sdk.ts @@ -108,10 +108,10 @@ export type AppConfig = { export type Asset = { __typename?: 'Asset' account: Scalars['String']['output'] + activities: Array attributes: Array> description: Scalars['String']['output'] image: Scalars['String']['output'] - lists: Array name: Scalars['String']['output'] } @@ -124,7 +124,7 @@ export type AssetActivity = { pointsLabel: Scalars['String']['output'] pointsTotal: Scalars['Float']['output'] startDate: Scalars['DateTime']['output'] - type: AssetActivityType + type: PresetActivity } export type AssetActivityEntry = { @@ -135,11 +135,6 @@ export type AssetActivityEntry = { url?: Maybe } -export enum AssetActivityType { - Payouts = 'Payouts', - Points = 'Points', -} - export type Claim = { __typename?: 'Claim' account: Scalars['String']['output'] @@ -342,6 +337,7 @@ export type Mutation = { adminUpdateUser?: Maybe adminUpdateWallet?: Maybe anonVerifyIdentityChallenge?: Maybe + createAssetActivity?: Maybe login?: Maybe logout?: Maybe register?: Maybe @@ -469,6 +465,11 @@ export type MutationAnonVerifyIdentityChallengeArgs = { input: VerifyIdentityChallengeInput } +export type MutationCreateAssetActivityArgs = { + account: Scalars['String']['input'] + type: PresetActivity +} + export type MutationLoginArgs = { input: LoginInput } @@ -591,6 +592,7 @@ export type PagingMeta = { export type Preset = { __typename?: 'Preset' + activities?: Maybe> color: Scalars['String']['output'] config?: Maybe createdAt?: Maybe @@ -601,6 +603,11 @@ export type Preset = { updatedAt?: Maybe } +export enum PresetActivity { + Payouts = 'Payouts', + Points = 'Points', +} + export type PresetAdminCreateInput = { description?: InputMaybe name: Scalars['String']['input'] @@ -703,7 +710,7 @@ export type Query = { appConfig: AppConfig currencies: Array getAsset: Asset - getAssetActivity: AssetActivity + getAssetActivity?: Maybe me?: Maybe metadataAll?: Maybe solanaGetBalance?: Maybe @@ -811,7 +818,7 @@ export type QueryGetAssetArgs = { export type QueryGetAssetActivityArgs = { account: Scalars['String']['input'] - type: AssetActivityType + type: PresetActivity } export type QueryMetadataAllArgs = { @@ -1109,7 +1116,7 @@ export type AssetDetailsFragment = { name: string description: string image: string - lists: Array + activities: Array attributes: Array> } @@ -1123,7 +1130,7 @@ export type AssetActivityEntryDetailsFragment = { export type AssetActivityDetailsFragment = { __typename?: 'AssetActivity' - type: AssetActivityType + type: PresetActivity label: string startDate: Date endDate: Date @@ -1150,21 +1157,21 @@ export type GetAssetQuery = { name: string description: string image: string - lists: Array + activities: Array attributes: Array> } } export type GetAssetActivityQueryVariables = Exact<{ account: Scalars['String']['input'] - type: AssetActivityType + type: PresetActivity }> export type GetAssetActivityQuery = { __typename?: 'Query' - item: { + item?: { __typename?: 'AssetActivity' - type: AssetActivityType + type: PresetActivity label: string startDate: Date endDate: Date @@ -1177,7 +1184,32 @@ export type GetAssetActivityQuery = { points?: number | null url?: string | null }> | null - } + } | null +} + +export type CreateAssetActivityMutationVariables = Exact<{ + account: Scalars['String']['input'] + type: PresetActivity +}> + +export type CreateAssetActivityMutation = { + __typename?: 'Mutation' + item?: { + __typename?: 'AssetActivity' + type: PresetActivity + label: string + startDate: Date + endDate: Date + pointsLabel: string + pointsTotal: number + entries?: Array<{ + __typename?: 'AssetActivityEntry' + timestamp: Date + message: string + points?: number | null + url?: string | null + }> | null + } | null } export type LoginMutationVariables = Exact<{ @@ -3147,6 +3179,7 @@ export type PresetDetailsFragment = { imageUrl?: string | null color: string config?: any | null + activities?: Array | null updatedAt?: Date | null } @@ -3167,6 +3200,7 @@ export type AdminFindManyPresetQuery = { imageUrl?: string | null color: string config?: any | null + activities?: Array | null updatedAt?: Date | null }> meta: { @@ -3197,6 +3231,7 @@ export type AdminFindOnePresetQuery = { imageUrl?: string | null color: string config?: any | null + activities?: Array | null updatedAt?: Date | null } | null } @@ -3216,6 +3251,7 @@ export type AdminCreatePresetMutation = { imageUrl?: string | null color: string config?: any | null + activities?: Array | null updatedAt?: Date | null } | null } @@ -3236,6 +3272,7 @@ export type AdminUpdatePresetMutation = { imageUrl?: string | null color: string config?: any | null + activities?: Array | null updatedAt?: Date | null } | null } @@ -3263,6 +3300,7 @@ export type UserFindManyPresetQuery = { imageUrl?: string | null color: string config?: any | null + activities?: Array | null updatedAt?: Date | null }> meta: { @@ -3293,6 +3331,7 @@ export type UserFindOnePresetQuery = { imageUrl?: string | null color: string config?: any | null + activities?: Array | null updatedAt?: Date | null } | null } @@ -4194,7 +4233,7 @@ export const AssetDetailsFragmentDoc = gql` name description image - lists + activities attributes } ` @@ -4407,6 +4446,7 @@ export const PresetDetailsFragmentDoc = gql` imageUrl color config + activities updatedAt } ` @@ -4456,13 +4496,21 @@ export const GetAssetDocument = gql` ${AssetDetailsFragmentDoc} ` export const GetAssetActivityDocument = gql` - query getAssetActivity($account: String!, $type: AssetActivityType!) { + query getAssetActivity($account: String!, $type: PresetActivity!) { item: getAssetActivity(account: $account, type: $type) { ...AssetActivityDetails } } ${AssetActivityDetailsFragmentDoc} ` +export const CreateAssetActivityDocument = gql` + mutation createAssetActivity($account: String!, $type: PresetActivity!) { + item: createAssetActivity(account: $account, type: $type) { + ...AssetActivityDetails + } + } + ${AssetActivityDetailsFragmentDoc} +` export const LoginDocument = gql` mutation login($input: LoginInput!) { login(input: $input) { @@ -5258,6 +5306,7 @@ export type SdkFunctionWrapper = ( const defaultWrapper: SdkFunctionWrapper = (action, _operationName, _operationType, variables) => action() const GetAssetDocumentString = print(GetAssetDocument) const GetAssetActivityDocumentString = print(GetAssetActivityDocument) +const CreateAssetActivityDocumentString = print(CreateAssetActivityDocument) const LoginDocumentString = print(LoginDocument) const LogoutDocumentString = print(LogoutDocument) const RegisterDocumentString = print(RegisterDocument) @@ -5391,6 +5440,27 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = variables, ) }, + createAssetActivity( + variables: CreateAssetActivityMutationVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise<{ + data: CreateAssetActivityMutation + errors?: GraphQLError[] + extensions?: any + headers: Headers + status: number + }> { + return withWrapper( + (wrappedRequestHeaders) => + client.rawRequest(CreateAssetActivityDocumentString, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + 'createAssetActivity', + 'mutation', + variables, + ) + }, login( variables: LoginMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, @@ -7361,14 +7431,14 @@ export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== und export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)) -export const AssetActivityTypeSchema = z.nativeEnum(AssetActivityType) - export const ClaimStatusSchema = z.nativeEnum(ClaimStatus) export const CommunityMemberRoleSchema = z.nativeEnum(CommunityMemberRole) export const IdentityProviderSchema = z.nativeEnum(IdentityProvider) +export const PresetActivitySchema = z.nativeEnum(PresetActivity) + export const UserRoleSchema = z.nativeEnum(UserRole) export const UserStatusSchema = z.nativeEnum(UserStatus) diff --git a/libs/sdk/src/graphql/feature-asset.graphql b/libs/sdk/src/graphql/feature-asset.graphql index 18c3dbd..2accd9c 100644 --- a/libs/sdk/src/graphql/feature-asset.graphql +++ b/libs/sdk/src/graphql/feature-asset.graphql @@ -3,7 +3,7 @@ fragment AssetDetails on Asset { name description image - lists + activities attributes } @@ -32,8 +32,14 @@ query getAsset($account: String!) { } } -query getAssetActivity($account: String!, $type: AssetActivityType!) { +query getAssetActivity($account: String!, $type: PresetActivity!) { item: getAssetActivity(account: $account, type: $type) { ...AssetActivityDetails } } + +mutation createAssetActivity($account: String!, $type: PresetActivity!) { + item: createAssetActivity(account: $account, type: $type) { + ...AssetActivityDetails + } +} diff --git a/libs/sdk/src/graphql/feature-preset.graphql b/libs/sdk/src/graphql/feature-preset.graphql index b89a9ad..70db2cb 100644 --- a/libs/sdk/src/graphql/feature-preset.graphql +++ b/libs/sdk/src/graphql/feature-preset.graphql @@ -6,6 +6,7 @@ fragment PresetDetails on Preset { imageUrl color config + activities updatedAt } diff --git a/libs/web/asset/data-access/src/lib/use-get-asset-activity.ts b/libs/web/asset/data-access/src/lib/use-get-asset-activity.ts index a1c3609..d2ba8df 100644 --- a/libs/web/asset/data-access/src/lib/use-get-asset-activity.ts +++ b/libs/web/asset/data-access/src/lib/use-get-asset-activity.ts @@ -1,8 +1,8 @@ -import { useQuery } from '@tanstack/react-query' -import { AssetActivity, AssetActivityType } from '@tokengator/sdk' +import { useMutation, useQuery } from '@tanstack/react-query' +import { AssetActivity, PresetActivity } from '@tokengator/sdk' import { useSdk } from '@tokengator/web-core-data-access' -export function useGetAssetActivity({ account, type }: { account: string; type: AssetActivityType }) { +export function useGetAssetActivity({ account, type }: { account: string; type: PresetActivity }) { const sdk = useSdk() return useQuery({ @@ -11,3 +11,12 @@ export function useGetAssetActivity({ account, type }: { account: string; type: sdk.getAssetActivity({ account, type }).then((res) => res.data?.item as AssetActivity | undefined), }) } + +export function useCreateAssetActivity({ account, type }: { account: string; type: PresetActivity }) { + const sdk = useSdk() + + return useMutation({ + mutationFn: async () => + sdk.createAssetActivity({ account, type }).then((res) => res.data?.item as AssetActivity | undefined), + }) +} diff --git a/libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx b/libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx index d0939dc..e1114de 100644 --- a/libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx +++ b/libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx @@ -1,8 +1,8 @@ -import { Accordion, Group, SimpleGrid, Text } from '@mantine/core' -import { UiCard, UiDebugModal, UiGroup, UiInfo, UiLoader, UiPage, UiWarning } from '@pubkey-ui/core' +import { Accordion, Button, Group, SimpleGrid, Text } from '@mantine/core' +import { UiCard, UiDebugModal, UiGroup, UiInfo, UiLoader, UiPage, UiStack, UiWarning } from '@pubkey-ui/core' import { useQuery } from '@tanstack/react-query' -import { Asset, AssetActivityType } from '@tokengator/sdk' -import { useGetAsset, useGetAssetActivity } from '@tokengator/web-asset-data-access' +import { Asset, PresetActivity } from '@tokengator/sdk' +import { useCreateAssetActivity, useGetAsset, useGetAssetActivity } from '@tokengator/web-asset-data-access' import { AssetActivityUiEntryList, AssetActivityUiPoints, AssetUiItem } from '@tokengator/web-asset-ui' import { useSdk } from '@tokengator/web-core-data-access' import { SolanaExplorerIcon } from '@tokengator/web-solana-ui' @@ -12,7 +12,7 @@ export function useMetadataAll(account: string) { const sdk = useSdk() return useQuery({ - queryKey: [], + queryKey: ['metadataAll', account], queryFn: async () => sdk.metadataAll({ account }).then((res) => res.data?.item), }) } @@ -54,7 +54,7 @@ export function UserAssetDetailFeature() { function UserAssetActivities({ asset }: { asset: Asset }) { return ( - {asset.lists?.map((type) => ( + {asset.activities?.map((type) => ( @@ -68,7 +68,7 @@ function UserAssetActivities({ asset }: { asset: Asset }) { ) } -function UserAssetActivityLabel({ account, type }: { account: string; type: AssetActivityType }) { +function UserAssetActivityLabel({ account, type }: { account: string; type: PresetActivity }) { const query = useGetAssetActivity({ account, type }) const activity = query.data @@ -84,8 +84,9 @@ function UserAssetActivityLabel({ account, type }: { account: string; type: Asse ) } -function UserAssetActivityDetails({ account, type }: { account: string; type: AssetActivityType }) { +function UserAssetActivityDetails({ account, type }: { account: string; type: PresetActivity }) { const query = useGetAssetActivity({ account, type }) + const mutation = useCreateAssetActivity({ account, type }) const activity = query.data const entries = activity?.entries || [] return query.isLoading ? ( @@ -93,6 +94,24 @@ function UserAssetActivityDetails({ account, type }: { account: string; type: As ) : activity ? ( ) : ( - + + + + + ) } diff --git a/libs/web/asset/ui/src/lib/asset-activity-ui-points.tsx b/libs/web/asset/ui/src/lib/asset-activity-ui-points.tsx index c97a90c..222bcb1 100644 --- a/libs/web/asset/ui/src/lib/asset-activity-ui-points.tsx +++ b/libs/web/asset/ui/src/lib/asset-activity-ui-points.tsx @@ -1,5 +1,5 @@ import { Text, TextProps } from '@mantine/core' -import { AssetActivity, AssetActivityEntry, AssetActivityType } from '@tokengator/sdk' +import { AssetActivity, AssetActivityEntry, PresetActivity } from '@tokengator/sdk' export function AssetActivityUiPoints({ activity, @@ -8,7 +8,7 @@ export function AssetActivityUiPoints({ }: TextProps & { activity: AssetActivity; entry?: AssetActivityEntry }) { const points = entry?.points ?? activity.pointsTotal ?? 0 const label = activity.pointsLabel ?? 'Points' - const prefix = activity.type === AssetActivityType.Payouts ? '$' : '' + const prefix = activity.type === PresetActivity.Payouts ? '$' : '' return ( diff --git a/libs/web/asset/ui/src/lib/asset-ui-item.tsx b/libs/web/asset/ui/src/lib/asset-ui-item.tsx index 616915a..5a438b7 100644 --- a/libs/web/asset/ui/src/lib/asset-ui-item.tsx +++ b/libs/web/asset/ui/src/lib/asset-ui-item.tsx @@ -13,11 +13,10 @@ export function AssetUiItem({ asset }: { asset: Asset }) { {asset.name}], ['Description', asset.description], ['account', asset.account], - ['lists', asset.lists.join(', ')], + ['activities', asset.activities.join(', ')], ...asset.attributes, ]} /> diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 25a2c1d..b5e5176 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -90,14 +90,15 @@ model IdentityChallenge { } model Preset { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt name String color String description String? imageUrl String? config Json + activities PresetActivity[] prices Price[] } @@ -161,6 +162,11 @@ enum IdentityProvider { Twitter } +enum PresetActivity { + Payouts + Points +} + enum UserRole { Admin User