From 8b94788b13426cdf381fbdda7aebc406f716aae4 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Tue, 24 May 2022 17:41:26 +0300 Subject: [PATCH] feat: build dontaions table & api --- functions/graphql/helpers/consts.js | 5 + functions/graphql/nexus-typegen.ts | 57 +++++++ functions/graphql/schema.graphql | 20 +++ functions/graphql/types/donation.js | 150 ++++++++++++++++++ functions/graphql/types/index.js | 4 +- functions/graphql/types/vote.js | 5 +- .../migration.sql | 16 ++ prisma/schema.prisma | 17 ++ src/features/Donations/index.tsx | 1 + .../Donations/pages/DonationsPage.tsx | 12 ++ src/features/Donations/pages/donate.graphql | 16 ++ src/graphql/index.tsx | 120 ++++++++++++++ 12 files changed, 419 insertions(+), 4 deletions(-) create mode 100644 functions/graphql/helpers/consts.js create mode 100644 functions/graphql/types/donation.js create mode 100644 prisma/migrations/20220524142531_create_donations_table/migration.sql create mode 100644 src/features/Donations/index.tsx create mode 100644 src/features/Donations/pages/DonationsPage.tsx create mode 100644 src/features/Donations/pages/donate.graphql diff --git a/functions/graphql/helpers/consts.js b/functions/graphql/helpers/consts.js new file mode 100644 index 0000000..1dcc8f1 --- /dev/null +++ b/functions/graphql/helpers/consts.js @@ -0,0 +1,5 @@ +const BOLT_FUN_LIGHTNING_ADDRESS = 'johns@getalby.com'; // #TODO, replace it by bolt-fun lightning address if there exist one + +module.exports = { + BOLT_FUN_LIGHTNING_ADDRESS, +} \ No newline at end of file diff --git a/functions/graphql/nexus-typegen.ts b/functions/graphql/nexus-typegen.ts index f97c0c8..c626bf8 100644 --- a/functions/graphql/nexus-typegen.ts +++ b/functions/graphql/nexus-typegen.ts @@ -76,6 +76,20 @@ export interface NexusGenObjects { id: number; // Int! title: string; // String! } + Donation: { // root type + amount: number; // Int! + createdAt: NexusGenScalars['Date']; // Date! + id: number; // Int! + paid: boolean; // Boolean! + payment_hash: string; // String! + payment_request: string; // String! + } + DonationsStats: { // root type + applications: number; // Int! + donations: number; // Int! + prizes: number; // Int! + touranments: number; // Int! + } Hackathon: { // root type cover_image: string; // String! description: string; // String! @@ -208,6 +222,21 @@ export interface NexusGenFieldTypes { title: string; // String! votes_sum: number; // Int! } + Donation: { // field return type + amount: number; // Int! + by: NexusGenRootTypes['User'] | null; // User + createdAt: NexusGenScalars['Date']; // Date! + id: number; // Int! + paid: boolean; // Boolean! + payment_hash: string; // String! + payment_request: string; // String! + } + DonationsStats: { // field return type + applications: number; // Int! + donations: number; // Int! + prizes: number; // Int! + touranments: number; // Int! + } Hackathon: { // field return type cover_image: string; // String! description: string; // String! @@ -226,7 +255,9 @@ export interface NexusGenFieldTypes { minSendable: number | null; // Int } Mutation: { // field return type + confirmDonation: NexusGenRootTypes['Donation']; // Donation! confirmVote: NexusGenRootTypes['Vote']; // Vote! + donate: NexusGenRootTypes['Donation']; // Donation! vote: NexusGenRootTypes['Vote']; // Vote! } PostComment: { // field return type @@ -258,6 +289,7 @@ export interface NexusGenFieldTypes { allTopics: NexusGenRootTypes['Topic'][]; // [Topic!]! getAllHackathons: NexusGenRootTypes['Hackathon'][]; // [Hackathon!]! getCategory: NexusGenRootTypes['Category']; // Category! + getDonationsStats: NexusGenRootTypes['DonationsStats'][]; // [DonationsStats!]! getFeed: NexusGenRootTypes['Post'][]; // [Post!]! getLnurlDetailsForProject: NexusGenRootTypes['LnurlDetails']; // LnurlDetails! getPostById: NexusGenRootTypes['Post']; // Post! @@ -369,6 +401,21 @@ export interface NexusGenFieldTypeNames { title: 'String' votes_sum: 'Int' } + Donation: { // field return type name + amount: 'Int' + by: 'User' + createdAt: 'Date' + id: 'Int' + paid: 'Boolean' + payment_hash: 'String' + payment_request: 'String' + } + DonationsStats: { // field return type name + applications: 'Int' + donations: 'Int' + prizes: 'Int' + touranments: 'Int' + } Hackathon: { // field return type name cover_image: 'String' description: 'String' @@ -387,7 +434,9 @@ export interface NexusGenFieldTypeNames { minSendable: 'Int' } Mutation: { // field return type name + confirmDonation: 'Donation' confirmVote: 'Vote' + donate: 'Donation' vote: 'Vote' } PostComment: { // field return type name @@ -419,6 +468,7 @@ export interface NexusGenFieldTypeNames { allTopics: 'Topic' getAllHackathons: 'Hackathon' getCategory: 'Category' + getDonationsStats: 'DonationsStats' getFeed: 'Post' getLnurlDetailsForProject: 'LnurlDetails' getPostById: 'Post' @@ -493,10 +543,17 @@ export interface NexusGenFieldTypeNames { export interface NexusGenArgTypes { Mutation: { + confirmDonation: { // args + payment_request: string; // String! + preimage: string; // String! + } confirmVote: { // args payment_request: string; // String! preimage: string; // String! } + donate: { // args + amount_in_sat: number; // Int! + } vote: { // args amount_in_sat: number; // Int! item_id: number; // Int! diff --git a/functions/graphql/schema.graphql b/functions/graphql/schema.graphql index b834674..07ff7dc 100644 --- a/functions/graphql/schema.graphql +++ b/functions/graphql/schema.graphql @@ -47,6 +47,23 @@ type Category { """Date custom scalar type""" scalar Date +type Donation { + amount: Int! + by: User + createdAt: Date! + id: Int! + paid: Boolean! + payment_hash: String! + payment_request: String! +} + +type DonationsStats { + applications: Int! + donations: Int! + prizes: Int! + touranments: Int! +} + type Hackathon { cover_image: String! description: String! @@ -67,7 +84,9 @@ type LnurlDetails { } type Mutation { + confirmDonation(payment_request: String!, preimage: String!): Donation! confirmVote(payment_request: String!, preimage: String!): Vote! + donate(amount_in_sat: Int!): Donation! vote(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote! } @@ -119,6 +138,7 @@ type Query { allTopics: [Topic!]! getAllHackathons(sortBy: String, topic: Int): [Hackathon!]! getCategory(id: Int!): Category! + getDonationsStats: [DonationsStats!]! getFeed(skip: Int = 0, sortBy: String = "all", take: Int = 10, topic: Int = 0): [Post!]! getLnurlDetailsForProject(project_id: Int!): LnurlDetails! getPostById(id: Int!, type: POST_TYPE!): Post! diff --git a/functions/graphql/types/donation.js b/functions/graphql/types/donation.js new file mode 100644 index 0000000..fb9d57d --- /dev/null +++ b/functions/graphql/types/donation.js @@ -0,0 +1,150 @@ +const { createHash } = require('crypto'); +const { parsePaymentRequest } = require('invoices'); +const { + intArg, + objectType, + stringArg, + extendType, + nonNull, +} = require('nexus'); +const { BOLT_FUN_LIGHTNING_ADDRESS } = require('../helpers/consts'); +const { prisma } = require('../prisma'); +const { getPaymetRequestForItem, hexToUint8Array } = require('./helpers'); + + + +const Donation = objectType({ + name: 'Donation', + definition(t) { + t.nonNull.int('id'); + t.nonNull.int('amount'); + t.nonNull.date('createdAt'); + t.nonNull.string('payment_request'); + t.nonNull.string('payment_hash'); + t.nonNull.boolean('paid'); + + t.field('by', { + type: 'User', + resolve: (parent) => { + return prisma.donation.findUnique({ where: { id: parent.id } }).donor(); + } + }); + } +}) + + + +const donateMutation = extendType({ + type: "Mutation", + definition(t) { + t.nonNull.field('donate', { + type: "Donation", + args: { + amount_in_sat: nonNull(intArg()) + }, + resolve: async (_, args) => { + + const { amount_in_sat } = args; + const lightning_address = BOLT_FUN_LIGHTNING_ADDRESS; + const pr = await getPaymetRequestForItem(lightning_address, args.amount_in_sat); + const invoice = parsePaymentRequest({ request: pr }); + + return prisma.donation.create({ + data: { + amount: amount_in_sat, + payment_request: pr, + payment_hash: invoice.id, + } + }); + } + }) + } +}) + +const confirmDonateMutation = extendType({ + type: "Mutation", + definition(t) { + t.nonNull.field('confirmDonation', { + type: "Donation", + args: { + payment_request: nonNull(stringArg()), + preimage: nonNull(stringArg()) + }, + resolve: async (_, args) => { + const paymentHash = createHash("sha256") + .update(hexToUint8Array(args.preimage)) + .digest("hex"); + // look for a vote for the payment request and the calculated payment hash + const donation = await prisma.donation.findFirst({ + where: { + payment_request: args.payment_request, + payment_hash: paymentHash, + }, + }); + + // if we find a donation it means the preimage is correct and we update the donation and mark it as paid + // can we write this nicer? + if (donation) { + + // return the current donation + return prisma.donation.update({ + where: { id: donation.id }, + data: { + paid: true, + preimage: args.preimage, + } + }); + } else { + throw new Error("Invalid preimage"); + } + } + }) + } +}) + + +const DonationsStats = objectType({ + name: 'DonationsStats', + definition(t) { + t.nonNull.int("prizes"); + t.nonNull.int("touranments"); + t.nonNull.int("donations"); + t.nonNull.int("applications"); + }, +}) + +const getDonationsStats = extendType({ + type: "Query", + definition(t) { + t.nonNull.list.nonNull.field('getDonationsStats', { + type: "DonationsStats", + resolve() { + return { + prizes: 2600, + touranments: 2, + donations: prisma.donation.aggregate({ + _sum: { + amount: true + }, + where: { + paid: true + } + }), + applications: prisma.project.count() + } + } + }) + } +}) + + + +module.exports = { + // Types + Donation, + DonationsStats, + // Queries + donateMutation, + confirmDonateMutation, + getDonationsStats, +} \ No newline at end of file diff --git a/functions/graphql/types/index.js b/functions/graphql/types/index.js index 51268f1..ddbf999 100644 --- a/functions/graphql/types/index.js +++ b/functions/graphql/types/index.js @@ -5,6 +5,7 @@ const vote = require('./vote') const post = require('./post') const users = require('./users') const hackathon = require('./hackathon') +const donation = require('./donation') module.exports = { ...scalars, @@ -13,5 +14,6 @@ module.exports = { ...vote, ...post, ...users, - ...hackathon + ...hackathon, + ...donation, } \ No newline at end of file diff --git a/functions/graphql/types/vote.js b/functions/graphql/types/vote.js index c7b074f..d790268 100644 --- a/functions/graphql/types/vote.js +++ b/functions/graphql/types/vote.js @@ -10,7 +10,8 @@ const { const { parsePaymentRequest } = require('invoices'); const { getPaymetRequestForItem, hexToUint8Array } = require('./helpers'); const { createHash } = require('crypto'); -const { prisma } = require('../prisma') +const { prisma } = require('../prisma'); +const { BOLT_FUN_LIGHTNING_ADDRESS } = require('../helpers/consts'); // the types of items we can vote to @@ -18,7 +19,6 @@ const VOTE_ITEM_TYPE = enumType({ name: 'VOTE_ITEM_TYPE', members: ['Story', 'Bounty', 'Question', 'Project', 'User', 'PostComment'], }) -const BOLT_FUN_LIGHTNING_ADDRESS = 'johns@getalby.com'; // #TODO, replace it by bolt-fun lightning address if there exist one const Vote = objectType({ @@ -133,7 +133,6 @@ const voteMutation = extendType({ const { item_id, item_type, amount_in_sat } = args; const lightning_address = (await getLightningAddress(item_id, item_type)) ?? BOLT_FUN_LIGHTNING_ADDRESS; const pr = await getPaymetRequestForItem(lightning_address, args.amount_in_sat); - console.log(pr); const invoice = parsePaymentRequest({ request: pr }); // #TODO remove votes rows that get added but not confirmed after some time diff --git a/prisma/migrations/20220524142531_create_donations_table/migration.sql b/prisma/migrations/20220524142531_create_donations_table/migration.sql new file mode 100644 index 0000000..f727b5a --- /dev/null +++ b/prisma/migrations/20220524142531_create_donations_table/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "Donation" ( + "id" SERIAL NOT NULL, + "amount" INTEGER NOT NULL, + "createdAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, + "payment_request" TEXT, + "payment_hash" TEXT, + "preimage" TEXT, + "paid" BOOLEAN NOT NULL DEFAULT false, + "donor_id" INTEGER, + + CONSTRAINT "Donation_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Donation" ADD CONSTRAINT "Donation_donor_id_fkey" FOREIGN KEY ("donor_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 835f2f8..4e98989 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,6 +44,7 @@ model User { stories Story[] questions Question[] posts_comments PostComment[] + donations Donation[] } // ----------------- @@ -182,3 +183,19 @@ model Hackathon { topics Topic[] } + +// ----------------- +// Donations +// ----------------- +model Donation { + id Int @id @default(autoincrement()) + amount Int + createdAt DateTime @default(now()) @db.Date + payment_request String? + payment_hash String? + preimage String? + paid Boolean @default(false) + + donor User? @relation(fields: [donor_id], references: [id]) + donor_id Int? +} diff --git a/src/features/Donations/index.tsx b/src/features/Donations/index.tsx new file mode 100644 index 0000000..27fcad3 --- /dev/null +++ b/src/features/Donations/index.tsx @@ -0,0 +1 @@ +export * from './pages/DonationsPage' \ No newline at end of file diff --git a/src/features/Donations/pages/DonationsPage.tsx b/src/features/Donations/pages/DonationsPage.tsx new file mode 100644 index 0000000..2f82e8e --- /dev/null +++ b/src/features/Donations/pages/DonationsPage.tsx @@ -0,0 +1,12 @@ + + +export default function HackathonsPage() { + + return ( +
+ +
+ ) +} diff --git a/src/features/Donations/pages/donate.graphql b/src/features/Donations/pages/donate.graphql new file mode 100644 index 0000000..4bb2d4f --- /dev/null +++ b/src/features/Donations/pages/donate.graphql @@ -0,0 +1,16 @@ +mutation Donate($amountInSat: Int!) { + donate(amount_in_sat: $amountInSat) { + id + amount + payment_request + payment_hash + } +} + +mutation ConfirmDonation($paymentRequest: String!, $preimage: String!) { + confirmDonation(payment_request: $paymentRequest, preimage: $preimage) { + id + amount + paid + } +} diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 5e06c4f..7e55423 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -63,6 +63,25 @@ export type Category = { votes_sum: Scalars['Int']; }; +export type Donation = { + __typename?: 'Donation'; + amount: Scalars['Int']; + by: Maybe; + createdAt: Scalars['Date']; + id: Scalars['Int']; + paid: Scalars['Boolean']; + payment_hash: Scalars['String']; + payment_request: Scalars['String']; +}; + +export type DonationsStats = { + __typename?: 'DonationsStats'; + applications: Scalars['Int']; + donations: Scalars['Int']; + prizes: Scalars['Int']; + touranments: Scalars['Int']; +}; + export type Hackathon = { __typename?: 'Hackathon'; cover_image: Scalars['String']; @@ -86,17 +105,30 @@ export type LnurlDetails = { export type Mutation = { __typename?: 'Mutation'; + confirmDonation: Donation; confirmVote: Vote; + donate: Donation; vote: Vote; }; +export type MutationConfirmDonationArgs = { + payment_request: Scalars['String']; + preimage: Scalars['String']; +}; + + export type MutationConfirmVoteArgs = { payment_request: Scalars['String']; preimage: Scalars['String']; }; +export type MutationDonateArgs = { + amount_in_sat: Scalars['Int']; +}; + + export type MutationVoteArgs = { amount_in_sat: Scalars['Int']; item_id: Scalars['Int']; @@ -154,6 +186,7 @@ export type Query = { allTopics: Array; getAllHackathons: Array; getCategory: Category; + getDonationsStats: Array; getFeed: Array; getLnurlDetailsForProject: LnurlDetails; getPostById: Post; @@ -317,6 +350,21 @@ export type SearchProjectsQueryVariables = Exact<{ export type SearchProjectsQuery = { __typename?: 'Query', searchProjects: Array<{ __typename?: 'Project', id: number, thumbnail_image: string, title: string, category: { __typename?: 'Category', title: string, id: number } }> }; +export type DonateMutationVariables = Exact<{ + amountInSat: Scalars['Int']; +}>; + + +export type DonateMutation = { __typename?: 'Mutation', donate: { __typename?: 'Donation', id: number, amount: number, payment_request: string, payment_hash: string } }; + +export type ConfirmDonationMutationVariables = Exact<{ + paymentRequest: Scalars['String']; + preimage: Scalars['String']; +}>; + + +export type ConfirmDonationMutation = { __typename?: 'Mutation', confirmDonation: { __typename?: 'Donation', id: number, amount: number, paid: boolean } }; + export type AllTopicsQueryVariables = Exact<{ [key: string]: never; }>; @@ -483,6 +531,78 @@ export function useSearchProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt export type SearchProjectsQueryHookResult = ReturnType; export type SearchProjectsLazyQueryHookResult = ReturnType; export type SearchProjectsQueryResult = Apollo.QueryResult; +export const DonateDocument = gql` + mutation Donate($amountInSat: Int!) { + donate(amount_in_sat: $amountInSat) { + id + amount + payment_request + payment_hash + } +} + `; +export type DonateMutationFn = Apollo.MutationFunction; + +/** + * __useDonateMutation__ + * + * To run a mutation, you first call `useDonateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDonateMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [donateMutation, { data, loading, error }] = useDonateMutation({ + * variables: { + * amountInSat: // value for 'amountInSat' + * }, + * }); + */ +export function useDonateMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DonateDocument, options); + } +export type DonateMutationHookResult = ReturnType; +export type DonateMutationResult = Apollo.MutationResult; +export type DonateMutationOptions = Apollo.BaseMutationOptions; +export const ConfirmDonationDocument = gql` + mutation ConfirmDonation($paymentRequest: String!, $preimage: String!) { + confirmDonation(payment_request: $paymentRequest, preimage: $preimage) { + id + amount + paid + } +} + `; +export type ConfirmDonationMutationFn = Apollo.MutationFunction; + +/** + * __useConfirmDonationMutation__ + * + * To run a mutation, you first call `useConfirmDonationMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useConfirmDonationMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [confirmDonationMutation, { data, loading, error }] = useConfirmDonationMutation({ + * variables: { + * paymentRequest: // value for 'paymentRequest' + * preimage: // value for 'preimage' + * }, + * }); + */ +export function useConfirmDonationMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ConfirmDonationDocument, options); + } +export type ConfirmDonationMutationHookResult = ReturnType; +export type ConfirmDonationMutationResult = Apollo.MutationResult; +export type ConfirmDonationMutationOptions = Apollo.BaseMutationOptions; export const AllTopicsDocument = gql` query allTopics { allTopics {