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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/consent/app/graphql/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,7 @@ export type Quiz = {

export type QuizClaimInput = {
readonly id: Scalars['ID']['input'];
readonly skipRewards?: InputMaybe<Scalars['Boolean']['input']>;
};

export type QuizClaimPayload = {
Expand Down Expand Up @@ -2246,8 +2247,8 @@ export function useCountryCodesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<CountryCodesQuery, CountryCodesQueryVariables>(CountryCodesDocument, options);
}
export function useCountryCodesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<CountryCodesQuery, CountryCodesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
export function useCountryCodesSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<CountryCodesQuery, CountryCodesQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<CountryCodesQuery, CountryCodesQueryVariables>(CountryCodesDocument, options);
}
export type CountryCodesQueryHookResult = ReturnType<typeof useCountryCodesQuery>;
Expand Down Expand Up @@ -2285,8 +2286,8 @@ export function useGetUserIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetUserIdQuery, GetUserIdQueryVariables>(GetUserIdDocument, options);
}
export function useGetUserIdSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<GetUserIdQuery, GetUserIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
export function useGetUserIdSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetUserIdQuery, GetUserIdQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<GetUserIdQuery, GetUserIdQueryVariables>(GetUserIdDocument, options);
}
export type GetUserIdQueryHookResult = ReturnType<typeof useGetUserIdQuery>;
Expand Down
235 changes: 118 additions & 117 deletions apps/dashboard/services/graphql/generated.ts

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions apps/map/services/galoy/graphql/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,7 @@ export type Quiz = {

export type QuizClaimInput = {
readonly id: Scalars['ID']['input'];
readonly skipRewards?: InputMaybe<Scalars['Boolean']['input']>;
};

export type QuizClaimPayload = {
Expand Down Expand Up @@ -2299,8 +2300,8 @@ export function useBusinessMapMarkersLazyQuery(baseOptions?: Apollo.LazyQueryHoo
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<BusinessMapMarkersQuery, BusinessMapMarkersQueryVariables>(BusinessMapMarkersDocument, options);
}
export function useBusinessMapMarkersSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<BusinessMapMarkersQuery, BusinessMapMarkersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
export function useBusinessMapMarkersSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<BusinessMapMarkersQuery, BusinessMapMarkersQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<BusinessMapMarkersQuery, BusinessMapMarkersQueryVariables>(BusinessMapMarkersDocument, options);
}
export type BusinessMapMarkersQueryHookResult = ReturnType<typeof useBusinessMapMarkersQuery>;
Expand Down
1 change: 1 addition & 0 deletions apps/pay/lib/graphql/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1620,6 +1620,7 @@ export type Quiz = {

export type QuizClaimInput = {
readonly id: Scalars['ID']['input'];
readonly skipRewards?: InputMaybe<Scalars['Boolean']['input']>;
};

export type QuizClaimPayload = {
Expand Down
94 changes: 94 additions & 0 deletions bats/core/api/quiz.bats
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,97 @@ setup_file() {
error_msg=$(graphql_output '.data.quizClaim.errors[0].code')
[[ "${error_msg}" =~ "QUIZ_CLAIMED_TOO_EARLY" ]] || exit 1
}

@test "quiz: completes a quiz question and gets paid once (skipRewards false)" {
token_name="alice"
question_id="sat"

exec_graphql $token_name 'wallets-for-account'
btc_initial_balance=$(graphql_output '
.data.me.defaultAccount.wallets[]
| select(.walletCurrency == "BTC")
.balance
')

variables=$(
jq -n \
--arg question_id "$question_id" \
'{input: {id: $question_id, skipRewards: false}}'
)
exec_graphql "$token_name" 'quiz-claim' "$variables"

errors=$(graphql_output '.data.quizClaim.errors')
[[ "$errors" == "[]" ]] || exit 1

quizzes=$(graphql_output '.data.quizClaim.quizzes')
[[ "$quizzes" != "null" ]] || exit 1

quiz_completed=$(echo "$quizzes" | jq --arg id "$question_id" '.[] | select(.id == $id) | .completed')
[[ "$quiz_completed" == "true" ]] || exit 1

exec_graphql $token_name 'wallets-for-account'
btc_balance_after_quiz=$(graphql_output '
.data.me.defaultAccount.wallets[]
| select(.walletCurrency == "BTC")
.balance
')
[[ "$btc_balance_after_quiz" -gt "$btc_initial_balance" ]] || exit 1
}

@test "quiz: completes a quiz question without receiving payment (skipRewards true)" {
token_name="alice"
question_id="whereBitcoinExist"

exec_graphql $token_name 'wallets-for-account'
btc_initial_balance=$(graphql_output '
.data.me.defaultAccount.wallets[]
| select(.walletCurrency == "BTC")
.balance
')

variables=$(
jq -n \
--arg question_id "$question_id" \
'{input: {id: $question_id, skipRewards: true}}'
)
exec_graphql "$token_name" 'quiz-claim' "$variables"

errors=$(graphql_output '.data.quizClaim.errors')
[[ "$errors" == "[]" ]] || exit 1

quizzes=$(graphql_output '.data.quizClaim.quizzes')
[[ "$quizzes" != "null" ]] || exit 1

quiz_completed=$(echo "$quizzes" | jq --arg id "$question_id" '.[] | select(.id == $id) | .completed')
[[ "$quiz_completed" == "true" ]] || exit 1

exec_graphql $token_name 'wallets-for-account'
btc_balance_after_quiz=$(graphql_output '
.data.me.defaultAccount.wallets[]
| select(.walletCurrency == "BTC")
.balance
')
[[ "$btc_balance_after_quiz" == "$btc_initial_balance" ]] || exit 1
}

@test "quiz: skipRewards true omits validations and payment" {
token_name="alice"
question_id="whoControlsBitcoin"

variables=$(
jq -n \
--arg question_id "$question_id" \
'{input: {id: $question_id, skipRewards: true}}'
)

exec_graphql "$token_name" 'quiz-claim' "$variables"

errors=$(graphql_output '.data.quizClaim.errors')
[[ "$errors" == "[]" ]] || exit 1

quizzes=$(graphql_output '.data.quizClaim.quizzes')
[[ "$quizzes" != "null" ]] || exit 1

quiz_completed=$(echo "$quizzes" | jq --arg id "$question_id" '.[] | select(.id == $id) | .completed')
[[ "$quiz_completed" == "true" ]] || exit 1
}
38 changes: 27 additions & 11 deletions core/api/src/app/quiz/claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,25 @@ const checkAddQuizAttemptPerPhoneLimits = async (
export const claimQuiz = async ({
quizQuestionId: quizQuestionIdString,
accountId: accountIdRaw,
skipRewards,
ip,
}: {
quizQuestionId: string
accountId: string
skipRewards: boolean
ip: IpAddress | undefined
}): Promise<ClaimQuizResult | ApplicationError> => {
const checkIp = await checkAddQuizAttemptPerIpLimits(ip)
if (checkIp instanceof Error) return checkIp

const accountId = checkedToAccountId(accountIdRaw)
if (accountId instanceof Error) return accountId

const quizzesConfig = getQuizzesConfig()

// TODO: quizQuestionId checkedFor
const quizId = quizQuestionIdString as QuizQuestionId
if (skipRewards) return addQuizAndList({ quizId, accountId })

const checkIp = await checkAddQuizAttemptPerIpLimits(ip)
if (checkIp instanceof Error) return checkIp

const quizzesConfig = getQuizzesConfig()

const amount = QuizzesValue[quizId]
if (!amount) return new InvalidQuizQuestionIdError()
Expand Down Expand Up @@ -156,8 +159,8 @@ export const claimQuiz = async ({
})
if (sendCheck instanceof Error) return sendCheck

const shouldGiveSats = await QuizRepository().add({ quizId, accountId })
if (shouldGiveSats instanceof Error) return shouldGiveSats
const claimResult = await addQuizAndList({ quizId, accountId })
if (claimResult instanceof Error) return claimResult

const payment = await intraledgerPaymentSendWalletIdForBtcWallet({
senderWalletId: funderWalletId,
Expand All @@ -168,10 +171,7 @@ export const claimQuiz = async ({
})
if (payment instanceof Error) return payment

const quizzesAfter = await listQuizzesByAccountId(accountId)
if (quizzesAfter instanceof Error) return quizzesAfter

return quizzesAfter
return claimResult
}

const FunderBalanceChecker = () => {
Expand All @@ -191,3 +191,19 @@ const FunderBalanceChecker = () => {

return { check }
}

const addQuizAndList = async ({
quizId,
accountId,
}: {
quizId: QuizQuestionId
accountId: AccountId
}): Promise<ClaimQuizResult | ApplicationError> => {
const shouldGiveSats = await QuizRepository().add({ quizId, accountId })
if (shouldGiveSats instanceof Error) return shouldGiveSats

const quizzesAfter = await listQuizzesByAccountId(accountId)
if (quizzesAfter instanceof Error) return quizzesAfter

return quizzesAfter
}
6 changes: 4 additions & 2 deletions core/api/src/graphql/public/root/mutation/quiz-claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ const QuizClaimInput = GT.Input({
name: "QuizClaimInput",
fields: () => ({
id: { type: GT.NonNull(GT.ID) },
skipRewards: { type: GT.Boolean, defaultValue: false },
}),
})

const QuizClaimMutation = GT.Field<
null,
GraphQLPublicContextAuth,
{ input: { id: string } }
{ input: { id: string; skipRewards: boolean } }
>({
extensions: {
complexity: 120,
Expand All @@ -24,11 +25,12 @@ const QuizClaimMutation = GT.Field<
input: { type: GT.NonNull(QuizClaimInput) },
},
resolve: async (_, args, { domainAccount, ip }) => {
const { id } = args.input
const { id, skipRewards } = args.input

const quizzes = await Quiz.claimQuiz({
quizQuestionId: id,
accountId: domainAccount.id,
skipRewards,
ip,
})
if (quizzes instanceof Error) {
Expand Down
1 change: 1 addition & 0 deletions core/api/src/graphql/public/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,7 @@ type Quiz {

input QuizClaimInput {
id: ID!
skipRewards: Boolean = false
}

type QuizClaimPayload {
Expand Down
42 changes: 42 additions & 0 deletions core/api/test/integration/app/quizzes/claim-quiz.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import * as RateLimitImpl from "@/services/rate-limit"

import { RateLimitConfig } from "@/domain/rate-limit"

import { createRandomUserAndBtcWallet } from "test/helpers"

afterEach(async () => {
jest.restoreAllMocks()
})
Expand All @@ -21,6 +23,7 @@ describe("addQuiz", () => {
const result = await Quiz.claimQuiz({
accountId: crypto.randomUUID() as AccountId,
quizQuestionId: "fakeQuizQuestionId",
skipRewards: false,
ip: undefined,
})
expect(result).toBeInstanceOf(InvalidIpMetadataError)
Expand All @@ -42,6 +45,7 @@ describe("addQuiz", () => {
const result = await Quiz.claimQuiz({
accountId: crypto.randomUUID() as AccountId,
quizQuestionId: "fakeQuizQuestionId",
skipRewards: false,
ip: "192.168.13.13" as IpAddress,
})

Expand All @@ -50,4 +54,42 @@ describe("addQuiz", () => {
// Restore system state
rateLimitServiceSpy.mockReset()
})

it("passes if ip is undefined and skipRewards is true", async () => {
const { accountId } = await createRandomUserAndBtcWallet()

const result = await Quiz.claimQuiz({
accountId,
quizQuestionId: "whoControlsBitcoin",
skipRewards: true,
ip: undefined,
})
expect(result).not.toBeInstanceOf(Error)
})

it("passes if ip limit hit but skipRewards is true", async () => {
const { RedisRateLimitService } = jest.requireActual("@/services/rate-limit")
const rateLimitServiceSpy = jest
.spyOn(RateLimitImpl, "RedisRateLimitService")
.mockReturnValue({
...RedisRateLimitService({
keyPrefix: RateLimitConfig.addQuizAttemptPerIp.key,
limitOptions: RateLimitConfig.addQuizAttemptPerIp.limits,
}),
consume: () => new RateLimiterExceededError(),
})

const { accountId } = await createRandomUserAndBtcWallet()

const result = await Quiz.claimQuiz({
accountId,
quizQuestionId: "copyBitcoin",
skipRewards: true,
ip: "192.168.13.13" as IpAddress,
})

expect(result).not.toBeInstanceOf(Error)

rateLimitServiceSpy.mockReset()
})
})
1 change: 1 addition & 0 deletions dev/config/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1748,6 +1748,7 @@ input QuizClaimInput
@join__type(graph: PUBLIC)
{
id: ID!
skipRewards: Boolean = false
}

type QuizClaimPayload
Expand Down
Loading