Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Community Backend Refactor #5800

Merged
merged 12 commits into from
Dec 6, 2023
10 changes: 10 additions & 0 deletions knowledge_base/Code-Style.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ In cases where TSDoc is inappropriate for inline documentation, the following st
- Use descriptive variable and function names to reduce the need for comments by making the code more self-explanatory.
- Datestamps, when used, should be formatted YYMMDD.

### Backend Errors
Backend errors currently come in two varieties:
- Errors instructive for the end user
- Errors instructive for the client

Errors instructive for the user should be formatted with formatErrorPretty from the errorUtils. These will be displayed
on the front end directly in the modal.
Errors instructive for the client should be directly passed to the backend. These should be handled by the backend for
the possibility of taking recovery actions.

## Change Log

- 231010: Updated by Graham Johnson with TSDoc guidelines. Import guidelines removed until new approach is implemented. (#5254).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Tendermint34Client } from '@cosmjs/tendermint-rpc';
import type { Cluster } from '@solana/web3.js';
import * as solw3 from '@solana/web3.js';
import BN from 'bn.js';
import { AppError } from 'common-common/src/errors';
import {
Expand All @@ -9,6 +11,9 @@ import {
NotificationCategories,
} from 'common-common/src/types';
import { Op } from 'sequelize';
import Web3 from 'web3';
import { z } from 'zod';
import { createCommunitySchema } from '../../../shared/schemas/createCommunitySchema';
import { bech32ToHex, urlHasValidHTTPPrefix } from '../../../shared/utils';

import { COSMOS_REGISTRY_API } from '../../config';
Expand All @@ -18,12 +23,7 @@ import type { CommunityAttributes } from '../../models/community';
import type { RoleAttributes } from '../../models/role';

import axios from 'axios';
import { ALL_COMMUNITIES } from '../../middleware/databaseValidationService';
import { UserInstance } from '../../models/user';
import {
MAX_COMMUNITY_IMAGE_SIZE_BYTES,
checkUrlFileSize,
} from '../../util/checkUrlFileSize';
import { RoleInstanceWithPermission } from '../../util/roles';
import testSubstrateSpec from '../../util/testSubstrateSpec';
import { ServerCommunitiesController } from '../server_communities_controller';
Expand All @@ -37,7 +37,6 @@ export const Errors = {
InvalidSymbolLength: 'Symbol should not exceed 9',
NoType: 'Must provide chain type',
NoBase: 'Must provide chain base',
NoNodeUrl: 'Must provide node url',
InvalidNodeUrl: 'Node url must begin with http://, https://, ws://, wss://',
InvalidNode: 'RPC url returned invalid response. Check your node url',
MustBeWs: 'Node must support websockets on ethereum',
Expand All @@ -61,22 +60,13 @@ export const Errors = {
InvalidGithub: 'Github must begin with https://github.com/',
InvalidAddress: 'Address is invalid',
NotAdmin: 'Must be admin',
ImageDoesntExist: `Image url provided doesn't exist`,
ImageTooLarge: `Image must be smaller than ${MAX_COMMUNITY_IMAGE_SIZE_BYTES}kb`,
UnegisteredCosmosChain: `Check https://cosmos.directory.
Provided chain_name is not registered in the Cosmos Chain Registry`,
UnegisteredCosmosChain: `Check https://cosmos.directory.
Provided chain_name is not registered in the Cosmos Chain Registry`,
};

export type CreateCommunityOptions = {
user: UserInstance;
community: Omit<CommunityAttributes, 'substrate_spec'> &
Omit<ChainNodeAttributes, 'id'> & {
id: string;
node_url: string;
substrate_spec: string;
address?: string;
decimals: number;
};
community: z.infer<typeof createCommunitySchema>;
};

export type CreateCommunityResult = {
Expand All @@ -102,34 +92,7 @@ export async function __createCommunity(
throw new AppError(Errors.NotAdmin);
}
}
if (!community.id || !community.id.trim()) {
throw new AppError(Errors.NoId);
}
if (community.id === ALL_COMMUNITIES) {
throw new AppError(Errors.ReservedId);
}
if (!community.name || !community.name.trim()) {
throw new AppError(Errors.NoName);
}
if (community.name.length > 255) {
throw new AppError(Errors.InvalidNameLength);
}
if (!community.default_symbol || !community.default_symbol.trim()) {
throw new AppError(Errors.NoSymbol);
}
if (community.default_symbol.length > 9) {
throw new AppError(Errors.InvalidSymbolLength);
}
if (!community.type || !community.type.trim()) {
throw new AppError(Errors.NoType);
}
if (!community.base || !community.base.trim()) {
throw new AppError(Errors.NoBase);
}

if (community.icon_url) {
await checkUrlFileSize(community.icon_url, MAX_COMMUNITY_IMAGE_SIZE_BYTES);
}
const existingBaseCommunity = await this.models.Community.findOne({
where: { base: community.base },
});
Expand Down Expand Up @@ -160,7 +123,6 @@ export async function __createCommunity(
community.base === ChainBase.Ethereum &&
community.type !== ChainType.Offchain
) {
const Web3 = (await import('web3')).default;
if (!Web3.utils.isAddress(community.address)) {
throw new AppError(Errors.InvalidAddress);
}
Expand Down Expand Up @@ -204,8 +166,7 @@ export async function __createCommunity(
community.base === ChainBase.Solana &&
community.type !== ChainType.Offchain
) {
const solw3 = await import('@solana/web3.js');
let pubKey;
let pubKey: solw3.PublicKey;
try {
pubKey = new solw3.PublicKey(community.address);
} catch (e) {
Expand Down Expand Up @@ -264,8 +225,7 @@ export async function __createCommunity(
throw new AppError(Errors.InvalidNodeUrl);
}
try {
const cosm = await import('@cosmjs/tendermint-rpc');
const tmClient = await cosm.Tendermint34Client.connect(url);
const tmClient = await Tendermint34Client.connect(url);
await tmClient.block();
} catch (err) {
throw new AppError(Errors.InvalidNode);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { AppError } from 'common-common/src/errors';
import { MixpanelCommunityCreationEvent } from '../../../shared/analytics/types';
import { createCommunitySchema } from '../../../shared/schemas/createCommunitySchema';
import {
CreateCommunityOptions,
CreateCommunityResult,
} from '../../controllers/server_communities_methods/create_community';
import { ServerControllers } from '../../routing/router';
import { TypedRequestBody, TypedResponse, success } from '../../types';
import { formatErrorPretty } from '../../util/errorFormat';

type CreateCommunityRequestBody = CreateCommunityOptions['community'];
type CreateCommunityResponse = CreateCommunityResult;
Expand All @@ -14,9 +17,21 @@ export const createCommunityHandler = async (
req: TypedRequestBody<CreateCommunityRequestBody>,
res: TypedResponse<CreateCommunityResponse>,
) => {
for (const key in req.body) {
if (req.body[key] === '' || req.body[key] === null) {
delete req.body[key];
}
}

const validationResult = await createCommunitySchema.safeParseAsync(req.body);

if (validationResult.success === false) {
throw new AppError(formatErrorPretty(validationResult));
}

kurtisassad marked this conversation as resolved.
Show resolved Hide resolved
const community = await controllers.communities.createCommunity({
user: req.user,
community: req.body,
community: validationResult.data,
});

controllers.analytics.track(
Expand Down
19 changes: 0 additions & 19 deletions packages/commonwealth/server/util/checkUrlFileSize.ts

This file was deleted.

9 changes: 9 additions & 0 deletions packages/commonwealth/server/util/errorFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SafeParseError } from 'zod';

export function formatErrorPretty(
validationResult: SafeParseError<any>,
): string {
return validationResult.error.issues
.map(({ path, message }) => `${path.join(': ')}: ${message}`)
.join(', ');
}
84 changes: 84 additions & 0 deletions packages/commonwealth/shared/schemas/createCommunitySchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
ChainBase,
ChainCategoryType,
ChainNetwork,
ChainType,
} from 'common-common/src/types';
import { z } from 'zod';
import { ALL_COMMUNITIES } from '../../server/middleware/databaseValidationService';
import { getFileSizeBytes } from '../../server/util/getFilesSizeBytes';

export const MAX_COMMUNITY_IMAGE_SIZE_KB = 500;

async function checkIconSize(val, ctx) {
const fileSizeBytes = await getFileSizeBytes(val);
if (fileSizeBytes === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Image url provided doesn't exist",
});
return;
}
if (fileSizeBytes >= MAX_COMMUNITY_IMAGE_SIZE_KB * 1024) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Image must be smaller than ${MAX_COMMUNITY_IMAGE_SIZE_KB}kb`,
});
}
}

export const createCommunitySchema = z.object({
id: z.string(),
name: z
.string()
.max(255)
.refine((data) => !data.includes(ALL_COMMUNITIES), {
message: `String must not contain '${ALL_COMMUNITIES}'`,
}),
chain_node_id: z.number().optional(), // corresponds to the chain field
community_namespace: z
.object({
namespace: z.string().optional(),
tx_receipt: z
.object({
transactionHash: z.string(),
blockNumber: z.number(),
})
.optional(), // this comes from NamespaceFactory#deployNamespace
})
.refine(
(data) => !!data.namespace === !!data.tx_receipt,
'Must contain both namespace and tx_receipt',
)
.optional(),
description: z.string().optional(),
icon_url: z
.string()
.url()
.superRefine(async (val, ctx) => await checkIconSize(val, ctx)),
social_links: z.array(z.string().url()).optional(),
tags: z.array(z.nativeEnum(ChainCategoryType)).default([]),
directory_page_enabled: z.boolean().default(false),
type: z.nativeEnum(ChainType).default(ChainType.Offchain),
base: z.nativeEnum(ChainBase),

// hidden optional params
alt_wallet_url: z.string().url().optional(),
eth_chain_id: z.coerce.number().optional(),
cosmos_chain_id: z.string().optional(),
address: z.string().optional(),
decimals: z.number().optional(),
substrate_spec: z.string().optional(),
bech32_prefix: z.string().optional(),
token_name: z.string().optional(),

// deprecated params to be removed
node_url: z.string().url(),
network: z.nativeEnum(ChainNetwork),
default_symbol: z.string().max(9),
website: z.string().url().optional(),
github: z.string().url().startsWith('https://github.com/').optional(),
telegram: z.string().url().startsWith('https://t.me/').optional(),
element: z.string().url().startsWith('https://matrix.to/').optional(),
discord: z.string().url().startsWith('https://discord.com/').optional(),
});
Loading