@galliun/sofi-sdk
Version:
SDK for interacting with the Galliun SocialFi protocol
658 lines (606 loc) • 16.7 kB
text/typescript
/* 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;
}