From 24daa646da4239c37a97174015ecd2214f59a131 Mon Sep 17 00:00:00 2001 From: Nicolas Burtey Date: Tue, 26 Dec 2023 12:01:15 -0600 Subject: [PATCH] chore: addressing comments --- bats/core/api/quiz.bats | 2 +- .../dev/apollo-federation/supergraph.graphql | 2 +- core/api/src/app/quiz/claim.ts | 119 +++++++++++++++--- core/api/src/domain/quiz/config.ts | 2 - core/api/src/domain/quiz/index.ts | 1 - core/api/src/domain/quiz/index.types.d.ts | 2 +- .../public/root/mutation/quiz-claim.ts | 3 +- .../public/root/mutation/quiz-completed.ts | 3 +- core/api/src/graphql/public/schema.graphql | 2 +- .../public/types/payload/quiz-claim.ts | 2 +- core/api/src/services/mongoose/quiz.ts | 1 - .../integration/app/quizzes/add-quiz.spec.ts | 2 - .../test/integration/services/quiz.spec.ts | 1 - core/api/test/unit/domain/quiz/index.spec.ts | 31 +++-- .../apollo-federation/supergraph.graphql | 2 +- 15 files changed, 128 insertions(+), 47 deletions(-) diff --git a/bats/core/api/quiz.bats b/bats/core/api/quiz.bats index abb301bea0..0224760b25 100644 --- a/bats/core/api/quiz.bats +++ b/bats/core/api/quiz.bats @@ -79,7 +79,7 @@ setup_file() { [[ "$btc_balance_after_retry" == "$btc_balance_after_quiz" ]] || exit 1 } -@test "quiz: new mutation" { +@test "quiz: completes a quiz question and gets paid once - time based quiz mutation" { token_name="alice" question_id="whatIsBitcoin" diff --git a/core/api/dev/apollo-federation/supergraph.graphql b/core/api/dev/apollo-federation/supergraph.graphql index 0b5cfcce82..bcb5800a00 100644 --- a/core/api/dev/apollo-federation/supergraph.graphql +++ b/core/api/dev/apollo-federation/supergraph.graphql @@ -1536,7 +1536,7 @@ type QuizClaimPayload @join__type(graph: PUBLIC) { errors: [Error!]! - quizzes: [Quiz] + quizzes: [Quiz!]! } input QuizCompletedInput diff --git a/core/api/src/app/quiz/claim.ts b/core/api/src/app/quiz/claim.ts index caffabd652..acb083272f 100644 --- a/core/api/src/app/quiz/claim.ts +++ b/core/api/src/app/quiz/claim.ts @@ -15,8 +15,6 @@ import { NoBtcWalletExistsForAccountError, NotEnoughBalanceForQuizError, QuizClaimedTooEarlyError, - UnauthorizedIPError, - UnknownRepositoryError, } from "@/domain/errors" import { WalletCurrency } from "@/domain/shared" import { RateLimitConfig } from "@/domain/rate-limit" @@ -35,7 +33,6 @@ import { import { consumeLimiter } from "@/services/rate-limit" import { getFunderWalletId } from "@/services/ledger/caching" import { AccountsIpsRepository } from "@/services/mongoose/accounts-ips" -import { QuizQuestionId } from "@/domain/quiz/index.types" type ClaimQuizResult = { id: QuizQuestionId @@ -44,16 +41,14 @@ type ClaimQuizResult = { notBefore: Date | undefined }[] -export const claimQuiz = async ({ +export const claimQuizLegacy = async ({ quizQuestionId: quizQuestionIdString, accountId: accountIdRaw, ip, - legacy, }: { quizQuestionId: string accountId: string ip: IpAddress | undefined - legacy: boolean }): Promise => { const check = await checkAddQuizAttemptPerIpLimits(ip) if (check instanceof Error) return check @@ -99,22 +94,12 @@ export const claimQuiz = async ({ if (validatedIPMetadata instanceof MissingIPMetadataError) return new InvalidIpMetadataError(validatedIPMetadata) - if (validatedIPMetadata instanceof UnauthorizedIPError) return validatedIPMetadata - - return new UnknownRepositoryError("claim quiz error") + return validatedIPMetadata } const quizzesBefore = await listQuizzesByAccountId(accountId) if (quizzesBefore instanceof Error) return quizzesBefore - if (!legacy) { - const quiz = quizzesBefore.find((quiz) => quiz.id === quizId) - if (quiz === undefined) return new InvalidQuizQuestionIdError() - - if (quiz.notBefore && quiz.notBefore > new Date()) - return new QuizClaimedTooEarlyError() - } - const recipientWallets = await WalletsRepository().listByAccountId(accountId) if (recipientWallets instanceof Error) return recipientWallets @@ -162,6 +147,106 @@ const checkAddQuizAttemptPerIpLimits = async ( }) } +export const claimQuiz = async ({ + quizQuestionId: quizQuestionIdString, + accountId: accountIdRaw, + ip, +}: { + quizQuestionId: string + accountId: string + ip: IpAddress | undefined +}): Promise => { + const check = await checkAddQuizAttemptPerIpLimits(ip) + if (check instanceof Error) return check + + const accountId = checkedToAccountId(accountIdRaw) + if (accountId instanceof Error) return accountId + + const quizzesConfig = getQuizzesConfig() + + // TODO: quizQuestionId checkedFor + const quizId = quizQuestionIdString as QuizQuestionId + + const amount = QuizzesValue[quizId] + if (!amount) return new InvalidQuizQuestionIdError() + + const funderWalletId = await getFunderWalletId() + const funderWallet = await WalletsRepository().findById(funderWalletId) + if (funderWallet instanceof Error) return funderWallet + const funderAccount = await AccountsRepository().findById(funderWallet.accountId) + if (funderAccount instanceof Error) return funderAccount + + const recipientAccount = await AccountsRepository().findById(accountId) + if (recipientAccount instanceof Error) return recipientAccount + + const user = await UsersRepository().findById(recipientAccount.kratosUserId) + if (user instanceof Error) return user + + const validatedPhoneMetadata = PhoneMetadataAuthorizer( + quizzesConfig.phoneMetadataValidationSettings, + ).authorize(user.phoneMetadata) + + if (validatedPhoneMetadata instanceof Error) { + return new InvalidPhoneForQuizError(validatedPhoneMetadata.name) + } + + const accountIP = await AccountsIpsRepository().findLastByAccountId(recipientAccount.id) + if (accountIP instanceof Error) return accountIP + + const validatedIPMetadata = IPMetadataAuthorizer( + quizzesConfig.ipMetadataValidationSettings, + ).authorize(accountIP.metadata) + if (validatedIPMetadata instanceof Error) { + if (validatedIPMetadata instanceof MissingIPMetadataError) + return new InvalidIpMetadataError(validatedIPMetadata) + + return validatedIPMetadata + } + + const quizzesBefore = await listQuizzesByAccountId(accountId) + if (quizzesBefore instanceof Error) return quizzesBefore + + const quiz = quizzesBefore.find((quiz) => quiz.id === quizId) + if (quiz === undefined) return new InvalidQuizQuestionIdError() + + if (quiz.notBefore && quiz.notBefore > new Date()) return new QuizClaimedTooEarlyError() + + const recipientWallets = await WalletsRepository().listByAccountId(accountId) + if (recipientWallets instanceof Error) return recipientWallets + + const recipientBtcWallet = recipientWallets.find( + (wallet) => wallet.currency === WalletCurrency.Btc, + ) + if (recipientBtcWallet === undefined) return new NoBtcWalletExistsForAccountError() + const recipientWalletId = recipientBtcWallet.id + + const shouldGiveSats = await QuizRepository().add({ quizId, accountId }) + if (shouldGiveSats instanceof Error) return shouldGiveSats + + const funderBalance = await getBalanceForWallet({ walletId: funderWalletId }) + if (funderBalance instanceof Error) return funderBalance + + const sendCheck = FunderBalanceChecker().check({ + balance: funderBalance as Satoshis, + amountToSend: amount, + }) + if (sendCheck instanceof Error) return sendCheck + + const payment = await intraledgerPaymentSendWalletIdForBtcWallet({ + senderWalletId: funderWalletId, + recipientWalletId, + amount, + memo: quizId, + senderAccount: funderAccount, + }) + if (payment instanceof Error) return payment + + const quizzesAfter = await listQuizzesByAccountId(accountId) + if (quizzesAfter instanceof Error) return quizzesAfter + + return quizzesAfter +} + const FunderBalanceChecker = () => { const check = ({ balance, diff --git a/core/api/src/domain/quiz/config.ts b/core/api/src/domain/quiz/config.ts index d8942ea968..e568e72059 100644 --- a/core/api/src/domain/quiz/config.ts +++ b/core/api/src/domain/quiz/config.ts @@ -1,5 +1,3 @@ -import { QuizQuestionId } from "./index.types" - export const milliSecondsBetweenSections = 60 * 60 * 12 * 1000 export const QuizzesValue: Record = { diff --git a/core/api/src/domain/quiz/index.ts b/core/api/src/domain/quiz/index.ts index 81636087e5..19ea6a2bea 100644 --- a/core/api/src/domain/quiz/index.ts +++ b/core/api/src/domain/quiz/index.ts @@ -1,7 +1,6 @@ export * from "./config" import { QuizzesValue, milliSecondsBetweenSections } from "./config" -import { QuizQuestionId } from "./index.types" import { QuizzesSectionsConfig } from "./sections" export interface QuizCompleted { diff --git a/core/api/src/domain/quiz/index.types.d.ts b/core/api/src/domain/quiz/index.types.d.ts index 4a310cfd7c..65682ba580 100644 --- a/core/api/src/domain/quiz/index.types.d.ts +++ b/core/api/src/domain/quiz/index.types.d.ts @@ -1,2 +1,2 @@ type QuizzesSectionsConfig = typeof import("./sections").QuizzesSectionsConfig -export type QuizQuestionId = QuizzesSectionsConfig[number]["quiz"][number] +type QuizQuestionId = QuizzesSectionsConfig[number]["quiz"][number] diff --git a/core/api/src/graphql/public/root/mutation/quiz-claim.ts b/core/api/src/graphql/public/root/mutation/quiz-claim.ts index 05832f6ef0..d71d420084 100644 --- a/core/api/src/graphql/public/root/mutation/quiz-claim.ts +++ b/core/api/src/graphql/public/root/mutation/quiz-claim.ts @@ -30,10 +30,9 @@ const QuizClaimMutation = GT.Field< quizQuestionId: id, accountId: domainAccount.id, ip, - legacy: false, }) if (quizzes instanceof Error) { - return { errors: [mapAndParseErrorForGqlResponse(quizzes)] } + return { errors: [mapAndParseErrorForGqlResponse(quizzes)], quizzes: [] } } return { diff --git a/core/api/src/graphql/public/root/mutation/quiz-completed.ts b/core/api/src/graphql/public/root/mutation/quiz-completed.ts index f6f00d4403..faa2122860 100644 --- a/core/api/src/graphql/public/root/mutation/quiz-completed.ts +++ b/core/api/src/graphql/public/root/mutation/quiz-completed.ts @@ -27,11 +27,10 @@ const QuizCompletedMutation = GT.Field< resolve: async (_, args, { domainAccount, ip }) => { const { id } = args.input - const quizzes = await Quiz.claimQuiz({ + const quizzes = await Quiz.claimQuizLegacy({ quizQuestionId: id, accountId: domainAccount.id, ip, - legacy: true, }) if (quizzes instanceof Error) { return { errors: [mapAndParseErrorForGqlResponse(quizzes)] } diff --git a/core/api/src/graphql/public/schema.graphql b/core/api/src/graphql/public/schema.graphql index f67d4b2f45..1f1c4b5723 100644 --- a/core/api/src/graphql/public/schema.graphql +++ b/core/api/src/graphql/public/schema.graphql @@ -1191,7 +1191,7 @@ input QuizClaimInput { type QuizClaimPayload { errors: [Error!]! - quizzes: [Quiz] + quizzes: [Quiz!]! } input QuizCompletedInput { diff --git a/core/api/src/graphql/public/types/payload/quiz-claim.ts b/core/api/src/graphql/public/types/payload/quiz-claim.ts index 2e9a36e904..12ef852395 100644 --- a/core/api/src/graphql/public/types/payload/quiz-claim.ts +++ b/core/api/src/graphql/public/types/payload/quiz-claim.ts @@ -10,7 +10,7 @@ const QuizClaimPayload = GT.Object({ type: GT.NonNullList(IError), }, quizzes: { - type: GT.List(Quiz), + type: GT.NonNullList(Quiz), }, }), }) diff --git a/core/api/src/services/mongoose/quiz.ts b/core/api/src/services/mongoose/quiz.ts index 32f0d3849e..93c92aeb73 100644 --- a/core/api/src/services/mongoose/quiz.ts +++ b/core/api/src/services/mongoose/quiz.ts @@ -3,7 +3,6 @@ import { Quiz } from "./schema" import { QuizCompleted } from "@/domain/quiz" import { QuizAlreadyPresentError, UnknownRepositoryError } from "@/domain/errors" -import { QuizQuestionId } from "@/domain/quiz/index.types" interface ExtendedError extends Error { code?: number diff --git a/core/api/test/integration/app/quizzes/add-quiz.spec.ts b/core/api/test/integration/app/quizzes/add-quiz.spec.ts index 3d1e071dc4..e1f3ba6f9a 100644 --- a/core/api/test/integration/app/quizzes/add-quiz.spec.ts +++ b/core/api/test/integration/app/quizzes/add-quiz.spec.ts @@ -22,7 +22,6 @@ describe("addQuiz", () => { accountId: crypto.randomUUID() as AccountId, quizQuestionId: "fakeQuizQuestionId", ip: undefined, - legacy: true, }) expect(result).toBeInstanceOf(InvalidIpMetadataError) }) @@ -44,7 +43,6 @@ describe("addQuiz", () => { accountId: crypto.randomUUID() as AccountId, quizQuestionId: "fakeQuizQuestionId", ip: "192.168.13.13" as IpAddress, - legacy: true, }) expect(result).toBeInstanceOf(UserAddQuizAttemptIpRateLimiterExceededError) diff --git a/core/api/test/integration/services/quiz.spec.ts b/core/api/test/integration/services/quiz.spec.ts index 2a50a730f9..452c7e9100 100644 --- a/core/api/test/integration/services/quiz.spec.ts +++ b/core/api/test/integration/services/quiz.spec.ts @@ -2,7 +2,6 @@ import crypto from "crypto" import { QuizAlreadyPresentError } from "@/domain/errors" import { QuizRepository } from "@/services/mongoose" -import { QuizQuestionId } from "@/domain/quiz/index.types" describe("QuizRepository", () => { const accountId = crypto.randomUUID() as AccountId diff --git a/core/api/test/unit/domain/quiz/index.spec.ts b/core/api/test/unit/domain/quiz/index.spec.ts index 7d66eb2128..7a1b3b1b93 100644 --- a/core/api/test/unit/domain/quiz/index.spec.ts +++ b/core/api/test/unit/domain/quiz/index.spec.ts @@ -1,5 +1,4 @@ import { fillQuizInformation } from "@/domain/quiz" -import { QuizQuestionId } from "@/domain/quiz/index.types" describe("quiz", () => { it("completed is false by default", () => { @@ -13,7 +12,7 @@ describe("quiz", () => { expect(filledInfo.quizzes[0].notBefore).toBe(undefined) }) - it("one element completed", () => { + it("test that we are on section 0 after 1 element has been completed", () => { const quizCompleted = { quizId: "sat" as QuizQuestionId, createdAt: new Date() } const filledInfo = fillQuizInformation([quizCompleted]) @@ -28,20 +27,29 @@ describe("quiz", () => { expect(filledInfo.currentSection).toBe(0) }) - it("two elements completed", () => { + it("test that we are on section 0 after 2 elements has been completed", () => { const quizzesCompleted = [ { quizId: "sat" as QuizQuestionId, createdAt: new Date() }, { quizId: "whereBitcoinExist" as QuizQuestionId, createdAt: new Date() }, ] const filledInfo = fillQuizInformation(quizzesCompleted) - expect(filledInfo.quizzes.find((quiz) => quiz.completed === true)).toEqual({ - amount: 1, - completed: true, - id: "sat", - section: 0, - notBefore: undefined, - }) + expect(filledInfo.quizzes.filter((quiz) => quiz.completed === true)).toEqual([ + { + amount: 1, + completed: true, + id: "sat", + section: 0, + notBefore: undefined, + }, + { + amount: 1, + completed: true, + id: "whereBitcoinExist", + section: 0, + notBefore: undefined, + }, + ]) expect(filledInfo.currentSection).toBe(0) }) @@ -57,9 +65,6 @@ describe("quiz", () => { const filledInfo = fillQuizInformation(quizzesCompleted) expect(filledInfo.currentSection).toBe(1) - console.log(filledInfo.quizzes[4]) - console.log(filledInfo.quizzes[5]) - expect(filledInfo.quizzes[4].notBefore).toBeUndefined() expect(filledInfo.quizzes[5].notBefore?.getTime()).toBeCloseTo( new Date().getTime() + 12 * 60 * 60 * 1000, diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index 0b5cfcce82..bcb5800a00 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -1536,7 +1536,7 @@ type QuizClaimPayload @join__type(graph: PUBLIC) { errors: [Error!]! - quizzes: [Quiz] + quizzes: [Quiz!]! } input QuizCompletedInput