diff --git a/package.json b/package.json index 0042b10..3107765 100644 --- a/package.json +++ b/package.json @@ -36,5 +36,5 @@ }, "name": "@cirlces-sdk/root", "license": "MIT", - "version": "0.14.0" + "version": "0.14.1" } diff --git a/packages/abi-v1/package.json b/packages/abi-v1/package.json index 6084b9b..5158488 100644 --- a/packages/abi-v1/package.json +++ b/packages/abi-v1/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v1", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/abi-v2/package.json b/packages/abi-v2/package.json index 40db207..fa0b4e7 100644 --- a/packages/abi-v2/package.json +++ b/packages/abi-v2/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v2", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/adapter-cometh/package.json b/packages/adapter-cometh/package.json index f2de349..4a15c29 100644 --- a/packages/adapter-cometh/package.json +++ b/packages/adapter-cometh/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/adapter-cometh", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/adapter": "0.14.0", + "@circles-sdk/adapter": "0.14.1", "@cometh/connect-sdk": "1.2.29" }, "keywords": [], diff --git a/packages/adapter-ethers/package.json b/packages/adapter-ethers/package.json index 115460b..682ed39 100644 --- a/packages/adapter-ethers/package.json +++ b/packages/adapter-ethers/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/adapter-ethers", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "ethers": "^6.13.2", - "@circles-sdk/adapter": "0.14.0", - "@circles-sdk/utils": "0.14.0" + "@circles-sdk/adapter": "0.14.1", + "@circles-sdk/utils": "0.14.1" }, "keywords": [], "author": "", diff --git a/packages/adapter-safe-app/package.json b/packages/adapter-safe-app/package.json index 514bdc2..81cf7c8 100644 --- a/packages/adapter-safe-app/package.json +++ b/packages/adapter-safe-app/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/adapter-safe-app", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/adapter": "0.14.0", + "@circles-sdk/adapter": "0.14.1", "@safe-global/safe-apps-sdk": "^9.1.0" }, "keywords": [], diff --git a/packages/adapter-safe/package.json b/packages/adapter-safe/package.json index 1447823..2c44320 100644 --- a/packages/adapter-safe/package.json +++ b/packages/adapter-safe/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/adapter-safe", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/adapter/package.json b/packages/adapter/package.json index 3406968..079811c 100644 --- a/packages/adapter/package.json +++ b/packages/adapter/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/adapter", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/data/package.json b/packages/data/package.json index e384767..e608ce1 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/data", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.14.0" + "@circles-sdk/utils": "0.14.1" }, "keywords": [], "author": "", diff --git a/packages/data/src/circlesData.ts b/packages/data/src/circlesData.ts index aed9408..e082720 100644 --- a/packages/data/src/circlesData.ts +++ b/packages/data/src/circlesData.ts @@ -356,11 +356,24 @@ export class CirclesData implements CirclesDataInterface { /** * Gets all trust relations of an avatar and groups mutual trust relations together. * @param avatarAddress The address to get the trust relations for. + * @param version The version of the trust relations to get (default: undefined - queries both). */ - async getAggregatedTrustRelations(avatarAddress: string): Promise { + /** + * Retrieves and aggregates trust relations for a given avatar. + * + * - Fetches all trust relations involving the avatar. + * - Groups trust relations based on the counterpart (truster/trustee). + * - Determines the type of relationship: mutual trust, trusts, or trusted by. + * - Handles cases where relationships differ across versions and includes detailed metadata. + * + * @param avatarAddress The address of the avatar to retrieve trust relations for. + * @param version Optional version filter (defaults to retrieving all versions). + * @returns Aggregated trust relations, including relation type, versions, and timestamp. + */ + async getAggregatedTrustRelations(avatarAddress: string, version?: number): Promise { const pageSize = 1000; const trustsQuery = this.getTrustRelations(avatarAddress, pageSize); - const trustListRows: TrustListRow[] = []; + let trustListRows: TrustListRow[] = []; // Fetch all trust relations while (await trustsQuery.queryNextPage()) { @@ -370,41 +383,66 @@ export class CirclesData implements CirclesDataInterface { if (resultRows.length < pageSize) break; } + // Filter by version if provided + if (version !== undefined) { + trustListRows = trustListRows.filter(row => row.version === version); + } + // Group trust list rows by truster and trustee - const trustBucket: { [avatar: string]: TrustListRow[] } = {}; + const trustBucket: { [avatar: string]: { rows: TrustListRow[]; version: Set } } = {}; trustListRows.forEach(row => { + const addToBucket = (key: string) => { + if (!trustBucket[key]) { + trustBucket[key] = {rows: [], version: new Set()}; + } + trustBucket[key].rows.push(row); + trustBucket[key].version.add(row.version); + }; + if (row.truster !== avatarAddress) { - trustBucket[row.truster] = trustBucket[row.truster] || []; - trustBucket[row.truster].push(row); + addToBucket(row.truster); } if (row.trustee !== avatarAddress) { - trustBucket[row.trustee] = trustBucket[row.trustee] || []; - trustBucket[row.trustee].push(row); + addToBucket(row.trustee); } }); // Determine trust relations return Object.entries(trustBucket) .filter(([avatar]) => avatar !== avatarAddress) - .map(([avatar, rows]) => { + .map(([avatar, {rows, version}]) => { + const versionRelations: { [key: number]: TrustRelation } = {}; const maxTimestamp = Math.max(...rows.map(o => o.timestamp)); - let relation: TrustRelation; - - if (rows.length === 2) { - relation = 'mutuallyTrusts'; - } else if (rows[0].trustee === avatarAddress) { - relation = 'trustedBy'; - } else if (rows[0].truster === avatarAddress) { - relation = 'trusts'; - } else { - throw new Error(`Unexpected trust list row. Couldn't determine trust relation.`); - } + + // Process each version separately + Array.from(version).forEach(ver => { + const versionRows = rows.filter(row => row.version === ver); + + if (versionRows.length === 2) { + versionRelations[ver] = 'mutuallyTrusts'; + } else if (versionRows[0]?.trustee === avatarAddress) { + versionRelations[ver] = 'trustedBy'; + } else if (versionRows[0]?.truster === avatarAddress) { + versionRelations[ver] = 'trusts'; + } else { + throw new Error(`Unexpected trust list row for version ${ver}. Couldn't determine trust relation.`); + } + }); + + // Combine relations for all versions + const distinctRelations = Array.from(new Set(Object.values(versionRelations))); + + // If relations differ between versions, mark as "variesByVersion" + const combinedRelation = + distinctRelations.length === 1 ? distinctRelations[0] : 'variesByVersion'; return { subjectAvatar: avatarAddress, - relation: relation, + relation: combinedRelation, objectAvatar: avatar, - timestamp: maxTimestamp + timestamp: maxTimestamp, + versions: Array.from(version), + versionSpecificRelations: versionRelations }; }); } @@ -416,7 +454,7 @@ export class CirclesData implements CirclesDataInterface { * @returns The avatar info or undefined if the avatar is not found. */ async getAvatarInfo(avatar: string): Promise { - const avatarInfos = await this.getAvatarInfos([avatar]); + const avatarInfos = await this.getAvatarInfoBatch([avatar]); return avatarInfos.length > 0 ? avatarInfos[0] : undefined; } @@ -425,7 +463,7 @@ export class CirclesData implements CirclesDataInterface { * @param avatars The addresses to check. * @returns An array of avatar info objects. */ - async getAvatarInfos(avatars: string[]): Promise { + async getAvatarInfoBatch(avatars: string[]): Promise { if (avatars.length === 0) { return []; } @@ -566,35 +604,46 @@ export class CirclesData implements CirclesDataInterface { } /** - * Gets the invitations sent by an avatar. + * Checks if an avatar has been invited to circles by another avatar. * @param avatar The avatar to get the invitations for. - * @param pageSize The maximum number of invitations per page. - * @returns A CirclesQuery object to fetch the invitations. + * @returns A list of inviters or an empty list if no invitations are found (or the inviter doesn't have enough balance to pay for the invitation fees). */ - getInvitations(avatar: string, pageSize: number): CirclesQuery { - return new CirclesQuery(this.rpc, { - namespace: 'CrcV2', - table: 'InviteHuman', - columns: [ - 'blockNumber', - 'transactionIndex', - 'logIndex', - 'timestamp', - 'transactionHash', - 'inviter', - 'invited' - ], - filter: [ - { - Type: 'FilterPredicate', - FilterType: 'Equals', - Column: 'inviter', - Value: avatar.toLowerCase() - } - ], - sortOrder: 'DESC', - limit: pageSize - }); + async getInvitations(avatar: string): Promise { + const MIN_TOKENS_REQUIRED = 96; + + // Check if the avatar is still on v1 (else not interesting for invitations) + const avatarInfo = await this.getAvatarInfo(avatar); + if (avatarInfo?.version == 2) { + return []; + } + + // Find all avatars trusting the given avatar. + // (mutual trust cannot exist in invitation state - to trust back, the avatar must be on v2 already) + const v2Relations = await this.getAggregatedTrustRelations(avatar, 2); + const v2Trusters = v2Relations + .filter(o => o.relation == "trusts") + .map(o => o.subjectAvatar); + + const humanInviters: AvatarRow[] = []; + const trusterInfoBatch = await this.getAvatarInfoBatch(v2Trusters); + + for (const trusterInfo of trusterInfoBatch) { + // Only humans can inviter other humans + if (!trusterInfo?.isHuman) { + continue; + } + + // If the inviter doesn't have enough tokens, the user cannot accept their invitation. + // Invitation fees must be paid in the inviter's own token. + const balances = await this.getTokenBalances(trusterInfo.avatar); + const inviterOwnToken = balances.find(o => o.tokenAddress == trusterInfo.avatar); + if (inviterOwnToken && inviterOwnToken.circles >= MIN_TOKENS_REQUIRED) { + // The inviter has enough tokens to pay for the invitation + humanInviters.push(trusterInfo); + } + } + + return humanInviters; } /** diff --git a/packages/data/src/circlesDataInterface.ts b/packages/data/src/circlesDataInterface.ts index 016c525..c9f1dc5 100644 --- a/packages/data/src/circlesDataInterface.ts +++ b/packages/data/src/circlesDataInterface.ts @@ -27,6 +27,14 @@ export interface CirclesDataInterface { */ getAvatarInfo(avatar: string): Promise; + /** + * Gets basic information about avatars. + * This includes the signup timestamp, circles version, avatar type and token address/id. + * @param avatar The addresses to check. + * @returns The avatar information or undefined if the address is not an avatar. + */ + getAvatarInfoBatch(avatar: string[]): Promise; + /** * Gets the token info for a given token address. * @param address The address of the token. @@ -74,8 +82,9 @@ export interface CirclesDataInterface { /** * Gets all trust relations of an avatar and groups mutual trust relations together. * @param avatar The address to get the trust relations for. + * @param version The version of the trust relations to get (default: undefined - queries both). */ - getAggregatedTrustRelations(avatar: string): Promise; + getAggregatedTrustRelations(avatar: string, version?: number): Promise; /** * Subscribes to Circles events. @@ -88,7 +97,8 @@ export interface CirclesDataInterface { * @param avatar The address to get the invitations for. * @param pageSize The maximum number of invitations per page. */ - getInvitations(avatar: string, pageSize: number): CirclesQuery; + // getInvitations(avatar: string, pageSize: number): CirclesQuery; + getInvitations(avatar: string, pageSize: number): Promise; /** * Gets the avatar that invited the given avatar. diff --git a/packages/data/src/rows/trustRelationRow.ts b/packages/data/src/rows/trustRelationRow.ts index 4a96a28..b96c0a1 100644 --- a/packages/data/src/rows/trustRelationRow.ts +++ b/packages/data/src/rows/trustRelationRow.ts @@ -5,27 +5,41 @@ export type TrustRelation = 'trusts' | 'trustedBy' | 'mutuallyTrusts' - | 'selfTrusts'; + | 'selfTrusts' + | 'variesByVersion'; /** - * A single avatar to avatar trust relation that can be either one-way or mutual. + * A single avatar-to-avatar trust relation that can be either one-way, mutual, or version-specific. */ export interface TrustRelationRow { /** * The avatar. */ subjectAvatar: string; + /** * The trust relation. + * Can be one of the defined TrustRelation values or "variesByVersion" for mixed states across versions. */ relation: TrustRelation; + /** * Who's trusted by or is trusting the avatar. */ objectAvatar: string; /** - * When the last trust relation (in either direction) was last established. + * When the last trust relation (in either direction) was established. */ timestamp: number; + + /** + * The versions involved in this trust relation. + */ + versions: number[]; + + /** + * A map of version-specific trust relations, providing granular details per version. + */ + versionSpecificRelations?: { [version: number]: TrustRelation }; } \ No newline at end of file diff --git a/packages/profiles/package.json b/packages/profiles/package.json index 1881f45..75244f2 100644 --- a/packages/profiles/package.json +++ b/packages/profiles/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/profiles", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.14.0" + "@circles-sdk/utils": "0.14.1" }, "keywords": [], "author": "", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c6fb4b4..0a0a6cf 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/sdk", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,11 +17,11 @@ "author": "", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.14.0", - "@circles-sdk/abi-v2": "0.14.0", - "@circles-sdk/data": "0.14.0", - "@circles-sdk/profiles": "0.14.0", - "@circles-sdk/adapter-ethers": "0.14.0", + "@circles-sdk/abi-v1": "0.14.1", + "@circles-sdk/abi-v2": "0.14.1", + "@circles-sdk/data": "0.14.1", + "@circles-sdk/profiles": "0.14.1", + "@circles-sdk/adapter-ethers": "0.14.1", "ethers": "^6.13.2", "multihashes": "^4.0.3" }, diff --git a/packages/sdk/src/v1/v1Avatar.ts b/packages/sdk/src/v1/v1Avatar.ts index 2eb9a76..1359971 100644 --- a/packages/sdk/src/v1/v1Avatar.ts +++ b/packages/sdk/src/v1/v1Avatar.ts @@ -2,9 +2,9 @@ import { ContractRunner, ContractTransactionReceipt, ethers, TransactionReceipt } from 'ethers'; -import {Sdk} from '../sdk'; -import {AvatarInterface} from '../AvatarInterface'; -import {Token, Token__factory} from '@circles-sdk/abi-v1'; +import { Sdk } from '../sdk'; +import { AvatarInterface } from '../AvatarInterface'; +import { Token, Token__factory } from '@circles-sdk/abi-v1'; import { AvatarRow, CirclesQuery, @@ -12,8 +12,8 @@ import { TransactionHistoryRow, TrustRelationRow } from '@circles-sdk/data'; -import {crcToTc} from '@circles-sdk/utils'; -import {TransactionResponse} from "@circles-sdk/adapter"; +import { crcToTc } from '@circles-sdk/utils'; +import { TransactionResponse } from "@circles-sdk/adapter"; export class V1Avatar implements AvatarInterface { public readonly sdk: Sdk; @@ -250,7 +250,7 @@ export class V1Avatar implements AvatarInterface { } async getTrustRelations(): Promise { - return this.sdk.data.getAggregatedTrustRelations(this.address); + return this.sdk.data.getAggregatedTrustRelations(this.address, 1); } async getTransactionHistory(pageSize: number): Promise> { diff --git a/packages/sdk/src/v2/v2Avatar.ts b/packages/sdk/src/v2/v2Avatar.ts index c599963..fa457e4 100644 --- a/packages/sdk/src/v2/v2Avatar.ts +++ b/packages/sdk/src/v2/v2Avatar.ts @@ -117,7 +117,7 @@ export class V2Avatar implements AvatarInterfaceV2 { } async getTrustRelations(): Promise { - return this.sdk.data.getAggregatedTrustRelations(this.address); + return this.sdk.data.getAggregatedTrustRelations(this.address, 2); } async getBalances(): Promise { diff --git a/packages/utils/package.json b/packages/utils/package.json index 829a4f1..59d6190 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/utils", - "version": "0.14.0", + "version": "0.14.1", "description": "", "type": "module", "main": "./dist/index.js",