@pump-fun/shared-contracts
Version:
Shared contracts for Pump.fun microservices.
275 lines (239 loc) • 10.5 kB
text/typescript
import { z } from "zod";
// Define notification channels
export const NotificationChannelSchema = z.enum(["push", "email"]);
export type NotificationChannel = z.infer<typeof NotificationChannelSchema>;
// Define notification preferences with channel support
export const NotificationPreferenceSchema = z.object({
channels: z
.object({
email: z.boolean().default(false),
push: z.boolean().default(false),
})
.default({ email: false, push: false }),
enabled: z.boolean(),
});
export type NotificationPreference = z.infer<typeof NotificationPreferenceSchema>;
// Define category-level preferences (allows toggling entire categories)
export const CategoryPreferenceSchema = z.object({
enabled: z.boolean().default(true),
});
export const UserSettingsSchema = z.object({
// Legacy boolean fields (deprecated - kept for backwards compatibility)
allowChatRequestNotifications: z.boolean().optional(),
allowCoinWatcherNotifications: z.boolean().optional(),
allowDmNotifications: z.boolean().optional(),
allowFollowerNotifications: z.boolean().optional(),
allowGroupNotifications: z.boolean().optional(),
allowHolderMilestoneNotifications: z.boolean().optional(),
allowLargePurchaseNotifications: z.boolean().optional(),
allowLikeNotifications: z.boolean().optional(),
allowLivestreamNotifications: z.boolean().optional(),
allowMentionNotifications: z.boolean().optional(),
allowNewCoinNotifications: z.boolean().optional(),
allowPriceMovementNotifications: z.boolean().optional(),
allowThresholdNotifications: z.boolean().optional(),
allowWatchlistMovementNotifications: z.boolean().optional(),
createdAt: z.number().optional(),
// Email preferences
emailAddress: z.string().email().optional(),
emailVerified: z.boolean().optional(),
// New structure supporting multiple channels per notification type, organized by category
notificationPreferences: z
.object({
// Category-level toggles
categories: z
.object({
content: CategoryPreferenceSchema.optional(),
creator: CategoryPreferenceSchema.optional(),
livestream: CategoryPreferenceSchema.optional(),
milestones: CategoryPreferenceSchema.optional(),
portfolio: CategoryPreferenceSchema.optional(),
social: CategoryPreferenceSchema.optional(),
trending: CategoryPreferenceSchema.optional(),
watchlist: CategoryPreferenceSchema.optional(),
})
.optional(),
// Content notifications
content: z
.object({
article: NotificationPreferenceSchema.optional(), // new news article
trending: NotificationPreferenceSchema.optional(), // [coin] is now trending
})
.optional(),
creator: z.object({
revenueShareReceived: NotificationPreferenceSchema.optional(), // you got money
}),
milestones: z
.object({
marketCapMilestone: NotificationPreferenceSchema.optional(), // hit $500M, $1B, etc
})
.optional(),
// Portfolio Notifications - Coins you hold
portfolio: z
.object({
coinGraduated: NotificationPreferenceSchema.optional(), // coin you hold/watch graduated
devActivity: NotificationPreferenceSchema.optional(), // dev buys / sells
livestreamStarted: NotificationPreferenceSchema.optional(), // coin you hold started streaming
priceMovement: NotificationPreferenceSchema.optional(), // hit market cap/price threshold
sinceAped: NotificationPreferenceSchema.optional(), // coin graduated to top 100
})
.optional(),
// Social interactions
social: z
.object({
chatRequest: NotificationPreferenceSchema.optional(),
directMessage: NotificationPreferenceSchema.optional(),
follower: NotificationPreferenceSchema.optional(),
group: NotificationPreferenceSchema.optional(), // creator you hold/watch started streaming
like: NotificationPreferenceSchema.optional(),
livestreamStarted: NotificationPreferenceSchema.optional(),
mention: NotificationPreferenceSchema.optional(),
referralSignup: NotificationPreferenceSchema.optional(),
})
.optional(),
// Now Trending
trending: z
.object({
nowTrending: NotificationPreferenceSchema.optional(), // [coin] is now trending
upSinceListing: NotificationPreferenceSchema.optional(), // [coin] is up X% since listing
})
.optional(),
// Watchlist Alerts
watchlist: z
.object({
allTimeHigh: NotificationPreferenceSchema.optional(), // moved by X% within intervals
coinGraduated: NotificationPreferenceSchema.optional(), // hit market cap/price threshold
devActivity: NotificationPreferenceSchema.optional(), // moved X% since added to watchlist
holderMilestone: NotificationPreferenceSchema.optional(), // creator you hold/watch started streaming
largePurchase: NotificationPreferenceSchema.optional(), // someone bought $X worth
livestreamStarted: NotificationPreferenceSchema.optional(), // new ATH
priceMovement: NotificationPreferenceSchema.optional(), // hit X holders
sinceAdded: NotificationPreferenceSchema.optional(), // dev buy/sells
threshold: NotificationPreferenceSchema.optional(), // coin graduated to top 100
})
.optional(),
})
.optional(),
PK: z.string().startsWith("WALLET#"),
// Configuration for price movement intervals (user can select which intervals they want)
priceMovementIntervals: z.array(z.enum(["10m", "1h", "6h", "24h"])).optional(),
SK: z.literal("SETTINGS"),
updatedAt: z.number().optional(), // Unix timestamp in seconds or milliseconds
walletAddress: z.string(), // Unix timestamp in seconds or milliseconds
});
export type UserSettingsItem = z.infer<typeof UserSettingsSchema>;
export const NotificationTypes = {
AllTimeHigh: "allowAllTimeHighNotifications",
Article: "allowArticleNotifications",
ChatRequest: "allowChatRequestNotifications",
CoinWatcher: "allowCoinWatcherNotifications",
DirectMessage: "allowDmNotifications",
Follower: "allowFollowerNotifications",
Group: "allowGroupNotifications",
HolderMilestone: "allowHolderMilestoneNotifications",
LargePurchase: "allowLargePurchaseNotifications",
Like: "allowLikeNotifications",
Livestream: "allowLivestreamNotifications",
Mention: "allowMentionNotifications",
NewCoin: "allowNewCoinNotifications",
PriceMovement: "allowPriceMovementNotifications",
Threshold: "allowThresholdNotifications",
WatchlistMovement: "allowWatchlistMovementNotifications",
} as const;
// Notification type keys for the new structure
export const NotificationTypeKeys = {
AllTimeHigh: "allTimeHigh",
Article: "article",
ChatRequest: "chatRequest",
CoinWatcher: "coinWatcher",
DirectMessage: "directMessage",
Follower: "follower",
Group: "group",
HolderMilestone: "holderMilestone",
LargePurchase: "largePurchase",
Like: "like",
Livestream: "livestream",
Mention: "mention",
NewCoin: "newCoin",
PriceMovement: "priceMovement",
Threshold: "threshold",
WatchlistMovement: "watchlistMovement",
} as const;
export type NotificationTypeKey = (typeof NotificationTypeKeys)[keyof typeof NotificationTypeKeys];
// Helper type for notification preferences object
export type UserNotificationPreferences = z.infer<typeof UserSettingsSchema>["notificationPreferences"];
// Type for categories that contain notifications
export type NotificationCategory = Exclude<keyof NonNullable<UserNotificationPreferences>, "categories">;
// Helper function to check if a notification is enabled for a specific channel
export function isNotificationEnabledForChannel(
preferences: UserNotificationPreferences,
category: NotificationCategory,
notificationType: string,
channel: NotificationChannel,
): boolean {
if (!preferences) return false;
// Check if category is enabled
if (preferences.categories?.[category]?.enabled === false) {
return false;
}
const categoryPreferences = preferences[category];
if (!categoryPreferences || typeof categoryPreferences !== "object") {
return false;
}
const pref = categoryPreferences[notificationType as keyof typeof categoryPreferences];
// Type guard to check if pref is a NotificationPreference
if (!pref || typeof pref !== "object" || !("enabled" in pref) || !("channels" in pref)) {
return false;
}
// Now TypeScript knows pref has the shape we need
const typedPref = pref as NotificationPreference;
return typedPref.enabled === true && typedPref.channels[channel] === true;
}
// Helper function to check if a category is enabled
export function isCategoryEnabled(
preferences: UserNotificationPreferences,
category: keyof NonNullable<NonNullable<UserNotificationPreferences>["categories"]>,
): boolean {
return preferences?.categories?.[category]?.enabled !== false;
}
// Helper to get all enabled notifications for a user
export function getEnabledNotifications(
preferences: UserNotificationPreferences,
channel: NotificationChannel,
): Array<{ category: string; type: string }> {
const enabled: Array<{ category: string; type: string }> = [];
if (!preferences) return enabled;
const categories: NotificationCategory[] = [
"creator",
"trending",
"watchlist",
"portfolio",
"milestones",
"social",
"content",
];
for (const category of categories) {
// Check if this category has a category-level toggle
const hasCategories = preferences.categories && category in preferences.categories;
if (
hasCategories &&
!isCategoryEnabled(
preferences,
category as keyof NonNullable<NonNullable<UserNotificationPreferences>["categories"]>,
)
) {
continue;
}
const categoryPreferences = preferences[category];
if (!categoryPreferences || typeof categoryPreferences !== "object") continue;
for (const [type, pref] of Object.entries(categoryPreferences)) {
if (pref && typeof pref === "object" && "enabled" in pref && "channels" in pref) {
const typedPref = pref as NotificationPreference;
if (typedPref.enabled && typedPref.channels[channel] === true) {
enabled.push({ category, type });
}
}
}
}
return enabled;
}