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:
Pacien Boisson
2021-02-22 12:58:31 +04:00
committed by GitHub
parent 03b5c413a4
commit d094e1c4ba
25 changed files with 944 additions and 21 deletions

View File

@@ -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
}

View File

@@ -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: []

View File

@@ -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',
),
};

View File

@@ -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,
};
}

View File

@@ -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);
}
};

View File

@@ -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,
};
}

View File

@@ -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);
}
};

View File

@@ -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;
}

View 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));

View File

@@ -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);
}
};

View File

@@ -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,
};
}

View File

@@ -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);

View File

@@ -1,4 +0,0 @@
export type UpdateBoxProfileResponse = {
success: boolean;
updatedProfiles: Array<string>;
};

View File

@@ -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,

View File

@@ -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);

View File

@@ -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
}
}
`;

View File

@@ -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
}
}
`;

View 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"
}
]

View 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);
}

View File

@@ -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();

View File

@@ -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"

View File

@@ -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';

View 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;

View File

@@ -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
"""

View File

@@ -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==