UNPKG

@galliun/sofi-sdk

Version:

SDK for interacting with the Galliun SocialFi protocol

658 lines (606 loc) 16.7 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ import { bcs } from '@mysten/sui/bcs'; import type { SuiClient, SuiObjectResponse } from '@mysten/sui/client'; import { Transaction } from '@mysten/sui/transactions'; import { GoalModule, ProfileModule, TipModule } from './SoFiFunctions'; import { devInspectAndGetReturnValues, normalizeCoinType, objResToFields, objResToType, } from './util.js'; /** * A supported coin type with tick size */ export interface SupportedCoin { coin_type: string; tick_size: bigint; } /** * A point per coin amount */ export interface PointPerCoinAmount { coin_type: string; points_per_amount: bigint; active: boolean; } /** * A premium price object */ export interface PremiumPrice { coin_type: string; price: bigint; } /** * A goal::goal::Goal object representing a funding goal */ export interface Goal { id: string; profile: string; creator: string; amount: bigint; description: string; over_fund: boolean; title: string; media: string[]; expires_at: bigint | null; coin_type: string; payment_records_id: string; payment_records: PaymentRecord[] | null; paid_amount: bigint; creation_time: number; status: number; collaborators: string[] | null; pending_collaborators: string[] | null; // Status flags is_inprogress: boolean; is_completed: boolean; is_canceled: boolean; is_expired: boolean; // Balance tracking balance_bag_id: string; over_funded: boolean; balance?: bigint; } /** * A contribution record for a goal */ export interface PaymentRecord { timestamp: number; contributor: string; amount: bigint; } // Status constants matching contract const GOAL_STATUS = { INPROGRESS: 0, COMPLETED: 1, CANCELED: 2, EXPIRED: 3, } as const; /** * Checks if an object response is a goal * @param resp Object response from the blockchain * @returns True if the object is a goal */ export function isGoalType(resp: SuiObjectResponse): boolean { try { const type = objResToType(resp); return type.includes('::goal::Goal'); } catch (_err) { return false; } } /** * Parses a goal object from a response * @param resp Object response from the blockchain * @returns Parsed goal object or null if invalid */ export function parseGoalObj(resp: SuiObjectResponse): Goal | null { if (!isGoalType(resp)) { return null; } let fields: Record<string, any>; try { fields = objResToFields(resp); } catch (_err) { return null; } const status = Number(fields.status); return { id: fields.id.id, profile: fields.profile, creator: fields.creator, title: fields.title, media: fields.media, amount: BigInt(fields.amount), description: fields.description, over_fund: fields.over_fund, expires_at: fields.expires_at ? BigInt(fields.expires_at) : null, coin_type: normalizeCoinType(fields.coin_type), payment_records_id: fields.payment_records.fields.contents.fields.id.id, payment_records: null, paid_amount: BigInt(fields.paid_amount), creation_time: Number(fields.creation_time), status, collaborators: fields.collaborators.fields.contents, pending_collaborators: fields.pending_collaborators.fields.contents, is_inprogress: status === GOAL_STATUS.INPROGRESS, is_completed: status === GOAL_STATUS.COMPLETED, is_canceled: status === GOAL_STATUS.CANCELED, is_expired: status === GOAL_STATUS.EXPIRED, balance_bag_id: fields.balance_bag.fields.id.id, over_funded: fields.over_funded, balance: BigInt(fields.balance || 0), }; } /** * Parses accepted coin types from object responses * @param collaborators Array of collaborator object responses * @returns Array of normalized collaborator type strings */ export function parseCollaborators(collaborators: any[]): string[] { return collaborators .map((collaborator) => { if (collaborator.data?.content) { const fields: any = objResToFields(collaborator); return fields.name; } return null; }) .filter((collaborator): collaborator is string => collaborator !== null); } /** * Parses pending collaborators from object responses * @param pending_collaborators Array of pending collaborator object responses * @returns Array of normalized pending collaborator type strings */ export function parsePendingCollaborators(pending_collaborators: any[]): string[] { return pending_collaborators .map((pending_collaborator) => { if (pending_collaborator.data?.content) { const fields: any = objResToFields(pending_collaborator); return fields.name; } return null; }) .filter((pending_collaborator): pending_collaborator is string => pending_collaborator !== null); } /** * Parses a contribution record from a response * @param resp Object response from the blockchain * @returns Parsed contribution record */ export function parsePaymentRecord(resp: SuiObjectResponse): PaymentRecord { const fields: any = objResToFields(resp); const values = fields.value.fields; return { timestamp: Number(values.timestamp), contributor: values.contributor, amount: BigInt(values.amount), }; } /** * Type guard for Goal */ export function isGoal(obj: unknown): obj is Goal { return typeof obj === 'object' && obj !== null && 'id' in obj && 'profile' in obj; } /** * Gets goal balance using on-chain view function * @param suiClient SUI client instance * @param packageId Package ID of the protocol contract * @param goal Goal object to get balance for * @param sender Optional sender address * @returns Goal balance amount * @throws Error if balance fetch fails */ export async function parseGoalBalance( suiClient: SuiClient, packageId: string, goal: Goal, sender?: string, ): Promise<bigint> { const tx = new Transaction(); GoalModule.balance_bag_amount(tx, packageId, goal.coin_type, goal.id); const results = await devInspectAndGetReturnValues(suiClient, tx, [[bcs.U64]], sender); if (results.length > 0) { return BigInt(results[0][0]); } throw new Error('Failed to get goal balance'); } /** * Parses contribution records from responses * @param suiClient SUI client instance * @param goal Goal object to parse records for * @param recordIds Array of record IDs to fetch * @returns Array of parsed contribution records */ export async function parsePaymentRecords( suiClient: SuiClient, goal: Goal, recordIds: string[], ): Promise<PaymentRecord[]> { const records = await suiClient.multiGetObjects({ ids: recordIds, options: { showContent: true }, }); return records.map(parsePaymentRecord); } // Configuration Types /** * Global configuration object */ export interface GlobalConfig { id: string; package_version: number; supported_coins_id: string; supported_coins: SupportedCoin[] | null; point_per_coin_amounts_id: string; point_per_coin_amounts: PointPerCoinAmount[] | null; premium_price: PremiumPrice; tip_fee_bps: number; goal_fee_bps: number; treasury_addr: string; reserved_words_id: string; reserved_words: string[] | null; } /** * Admin capability object */ export interface AdminCap { id: string; } /** * Balance bag object for tracking coin balances */ export interface BalanceBag { id: string; type: string; balance: bigint; } /** * Parses a global config object from a response * @param resp Object response from the blockchain * @returns Parsed config object or null if invalid */ export function parseGlobalConfig(resp: SuiObjectResponse): GlobalConfig | null { try { const fields: any = objResToFields(resp); return { id: fields.id.id, package_version: Number(fields.package_version), supported_coins_id: fields.supported_coins.fields.id.id, supported_coins: null, point_per_coin_amounts_id: fields.point_per_coin_amounts.fields.id.id, point_per_coin_amounts: null, premium_price: { coin_type: normalizeCoinType(fields.premium_price.fields.coin_type), price: BigInt(fields.premium_price.fields.price), }, reserved_words_id: fields.reserved_words.fields.id.id, reserved_words: null, tip_fee_bps: Number(fields.tip_fee_bps), goal_fee_bps: Number(fields.goal_fee_bps), treasury_addr: fields.treasury_addr, }; } catch (_err) { return null; } } /** * Parses supported coins from object responses * @param coins Array of coin object responses * @returns Array of normalized coin type strings */ export function parseSupportedCoins(coins: any[]): SupportedCoin[] { return coins .map((coin) => { if (coin.data?.content) { const fields: any = objResToFields(coin); return { coin_type: normalizeCoinType(fields.value.fields.coin_type), tick_size: BigInt(fields.value.fields.tick_size), }; } return null; }) .filter((coin): coin is SupportedCoin => coin !== null); } /** * Parses point per coin amounts from object responses * @param coins Array of coin object responses * @returns Array of normalized coin type strings */ export function parsePointPerCoinAmounts(coins: any[]): PointPerCoinAmount[] { return coins .map((coin) => { if (coin.data?.content) { const fields: any = objResToFields(coin); return { coin_type: normalizeCoinType(fields.value.fields.coin_type), points_per_amount: BigInt(fields.value.fields.points_per_amount), active: fields.value.fields.active, }; } return null; }) .filter((coin): coin is PointPerCoinAmount => coin !== null); } /** * Parses reserved words from object responses * @param words Array of coin object responses * @returns Array of normalized coin type strings */ export function parseReservedWords(words: any[]): string[] { return words .map((word) => { if (word.data?.content) { const fields: any = objResToFields(word); return fields.name; } return null; }) .filter((word): word is string => word !== null); } /** * Parses an admin cap object from a response * @param resp Object response from the blockchain * @returns Parsed admin cap object or null if invalid */ export function parseAdminCap(resp: SuiObjectResponse): AdminCap | null { try { const fields: any = objResToFields(resp); return { id: fields.id.id, }; } catch (_err) { return null; } } /** * Parses a balance bag object from fields * @param fields Balance bag fields from response * @returns Parsed balance bag object */ export function parseBalanceBag(fields: any): BalanceBag { return { id: fields.id.id, type: fields.type, balance: BigInt(fields.balance || 0), }; } /** * A point per coin amount */ export interface AcceptedCoinTypes { coin_type: string; accepted: boolean; } /** * A profile::profile::Profile object representing a user profile */ /** * Profile data structure */ export interface Profile { id: string; owner: string; username: string; display_name: string; bio: string; profile_picture: string; background_picture: string; points: number; points_id: string; links_id: string; links: Link[] | []; accepted_coin_types_id: string; accepted_coin_types: AcceptedCoinTypes[]; favourites_id: string; favourites: string[]; accept_tips: boolean; premium: number; created_at: number; updated_at: number; } /** * A tip::tip::Tip object representing a tip to a profile */ export interface Tip { id: string; sender: string; profile: string; amount: bigint; message: string | null; creation_time: number; coin_type: string; } export interface Link { name: string; url: string; media: string; position: number; } /** * Checks if an object response is a profile * @param resp Object response from the blockchain * @returns True if the object is a profile */ export function isProfileType(resp: SuiObjectResponse): boolean { try { const type = objResToType(resp); return type.includes('::profile::Profile'); } catch (_err) { return false; } } /** * Checks if an object response is a tip * @param resp Object response from the blockchain * @returns True if the object is a tip */ export function isTipType(resp: SuiObjectResponse): boolean { try { const type = objResToType(resp); return type.includes('::tip::Tip'); } catch (_err) { return false; } } /** * Parses a profile response into a ProfileData object * @param resp The response from the blockchain * @returns Parsed profile object or null */ export function parseProfileObj(resp: SuiObjectResponse): Profile | null { try { const data = resp.data?.content?.dataType === 'moveObject' ? resp.data?.content?.fields as any : null; if (!data) return null; return { id: resp.data?.objectId || '', owner: data.owner || '', username: data.username || '', display_name: data.display_name || '', bio: data.bio || '', profile_picture: data.profile_picture || '', background_picture: data.background_picture || '', accepted_coin_types: data.accepted_coin_types || [], accepted_coin_types_id: data.accepted_coin_types.fields.id.id, links: [], links_id: data.links.fields.contents.fields.id.id, points: data.points || 0, points_id: data.points_id || '', favourites_id: data.favourites.fields.contents.fields.id.id, favourites: data.favourites || [], accept_tips: data.accept_tips || false, premium: Number(data.premium || 0), created_at: Number(data.created_at || 0), updated_at: Number(data.updated_at || 0), }; } catch (error) { console.error('Error parsing profile response:', error); return null; } } /** * Parses links from object responses * @param links Array of coin object responses * @returns Array of normalized coin type strings */ export function parseLinks(links: any[]): Link[] { return links .map((link) => { if (link.data?.content) { const fields: any = objResToFields(link); return { name: fields.value.fields.name, url: fields.value.fields.url, media: fields.value.fields.media, position: fields.value.fields.position, }; } return null; }) .filter((link): link is Link => link !== null); } /** * Parses accepted coin types from object responses * @param coins Array of coin object responses * @returns Array of normalized coin type strings */ export function parseAcceptedCoinTypes(coins: any[]): AcceptedCoinTypes[] { return coins .map((coin) => { if (coin.data?.content) { const fields: any = objResToFields(coin); return { coin_type: normalizeCoinType(fields.name), accepted: fields.value, }; } return null; }) .filter((coin): coin is AcceptedCoinTypes => coin !== null); } /** * Parses favourites from object responses * @param favourites Array of coin object responses * @returns Array of normalized coin type strings */ export function parseFavourites(favourites: any[]): string[] { return favourites .map((favourite) => { if (favourite.data?.content) { const fields: any = objResToFields(favourite); return fields.value; } return null; }) .filter((favourite): favourite is string => favourite !== null); } /** * Parses following from object responses * @param following Array of coin object responses * @returns Array of normalized coin type strings */ export function parseFollowing(following: any[]): string[] { return following .map((following) => { if (following.data?.content) { const fields: any = objResToFields(following); return fields.name; } return null; }) .filter((following): following is string => following !== null); } /** * Parses points from object responses * @param points Array of coin object responses * @returns Array of normalized coin type strings */ export function parsePoints(points: SuiObjectResponse): number { if (points && points?.data?.content) { const fields: any = objResToFields(points); return Number(fields.points); } return 0; } /** * Parses a tip object from a response * @param resp Object response from the blockchain * @returns Parsed tip object or null if invalid */ export function parseTipObj(resp: SuiObjectResponse): Tip | null { if (!isTipType(resp)) { return null; } let fields: Record<string, any>; try { fields = objResToFields(resp); } catch (_err) { return null; } return { id: fields.id.id, sender: fields.sender, profile: fields.profile, amount: BigInt(fields.amount), message: fields.message || null, creation_time: Number(fields.created_at), coin_type: normalizeCoinType(fields.coin_type), }; } /** * Type guard for Profile */ export function isProfile(obj: unknown): obj is Profile { return typeof obj === 'object' && obj !== null && 'id' in obj && 'owner' in obj; } /** * Type guard for Tip */ export function isTip(obj: unknown): obj is Tip { return typeof obj === 'object' && obj !== null && 'id' in obj && 'from' in obj; }