mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-04-02 03:00:32 -04:00
Quest create handlers (#340)
* create quest handler * Updating action schema and removing backend-only insert permissions * Better handle auth bearer token * Basic tests for creating quests * Check if player has > 100 pSEED to allow creating a quest * move quests actions into its own router * create quest completion * updateCompletion handler * update types * Improving handler input types * Improve types and logic * Removing types file and using autogenerated ones * Reject other submissions when accepting a unique quest completion * Fix linting errors * Fix CreateQuestCompletionInput maybe * error messages * Puttin pSEED contractnaddress in config file Co-authored-by: Hammad Jutt <jutt@ualberta.ca>
This commit is contained in:
@@ -1,12 +1,81 @@
|
||||
type Mutation {
|
||||
createQuest (
|
||||
quest: CreateQuestInput!
|
||||
): CreateQuestOutput
|
||||
}
|
||||
|
||||
|
||||
type Mutation {
|
||||
createQuestCompletion (
|
||||
questCompletion: CreateQuestCompletionInput!
|
||||
): CreateQuestCompletionOutput
|
||||
}
|
||||
|
||||
|
||||
type Mutation {
|
||||
updateBoxProfile : UpdateBoxProfileResponse
|
||||
}
|
||||
|
||||
|
||||
type Mutation {
|
||||
updateQuestCompletion (
|
||||
updateData: UpdateQuestCompletionInput!
|
||||
): UpdateQuestCompletionOutput
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
enum QuestRepetition_ActionEnum {
|
||||
UNIQUE
|
||||
PERSONAL
|
||||
RECURRING
|
||||
}
|
||||
|
||||
enum QuestCompletionStatus_ActionEnum {
|
||||
ACCEPTED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
input CreateQuestInput {
|
||||
guild_id : uuid!
|
||||
title : String!
|
||||
description : String
|
||||
external_link : String
|
||||
repetition : QuestRepetition_ActionEnum
|
||||
cooldown : Int
|
||||
}
|
||||
|
||||
input CreateQuestCompletionInput {
|
||||
quest_id : String!
|
||||
submission_link : String
|
||||
submission_text : String
|
||||
}
|
||||
|
||||
input UpdateQuestCompletionInput {
|
||||
quest_completion_id : String!
|
||||
status : QuestCompletionStatus_ActionEnum!
|
||||
}
|
||||
|
||||
type UpdateBoxProfileResponse {
|
||||
success : Boolean!
|
||||
updatedProfiles : [String!]!
|
||||
}
|
||||
|
||||
type CreateQuestOutput {
|
||||
success : Boolean!
|
||||
quest_id : uuid
|
||||
error : String
|
||||
}
|
||||
|
||||
type CreateQuestCompletionOutput {
|
||||
success : Boolean!
|
||||
error : String
|
||||
quest_completion_id : uuid
|
||||
}
|
||||
|
||||
type UpdateQuestCompletionOutput {
|
||||
success : Boolean!
|
||||
error : String
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
actions:
|
||||
- name: createQuest
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{ACTION_BASE_ENDPOINT}}/quests/createQuest'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: player
|
||||
- name: createQuestCompletion
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{ACTION_BASE_ENDPOINT}}/quests/createCompletion'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: player
|
||||
- name: updateBoxProfile
|
||||
definition:
|
||||
kind: synchronous
|
||||
@@ -6,9 +20,41 @@ actions:
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: player
|
||||
- name: updateQuestCompletion
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{ACTION_BASE_ENDPOINT}}/quests/updateCompletion'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: player
|
||||
custom_types:
|
||||
enums: []
|
||||
input_objects: []
|
||||
enums:
|
||||
- name: QuestRepetition_ActionEnum
|
||||
values:
|
||||
- description: null
|
||||
is_deprecated: null
|
||||
value: UNIQUE
|
||||
- description: null
|
||||
is_deprecated: null
|
||||
value: PERSONAL
|
||||
- description: null
|
||||
is_deprecated: null
|
||||
value: RECURRING
|
||||
- name: QuestCompletionStatus_ActionEnum
|
||||
values:
|
||||
- description: null
|
||||
is_deprecated: null
|
||||
value: ACCEPTED
|
||||
- description: null
|
||||
is_deprecated: null
|
||||
value: REJECTED
|
||||
input_objects:
|
||||
- name: CreateQuestInput
|
||||
- name: CreateQuestCompletionInput
|
||||
- name: UpdateQuestCompletionInput
|
||||
objects:
|
||||
- name: UpdateBoxProfileResponse
|
||||
- name: CreateQuestOutput
|
||||
- name: CreateQuestCompletionOutput
|
||||
- name: UpdateQuestCompletionOutput
|
||||
scalars: []
|
||||
|
||||
@@ -5,6 +5,8 @@ interface IConfig {
|
||||
adminKey: string;
|
||||
ipfsEndpoint: string;
|
||||
imgixToken: string;
|
||||
infuraId: string;
|
||||
pSEEDAddress: string;
|
||||
}
|
||||
|
||||
function parseEnv<T extends string | number>(
|
||||
@@ -35,4 +37,12 @@ export const CONFIG: IConfig = {
|
||||
),
|
||||
ipfsEndpoint: parseEnv(process.env.IPFS_ENDPOINT, 'https://ipfs.infura.io'),
|
||||
imgixToken: parseEnv(process.env.IMGIX_TOKEN, ''),
|
||||
pSEEDAddress: parseEnv(
|
||||
process.env.PSEED_ADDRESS,
|
||||
'0x34a01c0a95b0592cc818cd846c3cf285d6c85a31',
|
||||
),
|
||||
infuraId: parseEnv(
|
||||
process.env.NEXT_PUBLIC_INFURA_ID,
|
||||
'781d8466252d47508e177b8637b1c2fd',
|
||||
),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
CreateQuestCompletionInput,
|
||||
CreateQuestCompletionOutput,
|
||||
Quest_Completion_Insert_Input,
|
||||
QuestRepetition_Enum,
|
||||
QuestStatus_Enum,
|
||||
} from '../../../../lib/autogen/hasura-sdk';
|
||||
import { client } from '../../../../lib/hasuraClient';
|
||||
|
||||
export async function createCompletion(
|
||||
playerId: string,
|
||||
questCompletion: CreateQuestCompletionInput,
|
||||
): Promise<CreateQuestCompletionOutput> {
|
||||
if (!questCompletion.submission_link && !questCompletion.submission_text) {
|
||||
throw new Error('Must provide at least a submission link or text');
|
||||
}
|
||||
|
||||
const { quest_by_pk: quest } = await client.GetQuestById({
|
||||
quest_id: questCompletion.quest_id,
|
||||
});
|
||||
if (!quest) {
|
||||
throw new Error('Quest not found');
|
||||
}
|
||||
|
||||
if (quest.status !== QuestStatus_Enum.Open) {
|
||||
throw new Error('Quest must be open');
|
||||
}
|
||||
|
||||
// Personal or unique, check if not already done by player
|
||||
if (
|
||||
quest.repetition === QuestRepetition_Enum.Unique ||
|
||||
quest.repetition === QuestRepetition_Enum.Personal
|
||||
) {
|
||||
const {
|
||||
quest_completion: existingQuestCompletions,
|
||||
} = await client.GetQuestCompletions({
|
||||
player_id: playerId,
|
||||
quest_id: questCompletion.quest_id,
|
||||
});
|
||||
if (existingQuestCompletions.length > 0) {
|
||||
throw new Error(
|
||||
'You already submitted a completion this personal/unique quest',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Recurring, check if not already done by player within cooldown
|
||||
if (quest.repetition === QuestRepetition_Enum.Recurring && quest.cooldown) {
|
||||
const {
|
||||
quest_completion: existingQuestCompletions,
|
||||
} = await client.GetLastQuestCompletionForPlayer({
|
||||
player_id: playerId,
|
||||
quest_id: quest.id,
|
||||
});
|
||||
if (existingQuestCompletions.length > 0) {
|
||||
const existingQuestCompletion = existingQuestCompletions[0];
|
||||
const submittedAt = new Date(existingQuestCompletion.submitted_at);
|
||||
const now = new Date();
|
||||
const diff = +now - +submittedAt;
|
||||
if (diff < quest.cooldown * 1000) {
|
||||
throw new Error(
|
||||
'Player have to wait before being able to do this quest again',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const questCompletionInput: Quest_Completion_Insert_Input = {
|
||||
...questCompletion,
|
||||
completed_by_player_id: playerId,
|
||||
};
|
||||
const createQuestCompletionResult = await client.CreateQuestCompletion({
|
||||
objects: questCompletionInput,
|
||||
});
|
||||
const questCompletionCreated =
|
||||
createQuestCompletionResult.insert_quest_completion?.returning[0];
|
||||
if (!questCompletionCreated) {
|
||||
throw new Error('Error while completing quest');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
quest_completion_id: questCompletionCreated.id,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { CreateQuestCompletionOutput, Mutation_RootCreateQuestCompletionArgs } from '../../../../lib/autogen/hasura-sdk';
|
||||
import { createCompletion } from './createCompletion';
|
||||
|
||||
export const createCompletionHandler = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> => {
|
||||
const {
|
||||
input,
|
||||
session_variables: sessionVariables,
|
||||
} = req.body;
|
||||
|
||||
const role = sessionVariables['x-hasura-role'];
|
||||
const playerId = sessionVariables['x-hasura-user-id'];
|
||||
|
||||
try {
|
||||
if (role !== 'player') {
|
||||
throw new Error('Expected player role');
|
||||
}
|
||||
|
||||
const { questCompletion }: Mutation_RootCreateQuestCompletionArgs = input;
|
||||
const result = await createCompletion(playerId, questCompletion);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const errorResponse: CreateQuestCompletionOutput = {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
res.json(errorResponse);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
CreateQuestInput,
|
||||
CreateQuestOutput,
|
||||
Quest_Insert_Input,
|
||||
QuestRepetition_Enum,
|
||||
} from '../../../../lib/autogen/hasura-sdk';
|
||||
import { client } from '../../../../lib/hasuraClient';
|
||||
import { isAllowedToCreateQuest } from './permissions';
|
||||
|
||||
export async function createQuest(
|
||||
playerId: string,
|
||||
quest: CreateQuestInput,
|
||||
): Promise<CreateQuestOutput> {
|
||||
// Workaround as Hasura can't share enums between root schema and custom actions
|
||||
const questRepetition = quest.repetition as
|
||||
| QuestRepetition_Enum
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
if (questRepetition === QuestRepetition_Enum.Recurring && !quest.cooldown) {
|
||||
throw new Error('Recurring quests need to have a cooldown');
|
||||
}
|
||||
if (questRepetition !== QuestRepetition_Enum.Recurring && quest.cooldown) {
|
||||
throw new Error('Non recurring quests cannot have a cooldown');
|
||||
}
|
||||
|
||||
const playerData = await client.GetPlayer({ playerId });
|
||||
const ethAddress = playerData.player_by_pk?.ethereum_address;
|
||||
if (!ethAddress) {
|
||||
throw new Error('Ethereum address not found for player');
|
||||
}
|
||||
|
||||
const allowed = await isAllowedToCreateQuest(ethAddress);
|
||||
if (!allowed) {
|
||||
throw new Error('Player not allowed to create quests');
|
||||
}
|
||||
|
||||
const questInput: Quest_Insert_Input = {
|
||||
...quest,
|
||||
repetition: questRepetition,
|
||||
created_by_player_id: playerId,
|
||||
};
|
||||
|
||||
const data = await client.CreateQuest({ objects: questInput });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
quest_id: data.insert_quest?.returning[0].id,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { CreateQuestOutput, Mutation_RootCreateQuestArgs } from '../../../../lib/autogen/hasura-sdk';
|
||||
import { createQuest } from './createQuest';
|
||||
|
||||
export const createQuestHandler = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> => {
|
||||
const {
|
||||
input,
|
||||
session_variables: sessionVariables,
|
||||
} = req.body;
|
||||
|
||||
const role = sessionVariables['x-hasura-role'];
|
||||
const playerId = sessionVariables['x-hasura-user-id'];
|
||||
|
||||
try {
|
||||
if (role !== 'player') {
|
||||
throw new Error('Expected player role');
|
||||
}
|
||||
|
||||
const createQuestArgs: Mutation_RootCreateQuestArgs = input;
|
||||
const result = await createQuest(playerId, createQuestArgs.quest);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const errorResponse: CreateQuestOutput = {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
res.json(errorResponse);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { numbers } from '@metafam/utils';
|
||||
|
||||
import { CONFIG } from '../../../../config';
|
||||
import { getERC20Contract } from '../../../../lib/ethereum';
|
||||
|
||||
const { BN, amountToDecimal } = numbers;
|
||||
|
||||
/**
|
||||
* As a first iteration, we only allow people to create quests if they hold more that 100 pSEED tokens
|
||||
*/
|
||||
|
||||
export async function isAllowedToCreateQuest(
|
||||
playerAddress: string,
|
||||
): Promise<boolean> {
|
||||
const pSEEDContractAddress = CONFIG.pSEEDAddress;
|
||||
const pSEEDContract = getERC20Contract(pSEEDContractAddress);
|
||||
const pSEEDBalance = await pSEEDContract.balanceOf(playerAddress);
|
||||
const pSEEDDecimals = await pSEEDContract.decimals();
|
||||
const minimumPooledSeedBalance = new BN(100);
|
||||
const pSEEDBalanceInDecimal = amountToDecimal(pSEEDBalance, pSEEDDecimals);
|
||||
|
||||
const allowed = new BN(pSEEDBalanceInDecimal).gt(minimumPooledSeedBalance);
|
||||
|
||||
return allowed;
|
||||
}
|
||||
12
packages/backend/src/handlers/actions/quests/routes.ts
Normal file
12
packages/backend/src/handlers/actions/quests/routes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import express from 'express';
|
||||
|
||||
import { asyncHandlerWrapper } from '../../../lib/apiHelpers';
|
||||
import { createCompletionHandler } from './createCompletion/handler';
|
||||
import { createQuestHandler } from './createQuest/handler';
|
||||
import { updateCompletionHandler } from './updateCompletion/handler';
|
||||
|
||||
export const questsRoutes = express.Router();
|
||||
|
||||
questsRoutes.post('/createQuest', asyncHandlerWrapper(createQuestHandler));
|
||||
questsRoutes.post('/createCompletion', asyncHandlerWrapper(createCompletionHandler));
|
||||
questsRoutes.post('/updateCompletion', asyncHandlerWrapper(updateCompletionHandler));
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { Mutation_RootUpdateQuestCompletionArgs,UpdateQuestCompletionOutput } from '../../../../lib/autogen/hasura-sdk';
|
||||
import { updateCompletion } from './updateCompletion';
|
||||
|
||||
export const updateCompletionHandler = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> => {
|
||||
const {
|
||||
input,
|
||||
session_variables: sessionVariables,
|
||||
} = req.body;
|
||||
|
||||
const role = sessionVariables['x-hasura-role'];
|
||||
const playerId = sessionVariables['x-hasura-user-id'];
|
||||
|
||||
try {
|
||||
if (role !== 'player') {
|
||||
throw new Error('Expected player role');
|
||||
}
|
||||
|
||||
const updateCompletionArgs: Mutation_RootUpdateQuestCompletionArgs = input;
|
||||
const result = await updateCompletion(playerId, updateCompletionArgs.updateData);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const errorResponse: UpdateQuestCompletionOutput = {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
res.json(errorResponse);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
QuestCompletionStatus_ActionEnum,
|
||||
QuestCompletionStatus_Enum,
|
||||
QuestRepetition_Enum,
|
||||
QuestStatus_Enum,
|
||||
UpdateQuestCompletionInput,
|
||||
UpdateQuestCompletionOutput,
|
||||
} from '../../../../lib/autogen/hasura-sdk';
|
||||
import { client } from '../../../../lib/hasuraClient';
|
||||
|
||||
export async function updateCompletion(
|
||||
playerId: string,
|
||||
updateData: UpdateQuestCompletionInput,
|
||||
): Promise<UpdateQuestCompletionOutput> {
|
||||
|
||||
const { quest_completion_by_pk: questCompletion } = await client.GetQuestCompletionById({ quest_completion_id: updateData.quest_completion_id });
|
||||
if (!questCompletion) {
|
||||
throw new Error('Quest completion not found');
|
||||
}
|
||||
const { quest_by_pk: quest } = await client.GetQuestById({ quest_id: questCompletion.quest_id });
|
||||
if (!quest) {
|
||||
throw new Error('Quest not found');
|
||||
}
|
||||
|
||||
if (quest.status !== QuestStatus_Enum.Open) {
|
||||
throw new Error('Quest must be open');
|
||||
}
|
||||
if (quest.created_by_player_id !== playerId) {
|
||||
throw new Error('Only quest creator can update a completion');
|
||||
}
|
||||
if (questCompletion.status !== QuestCompletionStatus_Enum.Pending) {
|
||||
throw new Error('Quest completion already marked as done');
|
||||
}
|
||||
|
||||
// Workaround as Hasura can't share enums between root schema and custom actions
|
||||
const newQuestCompletionStatus = updateData.status === QuestCompletionStatus_ActionEnum.Accepted ? QuestCompletionStatus_Enum.Accepted : QuestCompletionStatus_Enum.Rejected;
|
||||
|
||||
const updateQuestCompletionResult = await client.UpdateQuestCompletionStatus({
|
||||
quest_completion_id: questCompletion.id,
|
||||
status: newQuestCompletionStatus,
|
||||
});
|
||||
const questCompletionUpdated = updateQuestCompletionResult.update_quest_completion_by_pk;
|
||||
if(!questCompletionUpdated) {
|
||||
throw new Error('Error while updating quest completion');
|
||||
}
|
||||
|
||||
if (newQuestCompletionStatus === QuestCompletionStatus_Enum.Accepted && quest.repetition === QuestRepetition_Enum.Unique) {
|
||||
const updateQuestStatusResult = await client.UpdateQuestStatus({
|
||||
quest_id: quest.id,
|
||||
status: QuestStatus_Enum.Closed,
|
||||
});
|
||||
const questStatusUpdated = updateQuestStatusResult.update_quest_by_pk;
|
||||
if(!questStatusUpdated) {
|
||||
throw new Error('Error while setting unique quest status to closed after being completed');
|
||||
}
|
||||
await client.RejectOtherQuestCompletions({
|
||||
accepted_quest_completion_id: questCompletion.id,
|
||||
quest_id: quest.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
|
||||
import { asyncHandlerWrapper } from '../../lib/apiHelpers';
|
||||
import { migrateSourceCredAccounts } from './migrateSourceCredAccounts/handler';
|
||||
import { questsRoutes } from './quests/routes';
|
||||
import { updateBoxProfileHandler } from './updateBoxProfile/handler';
|
||||
|
||||
export const actionRoutes = express.Router();
|
||||
@@ -15,3 +16,5 @@ actionRoutes.post(
|
||||
'/migrateSourceCredAccounts',
|
||||
asyncHandlerWrapper(migrateSourceCredAccounts),
|
||||
);
|
||||
|
||||
actionRoutes.use('/quests', questsRoutes);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export type UpdateBoxProfileResponse = {
|
||||
success: boolean;
|
||||
updatedProfiles: Array<string>;
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
import Box from '3box';
|
||||
|
||||
import { AccountType_Enum } from '../../../lib/autogen/hasura-sdk';
|
||||
import { AccountType_Enum, UpdateBoxProfileResponse } from '../../../lib/autogen/hasura-sdk';
|
||||
import { client } from '../../../lib/hasuraClient';
|
||||
import { UpdateBoxProfileResponse } from '../types';
|
||||
|
||||
export async function updateVerifiedAccounts(
|
||||
playerId: string,
|
||||
|
||||
@@ -10,6 +10,9 @@ const unauthorizedVariables = {
|
||||
function getHeaderToken(req: Request): string | null {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) return null;
|
||||
if (authHeader.substring(0, 7) !== 'Bearer ')
|
||||
throw new Error('invalid token type');
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
if (token.length === 0) return null;
|
||||
return token;
|
||||
@@ -19,7 +22,13 @@ export const authHandler = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> => {
|
||||
const token = getHeaderToken(req);
|
||||
let token;
|
||||
try {
|
||||
token = getHeaderToken(req);
|
||||
} catch (_) {
|
||||
res.status(401).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
res.json(unauthorizedVariables);
|
||||
|
||||
@@ -101,3 +101,65 @@ export const UpdatePlayer = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CreateQuest = gql`
|
||||
mutation CreateQuest($objects: [quest_insert_input!]!) {
|
||||
insert_quest(objects: $objects) {
|
||||
affected_rows
|
||||
returning {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CreateQuestCompletion = gql`
|
||||
mutation CreateQuestCompletion($objects: [quest_completion_insert_input!]!) {
|
||||
insert_quest_completion(objects: $objects) {
|
||||
affected_rows
|
||||
returning {
|
||||
id
|
||||
quest_id
|
||||
completed_by_player_id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UpdateQuestStatus = gql`
|
||||
mutation UpdateQuestStatus($quest_id: uuid!, $status: QuestStatus_enum!) {
|
||||
update_quest_by_pk(
|
||||
pk_columns: {id: $quest_id},
|
||||
_set: { status: $status }
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UpdateQuestCompletionStatus = gql`
|
||||
mutation UpdateQuestCompletionStatus($quest_completion_id: uuid!, $status: QuestCompletionStatus_enum!) {
|
||||
update_quest_completion_by_pk(
|
||||
pk_columns: {id: $quest_completion_id},
|
||||
_set: { status: $status }
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RejectOtherQuestCompletions = gql`
|
||||
mutation RejectOtherQuestCompletions($accepted_quest_completion_id: uuid!, $quest_id: uuid!) {
|
||||
update_quest_completion(
|
||||
where: {
|
||||
_and: [
|
||||
{ id: { _neq: $accepted_quest_completion_id } },
|
||||
{ quest_id: { _eq: $quest_id } }
|
||||
]
|
||||
},
|
||||
_set: { status: REJECTED }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -16,3 +16,60 @@ export const GetPlayerFromEth = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GetQuestById = gql`
|
||||
query GetQuestById($quest_id: uuid!) {
|
||||
quest_by_pk(id: $quest_id) {
|
||||
id
|
||||
cooldown
|
||||
status
|
||||
repetition
|
||||
created_by_player_id
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export const GetQuestCompletions = gql`
|
||||
query GetQuestCompletions($quest_id: uuid!, $player_id: uuid!) {
|
||||
quest_completion(
|
||||
where: {
|
||||
quest_id: {_eq: $quest_id},
|
||||
completed_by_player_id: {_eq: $player_id}
|
||||
}
|
||||
) {
|
||||
id
|
||||
quest_id
|
||||
completed_by_player_id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GetQuestCompletionById = gql`
|
||||
query GetQuestCompletionById($quest_completion_id: uuid!) {
|
||||
quest_completion_by_pk(id: $quest_completion_id) {
|
||||
id
|
||||
quest_id
|
||||
completed_by_player_id
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GetLastQuestCompletionForPlayer = gql`
|
||||
query GetLastQuestCompletionForPlayer($quest_id: uuid!, $player_id: uuid!) {
|
||||
quest_completion(
|
||||
limit: 1,
|
||||
order_by: {submitted_at: desc},
|
||||
where: {
|
||||
quest_id: {_eq: $quest_id},
|
||||
completed_by_player_id: {_eq: $player_id}
|
||||
}
|
||||
) {
|
||||
id
|
||||
quest_id
|
||||
completed_by_player_id
|
||||
submitted_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
222
packages/backend/src/lib/abis/ERC20.json
Normal file
222
packages/backend/src/lib/abis/ERC20.json
Normal file
@@ -0,0 +1,222 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"payable": true,
|
||||
"stateMutability": "payable",
|
||||
"type": "fallback"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
||||
12
packages/backend/src/lib/ethereum.ts
Normal file
12
packages/backend/src/lib/ethereum.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {ethers} from "ethers";
|
||||
|
||||
import { CONFIG } from '../config';
|
||||
import ERC20_ABI from './abis/ERC20.json';
|
||||
|
||||
const { infuraId } = CONFIG;
|
||||
|
||||
export const defaultProvider = new ethers.providers.InfuraProvider(1, infuraId);
|
||||
|
||||
export function getERC20Contract(contractAddress: string, provider: ethers.providers.BaseProvider = defaultProvider) {
|
||||
return new ethers.Contract(contractAddress, ERC20_ABI, provider);
|
||||
}
|
||||
@@ -3,11 +3,24 @@ import { GraphQLClient } from 'graphql-request';
|
||||
import { CONFIG } from '../config';
|
||||
import { getSdk } from './autogen/hasura-sdk';
|
||||
|
||||
export const client = getSdk(
|
||||
new GraphQLClient(CONFIG.graphqlURL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-hasura-access-key': CONFIG.adminKey,
|
||||
},
|
||||
}),
|
||||
);
|
||||
interface GetClientParams {
|
||||
role?: string;
|
||||
userId?: string;
|
||||
backendOnly?: boolean;
|
||||
}
|
||||
|
||||
export const getClient = (params: GetClientParams = {}) =>
|
||||
getSdk(
|
||||
new GraphQLClient(CONFIG.graphqlURL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-hasura-access-key': CONFIG.adminKey,
|
||||
'x-hasura-role': params.role || 'admin',
|
||||
'x-hasura-user-id': params.userId || '',
|
||||
'x-hasura-use-backend-only-permissions':
|
||||
params.backendOnly === true ? 'true' : 'false',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const client = getClient();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"precommit": "yarn lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.1",
|
||||
"ethers": "5.0.17",
|
||||
"js-base64": "3.5.2",
|
||||
"uuid": "8.3.2"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as did from './did';
|
||||
|
||||
export { did };
|
||||
export * as did from './did';
|
||||
export * from './arrayHelpers';
|
||||
export * from './promiseHelpers';
|
||||
export * as numbers from './numbers';
|
||||
|
||||
24
packages/utils/src/numbers.ts
Normal file
24
packages/utils/src/numbers.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import BN from 'bignumber.js';
|
||||
|
||||
export { BN };
|
||||
|
||||
export function amountToInt(amount: string, decimals: number): string {
|
||||
return new BN(amount)
|
||||
.times(10 ** decimals)
|
||||
.dp(0)
|
||||
.toFixed();
|
||||
}
|
||||
|
||||
export function amountToDecimal(amount: string, decimals: number): string {
|
||||
return new BN(amount).div(10 ** decimals).toFixed();
|
||||
}
|
||||
|
||||
export function truncateNumber(
|
||||
n: string,
|
||||
sd: number = SIGNIFICANT_DIGITS,
|
||||
roundingMode?: BN.RoundingMode,
|
||||
): string {
|
||||
return new BN(n).sd(sd, roundingMode).toFixed();
|
||||
}
|
||||
|
||||
export const SIGNIFICANT_DIGITS = 7;
|
||||
@@ -196,6 +196,33 @@ type CollectiblesFavorites {
|
||||
tokenId: String
|
||||
}
|
||||
|
||||
input CreateQuestCompletionInput {
|
||||
quest_id: String!
|
||||
submission_link: String
|
||||
submission_text: String
|
||||
}
|
||||
|
||||
type CreateQuestCompletionOutput {
|
||||
error: String
|
||||
quest_completion_id: uuid
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
input CreateQuestInput {
|
||||
cooldown: Int
|
||||
description: String
|
||||
external_link: String
|
||||
guild_id: uuid!
|
||||
repetition: QuestRepetition_ActionEnum
|
||||
title: String!
|
||||
}
|
||||
|
||||
type CreateQuestOutput {
|
||||
error: String
|
||||
quest_id: uuid
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
"""
|
||||
columns and relationships of "EnneagramType"
|
||||
"""
|
||||
@@ -1618,6 +1645,16 @@ type Moloch {
|
||||
|
||||
"""mutation root"""
|
||||
type mutation_root {
|
||||
"""
|
||||
perform the action: "createQuest"
|
||||
"""
|
||||
createQuest(quest: CreateQuestInput!): CreateQuestOutput
|
||||
|
||||
"""
|
||||
perform the action: "createQuestCompletion"
|
||||
"""
|
||||
createQuestCompletion(questCompletion: CreateQuestCompletionInput!): CreateQuestCompletionOutput
|
||||
|
||||
"""
|
||||
delete data from the table: "AccountType"
|
||||
"""
|
||||
@@ -2307,6 +2344,11 @@ type mutation_root {
|
||||
"""
|
||||
updateBoxProfile: UpdateBoxProfileResponse
|
||||
|
||||
"""
|
||||
perform the action: "updateQuestCompletion"
|
||||
"""
|
||||
updateQuestCompletion(updateData: UpdateQuestCompletionInput!): UpdateQuestCompletionOutput
|
||||
|
||||
"""
|
||||
update data of the table: "AccountType"
|
||||
"""
|
||||
@@ -6150,6 +6192,11 @@ type QuestCompletionStatus {
|
||||
status: String!
|
||||
}
|
||||
|
||||
enum QuestCompletionStatus_ActionEnum {
|
||||
ACCEPTED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
"""
|
||||
aggregated selection of "QuestCompletionStatus"
|
||||
"""
|
||||
@@ -6360,6 +6407,12 @@ type QuestRepetition {
|
||||
repetition: String!
|
||||
}
|
||||
|
||||
enum QuestRepetition_ActionEnum {
|
||||
PERSONAL
|
||||
RECURRING
|
||||
UNIQUE
|
||||
}
|
||||
|
||||
"""
|
||||
aggregated selection of "QuestRepetition"
|
||||
"""
|
||||
@@ -8074,6 +8127,16 @@ type UpdateBoxProfileResponse {
|
||||
updatedProfiles: [String!]!
|
||||
}
|
||||
|
||||
input UpdateQuestCompletionInput {
|
||||
quest_completion_id: String!
|
||||
status: QuestCompletionStatus_ActionEnum!
|
||||
}
|
||||
|
||||
type UpdateQuestCompletionOutput {
|
||||
error: String
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
scalar uuid
|
||||
|
||||
"""
|
||||
|
||||
@@ -9908,7 +9908,7 @@ bignumber.js@^6.0.0:
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-6.0.0.tgz#bbfa047644609a5af093e9cbd83b0461fa3f6002"
|
||||
integrity sha512-x247jIuy60/+FtMRvscqfxtVHQf8AGx2hm9c6btkgC0x/hp9yt+teISNhvF8WlwRkCc5yF2fDECH8SIMe8j+GA==
|
||||
|
||||
bignumber.js@^9.0.0:
|
||||
bignumber.js@^9.0.0, bignumber.js@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5"
|
||||
integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==
|
||||
|
||||
Reference in New Issue
Block a user