diff --git a/bats/core/api/earn.bats b/bats/core/api/quiz.bats similarity index 75% rename from bats/core/api/earn.bats rename to bats/core/api/quiz.bats index 8414fcb3ab..1f3bcab4d8 100644 --- a/bats/core/api/earn.bats +++ b/bats/core/api/quiz.bats @@ -14,10 +14,9 @@ setup_file() { "alice.btc_wallet_id" \ "funder.btc_wallet_id" \ "10000" - } -@test "earn: completes a quiz question and gets paid once" { +@test "quiz: completes a quiz question and gets paid once" { token_name="alice" question_id="sat" @@ -29,7 +28,11 @@ setup_file() { .balance ') - # Do earn + exec_graphql $token_name 'quiz' + completed=$(graphql_output '.data.me.defaultAccount.quiz' | jq '.[] | select(.id == "sat") | .completed') + [[ "${completed}" == "false" ]] || exit 1 + + # Do quiz variables=$( jq -n \ --arg question_id "$question_id" \ @@ -41,21 +44,25 @@ setup_file() { quiz_completed=$(graphql_output '.data.quizCompleted.quiz.completed') [[ "${quiz_completed}" == "true" ]] || exit 1 + exec_graphql $token_name 'quiz' + completed=$(graphql_output '.data.me.defaultAccount.quiz' | jq '.[] | select(.id == "sat") | .completed') + [[ "${completed}" == "true" ]] || exit 1 + # Check balance after complete exec_graphql $token_name 'wallets-for-account' - btc_balance_after_earn=$(graphql_output ' + btc_balance_after_quiz=$(graphql_output ' .data.me.defaultAccount.wallets[] | select(.walletCurrency == "BTC") .balance ') - [[ "$btc_balance_after_earn" -gt "$btc_initial_balance" ]] || exit 1 + [[ "$btc_balance_after_quiz" -gt "$btc_initial_balance" ]] || exit 1 # Check memo exec_graphql "$token_name" 'transactions' '{"first": 1}' txn_memo=$(graphql_output '.data.me.defaultAccount.transactions.edges[0].node.memo') [[ "${txn_memo}" == "${question_id}" ]] || exit 1 - # Retry earn + # Retry quiz exec_graphql "$token_name" 'quiz-question' "$variables" errors=$(graphql_output '.data.quizCompleted.errors') [[ "${errors}" != "null" ]] || exit 1 @@ -69,5 +76,5 @@ setup_file() { | select(.walletCurrency == "BTC") .balance ') - [[ "$btc_balance_after_retry" == "$btc_balance_after_earn" ]] || exit 1 + [[ "$btc_balance_after_retry" == "$btc_balance_after_quiz" ]] || exit 1 } diff --git a/bats/gql/quiz.gql b/bats/gql/quiz.gql new file mode 100644 index 0000000000..ebd2395d1f --- /dev/null +++ b/bats/gql/quiz.gql @@ -0,0 +1,15 @@ +query myQuizQuestions { + me { + id + defaultAccount { + id + ... on ConsumerAccount { + quiz { + id + amount + completed + } + } + } + } +} \ No newline at end of file diff --git a/core/api/src/app/index.ts b/core/api/src/app/index.ts index 243fd1ed98..356626c94c 100644 --- a/core/api/src/app/index.ts +++ b/core/api/src/app/index.ts @@ -3,6 +3,7 @@ import * as AuthenticationMod from "./authentication" import * as AdminMod from "./admin" import * as CallbackMod from "./callback" import * as CommMod from "./comm" +import * as QuizMod from "./quiz" import * as LightningMod from "./lightning" import * as OnChainMod from "./on-chain" import * as PricesMod from "./prices" @@ -20,6 +21,7 @@ const allFunctions = { Admin: { ...AdminMod }, Callback: { ...CallbackMod }, Comm: { ...CommMod }, + Quiz: { ...QuizMod }, Lightning: { ...LightningMod }, OnChain: { ...OnChainMod }, Prices: { ...PricesMod }, @@ -49,6 +51,7 @@ export const { Admin, Callback, Comm, + Quiz, Lightning, OnChain, Prices, diff --git a/core/api/src/app/payments/index.ts b/core/api/src/app/payments/index.ts index 0b1d63068e..c10e216d1f 100644 --- a/core/api/src/app/payments/index.ts +++ b/core/api/src/app/payments/index.ts @@ -1,4 +1,3 @@ -export * from "./add-earn" export * from "./get-protocol-fee" export * from "./reimburse-fee" export * from "./send-intraledger" diff --git a/core/api/src/app/payments/add-earn.ts b/core/api/src/app/quiz/add.ts similarity index 77% rename from core/api/src/app/payments/add-earn.ts rename to core/api/src/app/quiz/add.ts index bd614c5b3d..f289c1cd2b 100644 --- a/core/api/src/app/payments/add-earn.ts +++ b/core/api/src/app/quiz/add.ts @@ -1,6 +1,8 @@ -import { intraledgerPaymentSendWalletIdForBtcWallet } from "./send-intraledger" +import { intraledgerPaymentSendWalletIdForBtcWallet } from "../payments/send-intraledger" -import { getRewardsConfig, OnboardingEarn } from "@/config" +import { QuizzesValue } from "@/domain/earn" + +import { getQuizzesConfig } from "@/config" import { getBalanceForWallet } from "@/app/wallets" @@ -9,7 +11,7 @@ import { InvalidQuizQuestionIdError, MissingIPMetadataError, NoBtcWalletExistsForAccountError, - NotEnoughBalanceForRewardError, + NotEnoughBalanceForQuizError, UnauthorizedIPError, UnknownRepositoryError, } from "@/domain/errors" @@ -17,13 +19,13 @@ import { WalletCurrency } from "@/domain/shared" import { RateLimitConfig } from "@/domain/rate-limit" import { checkedToAccountId } from "@/domain/accounts" import { PhoneMetadataAuthorizer } from "@/domain/users" -import { InvalidPhoneForRewardError } from "@/domain/users/errors" +import { InvalidPhoneForQuizError } from "@/domain/users/errors" import { RateLimiterExceededError } from "@/domain/rate-limit/errors" import { IPMetadataAuthorizer } from "@/domain/accounts-ips/ip-metadata-authorizer" import { AccountsRepository, - RewardsRepository, + QuizRepository, WalletsRepository, UsersRepository, } from "@/services/mongoose" @@ -31,7 +33,7 @@ import { consumeLimiter } from "@/services/rate-limit" import { getFunderWalletId } from "@/services/ledger/caching" import { AccountsIpsRepository } from "@/services/mongoose/accounts-ips" -export const addEarn = async ({ +export const completeQuiz = async ({ quizQuestionId: quizQuestionIdString, accountId: accountIdRaw, ip, @@ -40,18 +42,18 @@ export const addEarn = async ({ accountId: string ip: IpAddress | undefined }): Promise => { - const check = await checkAddEarnAttemptPerIpLimits(ip) + const check = await checkAddQuizAttemptPerIpLimits(ip) if (check instanceof Error) return check const accountId = checkedToAccountId(accountIdRaw) if (accountId instanceof Error) return accountId - const rewardsConfig = getRewardsConfig() + const quizzesConfig = getQuizzesConfig() // TODO: quizQuestionId checkedFor - const quizQuestionId = quizQuestionIdString as QuizQuestionId + const quizId = quizQuestionIdString as QuizQuestionId - const amount = OnboardingEarn[quizQuestionId] + const amount = QuizzesValue[quizId] if (!amount) return new InvalidQuizQuestionIdError() const funderWalletId = await getFunderWalletId() @@ -67,18 +69,18 @@ export const addEarn = async ({ if (user instanceof Error) return user const validatedPhoneMetadata = PhoneMetadataAuthorizer( - rewardsConfig.phoneMetadataValidationSettings, + quizzesConfig.phoneMetadataValidationSettings, ).authorize(user.phoneMetadata) if (validatedPhoneMetadata instanceof Error) { - return new InvalidPhoneForRewardError(validatedPhoneMetadata.name) + return new InvalidPhoneForQuizError(validatedPhoneMetadata.name) } const accountIP = await AccountsIpsRepository().findLastByAccountId(recipientAccount.id) if (accountIP instanceof Error) return accountIP const validatedIPMetadata = IPMetadataAuthorizer( - rewardsConfig.ipMetadataValidationSettings, + quizzesConfig.ipMetadataValidationSettings, ).authorize(accountIP.metadata) if (validatedIPMetadata instanceof Error) { if (validatedIPMetadata instanceof MissingIPMetadataError) @@ -86,7 +88,7 @@ export const addEarn = async ({ if (validatedIPMetadata instanceof UnauthorizedIPError) return validatedIPMetadata - return new UnknownRepositoryError("add earn error") + return new UnknownRepositoryError("add quiz error") } const recipientWallets = await WalletsRepository().listByAccountId(accountId) @@ -98,8 +100,8 @@ export const addEarn = async ({ if (recipientBtcWallet === undefined) return new NoBtcWalletExistsForAccountError() const recipientWalletId = recipientBtcWallet.id - const shouldGiveReward = await RewardsRepository(accountId).add(quizQuestionId) - if (shouldGiveReward instanceof Error) return shouldGiveReward + 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 @@ -114,21 +116,21 @@ export const addEarn = async ({ senderWalletId: funderWalletId, recipientWalletId, amount, - memo: quizQuestionId, + memo: quizId, senderAccount: funderAccount, }) if (payment instanceof Error) return payment - return { id: quizQuestionId, earnAmount: amount } + return { id: quizId, earnAmount: amount } } -const checkAddEarnAttemptPerIpLimits = async ( +const checkAddQuizAttemptPerIpLimits = async ( ip: IpAddress | undefined, ): Promise => { if (!ip) return new InvalidIpMetadataError() return consumeLimiter({ - rateLimitConfig: RateLimitConfig.addEarnAttemptPerIp, + rateLimitConfig: RateLimitConfig.addQuizAttemptPerIp, keyToConsume: ip, }) } @@ -142,7 +144,7 @@ const FunderBalanceChecker = () => { amountToSend: Satoshis }): ValidationError | true => { if (balance < amountToSend) { - return new NotEnoughBalanceForRewardError(JSON.stringify({ balance, amountToSend })) + return new NotEnoughBalanceForQuizError(JSON.stringify({ balance, amountToSend })) } return true diff --git a/core/api/src/app/quiz/get.ts b/core/api/src/app/quiz/get.ts new file mode 100644 index 0000000000..356e0e0193 --- /dev/null +++ b/core/api/src/app/quiz/get.ts @@ -0,0 +1,17 @@ +import { QuizzesValue } from "@/domain/earn/config" +import { QuizRepository } from "@/services/mongoose" + +export const getQuizzesByAccountId = async (accountId: AccountId) => { + const quizzes = await QuizRepository().fetchAll(accountId) + if (quizzes instanceof Error) return quizzes + + const solvedQuizId = quizzes.map((quiz) => quiz.quizId) + + const result = Object.entries(QuizzesValue).map(([id, amount]) => ({ + id: id as QuizQuestionId, + amount, + completed: solvedQuizId.indexOf(id) > -1, + })) + + return result +} diff --git a/core/api/src/app/quiz/index.ts b/core/api/src/app/quiz/index.ts new file mode 100644 index 0000000000..aa853ba7b7 --- /dev/null +++ b/core/api/src/app/quiz/index.ts @@ -0,0 +1,2 @@ +export * from "./add" +export * from "./get" diff --git a/core/api/src/config/index.ts b/core/api/src/config/index.ts index bb659932ff..d2256b1c20 100644 --- a/core/api/src/config/index.ts +++ b/core/api/src/config/index.ts @@ -13,6 +13,7 @@ export * from "./schema" import { ConfigError } from "./error" import { toDays } from "@/domain/primitives" +import { QuizzesValue } from "@/domain/earn" export const MS_PER_SEC = 1000 as MilliSeconds export const MS_PER_5_MINS = (60 * 5 * MS_PER_SEC) as MilliSeconds @@ -81,137 +82,10 @@ export const getLoopConfig = () => { throw new ConfigError("getLoopConfig() was called though swapEnabled is false") } -// onboarding -export const OnboardingEarn: Record = { - walletDownloaded: 1 as Satoshis, - walletActivated: 1 as Satoshis, - whatIsBitcoin: 1 as Satoshis, - sat: 2 as Satoshis, - whereBitcoinExist: 2 as Satoshis, - whoControlsBitcoin: 2 as Satoshis, - copyBitcoin: 3 as Satoshis, - moneySocialAgreement: 2 as Satoshis, - coincidenceOfWants: 2 as Satoshis, - moneyEvolution: 2 as Satoshis, - whyStonesShellGold: 3 as Satoshis, - moneyIsImportant: 3 as Satoshis, - moneyImportantGovernement: 4 as Satoshis, - WhatIsFiat: 2 as Satoshis, - whyCareAboutFiatMoney: 2 as Satoshis, - GovernementCanPrintMoney: 2 as Satoshis, - FiatLosesValueOverTime: 3 as Satoshis, - OtherIssues: 4 as Satoshis, - LimitedSupply: 2 as Satoshis, - Decentralized: 2 as Satoshis, - NoCounterfeitMoney: 2 as Satoshis, - HighlyDivisible: 3 as Satoshis, - securePartOne: 3 as Satoshis, - securePartTwo: 4 as Satoshis, - originsOfMoney: 2 as Satoshis, - primitiveMoney: 2 as Satoshis, - anticipatingDemand: 3 as Satoshis, - nashEquilibrium: 3 as Satoshis, - singleStoreOfValue: 4 as Satoshis, - whatIsGoodSOV: 2 as Satoshis, - durability: 2 as Satoshis, - portability: 2 as Satoshis, - fungibility: 2 as Satoshis, - verifiability: 3 as Satoshis, - divisibility: 3 as Satoshis, - scarce: 3 as Satoshis, - establishedHistory: 4 as Satoshis, - censorshipResistance: 5 as Satoshis, - evolutionMoney: 2 as Satoshis, - collectible: 2 as Satoshis, - storeOfValue: 2 as Satoshis, - mediumOfExchange: 3 as Satoshis, - unitOfAccount: 3 as Satoshis, - partlyMonetized: 3 as Satoshis, - monetizationStage: 4 as Satoshis, - notFromGovernment: 2 as Satoshis, - primaryFunction: 2 as Satoshis, - monetaryMetals: 3 as Satoshis, - stockToFlow: 3 as Satoshis, - hardMoney: 4 as Satoshis, - convergingOnGold: 2 as Satoshis, - originsOfPaperMoney: 2 as Satoshis, - fractionalReserve: 2 as Satoshis, - bankRun: 2 as Satoshis, - modernCentralBanking: 3 as Satoshis, - goldBacked: 3 as Satoshis, - brettonWoods: 4 as Satoshis, - globalReserve: 5 as Satoshis, - nixonShock: 2 as Satoshis, - fiatEra: 2 as Satoshis, - digitalFiat: 2 as Satoshis, - plasticCredit: 3 as Satoshis, - doubleSpendProblem: 3 as Satoshis, - satoshisBreakthrough: 3 as Satoshis, - nativelyDigital: 4 as Satoshis, - CBDCs: 5 as Satoshis, - rootProblem: 2 as Satoshis, - bitcoinCreator: 2 as Satoshis, - fiatRequiresTrust: 3 as Satoshis, - moneyPrinting: 3 as Satoshis, - genesisBlock: 4 as Satoshis, - cypherpunks: 13 as Satoshis, - peer2Peer: 2 as Satoshis, - blockchain: 2 as Satoshis, - privateKey: 2 as Satoshis, - publicKey: 3 as Satoshis, - mining: 3 as Satoshis, - proofOfWork: 3 as Satoshis, - difficultyAdjustment: 4 as Satoshis, - halving: 5 as Satoshis, - bitcoinDrawbacks: 2 as Satoshis, - blocksizeWars: 2 as Satoshis, - lightningNetwork: 2 as Satoshis, - instantPayments: 3 as Satoshis, - micropayments: 3 as Satoshis, - scalability: 3 as Satoshis, - paymentChannels: 4 as Satoshis, - routing: 5 as Satoshis, - itsaBubble: 2 as Satoshis, - itstooVolatile: 2 as Satoshis, - itsnotBacked: 3 as Satoshis, - willbecomeObsolete: 3 as Satoshis, - toomuchEnergy: 4 as Satoshis, - strandedEnergy: 5 as Satoshis, - internetDependent: 2 as Satoshis, - forcrimeOnly: 2 as Satoshis, - ponziScheme: 2 as Satoshis, - bitcoinisTooSlow: 3 as Satoshis, - supplyLimit: 3 as Satoshis, - governmentBan: 4 as Satoshis, - concentratedOwnership: 2 as Satoshis, - centralizedMining: 2 as Satoshis, - tooExpensive: 2 as Satoshis, - prohibitivelyHigh: 3 as Satoshis, - willBeHoarded: 4 as Satoshis, - canBeDuplicated: 5 as Satoshis, - scarcity: 2 as Satoshis, - monetaryPremium: 2 as Satoshis, - greshamsLaw: 2 as Satoshis, - thiersLaw: 3 as Satoshis, - cantillonEffect: 4 as Satoshis, - schellingPoint: 5 as Satoshis, - opportunityCost: 2 as Satoshis, - timePreference: 2 as Satoshis, - impossibleTrinity: 3 as Satoshis, - jevonsParadox: 3 as Satoshis, - powerLaws: 4 as Satoshis, - winnerTakeAll: 5 as Satoshis, - unitBias: 2 as Satoshis, - veblenGood: 2 as Satoshis, - malinvestment: 3 as Satoshis, - asymmetricPayoff: 4 as Satoshis, - ansoffMatrix: 5 as Satoshis, -} as const - export const memoSharingConfig = { memoSharingSatsThreshold: MEMO_SHARING_SATS_THRESHOLD, memoSharingCentsThreshold: MEMO_SHARING_CENTS_THRESHOLD, - authorizedMemos: Object.keys(OnboardingEarn), + authorizedMemos: Object.keys(QuizzesValue), } as const export const getCallbackServiceConfig = (): SvixConfig => { diff --git a/core/api/src/config/types.d.ts b/core/api/src/config/types.d.ts index 0052969f54..a30607426a 100644 --- a/core/api/src/config/types.d.ts +++ b/core/api/src/config/types.d.ts @@ -14,7 +14,7 @@ type CaptchaConfig = { mandatory: boolean } -type RewardsConfig = { +type QuizzesConfig = { phoneMetadataValidationSettings: PhoneMetadataValidationSettings ipMetadataValidationSettings: IpMetadataValidationSettings } diff --git a/core/api/src/config/yaml.ts b/core/api/src/config/yaml.ts index 9315824b8a..5448305a79 100644 --- a/core/api/src/config/yaml.ts +++ b/core/api/src/config/yaml.ts @@ -212,7 +212,7 @@ export const getDeviceAccountCreateAttemptLimits = () => export const getAppcheckJtiAttemptLimits = () => getRateLimits(yamlConfig.rateLimits.requestCodePerAppcheckJti) -export const getAddEarnPerIpLimits = () => +export const getAddQuizPerIpLimits = () => getRateLimits(yamlConfig.rateLimits.addQuizPerIp) export const getOnChainWalletConfig = () => ({ @@ -268,7 +268,7 @@ export const getCronConfig = (config = yamlConfig): CronConfig => config.cronCon export const getCaptcha = (config = yamlConfig): CaptchaConfig => config.captcha -export const getRewardsConfig = (): RewardsConfig => { +export const getQuizzesConfig = (): QuizzesConfig => { const denyPhoneCountries = yamlConfig.quizzes.denyPhoneCountries || [] const allowPhoneCountries = yamlConfig.quizzes.allowPhoneCountries || [] const denyIPCountries = yamlConfig.quizzes.denyIPCountries || [] diff --git a/core/api/src/domain/accounts/index.types.d.ts b/core/api/src/domain/accounts/index.types.d.ts index fb38e2297a..b9d8747b90 100644 --- a/core/api/src/domain/accounts/index.types.d.ts +++ b/core/api/src/domain/accounts/index.types.d.ts @@ -107,7 +107,6 @@ type Account = { coordinates: Coordinates | null contactEnabled: boolean readonly contacts: AccountContact[] - readonly quiz: Quiz[] notificationSettings: NotificationSettings kratosUserId: UserId displayCurrency: DisplayCurrency diff --git a/core/api/src/domain/earn/config.ts b/core/api/src/domain/earn/config.ts new file mode 100644 index 0000000000..2f07faace6 --- /dev/null +++ b/core/api/src/domain/earn/config.ts @@ -0,0 +1,125 @@ +export const QuizzesValue: Record = { + walletDownloaded: 1 as Satoshis, + walletActivated: 1 as Satoshis, + whatIsBitcoin: 1 as Satoshis, + sat: 1 as Satoshis, + whereBitcoinExist: 1 as Satoshis, + whoControlsBitcoin: 1 as Satoshis, + copyBitcoin: 1 as Satoshis, + moneySocialAgreement: 2 as Satoshis, + coincidenceOfWants: 2 as Satoshis, + moneyEvolution: 2 as Satoshis, + whyStonesShellGold: 3 as Satoshis, + moneyIsImportant: 3 as Satoshis, + moneyImportantGovernement: 4 as Satoshis, + WhatIsFiat: 2 as Satoshis, + whyCareAboutFiatMoney: 2 as Satoshis, + GovernementCanPrintMoney: 2 as Satoshis, + FiatLosesValueOverTime: 3 as Satoshis, + OtherIssues: 4 as Satoshis, + LimitedSupply: 2 as Satoshis, + Decentralized: 2 as Satoshis, + NoCounterfeitMoney: 2 as Satoshis, + HighlyDivisible: 3 as Satoshis, + securePartOne: 3 as Satoshis, + securePartTwo: 4 as Satoshis, + originsOfMoney: 2 as Satoshis, + primitiveMoney: 2 as Satoshis, + anticipatingDemand: 3 as Satoshis, + nashEquilibrium: 3 as Satoshis, + singleStoreOfValue: 4 as Satoshis, + whatIsGoodSOV: 2 as Satoshis, + durability: 2 as Satoshis, + portability: 2 as Satoshis, + fungibility: 2 as Satoshis, + verifiability: 3 as Satoshis, + divisibility: 3 as Satoshis, + scarce: 3 as Satoshis, + establishedHistory: 4 as Satoshis, + censorshipResistance: 5 as Satoshis, + evolutionMoney: 2 as Satoshis, + collectible: 2 as Satoshis, + storeOfValue: 2 as Satoshis, + mediumOfExchange: 3 as Satoshis, + unitOfAccount: 3 as Satoshis, + partlyMonetized: 3 as Satoshis, + monetizationStage: 4 as Satoshis, + notFromGovernment: 2 as Satoshis, + primaryFunction: 2 as Satoshis, + monetaryMetals: 3 as Satoshis, + stockToFlow: 3 as Satoshis, + hardMoney: 4 as Satoshis, + convergingOnGold: 2 as Satoshis, + originsOfPaperMoney: 2 as Satoshis, + fractionalReserve: 2 as Satoshis, + bankRun: 2 as Satoshis, + modernCentralBanking: 3 as Satoshis, + goldBacked: 3 as Satoshis, + brettonWoods: 4 as Satoshis, + globalReserve: 5 as Satoshis, + nixonShock: 2 as Satoshis, + fiatEra: 2 as Satoshis, + digitalFiat: 2 as Satoshis, + plasticCredit: 3 as Satoshis, + doubleSpendProblem: 3 as Satoshis, + satoshisBreakthrough: 3 as Satoshis, + nativelyDigital: 4 as Satoshis, + CBDCs: 5 as Satoshis, + rootProblem: 2 as Satoshis, + bitcoinCreator: 2 as Satoshis, + fiatRequiresTrust: 3 as Satoshis, + moneyPrinting: 3 as Satoshis, + genesisBlock: 4 as Satoshis, + cypherpunks: 13 as Satoshis, + peer2Peer: 2 as Satoshis, + blockchain: 2 as Satoshis, + privateKey: 2 as Satoshis, + publicKey: 3 as Satoshis, + mining: 3 as Satoshis, + proofOfWork: 3 as Satoshis, + difficultyAdjustment: 4 as Satoshis, + halving: 5 as Satoshis, + bitcoinDrawbacks: 2 as Satoshis, + blocksizeWars: 2 as Satoshis, + lightningNetwork: 2 as Satoshis, + instantPayments: 3 as Satoshis, + micropayments: 3 as Satoshis, + scalability: 3 as Satoshis, + paymentChannels: 4 as Satoshis, + routing: 5 as Satoshis, + itsaBubble: 2 as Satoshis, + itstooVolatile: 2 as Satoshis, + itsnotBacked: 3 as Satoshis, + willbecomeObsolete: 3 as Satoshis, + toomuchEnergy: 4 as Satoshis, + strandedEnergy: 5 as Satoshis, + internetDependent: 2 as Satoshis, + forcrimeOnly: 2 as Satoshis, + ponziScheme: 2 as Satoshis, + bitcoinisTooSlow: 3 as Satoshis, + supplyLimit: 3 as Satoshis, + governmentBan: 4 as Satoshis, + concentratedOwnership: 2 as Satoshis, + centralizedMining: 2 as Satoshis, + tooExpensive: 2 as Satoshis, + prohibitivelyHigh: 3 as Satoshis, + willBeHoarded: 4 as Satoshis, + canBeDuplicated: 5 as Satoshis, + scarcity: 2 as Satoshis, + monetaryPremium: 2 as Satoshis, + greshamsLaw: 2 as Satoshis, + thiersLaw: 3 as Satoshis, + cantillonEffect: 4 as Satoshis, + schellingPoint: 5 as Satoshis, + opportunityCost: 2 as Satoshis, + timePreference: 2 as Satoshis, + impossibleTrinity: 3 as Satoshis, + jevonsParadox: 3 as Satoshis, + powerLaws: 4 as Satoshis, + winnerTakeAll: 5 as Satoshis, + unitBias: 2 as Satoshis, + veblenGood: 2 as Satoshis, + malinvestment: 3 as Satoshis, + asymmetricPayoff: 4 as Satoshis, + ansoffMatrix: 5 as Satoshis, +} as const diff --git a/core/api/src/domain/earn/index.ts b/core/api/src/domain/earn/index.ts new file mode 100644 index 0000000000..6502e92cf5 --- /dev/null +++ b/core/api/src/domain/earn/index.ts @@ -0,0 +1 @@ +export * from "./config" diff --git a/core/api/src/domain/errors.ts b/core/api/src/domain/errors.ts index 98f6e440e4..4266c056a9 100644 --- a/core/api/src/domain/errors.ts +++ b/core/api/src/domain/errors.ts @@ -66,7 +66,7 @@ export class CouldNotFindAccountFromPhoneError extends CouldNotFindError {} export class CouldNotFindTransactionsForAccountError extends CouldNotFindError {} export class CouldNotFindAccountFromKratosIdError extends CouldNotFindError {} -export class RewardAlreadyPresentError extends DomainError {} +export class QuizAlreadyPresentError extends DomainError {} export class NotImplementedError extends DomainError {} export class NotReachableError extends DomainError {} @@ -116,7 +116,7 @@ export class MissingIPMetadataError extends ValidationError {} export class InvalidIpMetadataError extends ValidationError { level = ErrorLevel.Critical } -export class NotEnoughBalanceForRewardError extends ValidationError { +export class NotEnoughBalanceForQuizError extends ValidationError { level = ErrorLevel.Warn } diff --git a/core/api/src/domain/rate-limit/errors.ts b/core/api/src/domain/rate-limit/errors.ts index 9de6139f86..ce3962a5c4 100644 --- a/core/api/src/domain/rate-limit/errors.ts +++ b/core/api/src/domain/rate-limit/errors.ts @@ -18,4 +18,4 @@ export class InvoiceCreateForRecipientRateLimiterExceededError extends RateLimit export class OnChainAddressCreateRateLimiterExceededError extends RateLimiterExceededError {} export class DeviceAccountCreateRateLimiterExceededError extends RateLimiterExceededError {} export class UserCodeAttemptAppcheckJtiLimiterExceededError extends RateLimiterExceededError {} -export class UserAddEarnAttemptIpRateLimiterExceededError extends RateLimiterExceededError {} +export class UserAddQuizAttemptIpRateLimiterExceededError extends RateLimiterExceededError {} diff --git a/core/api/src/domain/rate-limit/index.ts b/core/api/src/domain/rate-limit/index.ts index 187b8d4c3b..8d954dbe5e 100644 --- a/core/api/src/domain/rate-limit/index.ts +++ b/core/api/src/domain/rate-limit/index.ts @@ -8,7 +8,7 @@ import { UserCodeAttemptIdentifierRateLimiterExceededError, DeviceAccountCreateRateLimiterExceededError, UserCodeAttemptAppcheckJtiLimiterExceededError, - UserAddEarnAttemptIpRateLimiterExceededError, + UserAddQuizAttemptIpRateLimiterExceededError, } from "./errors" import { @@ -21,7 +21,7 @@ import { getRequestCodePerIpLimits, getRequestCodePerLoginIdentifierLimits, getAppcheckJtiAttemptLimits, - getAddEarnPerIpLimits, + getAddQuizPerIpLimits, } from "@/config" export const RateLimitPrefix = { @@ -34,7 +34,7 @@ export const RateLimitPrefix = { onChainAddressCreate: "onchain_address_create", deviceAccountCreate: "device_account_create", requestCodeAttemptPerAppcheckJti: "request_code_attempt_appcheck_jti", - addEarnAttemptPerIp: "add_earn_attempt_ip", + addQuizAttemptPerIp: "add_quiz_attempt_ip", } as const type RateLimitPrefixKey = keyof typeof RateLimitPrefix @@ -85,9 +85,9 @@ export const RateLimitConfig: { [key in RateLimitPrefixKey]: RateLimitConfig } = limits: getAppcheckJtiAttemptLimits(), error: UserCodeAttemptAppcheckJtiLimiterExceededError, }, - addEarnAttemptPerIp: { - key: RateLimitPrefix.addEarnAttemptPerIp, - limits: getAddEarnPerIpLimits(), - error: UserAddEarnAttemptIpRateLimiterExceededError, + addQuizAttemptPerIp: { + key: RateLimitPrefix.addQuizAttemptPerIp, + limits: getAddQuizPerIpLimits(), + error: UserAddQuizAttemptIpRateLimiterExceededError, }, } diff --git a/core/api/src/domain/users/errors.ts b/core/api/src/domain/users/errors.ts index 39b9e2ce14..6e17db2b63 100644 --- a/core/api/src/domain/users/errors.ts +++ b/core/api/src/domain/users/errors.ts @@ -7,7 +7,7 @@ export class PhoneCarrierTypeNotAllowedError extends UnauthorizedPhoneError {} export class PhoneCountryNotAllowedError extends UnauthorizedPhoneError {} export class InvalidPhoneForOnboardingError extends UnauthorizedPhoneError {} -export class InvalidPhoneForRewardError extends UnauthorizedPhoneError {} +export class InvalidPhoneForQuizError extends UnauthorizedPhoneError {} export class InvalidPhoneMetadataForOnboardingError extends UnauthorizedPhoneError { level = ErrorLevel.Critical } diff --git a/core/api/src/graphql/error-map.ts b/core/api/src/graphql/error-map.ts index d3861532c9..7b16e5abae 100644 --- a/core/api/src/graphql/error-map.ts +++ b/core/api/src/graphql/error-map.ts @@ -274,7 +274,7 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { "Too many login attempts on same network, please wait for a while and try again." return new TooManyRequestError({ message, logger: baseLogger }) - case "UserAddEarnAttemptIpRateLimiterExceededError": + case "UserAddQuizAttemptIpRateLimiterExceededError": message = "Too many attempts, please wait for a while and try again." return new TooManyRequestError({ message, logger: baseLogger }) @@ -286,13 +286,13 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { message = "Invalid push notification setting was passed." return new ValidationInternalError({ message, logger: baseLogger }) - case "RewardAlreadyPresentError": - message = "Reward for quiz question was already claimed." + case "QuizAlreadyPresentError": + message = "Quiz question was already claimed." return new ValidationInternalError({ message, logger: baseLogger }) - case "NotEnoughBalanceForRewardError": + case "NotEnoughBalanceForQuizError": message = - "Rewards wallet temporarily depleted. Please contact support if problem persists." + "Quiz wallet temporarily depleted. Please contact support if problem persists." return new ValidationInternalError({ message, logger: baseLogger }) case "SubOneCentSatAmountForUsdSelfSendError": @@ -300,8 +300,8 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { message = "Amount sent was too low for recipient's usd wallet." return new ValidationInternalError({ message, logger: baseLogger }) - case "InvalidPhoneForRewardError": - message = "Unsupported phone carrier for rewards." + case "InvalidPhoneForQuizError": + message = "Unsupported phone carrier for quiz." return new ValidationInternalError({ message, logger: baseLogger }) case "InvalidIpMetadataError": diff --git a/core/api/src/graphql/error.ts b/core/api/src/graphql/error.ts index 3e2e88c62f..a56c24bf21 100644 --- a/core/api/src/graphql/error.ts +++ b/core/api/src/graphql/error.ts @@ -381,9 +381,9 @@ export class InvalidPhoneForOnboardingError extends CustomGraphQLError { export class UnauthorizedIPMetadataCountryError extends CustomGraphQLError { constructor(errData: CustomGraphQLErrorData) { super({ - message: "Country not not authorized for rewards.", + message: "Country not not authorized for quizzes.", forwardToClient: true, - code: "UNAUTHORIZED_COUNTRY_IP_FOR_REWARD", + code: "UNAUTHORIZED_COUNTRY_IP_FOR_QUIZZES", ...errData, }) } @@ -392,9 +392,9 @@ export class UnauthorizedIPMetadataCountryError extends CustomGraphQLError { export class UnauthorizedIPMetadataProxyError extends CustomGraphQLError { constructor(errData: CustomGraphQLErrorData) { super({ - message: "VPN ips are not authorized for rewards.", + message: "VPN ips are not authorized for quizzes.", forwardToClient: true, - code: "UNAUTHORIZED_VPN_IP_FOR_REWARD", + code: "UNAUTHORIZED_VPN_IP_FOR_QUIZZES", ...errData, }) } @@ -403,9 +403,9 @@ export class UnauthorizedIPMetadataProxyError extends CustomGraphQLError { export class UnauthorizedIPError extends CustomGraphQLError { constructor(errData: CustomGraphQLErrorData) { super({ - message: "This ip is unauthorized for rewards.", + message: "This ip is unauthorized for quizzes.", forwardToClient: true, - code: "UNAUTHORIZED_IP_FOR_REWARD", + code: "UNAUTHORIZED_IP_FOR_QUIZZES", ...errData, }) } 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 191ff02f37..0c932b6905 100644 --- a/core/api/src/graphql/public/root/mutation/quiz-completed.ts +++ b/core/api/src/graphql/public/root/mutation/quiz-completed.ts @@ -1,4 +1,4 @@ -import { Payments } from "@/app" +import { Quiz } from "@/app" import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map" import { GT } from "@/graphql/index" @@ -26,7 +26,7 @@ const QuizCompletedMutation = GT.Field< resolve: async (_, args, { domainAccount, ip }) => { const { id } = args.input - const question = await Payments.addEarn({ + const question = await Quiz.completeQuiz({ quizQuestionId: id, accountId: domainAccount.id, ip, diff --git a/core/api/src/graphql/public/types/object/consumer-account.ts b/core/api/src/graphql/public/types/object/consumer-account.ts index a4abb4b02e..8c194a8fe9 100644 --- a/core/api/src/graphql/public/types/object/consumer-account.ts +++ b/core/api/src/graphql/public/types/object/consumer-account.ts @@ -34,6 +34,7 @@ import DisplayCurrency from "@/graphql/shared/types/scalar/display-currency" import { listEndpoints } from "@/app/callback" import { IInvoiceConnection } from "@/graphql/shared/types/abstract/invoice" +import { getQuizzesByAccountId } from "@/app/quiz" const ConsumerAccount = GT.Object({ name: "ConsumerAccount", @@ -167,7 +168,16 @@ const ConsumerAccount = GT.Object({ quiz: { type: GT.NonNullList(Quiz), description: "List the quiz questions of the consumer account", - resolve: (source) => source.quiz, + resolve: async (source) => { + const accountId = source.id + const result = await getQuizzesByAccountId(accountId) + + if (result instanceof Error) { + throw mapError(result) + } + + return result + }, }, transactions: { diff --git a/core/api/src/migrations/20231220170640-quiz-new-collection-1.ts b/core/api/src/migrations/20231220170640-quiz-new-collection-1.ts new file mode 100644 index 0000000000..a3b793d959 --- /dev/null +++ b/core/api/src/migrations/20231220170640-quiz-new-collection-1.ts @@ -0,0 +1,44 @@ +/* eslint @typescript-eslint/ban-ts-comment: "off" */ +// @ts-nocheck +async function migrateAccounts(db, batchSize = 100) { + const cursor = db.collection("accounts").find() + + let batchCount = 0 + while (await cursor.hasNext()) { + const quizUpdates = [] + for (let i = 0; i < batchSize && (await cursor.hasNext()); i++) { + const account = await cursor.next() + const uniqueEarn = [...new Set(account.earn)] + for (const quizId of uniqueEarn) { + quizUpdates.push({ + insertOne: { + accountId: account.id, + quizId, + createAt: new Date(), + }, + }) + } + } + + batchCount += quizUpdates.length + + if (quizUpdates.length > 0) { + await db.collection("quizzes").bulkWrite(quizUpdates) + console.log(`Processed ${batchCount} accounts`) + } + } + + console.log(`Processed ${batchCount} accounts`) +} + +module.exports = { + async up(db) { + await db + .collection("quizzes") + .createIndex({ accountId: 1, quizId: 1 }, { unique: true }) + + console.log("Begin migration to Quiz collection") + await migrateAccounts(db) + console.log("Migration completed") + }, +} diff --git a/core/api/src/services/mongoose/accounts.ts b/core/api/src/services/mongoose/accounts.ts index 9db2934410..8fbed81253 100644 --- a/core/api/src/services/mongoose/accounts.ts +++ b/core/api/src/services/mongoose/accounts.ts @@ -1,7 +1,5 @@ import { parseRepositoryError } from "./utils" -import { OnboardingEarn } from "@/config" - import { AccountStatus } from "@/domain/accounts" import { CouldNotFindAccountError, @@ -219,12 +217,6 @@ const translateToAccount = (result: AccountRecord): Account => ({ }, }, - quiz: Object.entries(OnboardingEarn).map(([id, amount]) => ({ - id: id as QuizQuestionId, - amount, - completed: result.earn.indexOf(id) > -1, - })), - kratosUserId: result.kratosUserId as UserId, displayCurrency: (result.displayCurrency || UsdDisplayCurrency) as DisplayCurrency, }) diff --git a/core/api/src/services/mongoose/index.ts b/core/api/src/services/mongoose/index.ts index 2b7687103e..75b49f12fb 100644 --- a/core/api/src/services/mongoose/index.ts +++ b/core/api/src/services/mongoose/index.ts @@ -1,7 +1,7 @@ export * from "./accounts" export * from "./ln-payments" export * from "./payment-flow" -export * from "./rewards" +export * from "./quiz" export * from "./users" export * from "./wallets" export * from "./wallet-invoices" diff --git a/core/api/src/services/mongoose/quiz.ts b/core/api/src/services/mongoose/quiz.ts new file mode 100644 index 0000000000..0aec9a8795 --- /dev/null +++ b/core/api/src/services/mongoose/quiz.ts @@ -0,0 +1,44 @@ +import { Quiz } from "./schema" + +import { QuizAlreadyPresentError, UnknownRepositoryError } from "@/domain/errors" + +interface ExtendedError extends Error { + code?: number +} + +export const QuizRepository = () => { + const add = async ({ + quizId, + accountId, + }: { + quizId: QuizQuestionId + accountId: AccountId + }) => { + try { + await Quiz.create({ accountId, quizId }) + + return true + } catch (err) { + if (err instanceof Error) { + const error = err as ExtendedError + if (error?.code === 11000) return new QuizAlreadyPresentError() + } + + return new UnknownRepositoryError(err) + } + } + + const fetchAll = async (accountId: AccountId) => { + try { + const result = await Quiz.find({ accountId }) + return result + } catch (err) { + return new UnknownRepositoryError(err) + } + } + + return { + add, + fetchAll, + } +} diff --git a/core/api/src/services/mongoose/rewards.ts b/core/api/src/services/mongoose/rewards.ts deleted file mode 100644 index f4658dd182..0000000000 --- a/core/api/src/services/mongoose/rewards.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Account } from "./schema" - -import { RewardAlreadyPresentError, UnknownRepositoryError } from "@/domain/errors" - -// FIXME: improve boundary -export const RewardsRepository = (accountId: AccountId) => { - const add = async (quizQuestionId: QuizQuestionId) => { - try { - // by default, mongodb return the previous state before the update - const oldState = await Account.findOneAndUpdate( - { id: accountId }, - { $push: { earn: quizQuestionId } }, - // { upsert: true }, - ) - - if (!oldState) { - return new UnknownRepositoryError("account not found") - } - - const rewardNotFound = - oldState.earn.findIndex((item) => item === quizQuestionId) === -1 - - return rewardNotFound || new RewardAlreadyPresentError() - } catch (err) { - return new UnknownRepositoryError("reward issue") - } - } - - return { - add, - } -} diff --git a/core/api/src/services/mongoose/schema.ts b/core/api/src/services/mongoose/schema.ts index afede0eb8a..2fe82c8ab1 100644 --- a/core/api/src/services/mongoose/schema.ts +++ b/core/api/src/services/mongoose/schema.ts @@ -158,10 +158,6 @@ const AccountSchema = new Schema( type: Date, default: Date.now, }, - earn: { - type: [String], - default: [], - }, role: { type: String, // FIXME: role is a mix between 2 things here @@ -307,6 +303,26 @@ AccountSchema.index({ export const Account = mongoose.model("Account", AccountSchema) +const QuizSchema = new Schema({ + accountId: { + type: String, + ref: "Account", + required: true, + }, + quizId: { + type: String, + required: true, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}) + +QuizSchema.index({ accountId: 1, quizId: 1 }, { unique: true }) + +export const Quiz = mongoose.model("Quiz", QuizSchema) + const AccountIpsSchema = new Schema({ ip: { type: String, diff --git a/core/api/src/services/mongoose/schema.types.d.ts b/core/api/src/services/mongoose/schema.types.d.ts index 757d3e071f..68e230fdc2 100644 --- a/core/api/src/services/mongoose/schema.types.d.ts +++ b/core/api/src/services/mongoose/schema.types.d.ts @@ -86,7 +86,6 @@ interface AccountRecord { statusHistory: AccountStatusHistory withdrawFee?: number - earn: string[] contactEnabled: boolean contacts: ContactObjectForUser[] created_at: Date @@ -103,6 +102,12 @@ interface AccountRecord { save: () => Promise } +interface QuizRecord { + accountId: string + quizId: string + createdAt: Date +} + interface AccountIpsRecord { ip: string metadata: { diff --git a/core/api/test/integration/app/payments/add-earn.spec.ts b/core/api/test/integration/app/quizzes/add-quiz.spec.ts similarity index 75% rename from core/api/test/integration/app/payments/add-earn.spec.ts rename to core/api/test/integration/app/quizzes/add-quiz.spec.ts index 6d922b0f77..232e0b1105 100644 --- a/core/api/test/integration/app/payments/add-earn.spec.ts +++ b/core/api/test/integration/app/quizzes/add-quiz.spec.ts @@ -1,11 +1,11 @@ import crypto from "crypto" -import { Payments } from "@/app" +import { Quiz } from "@/app" import { InvalidIpMetadataError } from "@/domain/errors" import { RateLimiterExceededError, - UserAddEarnAttemptIpRateLimiterExceededError, + UserAddQuizAttemptIpRateLimiterExceededError, } from "@/domain/rate-limit/errors" import * as RateLimitImpl from "@/services/rate-limit" @@ -16,9 +16,9 @@ afterEach(async () => { jest.restoreAllMocks() }) -describe("addEarn", () => { +describe("addQuiz", () => { it("fails if ip is undefined", async () => { - const result = await Payments.addEarn({ + const result = await Quiz.completeQuiz({ accountId: crypto.randomUUID() as AccountId, quizQuestionId: "fakeQuizQuestionId", ip: undefined, @@ -33,19 +33,19 @@ describe("addEarn", () => { .spyOn(RateLimitImpl, "RedisRateLimitService") .mockReturnValue({ ...RedisRateLimitService({ - keyPrefix: RateLimitConfig.addEarnAttemptPerIp.key, - limitOptions: RateLimitConfig.addEarnAttemptPerIp.limits, + keyPrefix: RateLimitConfig.addQuizAttemptPerIp.key, + limitOptions: RateLimitConfig.addQuizAttemptPerIp.limits, }), consume: () => new RateLimiterExceededError(), }) - const result = await Payments.addEarn({ + const result = await Quiz.completeQuiz({ accountId: crypto.randomUUID() as AccountId, quizQuestionId: "fakeQuizQuestionId", ip: "192.168.13.13" as IpAddress, }) - expect(result).toBeInstanceOf(UserAddEarnAttemptIpRateLimiterExceededError) + expect(result).toBeInstanceOf(UserAddQuizAttemptIpRateLimiterExceededError) // Restore system state rateLimitServiceSpy.mockReset() diff --git a/core/api/test/integration/services/quiz.spec.ts b/core/api/test/integration/services/quiz.spec.ts new file mode 100644 index 0000000000..d3b329d592 --- /dev/null +++ b/core/api/test/integration/services/quiz.spec.ts @@ -0,0 +1,41 @@ +import crypto from "crypto" + +import { QuizAlreadyPresentError } from "@/domain/errors" +import { QuizRepository } from "@/services/mongoose" + +describe("QuizRepository", () => { + const accountId = crypto.randomUUID() as AccountId + const quizId = "fakeQuizQuestionId" as QuizQuestionId + + it("add quiz", async () => { + const result = await QuizRepository().add({ quizId, accountId }) + expect(result).toBe(true) + }) + + it("can't add quiz twice", async () => { + const result = await QuizRepository().add({ quizId, accountId }) + expect(result).toBeInstanceOf(QuizAlreadyPresentError) + }) + + it("fetch quiz", async () => { + const result = await QuizRepository().fetchAll(accountId) + expect(result).toHaveLength(1) + }) + + it("fetch quizzes", async () => { + const quiz2 = "fakeQuizQuestionId2" as QuizQuestionId + await QuizRepository().add({ accountId, quizId: quiz2 }) + + const result = await QuizRepository().fetchAll(accountId) + expect(result).toMatchObject([ + { + accountId, + quizId: quizId, + }, + { + accountId, + quizId: quiz2, + }, + ]) + }) +}) diff --git a/core/api/test/unit/domain/users/phone-metadata-authorizer.spec.ts b/core/api/test/unit/domain/users/phone-metadata-authorizer.spec.ts index e4b1df319e..2ab409228c 100644 --- a/core/api/test/unit/domain/users/phone-metadata-authorizer.spec.ts +++ b/core/api/test/unit/domain/users/phone-metadata-authorizer.spec.ts @@ -1,4 +1,4 @@ -import { getRewardsConfig, yamlConfig } from "@/config" +import { getQuizzesConfig, yamlConfig } from "@/config" import { PhoneCountryNotAllowedError, PhoneCarrierTypeNotAllowedError, @@ -18,8 +18,8 @@ beforeEach(async () => { } }) -const getPhoneMetadataRewardsSettings = () => - getRewardsConfig().phoneMetadataValidationSettings +const getPhoneMetadataQuizzesSettings = () => + getQuizzesConfig().phoneMetadataValidationSettings describe("PhoneMetadataAuthorizer - validate", () => { it("returns true for empty config", () => { @@ -42,7 +42,7 @@ describe("PhoneMetadataAuthorizer - validate", () => { it("returns true for a valid country", () => { const authorizersSV = PhoneMetadataAuthorizer( - getPhoneMetadataRewardsSettings(), + getPhoneMetadataQuizzesSettings(), ).authorize({ carrier: { error_code: "", @@ -56,7 +56,7 @@ describe("PhoneMetadataAuthorizer - validate", () => { expect(authorizersSV).toBe(true) const authorizersSV1 = PhoneMetadataAuthorizer( - getPhoneMetadataRewardsSettings(), + getPhoneMetadataQuizzesSettings(), ).authorize({ carrier: { error_code: "", @@ -70,7 +70,7 @@ describe("PhoneMetadataAuthorizer - validate", () => { expect(authorizersSV1).toBe(true) const validatorUS = PhoneMetadataAuthorizer( - getPhoneMetadataRewardsSettings(), + getPhoneMetadataQuizzesSettings(), ).authorize({ carrier: { error_code: "", @@ -102,7 +102,7 @@ describe("PhoneMetadataAuthorizer - validate", () => { it("returns error for invalid country", () => { const validator = PhoneMetadataAuthorizer( - getPhoneMetadataRewardsSettings(), + getPhoneMetadataQuizzesSettings(), ).authorize({ carrier: { error_code: "", @@ -154,14 +154,14 @@ describe("PhoneMetadataAuthorizer - validate", () => { it("returns error with undefined metadata", () => { const validator = PhoneMetadataAuthorizer( - getPhoneMetadataRewardsSettings(), + getPhoneMetadataQuizzesSettings(), ).authorize(undefined) expect(validator).toBeInstanceOf(ExpectedPhoneMetadataMissingError) }) it("returns error with voip type", () => { const validator = PhoneMetadataAuthorizer( - getPhoneMetadataRewardsSettings(), + getPhoneMetadataQuizzesSettings(), ).authorize({ carrier: { error_code: "", diff --git a/core/api/test/unit/domain/wallets/payment-input-validator.spec.ts b/core/api/test/unit/domain/wallets/payment-input-validator.spec.ts index 14da2f29e0..f3f593b1a2 100644 --- a/core/api/test/unit/domain/wallets/payment-input-validator.spec.ts +++ b/core/api/test/unit/domain/wallets/payment-input-validator.spec.ts @@ -29,7 +29,6 @@ describe("PaymentInputValidator", () => { }, contactEnabled: true, contacts: [], - quiz: [], kratosUserId: "kratosUserId" as UserId, displayCurrency: UsdDisplayCurrency, } diff --git a/typos.toml b/typos.toml index ddf7518014..428fa89fa4 100644 --- a/typos.toml +++ b/typos.toml @@ -23,3 +23,4 @@ default.extend-ignore-re = ["\"eyJ[a-zA-Z0-9_\\-\\.]+\""] # returned from twilio API referenced in src/debug/text-sms.ts Ons = "Ons" Lsat = "Lsat" +quizs = "quizs" \ No newline at end of file