Skip to content

Commit

Permalink
Merge branch 'master' of github.com:hicommonwealth/commonwealth into …
Browse files Browse the repository at this point in the history
…marcin/10325/reward-page
  • Loading branch information
masvelio committed Dec 20, 2024
2 parents fdb6b22 + 6da9d1d commit 54849c9
Show file tree
Hide file tree
Showing 59 changed files with 365 additions and 770 deletions.
41 changes: 37 additions & 4 deletions libs/model/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const {
ALCHEMY_PUBLIC_APP_KEY,
MEMBERSHIP_REFRESH_BATCH_SIZE,
MEMBERSHIP_REFRESH_TTL_SECONDS,
NEYNAR_BOT_UUID,
NEYNAR_API_KEY,
NEYNAR_CAST_CREATED_WEBHOOK_SECRET,
NEYNAR_REPLY_WEBHOOK_URL,
Expand Down Expand Up @@ -91,6 +92,7 @@ export const config = configure(
: 5,
FLAG_FARCASTER_CONTEST: FLAG_FARCASTER_CONTEST === 'true',
NEYNAR_API_KEY: NEYNAR_API_KEY,
NEYNAR_BOT_UUID: NEYNAR_BOT_UUID,
NEYNAR_CAST_CREATED_WEBHOOK_SECRET: NEYNAR_CAST_CREATED_WEBHOOK_SECRET,
NEYNAR_REPLY_WEBHOOK_URL: NEYNAR_REPLY_WEBHOOK_URL,
FARCASTER_ACTION_URL: FARCASTER_ACTION_URL,
Expand Down Expand Up @@ -195,10 +197,41 @@ export const config = configure(
MIN_USER_ETH: z.number(),
MAX_USER_POSTS_PER_CONTEST: z.number().int(),
FLAG_FARCASTER_CONTEST: z.boolean().nullish(),
NEYNAR_API_KEY: z.string().nullish(),
NEYNAR_CAST_CREATED_WEBHOOK_SECRET: z.string().nullish(),
NEYNAR_REPLY_WEBHOOK_URL: z.string().nullish(),
FARCASTER_ACTION_URL: z.string().nullish(),
NEYNAR_BOT_UUID: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'NEYNAR_BOT_UUID must be set to a non-default value in production.',
),
NEYNAR_API_KEY: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'NEYNAR_API_KEY must be set to a non-default value in production.',
),
NEYNAR_CAST_CREATED_WEBHOOK_SECRET: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'NEYNAR_CAST_CREATED_WEBHOOK_SECRET must be set to a non-default value in production.',
),
NEYNAR_REPLY_WEBHOOK_URL: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'NEYNAR_REPLY_WEBHOOK_URL must be set to a non-default value in production.',
),
FARCASTER_ACTION_URL: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'FARCASTER_ACTION_URL must be set to a non-default value in production.',
),
}),
AUTH: z
.object({
Expand Down
28 changes: 27 additions & 1 deletion libs/model/src/contest/Contests.projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
EvmEventSignatures,
commonProtocol as cp,
} from '@hicommonwealth/evm-protocols';
import { config } from '@hicommonwealth/model';
import { ContestScore, events } from '@hicommonwealth/schemas';
import { buildContestLeaderboardUrl, getBaseUrl } from '@hicommonwealth/shared';
import { QueryTypes } from 'sequelize';
import { z } from 'zod';
import { models } from '../database';
Expand All @@ -17,6 +19,8 @@ import {
decodeThreadContentUrl,
getChainNodeUrl,
getDefaultContestImage,
parseFarcasterContentUrl,
publishCast,
} from '../utils';

const log = logger(import.meta);
Expand Down Expand Up @@ -295,7 +299,9 @@ export function Contests(): Projection<typeof inputs> {
},

ContestContentAdded: async ({ payload }) => {
const { threadId } = decodeThreadContentUrl(payload.content_url);
const { threadId, isFarcaster } = decodeThreadContentUrl(
payload.content_url,
);
await models.ContestAction.create({
...payload,
contest_id: payload.contest_id || 0,
Expand All @@ -306,6 +312,26 @@ export function Contests(): Projection<typeof inputs> {
voting_power: '0',
created_at: new Date(),
});

// post confirmation via FC bot
if (isFarcaster) {
const contestManager = await models.ContestManager.findByPk(
payload.contest_address,
);
const leaderboardUrl = buildContestLeaderboardUrl(
getBaseUrl(config.APP_ENV),
contestManager!.community_id,
contestManager!.contest_address,
);
const { replyCastHash } = parseFarcasterContentUrl(
payload.content_url,
);
await publishCast(
replyCastHash,
({ username }) =>
`Hey @${username}, your entry has been submitted to the contest: ${leaderboardUrl}`,
);
}
},

ContestContentUpvoted: async ({ payload }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NeynarAPIClient } from '@neynar/nodejs-sdk';
import { config } from '../config';
import { models } from '../database';
import { mustExist } from '../middleware/guards';
import { emitEvent } from '../utils';
import { emitEvent, publishCast } from '../utils';

const log = logger(import.meta);

Expand All @@ -21,13 +21,19 @@ export function FarcasterReplyCastCreatedWebhook(): Command<
mustExist('Farcaster Cast Author FID', payload.data.author?.fid);

const client = new NeynarAPIClient(config.CONTESTS.NEYNAR_API_KEY!);

// get user verified address
const { users } = await client.fetchBulkUsers([payload.data.author.fid]);
const verified_address = users[0].verified_addresses.eth_addresses.at(0);
if (!verified_address) {
log.warn(
'Farcaster verified address not found for reply cast created event- content will be ignored.',
);
await publishCast(
payload.data.hash,
({ username }) =>
`Hey @${username}, you need a verified address to participate in the contest.`,
);
return;
}

Expand Down
3 changes: 1 addition & 2 deletions libs/model/src/services/openai/generateTokenIdea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,8 @@ const generateTokenIdea = async function* ({

// generate image url and send the generated url to the client (to save time on s3 upload)
const imageResponse = await openai.images.generate({
model: 'dall-e-3',
prompt: TOKEN_AI_PROMPTS_CONFIG.image(tokenIdea.name, tokenIdea.symbol),
size: '512x512',
size: '256x256',
n: 1,
response_format: 'url',
});
Expand Down
9 changes: 6 additions & 3 deletions libs/model/src/user/GetUserProfile.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ import * as schemas from '@hicommonwealth/schemas';
import { Op } from 'sequelize';
import { z } from 'zod';
import { models } from '../database';
import { mustExist } from '../middleware/guards';

export function GetUserProfile(): Query<typeof schemas.GetUserProfile> {
return {
...schemas.GetUserProfile,
auth: [],
secure: true,
secure: false,
body: async ({ actor, payload }) => {
const user_id = payload.userId ?? actor.user.id;
const user_id = payload.userId ?? actor.user?.id;

const user = await models.User.findOne({
where: { id: user_id },
attributes: ['profile', 'xp_points', 'referral_link'],
});

mustExist('User', user);

const addresses = await models.Address.findAll({
where: { user_id },
include: [
Expand Down Expand Up @@ -98,7 +101,7 @@ export function GetUserProfile(): Query<typeof schemas.GetUserProfile> {
commentThreads: commentThreads.map(
(c) => c.toJSON() as z.infer<typeof schemas.ThreadView>,
),
isOwner: actor.user.id === user_id,
isOwner: actor.user?.id === user_id,
// ensure Tag is present in typed response
tags: profileTags.map((t) => ({ id: t.Tag!.id!, name: t.Tag!.name })),
xp_points: user!.xp_points ?? 0,
Expand Down
6 changes: 0 additions & 6 deletions libs/model/src/utils/buildFarcasterContentUrl.ts

This file was deleted.

14 changes: 14 additions & 0 deletions libs/model/src/utils/farcasterUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function buildFarcasterContentUrl(
parentCastHash: string,
replyCashHash: string,
) {
return `/farcaster/${parentCastHash}/${replyCashHash}`;
}

export function parseFarcasterContentUrl(url: string) {
const [, , parentCastHash, replyCastHash] = url.split('/');
return {
parentCastHash,
replyCastHash,
};
}
2 changes: 1 addition & 1 deletion libs/model/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export * from './buildFarcasterContentUrl';
export * from './buildFarcasterWebhookName';
export * from './decodeContent';
export * from './defaultAvatar';
export * from './denormalizedCountUtils';
export * from './farcasterUtils';
export * from './getDefaultContestImage';
export * from './getDelta';
export * from './makeGetBalancesOptions';
Expand Down
27 changes: 27 additions & 0 deletions libs/model/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
safeTruncateBody,
type AbiType,
} from '@hicommonwealth/shared';
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
import { createHash } from 'crypto';
import { hasher } from 'node-object-hash';
import {
Expand Down Expand Up @@ -79,11 +80,13 @@ export function buildThreadContentUrl(communityId: string, threadId: number) {
export function decodeThreadContentUrl(contentUrl: string): {
communityId: string | null;
threadId: number | null;
isFarcaster: boolean;
} {
if (contentUrl.startsWith('/farcaster/')) {
return {
communityId: null,
threadId: null,
isFarcaster: true,
};
}
if (!contentUrl.includes('/discussion/')) {
Expand All @@ -95,6 +98,7 @@ export function decodeThreadContentUrl(contentUrl: string): {
return {
communityId,
threadId: parseInt(threadId, 10),
isFarcaster: false,
};
}

Expand Down Expand Up @@ -256,3 +260,26 @@ export function getSaltedApiKeyHash(apiKey: string, salt: string): string {
export function buildApiKeySaltCacheKey(address: string) {
return `salt_${address.toLowerCase()}`;
}

export async function publishCast(
replyCastHash: string,
messageBuilder: ({ username }: { username: string }) => string,
) {
const client = new NeynarAPIClient(config.CONTESTS.NEYNAR_API_KEY!);
try {
const {
result: { casts },
} = await client.fetchBulkCasts([replyCastHash]);
const username = casts[0].author.username!;
await client.publishCast(
config.CONTESTS.NEYNAR_BOT_UUID!,
messageBuilder({ username }),
{
replyTo: replyCastHash,
},
);
log.info(`FC bot published reply to ${replyCastHash}`);
} catch (err) {
log.error(`Failed to post as FC bot`, err as Error);
}
}
28 changes: 28 additions & 0 deletions libs/shared/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,31 @@ export async function alchemyGetTokenPrices({
cause: { status: res.status, statusText: res.statusText },
});
}

export const getBaseUrl = (
env: 'local' | 'CI' | 'frick' | 'frack' | 'beta' | 'demo' | 'production',
) => {
switch (env) {
case 'local':
case 'CI':
return 'http://localhost:8080';
case 'beta':
return 'https://qa.commonwealth.im';
case 'demo':
return 'https://demo.commonwealth.im';
case 'frick':
return 'https://frick.commonwealth.im';
case 'frack':
return 'https://frack.commonwealth.im';
default:
return `https://${PRODUCTION_DOMAIN}`;
}
};

export const buildContestLeaderboardUrl = (
baseUrl: string,
communityId: string,
contestAddress: string,
) => {
return `${baseUrl}/${communityId}/contests/${contestAddress}`;
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ export const useHandleInviteLink = ({
const activeChainId = app.activeChainId();

const generalInviteRoute = matchRoutes(
[{ path: '/dashboard/global' }],
[{ path: '/dashboard/global' }, { path: '/profile/id/*' }],
location,
);

const communityInviteRoute =
matchRoutes(
[{ path: '/:scope' }, { path: '/:scope/discussions/*' }],
[
{ path: '/:scope' },
{ path: '/:scope/discussions/*' },
{ path: '/:scope/discussion/*' },
],
location,
) && isInsideCommunity;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { useFlag } from 'hooks/useFlag';
import { trpc } from 'utils/trpcClient';
import useUserStore from '../../ui/user';

export const useGetReferralLinkQuery = () => {
return trpc.user.getReferralLink.useQuery({});
const user = useUserStore();
const referralsEnabled = useFlag('referrals');

return trpc.user.getReferralLink.useQuery(
{},
{
enabled: user?.isLoggedIn && referralsEnabled,
staleTime: Infinity,
cacheTime: Infinity,
},
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ETHEREUM_MAINNET_ID,
OSMOSIS_ID,
POLYGON_ETH_CHAIN_ID,
SKALE_ID,
alphabeticallyStakeWiseSortedChains as sortedChains,
} from './constants';
import {
Expand Down Expand Up @@ -116,6 +117,8 @@ const CommunityInformationForm = ({
return options?.find((o) => o.value === OSMOSIS_ID);
case CommunityType.Blast:
return options?.find((o) => o.value === BLAST_ID);
case CommunityType.Skale:
return options?.find((o) => o.value === SKALE_ID);
case CommunityType.Polygon:
case CommunityType.Solana:
return options?.[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const ETHEREUM_MAINNET_ID = '1';
export const BASE_ID = '8453';
export const OSMOSIS_ID = 'osmosis';
export const BLAST_ID = '81457';
export const SKALE_ID = '974399131';

const removeTestCosmosNodes = (nodeInfo: NodeInfo): boolean => {
return !(
Expand Down
Loading

0 comments on commit 54849c9

Please sign in to comment.