Skip to content

Commit

Permalink
feat: move to a new data structure
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicolas Burtey committed Dec 20, 2023
1 parent eaeb3f2 commit 86d9828
Show file tree
Hide file tree
Showing 33 changed files with 255 additions and 145 deletions.
21 changes: 14 additions & 7 deletions bats/core/api/earn.bats → bats/core/api/quiz.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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" \
Expand All @@ -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
Expand All @@ -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
}
15 changes: 15 additions & 0 deletions bats/gql/quiz.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
query myQuizQuestions {
me {
id
defaultAccount {
id
... on ConsumerAccount {
quiz {
id
amount
completed
}
}
}
}
}
4 changes: 2 additions & 2 deletions core/api/galoy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ buildVersion:
ios:
minBuildNumber: 362
lastBuildNumber: 362
rewards:
quizzes:
enableIpProxyCheck: true
allowPhoneCountries: []
denyPhoneCountries: []
Expand Down Expand Up @@ -83,7 +83,7 @@ rateLimits:
points: 3
duration: 3600
blockDuration: 3600
addEarnPerIp:
addQuizPerIp:
points: 125
duration: 86400
blockDuration: 604800
Expand Down
1 change: 0 additions & 1 deletion core/api/src/app/earn/index.ts

This file was deleted.

6 changes: 3 additions & 3 deletions core/api/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as AuthenticationMod from "./authentication"
import * as AdminMod from "./admin"
import * as CallbackMod from "./callback"
import * as CommMod from "./comm"
import * as EarnMod from "./earn"
import * as QuizMod from "./quiz"
import * as LightningMod from "./lightning"
import * as OnChainMod from "./on-chain"
import * as PricesMod from "./prices"
Expand All @@ -21,7 +21,7 @@ const allFunctions = {
Admin: { ...AdminMod },
Callback: { ...CallbackMod },
Comm: { ...CommMod },
Earn: { ...EarnMod },
Quiz: { ...QuizMod },
Lightning: { ...LightningMod },
OnChain: { ...OnChainMod },
Prices: { ...PricesMod },
Expand Down Expand Up @@ -51,7 +51,7 @@ export const {
Admin,
Callback,
Comm,
Earn,
Quiz,
Lightning,
OnChain,
Prices,
Expand Down
37 changes: 19 additions & 18 deletions core/api/src/app/earn/add-earn.ts → core/api/src/app/quiz/add.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { intraledgerPaymentSendWalletIdForBtcWallet } from "../payments/send-intraledger"

import { getRewardsConfig } from "@/config"
import { QuizzesValue } from "./config"

import { getQuizzesConfig } from "@/config"

import { getBalanceForWallet } from "@/app/wallets"

Expand All @@ -9,30 +11,29 @@ import {
InvalidQuizQuestionIdError,
MissingIPMetadataError,
NoBtcWalletExistsForAccountError,
NotEnoughBalanceForRewardError,
NotEnoughBalanceForQuizError,
UnauthorizedIPError,
UnknownRepositoryError,
} from "@/domain/errors"
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"
import { consumeLimiter } from "@/services/rate-limit"
import { getFunderWalletId } from "@/services/ledger/caching"
import { AccountsIpsRepository } from "@/services/mongoose/accounts-ips"
import { OnboardingEarn } from "./config"

export const addEarn = async ({
export const completeQuiz = async ({
quizQuestionId: quizQuestionIdString,
accountId: accountIdRaw,
ip,
Expand All @@ -41,18 +42,18 @@ export const addEarn = async ({
accountId: string
ip: IpAddress | undefined
}): Promise<QuizQuestion | ApplicationError> => {
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 amount = OnboardingEarn[quizQuestionId]
const amount = QuizzesValue[quizQuestionId]
if (!amount) return new InvalidQuizQuestionIdError()

const funderWalletId = await getFunderWalletId()
Expand All @@ -68,26 +69,26 @@ 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)
return new InvalidIpMetadataError(validatedIPMetadata)

if (validatedIPMetadata instanceof UnauthorizedIPError) return validatedIPMetadata

return new UnknownRepositoryError("add earn error")
return new UnknownRepositoryError("add quiz error")
}

const recipientWallets = await WalletsRepository().listByAccountId(accountId)
Expand All @@ -99,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(accountId).add(quizQuestionId)
if (shouldGiveSats instanceof Error) return shouldGiveSats

const funderBalance = await getBalanceForWallet({ walletId: funderWalletId })
if (funderBalance instanceof Error) return funderBalance
Expand All @@ -123,13 +124,13 @@ export const addEarn = async ({
return { id: quizQuestionId, earnAmount: amount }
}

const checkAddEarnAttemptPerIpLimits = async (
const checkAddQuizAttemptPerIpLimits = async (
ip: IpAddress | undefined,
): Promise<true | RateLimiterExceededError> => {
if (!ip) return new InvalidIpMetadataError()

return consumeLimiter({
rateLimitConfig: RateLimitConfig.addEarnAttemptPerIp,
rateLimitConfig: RateLimitConfig.addQuizAttemptPerIp,
keyToConsume: ip,
})
}
Expand All @@ -143,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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// onboarding
export const OnboardingEarn: Record<QuizQuestionId, Satoshis> = {
export const QuizzesValue: Record<QuizQuestionId, Satoshis> = {
walletDownloaded: 1 as Satoshis,
walletActivated: 1 as Satoshis,
whatIsBitcoin: 1 as Satoshis,
Expand Down
1 change: 1 addition & 0 deletions core/api/src/app/quiz/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./add"
4 changes: 2 additions & 2 deletions core/api/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export * from "./schema"
import { ConfigError } from "./error"

import { toDays } from "@/domain/primitives"
import { OnboardingEarn } from "@/app/earn/config"
import { QuizzesValue } from "@/app/quiz/config"

export const MS_PER_SEC = 1000 as MilliSeconds
export const MS_PER_5_MINS = (60 * 5 * MS_PER_SEC) as MilliSeconds
Expand Down Expand Up @@ -85,7 +85,7 @@ export const getLoopConfig = () => {
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 => {
Expand Down
10 changes: 5 additions & 5 deletions core/api/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const configSchema = {
required: ["ios", "android"],
additionalProperties: false,
},
rewards: {
quizzes: {
type: "object",
properties: {
enableIpProxyCheck: { type: "boolean" },
Expand Down Expand Up @@ -265,7 +265,7 @@ export const configSchema = {
onChainAddressCreateAttempt: rateLimitConfigSchema,
deviceAccountCreateAttempt: rateLimitConfigSchema,
requestCodePerAppcheckJti: rateLimitConfigSchema,
addEarnPerIp: rateLimitConfigSchema,
addQuizPerIp: rateLimitConfigSchema,
},
required: [
"requestCodePerLoginIdentifier",
Expand All @@ -277,7 +277,7 @@ export const configSchema = {
"onChainAddressCreateAttempt",
"deviceAccountCreateAttempt",
"requestCodePerAppcheckJti",
"addEarnPerIp",
"addQuizPerIp",
],
additionalProperties: false,
default: {
Expand Down Expand Up @@ -326,7 +326,7 @@ export const configSchema = {
duration: 3600,
blockDuration: 3600,
},
addEarnPerIp: {
addQuizPerIp: {
points: 125,
duration: 86400,
blockDuration: 604800,
Expand Down Expand Up @@ -645,7 +645,7 @@ export const configSchema = {
"dealer",
"ratioPrecision",
"buildVersion",
"rewards",
"quizzes",
"coldStorage",
"bria",
"lndScbBackupBucketName",
Expand Down
4 changes: 2 additions & 2 deletions core/api/src/config/schema.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type YamlSchema = {
android: BuildNumberInput
ios: BuildNumberInput
}
rewards: {
quizzes: {
enableIpProxyCheck: boolean
denyPhoneCountries: string[]
allowPhoneCountries: string[]
Expand Down Expand Up @@ -79,7 +79,7 @@ type YamlSchema = {
onChainAddressCreateAttempt: RateLimitInput
deviceAccountCreateAttempt: RateLimitInput
requestCodePerAppcheckJti: RateLimitInput
addEarnPerIp: RateLimitInput
addQuizPerIp: RateLimitInput
}
accounts: {
initialStatus: string
Expand Down
2 changes: 1 addition & 1 deletion core/api/src/config/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type CaptchaConfig = {
mandatory: boolean
}

type RewardsConfig = {
type QuizzesConfig = {
phoneMetadataValidationSettings: PhoneMetadataValidationSettings
ipMetadataValidationSettings: IpMetadataValidationSettings
}
Expand Down
20 changes: 10 additions & 10 deletions core/api/src/config/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ export const getDeviceAccountCreateAttemptLimits = () =>
export const getAppcheckJtiAttemptLimits = () =>
getRateLimits(yamlConfig.rateLimits.requestCodePerAppcheckJti)

export const getAddEarnPerIpLimits = () =>
getRateLimits(yamlConfig.rateLimits.addEarnPerIp)
export const getAddQuizPerIpLimits = () =>
getRateLimits(yamlConfig.rateLimits.addQuizPerIp)

export const getOnChainWalletConfig = () => ({
dustThreshold: yamlConfig.onChainWallet.dustThreshold,
Expand Down Expand Up @@ -268,13 +268,13 @@ export const getCronConfig = (config = yamlConfig): CronConfig => config.cronCon

export const getCaptcha = (config = yamlConfig): CaptchaConfig => config.captcha

export const getRewardsConfig = (): RewardsConfig => {
const denyPhoneCountries = yamlConfig.rewards.denyPhoneCountries || []
const allowPhoneCountries = yamlConfig.rewards.allowPhoneCountries || []
const denyIPCountries = yamlConfig.rewards.denyIPCountries || []
const allowIPCountries = yamlConfig.rewards.allowIPCountries || []
const denyASNs = yamlConfig.rewards.denyASNs || []
const allowASNs = yamlConfig.rewards.allowASNs || []
export const getQuizzesConfig = (): QuizzesConfig => {
const denyPhoneCountries = yamlConfig.quizzes.denyPhoneCountries || []
const allowPhoneCountries = yamlConfig.quizzes.allowPhoneCountries || []
const denyIPCountries = yamlConfig.quizzes.denyIPCountries || []
const allowIPCountries = yamlConfig.quizzes.allowIPCountries || []
const denyASNs = yamlConfig.quizzes.denyASNs || []
const allowASNs = yamlConfig.quizzes.allowASNs || []

return {
phoneMetadataValidationSettings: {
Expand All @@ -286,7 +286,7 @@ export const getRewardsConfig = (): RewardsConfig => {
allowCountries: allowIPCountries.map((c) => c.toUpperCase()),
denyASNs: denyASNs.map((c) => c.toUpperCase()),
allowASNs: allowASNs.map((c) => c.toUpperCase()),
checkProxy: yamlConfig.rewards.enableIpProxyCheck,
checkProxy: yamlConfig.quizzes.enableIpProxyCheck,
},
}
}
Expand Down
Loading

0 comments on commit 86d9828

Please sign in to comment.