diff --git a/.cache/cache.db b/.cache/cache.db new file mode 100644 index 0000000..aaf4e75 Binary files /dev/null and b/.cache/cache.db differ diff --git a/DOCS.md b/DOCS.md index e77ed5f..dcfe3e2 100644 --- a/DOCS.md +++ b/DOCS.md @@ -12,7 +12,8 @@ A unified view of all transfer transactions within the system, consolidating bot - `transactionHash` - `version` - `operator` (v2 only) - - `from`, `to` + - `from` + - `to` - `id` - `value` - `type` (transaction type; e.g., `Erc20WrapperTransfer`, `TransferSingle`, etc.) @@ -26,16 +27,16 @@ It's possible to get demurrages that happened during transfers or mints. query getUserTransfers($address: String) { Transfer( where:{ - to:{_eq:$address}, - transferType:{_neq:"Demurrage"} + to: {_eq:$address}, + transferType: {_neq:"Demurrage"} } - order_by:{timestamp: desc} + order_by: {timestamp: desc} ) { id - from + from { id } + to { id } transferType timestamp - to value demurrageFrom { id @@ -215,8 +216,10 @@ Defines the type of avatars. - `Invite`: v2 user that is not yet on circles and has at least one invite. - `RegisterGroup`: v2 group. - `RegisterOrganization`: v2 organization. - - `Unknown`: Placeholder during processing; unlikely to occur in steady state. - `Migrating`: v1 user that is migrating to v2. + - `Unknown`: Pending state. + +The `Unknown` pending state happens when a user created a v2 profile, but did not yet receive an invite or joined circles. Once the user receives de first invite, then the avatar type is `Invite`. Suppose you want to get the list of invited users by a given user. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2146554..9b9899c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,9 +64,6 @@ importers: '@rescript/react': specifier: 0.12.1 version: 0.12.1(react-dom@18.3.1(react@18.2.0))(react@18.2.0) - '@ryyppy/rescript-promise': - specifier: 2.1.0 - version: 2.1.0 bignumber.js: specifier: 9.1.2 version: 9.1.2 @@ -117,13 +114,10 @@ importers: version: 11.1.3 rescript-envsafe: specifier: 4.2.0 - version: 4.2.0(rescript-schema@8.2.0(rescript@11.1.3))(rescript@11.1.3) - rescript-express: - specifier: 0.4.1 - version: 0.4.1(express@4.19.2) + version: 4.2.0(rescript-schema@8.1.0(rescript@11.1.3))(rescript@11.1.3) rescript-schema: - specifier: 8.2.0 - version: 8.2.0(rescript@11.1.3) + specifier: 8.1.0 + version: 8.1.0(rescript@11.1.3) root: specifier: ../. version: link:.. @@ -253,9 +247,6 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' - '@ryyppy/rescript-promise@2.1.0': - resolution: {integrity: sha512-+dW6msBrj2Lr2hbEMX+HoWCvN89qVjl94RwbYWJgHQuj8jm/izdPC0YzxgpGoEFdeAEW2sOozoLcYHxT6o5WXQ==} - '@scure/base@1.1.9': resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} @@ -1538,21 +1529,11 @@ packages: rescript: 11.x rescript-schema: 6.x || 7.x || 8.x - rescript-express@0.4.1: - resolution: {integrity: sha512-R+xAQKANfIFAIcxhQrkLn58IZQwhMZuQpVCH6UtTRNLWqlVtaogG9z4Rt0MQZgqYOjvrOtt51P0hOmYGg/90Fw==} - peerDependencies: - express: ^4.17.1 - rescript-schema@8.1.0: resolution: {integrity: sha512-syU4Wvy2tJzBKkqa8ad1yYSDIh8EoKGUsZv6Wa0YpERqb7H6Kemtc6zU+BxIghahzShb6L7RMfVNuKC48KluIg==} peerDependencies: rescript: 11.x - rescript-schema@8.2.0: - resolution: {integrity: sha512-pfRNB9kvafUYe+RgvsExiazJioi/iV8gmjK3xOw/jeSgPIIDoVhflnmpoz+vuiwlulYkNT/7u2+2EsCrph7QKA==} - peerDependencies: - rescript: 11.x - rescript@11.1.3: resolution: {integrity: sha512-bI+yxDcwsv7qE34zLuXeO8Qkc2+1ng5ErlSjnUIZdrAWKoGzHXpJ6ZxiiRBUoYnoMsgRwhqvrugIFyNgWasmsw==} engines: {node: '>=10'} @@ -2090,8 +2071,6 @@ snapshots: react: 18.2.0 react-dom: 18.3.1(react@18.2.0) - '@ryyppy/rescript-promise@2.1.0': {} - '@scure/base@1.1.9': {} '@scure/bip32@1.4.0': @@ -3393,23 +3372,15 @@ snapshots: require-directory@2.1.1: {} - rescript-envsafe@4.2.0(rescript-schema@8.2.0(rescript@11.1.3))(rescript@11.1.3): + rescript-envsafe@4.2.0(rescript-schema@8.1.0(rescript@11.1.3))(rescript@11.1.3): dependencies: rescript: 11.1.3 - rescript-schema: 8.2.0(rescript@11.1.3) - - rescript-express@0.4.1(express@4.19.2): - dependencies: - express: 4.19.2 + rescript-schema: 8.1.0(rescript@11.1.3) rescript-schema@8.1.0(rescript@11.1.3): dependencies: rescript: 11.1.3 - rescript-schema@8.2.0(rescript@11.1.3): - dependencies: - rescript: 11.1.3 - rescript@11.1.3: {} restore-cursor@3.1.0: diff --git a/src/cache.ts b/src/cache.ts index 479ec10..04e3eb6 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -5,8 +5,8 @@ import { Profile as Metadata } from "./types"; const db = new sqlite3.Database(".cache/cache.db"); export class ProfileCache { - static async init() { - const cache = new ProfileCache("cache_v2"); + static async init(version: number) { + const cache = new ProfileCache("cache_cv" + version); await cache.createTableIfNotExists(); return cache; } @@ -37,7 +37,7 @@ export class ProfileCache { }); } - public read(id: string): Promise<{ cidV0: string; data: Metadata } | null> { + public read(id: string): Promise<{ cidV0: string | null; data: Metadata } | null> { return new Promise((resolve, reject) => { const query = `SELECT data, cidV0 FROM ${this.key} WHERE id = ?`; db.get(query, [id], (err, row: any) => { @@ -53,7 +53,7 @@ export class ProfileCache { }); } - public async add(id: string, cidV0: string, metadata: Metadata) { + public async add(id: string, cidV0: string | null, metadata: Metadata) { const query = `INSERT INTO ${this.key} (id, cidV0, data) VALUES (?, ?, ?)`; const data = JSON.stringify(metadata); diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index 386333b..0000000 --- a/src/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -const GNOSIS_CHIADO_CHAIN_ID = 10200; -const chainId = GNOSIS_CHIADO_CHAIN_ID; - -export const MIGRATION_CONTRACT_ADDRESS = chainId == GNOSIS_CHIADO_CHAIN_ID ? '0x12e815963a0b910288c7256cad0d345c8f5db08e' : '0x8C9BeAccb6b7DBd3AeffB5D77cab36b62Fe98882'; -export const HUB_V1_CONTRACT_ADDRESS = chainId == GNOSIS_CHIADO_CHAIN_ID ? '0xdbF22D4e8962Db3b2F1d9Ff55be728A887e47710' : '0x29b9a7fbb8995b2423a71cc17cf9810798f6c543'; -export const HUB_V2_CONTRACT_ADDRESS = chainId == GNOSIS_CHIADO_CHAIN_ID ? '0xEddc960D3c78692BF38577054cb0a35114AE35e0' : ''; \ No newline at end of file diff --git a/src/event_handlers/hubV1.ts b/src/event_handlers/hubV1.ts index 1548bb7..7631c07 100644 --- a/src/event_handlers/hubV1.ts +++ b/src/event_handlers/hubV1.ts @@ -3,6 +3,7 @@ import { maxUint256 } from "viem"; import { incrementStats } from "../incrementStats"; import { handleTransfer } from "../common/handleTransfer"; import { defaultAvatarProps, makeAvatarBalanceEntityId } from "../utils"; +import { getProfileMetadataFromGardenApi } from "../gardenApi"; // ############### // #### TOKEN #### @@ -43,6 +44,22 @@ Hub.Signup.handlerWithLoader({ handler: async ({ event, context, loaderReturn }) => { const { avatarBalance } = loaderReturn; + const profileFromGarden = await getProfileMetadataFromGardenApi( + event.params.user + ); + + if (profileFromGarden && profileFromGarden.data) { + const { data } = profileFromGarden; + context.Profile.set({ + id: event.params.user, + description: undefined, + previewImageUrl: data?.previewImageUrl, + imageUrl: undefined, + name: data?.name, + symbol: undefined, + }); + } + context.Avatar.set({ ...defaultAvatarProps(event), version: 1, @@ -152,6 +169,10 @@ Hub.Trust.handlerWithLoader({ handler: async ({ event, context, loaderReturn }) => { const { trustId, trustRelation, oppositeTrustRelation } = loaderReturn; + if (event.params.user === event.params.canSendTo) { + return; + } + if (event.params.limit === 0n) { // this is untrust if (trustRelation && trustRelation.version === 1) { diff --git a/src/event_handlers/hubV2.ts b/src/event_handlers/hubV2.ts index c5a3a2d..dcce2dd 100644 --- a/src/event_handlers/hubV2.ts +++ b/src/event_handlers/hubV2.ts @@ -209,14 +209,12 @@ HubV2.PersonalMint.handlerWithLoader({ }); NameRegistry.UpdateMetadataDigest.handler(async ({ event, context }) => { - let profileMetadata: { cidV0: string; data: Profile | null } | null = null; + let profileMetadata: { cidV0: string | null; data: Profile | null } | null = null; try { profileMetadata = await getProfileMetadataFromIpfs( event.params.metadataDigest ); - } catch (_) { - console.log("Error in nameReg fetching Ipfs", _); - } + } catch (_) {} const avatar = await context.Avatar.get(event.params.avatar); @@ -228,7 +226,7 @@ NameRegistry.UpdateMetadataDigest.handler(async ({ event, context }) => { id: event.params.avatar, avatarType: "Unknown", tokenId: bytesToBigInt(toBytes(event.params.avatar)).toString(), - cidV0: profileMetadata?.cidV0, + cidV0: profileMetadata?.cidV0 ?? undefined, profile_id: event.params.avatar, }); } else { @@ -239,7 +237,7 @@ NameRegistry.UpdateMetadataDigest.handler(async ({ event, context }) => { context.Avatar.set({ ...avatar, avatarType: transitiveType, - cidV0: profileMetadata?.cidV0, + cidV0: profileMetadata?.cidV0 ?? undefined, }); } diff --git a/src/gardenApi.ts b/src/gardenApi.ts new file mode 100644 index 0000000..a72a983 --- /dev/null +++ b/src/gardenApi.ts @@ -0,0 +1,76 @@ +import { Profile } from "./types"; +import { ProfileCache } from "./cache"; + +type GardenApiResponse = { + status: string; + data: { + id: number; + username: string; + avatarUrl: string | undefined; + }[]; +}; +type GardenProfile = { + name: string; + previewImageUrl: string | undefined; +}; +/** + * This functions calls https://api.circles.garden/api/users?address[]=${address} + * which returns a GardenApiResponse object. + * GardenApiResponse contains an avatarUrl, which is also fetched and it's result is converted into a base 64 image. + * @param {string} address - The address of the user. + * @returns {Promise<{ data: GardenProfile | undefined; timeTaken: number }>} - The GardenProfile object and the time taken to fetch it. + */ +async function fetchGardenProfile( + address: string +): Promise<{ data: GardenProfile | undefined; timeTaken: number }> { + const startTime = Date.now(); + + try { + const response = await fetch( + `https://api.circles.garden/api/users?address[]=${address}` + ); + const json = (await response.json()) as GardenApiResponse; + + if (json.status !== "ok" || json.data.length === 0) { + return { data: undefined, timeTaken: Date.now() - startTime }; + } + + const user = json.data[0]; + + return { + data: { + name: user.username, + previewImageUrl: user.avatarUrl, + }, + timeTaken: Date.now() - startTime, + }; + } catch (error) { + console.error("Error fetching garden profile:", error); + return { data: undefined, timeTaken: Date.now() - startTime }; + } +} + +export async function getProfileMetadataFromGardenApi( + address: string +): Promise<{ data: Profile | null } | null> { + if (!address) { + return null; + } + + const cache = await ProfileCache.init(1); + const cacheResult = await cache.read(address); + + if (cacheResult) { + return { data: cacheResult.data }; + } + + const { data } = await fetchGardenProfile(address); + if (!data) { + return null; + } + + // v1 did not had cidV0 + await cache.add(address, null, data); + + return { data }; +} diff --git a/src/ipfs.ts b/src/ipfs.ts index 3bbe03d..2b12b2e 100644 --- a/src/ipfs.ts +++ b/src/ipfs.ts @@ -1,7 +1,5 @@ -import multihash from "multihashes"; import { Profile } from "./types"; -/* import { ProfileCache } from "./cache"; - */ import { Avatar, eventLog } from "generated"; +import { ProfileCache } from "./cache"; import { uint8ArrayToCidV0 } from "./utils"; // Simple config with only needed values @@ -74,18 +72,18 @@ async function fetchFromEndpoint( export async function getProfileMetadataFromIpfs( metadataDigest: string -): Promise<{ cidV0: string; data: Profile | null } | null> { +): Promise<{ cidV0: string | null; data: Profile | null } | null> { if (!metadataDigest) { return null; } const slicedDigest = metadataDigest.slice(2, metadataDigest.length); - /* const cache = await ProfileCache.init(); + const cache = await ProfileCache.init(2); const cacheResult = await cache.read(slicedDigest); if (cacheResult) { return cacheResult; - } */ + } const cidV0 = uint8ArrayToCidV0( Uint8Array.from(Buffer.from(slicedDigest, "hex")) @@ -93,11 +91,9 @@ export async function getProfileMetadataFromIpfs( // Try each endpoint until we get a successful response for (const endpoint of IPFS_ENDPOINTS) { - const { data, timeTaken } = await fetchFromEndpoint(endpoint, cidV0); - console.log(`IPFS fetch from ${endpoint} took ${timeTaken.toFixed(2)}ms`); + const { data } = await fetchFromEndpoint(endpoint, cidV0); if (data) { - console.log("Adding IPFS data to cache", data); - //await cache.add(slicedDigest, cidV0, data); + await cache.add(slicedDigest, cidV0, data); return { cidV0, data }; } } diff --git a/src/utils.ts b/src/utils.ts index 03e0dfc..1fbf4c9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,4 @@ import multihash from "multihashes"; -import { Profile } from "./types"; import { Avatar, eventLog } from "generated"; /**