Skip to content

Commit

Permalink
Merge pull request #10164 from hicommonwealth/malik.9898.pin-token-in…
Browse files Browse the repository at this point in the history
…-community

Added external token integration functionality in community integrations
  • Loading branch information
mzparacha authored Dec 16, 2024
2 parents 29d0ac7 + 369bf57 commit d3fdc6c
Show file tree
Hide file tree
Showing 71 changed files with 1,873 additions and 59 deletions.
1 change: 0 additions & 1 deletion libs/model/src/community/CreateTopic.command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { InvalidInput, InvalidState, type Command } from '@hicommonwealth/core';

import * as schemas from '@hicommonwealth/schemas';
import { models } from '../database';
import { authRoles } from '../middleware';
Expand Down
4 changes: 2 additions & 2 deletions libs/model/src/community/GetCommunities.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ export function GetCommunities(): Query<typeof schemas.GetCommunities> {
? `
AND (
SELECT "community_id"
FROM "Tokens" AS "Tokens"
WHERE ( "Tokens"."community_id" = "Community"."id" )
FROM "LaunchpadTokens" AS "LaunchpadTokens"
WHERE ( "LaunchpadTokens"."community_id" = "Community"."id" )
LIMIT 1
) IS ${community_type === CommunityType.Launchpad ? 'NOT' : ''} NULL
`
Expand Down
35 changes: 35 additions & 0 deletions libs/model/src/community/GetPinnedTokens.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type Query } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { Includeable } from 'sequelize';
import { models } from '../database';

export function GetPinnedTokens(): Query<typeof schemas.GetPinnedTokens> {
return {
...schemas.GetPinnedTokens,
auth: [],
secure: false,
body: async ({ payload }) => {
const { community_ids, with_chain_node } = payload;
if (community_ids.length === 0) return [];
const parsedIds = community_ids.split(',').filter((v) => v !== '');
if (parsedIds.length === 0) return [];

const include: Includeable[] = [];
if (with_chain_node) {
include.push({
model: models.ChainNode,
required: true,
});
}

return (
await models.PinnedToken.findAll({
where: {
community_id: parsedIds,
},
include,
})
).map((t) => t.get({ plain: true }));
},
};
}
2 changes: 1 addition & 1 deletion libs/model/src/community/GetTransactions.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function GetTransactions(): Query<typeof schemas.GetTransactions> {
'chain_node_name', cn.name
) AS community
FROM "LaunchpadTrades" lts
LEFT JOIN "Tokens" AS tkns ON tkns.token_address = lts.token_address
LEFT JOIN "LaunchpadTokens" AS tkns ON tkns.token_address = lts.token_address
LEFT JOIN "Communities" AS c ON c.namespace = tkns.namespace
LEFT JOIN "ChainNodes" AS cn ON cn.id = c.chain_node_id
${addressesList.length > 0 ? 'WHERE lts.trader_address IN (:addresses)' : ''}
Expand Down
110 changes: 110 additions & 0 deletions libs/model/src/community/PinToken.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { InvalidState, logger, type Command } from '@hicommonwealth/core';
import { commonProtocol as cp } from '@hicommonwealth/evm-protocols';
import { config } from '@hicommonwealth/model';
import * as schemas from '@hicommonwealth/schemas';
import { alchemyGetTokenPrices } from '@hicommonwealth/shared';
import { models } from '../database';
import { authRoles } from '../middleware';
import { mustExist } from '../middleware/guards';

const log = logger(import.meta);

export const PinTokenErrors = {
NotSupported: 'Pinned tokens only supported on Alchemy supported chains',
FailedToFetchPrice: 'Failed to fetch token price',
OnlyBaseSupport: 'Only Base (ETH) chain supported',
LaunchpadTokenFound: (communityId: string) =>
`Community ${communityId} has an attached launchpad token`,
};

export function PinToken(): Command<typeof schemas.PinToken> {
return {
...schemas.PinToken,
auth: [authRoles('admin')],
body: async ({ payload }) => {
const { community_id, contract_address, chain_node_id } = payload;

const chainNode = await models.ChainNode.scope('withPrivateData').findOne(
{
where: {
id: chain_node_id,
},
},
);
mustExist('ChainNode', chainNode);

if (chainNode.eth_chain_id !== cp.ValidChains.Base)
throw new InvalidState(PinTokenErrors.OnlyBaseSupport);

const community = await models.Community.findOne({
where: {
id: community_id,
},
});
mustExist('Community', community);

if (community.namespace) {
const launchpadToken = await models.LaunchpadToken.findOne({
where: {
namespace: community.namespace,
},
});

if (launchpadToken)
throw new InvalidState(
PinTokenErrors.LaunchpadTokenFound(community_id),
);
}

if (
!chainNode.url.includes('alchemy') ||
!chainNode.private_url?.includes('alchemy') ||
!chainNode.alchemy_metadata?.price_api_supported
) {
throw new InvalidState(PinTokenErrors.NotSupported);
}

let price: Awaited<ReturnType<typeof alchemyGetTokenPrices>> | undefined;
try {
price = await alchemyGetTokenPrices({
alchemyApiKey: config.ALCHEMY.APP_KEYS.PRIVATE,
tokenSources: [
{
contractAddress: contract_address,
alchemyNetworkId: chainNode.alchemy_metadata.network_id,
},
],
});
} catch (e: unknown) {
if (e instanceof Error)
log.error(e.message, e, {
contractAddress: contract_address,
alchemyNetworkId: chainNode.alchemy_metadata.network_id,
});
else {
log.error(JSON.stringify(e), undefined, {
contractAddress: contract_address,
alchemyNetworkId: chainNode.alchemy_metadata.network_id,
});
}
}

if (
!Array.isArray(price?.data) ||
price.data.length !== 1 ||
price.data[0].error
) {
log.error(PinTokenErrors.FailedToFetchPrice, undefined, {
price,
});
throw new InvalidState(PinTokenErrors.FailedToFetchPrice);
}

return await models.PinnedToken.create({
community_id,
chain_node_id,
contract_address,
});
},
};
}
28 changes: 28 additions & 0 deletions libs/model/src/community/UnpinToken.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { InvalidState, type Command } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { models } from '../database';
import { authRoles } from '../middleware';

export const UnpinTokenErrors = {
NotFound: 'Token not found',
};

export function UnpinToken(): Command<typeof schemas.UnpinToken> {
return {
...schemas.UnpinToken,
auth: [authRoles('admin')],
body: async ({ payload }) => {
const { community_id } = payload;
const pinnedToken = await models.PinnedToken.findOne({
where: {
community_id,
},
});

if (!pinnedToken) throw new InvalidState(UnpinTokenErrors.NotFound);

await pinnedToken.destroy();
return {};
},
};
}
3 changes: 3 additions & 0 deletions libs/model/src/community/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ export * from './GetCommunities.query';
export * from './GetCommunity.query';
export * from './GetCommunityStake.query';
export * from './GetMembers.query';
export * from './GetPinnedTokens.query';
export * from './GetStakeHistoricalPrice.query';
export * from './GetTopics.query';
export * from './GetTransactions.query';
export * from './JoinCommunity.command';
export * from './PinToken.command';
export * from './RefreshCommunityMemberships.command';
export * from './RefreshCustomDomain.query';
export * from './SetCommunityStake.command';
export * from './ToggleArchiveTopic.command';
export * from './UnpinToken.command';
export * from './UpdateCommunity.command';
export * from './UpdateCustomDomain.command';
export * from './UpdateGroup.command';
Expand Down
10 changes: 8 additions & 2 deletions libs/model/src/models/associations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export const buildAssociations = (db: DB) => {
.withMany(db.Topic, {
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
})
.withMany(db.PinnedToken, {
onDelete: 'CASCADE',
});

db.ContractAbi.withMany(db.EvmEventSource, { foreignKey: 'abi_id' });
Expand Down Expand Up @@ -113,7 +116,10 @@ export const buildAssociations = (db: DB) => {
as: 'selectedCommunity',
})
.withMany(db.Quest, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
.withMany(db.ContestManager, { onUpdate: 'CASCADE', onDelete: 'CASCADE' });
.withMany(db.ContestManager, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
.withMany(db.PinnedToken, {
onDelete: 'CASCADE',
});

db.Tags.withMany(db.ProfileTags, {
foreignKey: 'tag_id',
Expand Down Expand Up @@ -247,7 +253,7 @@ export const buildAssociations = (db: DB) => {
},
);

db.Token.withMany(db.LaunchpadTrade, {
db.LaunchpadToken.withMany(db.LaunchpadTrade, {
foreignKey: 'token_address',
});
};
1 change: 1 addition & 0 deletions libs/model/src/models/chain_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default (
block_explorer: { type: Sequelize.STRING, allowNull: true },
slip44: { type: Sequelize.INTEGER, allowNull: true },
max_ce_block_range: { type: Sequelize.INTEGER, allowNull: true },
alchemy_metadata: { type: Sequelize.JSONB, allowNull: true },
created_at: { type: Sequelize.DATE, allowNull: false },
updated_at: { type: Sequelize.DATE, allowNull: false },
},
Expand Down
6 changes: 4 additions & 2 deletions libs/model/src/models/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import LastProcessedEvmBlock from './lastProcessedEvmBlock';
import LaunchpadTrade from './launchpad_trade';
import Membership from './membership';
import Outbox from './outbox';
import PinnedToken from './pinned_token';
import Poll from './poll';
import ProfileTags from './profile_tags';
import { Quest, QuestAction, QuestActionMeta } from './quest';
Expand All @@ -38,7 +39,7 @@ import Tags from './tags';
import Thread from './thread';
import ThreadSubscription from './thread_subscriptions';
import ThreadVersionHistory from './thread_version_history';
import Token from './token';
import LaunchpadToken from './token';
import Topic from './topic';
import User from './user';
import Vote from './vote';
Expand Down Expand Up @@ -71,6 +72,7 @@ export const Factories = {
LaunchpadTrade,
Membership,
Outbox,
PinnedToken,
Poll,
ProfileTags,
Quest,
Expand All @@ -91,7 +93,7 @@ export const Factories = {
Vote,
Webhook,
Wallets,
Token,
LaunchpadToken,
XpLog,
};

Expand Down
7 changes: 1 addition & 6 deletions libs/model/src/models/outbox.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { EventContext, Outbox } from '@hicommonwealth/core';
import { Events } from '@hicommonwealth/schemas';
import { Outbox } from '@hicommonwealth/core';
import Sequelize from 'sequelize'; // must use "* as" to avoid scope errors
import { z } from 'zod';
import { ModelInstance } from './types';

export type OutboxAttributes = z.infer<typeof Outbox>;

export type InsertOutboxEvent = EventContext<Events> & {
created_at?: Date;
};

export type OutboxInstance = ModelInstance<OutboxAttributes>;

export default (
Expand Down
44 changes: 44 additions & 0 deletions libs/model/src/models/pinned_token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { PinnedToken } from '@hicommonwealth/schemas';
import Sequelize from 'sequelize';
import { z } from 'zod';
import type { ModelInstance } from './types';

export type PinnedTokenAttributes = z.infer<typeof PinnedToken>;

export type PinnedTokenInstance = ModelInstance<PinnedTokenAttributes>;

export default (
sequelize: Sequelize.Sequelize,
): Sequelize.ModelStatic<PinnedTokenInstance> =>
sequelize.define<PinnedTokenInstance>(
'PinnedToken',
{
community_id: {
type: Sequelize.STRING,
primaryKey: true,
},
contract_address: {
type: Sequelize.STRING,
allowNull: false,
},
chain_node_id: {
type: Sequelize.INTEGER,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
underscored: true,
tableName: 'PinnedTokens',
},
);
14 changes: 7 additions & 7 deletions libs/model/src/models/token.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { Token } from '@hicommonwealth/schemas';
import { LaunchpadToken } from '@hicommonwealth/schemas';
import Sequelize from 'sequelize'; // must use "* as" to avoid scope errors
import { z } from 'zod';
import type { ChainNodeAttributes, ChainNodeInstance } from './chain_node';
import type { ModelInstance } from './types';

export type TokenAttributes = z.infer<typeof Token> & {
export type LaunchpadTokenAttributes = z.infer<typeof LaunchpadToken> & {
// associations
ChainNode?: ChainNodeAttributes;
};

export type TokenInstance = ModelInstance<TokenAttributes> & {
export type LaunchpadTokenInstance = ModelInstance<LaunchpadTokenAttributes> & {
// add mixins as needed
getChainNode: Sequelize.BelongsToGetAssociationMixin<ChainNodeInstance>;
};

export default (
sequelize: Sequelize.Sequelize,
): Sequelize.ModelStatic<TokenInstance> =>
sequelize.define<TokenInstance>(
'Token',
): Sequelize.ModelStatic<LaunchpadTokenInstance> =>
sequelize.define<LaunchpadTokenInstance>(
'LaunchpadToken',
{
// derivable when event received
token_address: { type: Sequelize.STRING, primaryKey: true },
Expand All @@ -44,7 +44,7 @@ export default (
icon_url: { type: Sequelize.STRING, allowNull: true },
},
{
tableName: 'Tokens',
tableName: 'LaunchpadTokens',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
Expand Down
Loading

0 comments on commit d3fdc6c

Please sign in to comment.