diff --git a/e2e/tests/core-sdk-collections.test.ts b/e2e/tests/core-sdk-collections.test.ts index 87acf2ce1..9629c9498 100644 --- a/e2e/tests/core-sdk-collections.test.ts +++ b/e2e/tests/core-sdk-collections.test.ts @@ -5,6 +5,7 @@ import { createSeller, createSellerAndOffer, initCoreSDKWithFundedWallet, + publishNftContractMetadata, seedWallet20, updateSeller, waitForGraphNodeIndexing @@ -18,6 +19,30 @@ describe("Offer collections", () => { let coreSDK_A: CoreSDK, coreSDK_B: CoreSDK; let wallet_A: Wallet, wallet_B: Wallet; const customCollectionId = "summer-2024-collection"; + const collectionMetadata1 = { + name: "MyCollection", + description: "This is my collection", + image: + "https://upload.wikimedia.org/wikipedia/en/c/c4/Various_Bored_Ape.jpg", + external_link: "www.mycollection.com", + collaborators: [ + "Doc", + "Grumpy", + "Happy", + "Sleepy", + "Bashful", + "Sneezy", + "Dopey" + ] + }; + const collectionMetadata2 = { + name: "MyCollection2", + description: "This is my 2nd collection", + image: + "https://en.wikipedia.org/wiki/Walt_Disney#/media/File:Steamboat-willie.jpg", + external_link: "www.mycollection2.com", + collaborators: ["Alice", "Bob", "Charlie"] + }; beforeEach(async () => { // Create seller1 with wallet_A, then update all roles to wallet_B address, // so that wallet_A can now be reused for another seller account @@ -228,7 +253,111 @@ describe("Offer collections", () => { }) ).rejects.toThrow(`collectionId length should not exceed 31 characters`); }); - test("Check collection metadata", async () => { - // TODO: + test("Check collection metadata for initial collection", async () => { + const { coreSDK: coreSDK, fundedWallet } = + await initCoreSDKWithFundedWallet(seedWallet20); + const collectionMetadata1Uri = await publishNftContractMetadata( + coreSDK, + collectionMetadata1 + ); + const seller = await createSeller(coreSDK, fundedWallet.address, { + sellerParams: { contractUri: collectionMetadata1Uri } + }); + expect(seller).toBeTruthy(); + expect(seller.collections.length).toEqual(1); + expect(seller.collections[0].metadata).toBeTruthy(); + expect(seller.collections[0].metadata.name).toEqual( + collectionMetadata1.name + ); + expect(seller.collections[0].metadata.externalLink).toEqual( + collectionMetadata1.external_link + ); + expect(seller.collections[0].metadata.collaborators.length).toEqual( + collectionMetadata1.collaborators.length + ); + }); + test("Check collection metadata for an additional collection", async () => { + const { coreSDK: coreSDK, fundedWallet } = + await initCoreSDKWithFundedWallet(seedWallet20); + const collectionMetadata2Uri = await publishNftContractMetadata( + coreSDK, + collectionMetadata2 + ); + const seller = await createSeller(coreSDK, fundedWallet.address); + expect(seller).toBeTruthy(); + const tx = await coreSDK.createNewCollection({ + contractUri: collectionMetadata2Uri, + royaltyPercentage: 0, + collectionId: collectionMetadata2.name + }); + await tx.wait(); + await waitForGraphNodeIndexing(); + const collections = await coreSDK.getOfferCollections({ + offerCollectionsFilter: { + sellerId: seller.id + } + }); + expect(collections.length).toEqual(2); + expect(collections[1].externalId).toEqual(collectionMetadata2.name); + expect(collections[1].metadata).toBeTruthy(); + expect(collections[1].metadata.name).toEqual(collectionMetadata2.name); + expect(collections[1].metadata.externalLink).toEqual( + collectionMetadata2.external_link + ); + expect(collections[1].metadata.collaborators.length).toEqual( + collectionMetadata2.collaborators.length + ); + }); + test("Check incomplete collection metadata", async () => { + const { coreSDK: coreSDK, fundedWallet } = + await initCoreSDKWithFundedWallet(seedWallet20); + const collectionMetadataUri = await publishNftContractMetadata(coreSDK, { + name: collectionMetadata1.name + // no other fields + }); + const seller = await createSeller(coreSDK, fundedWallet.address, { + sellerParams: { contractUri: collectionMetadataUri } + }); + expect(seller).toBeTruthy(); + expect(seller.collections.length).toEqual(1); + expect(seller.collections[0].metadata).toBeTruthy(); + expect(seller.collections[0].metadata.name).toEqual( + collectionMetadata1.name + ); + expect(seller.collections[0].metadata.externalLink).toEqual(null); + expect(seller.collections[0].metadata.collaborators.length).toEqual(0); + }); + test("Check invalid collection metadata", async () => { + const { coreSDK: coreSDK, fundedWallet } = + await initCoreSDKWithFundedWallet(seedWallet20); + const collectionMetadataUri = await publishNftContractMetadata(coreSDK, { + invalidKey: "invalidValue" + // no other fields + }); + const seller = await createSeller(coreSDK, fundedWallet.address, { + sellerParams: { contractUri: collectionMetadataUri } + }); + expect(seller).toBeTruthy(); + expect(seller.collections.length).toEqual(1); + expect(seller.collections[0].metadata).toBeTruthy(); + expect(seller.collections[0].metadata.name).toEqual(null); + expect(seller.collections[0].metadata.externalLink).toEqual(null); + expect(seller.collections[0].metadata.collaborators.length).toEqual(0); + }); + test("Check non existing collection metadata file", async () => { + const { coreSDK: coreSDK, fundedWallet } = + await initCoreSDKWithFundedWallet(seedWallet20); + const collectionMetadata1Uri = await publishNftContractMetadata( + coreSDK, + collectionMetadata1 + ); + const seller = await createSeller(coreSDK, fundedWallet.address, { + sellerParams: { + contractUri: collectionMetadata1Uri + "x" // tamper the IPFS link + } + }); + expect(seller).toBeTruthy(); + expect(seller.collections.length).toEqual(1); + expect(seller.collections[0].metadata).toBeFalsy(); }); }); diff --git a/e2e/tests/utils.ts b/e2e/tests/utils.ts index 8d1ab6039..f3a0cbc6a 100644 --- a/e2e/tests/utils.ts +++ b/e2e/tests/utils.ts @@ -14,7 +14,10 @@ import { } from "ethers"; import { CoreSDK, getEnvConfigs, accounts } from "../../packages/core-sdk/src"; import { base, seller } from "../../packages/metadata/src"; -import { IpfsMetadataStorage } from "../../packages/ipfs-storage/src"; +import { + BaseIpfsStorage, + IpfsMetadataStorage +} from "../../packages/ipfs-storage/src"; import { EthersAdapter } from "../../packages/ethers-sdk/src"; import { CreateOfferArgs } from "./../../packages/common/src/types/offers"; import { mockCreateOfferArgs } from "../../packages/common/tests/mocks"; @@ -48,6 +51,7 @@ import { } from "./mockAbis"; import { SellerFieldsFragment } from "../../packages/core-sdk/src/subgraph"; import { ZERO_ADDRESS } from "../../packages/core-sdk/tests/mocks"; +import { sortObjKeys } from "../../packages/ipfs-storage/src/utils"; const getFirstEnvConfig = (arg0: Parameters[0]) => getEnvConfigs(arg0)[0]; @@ -755,3 +759,15 @@ export async function commitToOffer(args: { const exchange = await args.buyerCoreSDK.getExchangeById(exchangeId); return exchange; } + +export async function publishNftContractMetadata( + coreSDK: CoreSDK, + metadata: Record +): Promise { + const ipfsStorage = new BaseIpfsStorage({ + url: getFirstEnvConfig("local").ipfsMetadataUrl + }); + const metadataWithSortedKeys = sortObjKeys(metadata); + const cid = await ipfsStorage.add(JSON.stringify(metadataWithSortedKeys)); + return "ipfs://" + cid; +} diff --git a/packages/core-sdk/src/accounts/queries.graphql b/packages/core-sdk/src/accounts/queries.graphql index ea0e15ace..ada754146 100644 --- a/packages/core-sdk/src/accounts/queries.graphql +++ b/packages/core-sdk/src/accounts/queries.graphql @@ -255,6 +255,15 @@ fragment BaseOfferCollectionFields on OfferCollection { collectionAddress externalIdHash externalId + metadata { + id + name + description + image + externalLink + createdAt + collaborators + } } fragment SellerFields on Seller { diff --git a/packages/core-sdk/src/subgraph.ts b/packages/core-sdk/src/subgraph.ts index a1ac09959..2a637b9fd 100644 --- a/packages/core-sdk/src/subgraph.ts +++ b/packages/core-sdk/src/subgraph.ts @@ -2962,6 +2962,138 @@ export enum MetadataType { ProductV1 = "PRODUCT_V1" } +/** + * Nft Contract Metadata + * + */ +export type NftContractMetadata = { + __typename?: "NftContractMetadata"; + collaborators?: Maybe>; + createdAt: Scalars["BigInt"]; + description?: Maybe; + externalLink?: Maybe; + id: Scalars["ID"]; + image?: Maybe; + name?: Maybe; +}; + +export type NftContractMetadata_Filter = { + /** Filter for the block changed event. */ + _change_block?: InputMaybe; + collaborators?: InputMaybe>; + collaborators_contains?: InputMaybe>; + collaborators_contains_nocase?: InputMaybe>; + collaborators_not?: InputMaybe>; + collaborators_not_contains?: InputMaybe>; + collaborators_not_contains_nocase?: InputMaybe>; + createdAt?: InputMaybe; + createdAt_gt?: InputMaybe; + createdAt_gte?: InputMaybe; + createdAt_in?: InputMaybe>; + createdAt_lt?: InputMaybe; + createdAt_lte?: InputMaybe; + createdAt_not?: InputMaybe; + createdAt_not_in?: InputMaybe>; + description?: InputMaybe; + description_contains?: InputMaybe; + description_contains_nocase?: InputMaybe; + description_ends_with?: InputMaybe; + description_ends_with_nocase?: InputMaybe; + description_gt?: InputMaybe; + description_gte?: InputMaybe; + description_in?: InputMaybe>; + description_lt?: InputMaybe; + description_lte?: InputMaybe; + description_not?: InputMaybe; + description_not_contains?: InputMaybe; + description_not_contains_nocase?: InputMaybe; + description_not_ends_with?: InputMaybe; + description_not_ends_with_nocase?: InputMaybe; + description_not_in?: InputMaybe>; + description_not_starts_with?: InputMaybe; + description_not_starts_with_nocase?: InputMaybe; + description_starts_with?: InputMaybe; + description_starts_with_nocase?: InputMaybe; + externalLink?: InputMaybe; + externalLink_contains?: InputMaybe; + externalLink_contains_nocase?: InputMaybe; + externalLink_ends_with?: InputMaybe; + externalLink_ends_with_nocase?: InputMaybe; + externalLink_gt?: InputMaybe; + externalLink_gte?: InputMaybe; + externalLink_in?: InputMaybe>; + externalLink_lt?: InputMaybe; + externalLink_lte?: InputMaybe; + externalLink_not?: InputMaybe; + externalLink_not_contains?: InputMaybe; + externalLink_not_contains_nocase?: InputMaybe; + externalLink_not_ends_with?: InputMaybe; + externalLink_not_ends_with_nocase?: InputMaybe; + externalLink_not_in?: InputMaybe>; + externalLink_not_starts_with?: InputMaybe; + externalLink_not_starts_with_nocase?: InputMaybe; + externalLink_starts_with?: InputMaybe; + externalLink_starts_with_nocase?: InputMaybe; + id?: InputMaybe; + id_gt?: InputMaybe; + id_gte?: InputMaybe; + id_in?: InputMaybe>; + id_lt?: InputMaybe; + id_lte?: InputMaybe; + id_not?: InputMaybe; + id_not_in?: InputMaybe>; + image?: InputMaybe; + image_contains?: InputMaybe; + image_contains_nocase?: InputMaybe; + image_ends_with?: InputMaybe; + image_ends_with_nocase?: InputMaybe; + image_gt?: InputMaybe; + image_gte?: InputMaybe; + image_in?: InputMaybe>; + image_lt?: InputMaybe; + image_lte?: InputMaybe; + image_not?: InputMaybe; + image_not_contains?: InputMaybe; + image_not_contains_nocase?: InputMaybe; + image_not_ends_with?: InputMaybe; + image_not_ends_with_nocase?: InputMaybe; + image_not_in?: InputMaybe>; + image_not_starts_with?: InputMaybe; + image_not_starts_with_nocase?: InputMaybe; + image_starts_with?: InputMaybe; + image_starts_with_nocase?: InputMaybe; + name?: InputMaybe; + name_contains?: InputMaybe; + name_contains_nocase?: InputMaybe; + name_ends_with?: InputMaybe; + name_ends_with_nocase?: InputMaybe; + name_gt?: InputMaybe; + name_gte?: InputMaybe; + name_in?: InputMaybe>; + name_lt?: InputMaybe; + name_lte?: InputMaybe; + name_not?: InputMaybe; + name_not_contains?: InputMaybe; + name_not_contains_nocase?: InputMaybe; + name_not_ends_with?: InputMaybe; + name_not_ends_with_nocase?: InputMaybe; + name_not_in?: InputMaybe>; + name_not_starts_with?: InputMaybe; + name_not_starts_with_nocase?: InputMaybe; + name_starts_with?: InputMaybe; + name_starts_with_nocase?: InputMaybe; +}; + +export enum NftContractMetadata_OrderBy { + Collaborators = "collaborators", + CreatedAt = "createdAt", + Description = "description", + ExternalLink = "externalLink", + Id = "id", + Image = "image", + Name = "name" +} + /** * Offer * @@ -3032,6 +3164,7 @@ export type OfferCollection = { externalId: Scalars["String"]; externalIdHash: Scalars["Bytes"]; id: Scalars["ID"]; + metadata?: Maybe; offers: Array; seller: Seller; sellerId: Scalars["BigInt"]; @@ -3100,6 +3233,27 @@ export type OfferCollection_Filter = { id_lte?: InputMaybe; id_not?: InputMaybe; id_not_in?: InputMaybe>; + metadata?: InputMaybe; + metadata_?: InputMaybe; + metadata_contains?: InputMaybe; + metadata_contains_nocase?: InputMaybe; + metadata_ends_with?: InputMaybe; + metadata_ends_with_nocase?: InputMaybe; + metadata_gt?: InputMaybe; + metadata_gte?: InputMaybe; + metadata_in?: InputMaybe>; + metadata_lt?: InputMaybe; + metadata_lte?: InputMaybe; + metadata_not?: InputMaybe; + metadata_not_contains?: InputMaybe; + metadata_not_contains_nocase?: InputMaybe; + metadata_not_ends_with?: InputMaybe; + metadata_not_ends_with_nocase?: InputMaybe; + metadata_not_in?: InputMaybe>; + metadata_not_starts_with?: InputMaybe; + metadata_not_starts_with_nocase?: InputMaybe; + metadata_starts_with?: InputMaybe; + metadata_starts_with_nocase?: InputMaybe; offers_?: InputMaybe; seller?: InputMaybe; sellerId?: InputMaybe; @@ -3138,6 +3292,7 @@ export enum OfferCollection_OrderBy { ExternalId = "externalId", ExternalIdHash = "externalIdHash", Id = "id", + Metadata = "metadata", Offers = "offers", Seller = "seller", SellerId = "sellerId" @@ -7194,6 +7349,7 @@ export type Query = { metadataAttributes: Array; metadataInterface?: Maybe; metadataInterfaces: Array; + nftContractMetadata: Array; offer?: Maybe; offerCollection?: Maybe; offerCollections: Array; @@ -7570,6 +7726,16 @@ export type QueryMetadataInterfacesArgs = { where?: InputMaybe; }; +export type QueryNftContractMetadataArgs = { + block?: InputMaybe; + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + subgraphError?: _SubgraphErrorPolicy_; + where?: InputMaybe; +}; + export type QueryOfferArgs = { block?: InputMaybe; id: Scalars["ID"]; @@ -9148,6 +9314,7 @@ export type Subscription = { metadataAttributes: Array; metadataInterface?: Maybe; metadataInterfaces: Array; + nftContractMetadata: Array; offer?: Maybe; offerCollection?: Maybe; offerCollections: Array; @@ -9524,6 +9691,16 @@ export type SubscriptionMetadataInterfacesArgs = { where?: InputMaybe; }; +export type SubscriptionNftContractMetadataArgs = { + block?: InputMaybe; + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + subgraphError?: _SubgraphErrorPolicy_; + where?: InputMaybe; +}; + export type SubscriptionOfferArgs = { block?: InputMaybe; id: Scalars["ID"]; @@ -10060,6 +10237,16 @@ export type GetSellerByIdQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }>; pendingSeller?: { __typename?: "PendingSeller"; @@ -10202,6 +10389,16 @@ export type GetSellerByIdQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -10977,6 +11174,16 @@ export type GetSellersQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }>; pendingSeller?: { __typename?: "PendingSeller"; @@ -11119,6 +11326,16 @@ export type GetSellersQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -12450,6 +12667,16 @@ export type GetDisputeResolverByIdQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -13177,6 +13404,16 @@ export type GetDisputeResolversQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -13987,6 +14224,16 @@ export type GetOfferCollectionsQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -14468,6 +14715,16 @@ export type GetOfferCollectionsQueryQuery = { owner: string; } | null; }>; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }>; }; @@ -14670,6 +14927,16 @@ export type OfferCollectionFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -15143,6 +15410,16 @@ export type OfferCollectionFieldsFragment = { owner: string; } | null; }>; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; export type BaseOfferCollectionFieldsFragment = { @@ -15153,6 +15430,16 @@ export type BaseOfferCollectionFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; export type SellerFieldsFragment = { @@ -15177,6 +15464,16 @@ export type SellerFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }>; pendingSeller?: { __typename?: "PendingSeller"; @@ -15319,6 +15616,16 @@ export type SellerFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -16502,6 +16809,16 @@ export type DisputeResolverFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -17927,6 +18244,16 @@ export type GetExchangeTokenByIdQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -18565,6 +18892,16 @@ export type GetExchangeTokensQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -19181,6 +19518,16 @@ export type ExchangeTokenFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -19972,6 +20319,16 @@ export type GetExchangeByIdQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -20692,6 +21049,16 @@ export type GetExchangesQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -21402,6 +21769,16 @@ export type ExchangeFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -22314,6 +22691,16 @@ export type GetBaseMetadataEntityByIdQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -23039,6 +23426,16 @@ export type GetBaseMetadataEntitiesQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -23754,6 +24151,16 @@ export type BaseMetadataEntityFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -24460,6 +24867,16 @@ export type BaseBaseMetadataEntityFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -25518,6 +25935,16 @@ export type GetProductV1ProductsWithVariantsQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -26452,6 +26879,16 @@ export type GetAllProductsWithNotVoidedVariantsQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -27400,6 +27837,16 @@ export type GetProductV1MetadataEntityByIdQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -28548,6 +28995,16 @@ export type GetProductV1MetadataEntitiesQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -29686,6 +30143,16 @@ export type ProductV1MetadataEntityFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -30815,6 +31282,16 @@ export type BaseProductV1MetadataEntityFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -32157,6 +32634,16 @@ export type BaseProductV1ProductWithVariantsFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -33080,6 +33567,16 @@ export type BaseProductV1ProductWithNotVoidedVariantsFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -34206,6 +34703,16 @@ export type GetOfferByIdQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -34924,6 +35431,16 @@ export type GetOffersQueryQuery = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -35691,6 +36208,16 @@ export type OfferFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -36284,6 +36811,16 @@ export type BaseOfferFieldsFragment = { collectionAddress: string; externalIdHash: string; externalId: string; + metadata?: { + __typename?: "NftContractMetadata"; + id: string; + name?: string | null; + description?: string | null; + image?: string | null; + externalLink?: string | null; + createdAt: string; + collaborators?: Array | null; + } | null; }; exchangeToken: { __typename?: "ExchangeToken"; @@ -36774,6 +37311,15 @@ export const BaseOfferCollectionFieldsFragmentDoc = gql` collectionAddress externalIdHash externalId + metadata { + id + name + description + image + externalLink + createdAt + collaborators + } } `; export const SellerMetadataMediaFieldsFragmentDoc = gql` diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 60148865e..59c0db601 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -460,6 +460,20 @@ type OfferCollection @entity { externalIdHash: Bytes! externalId: String! offers: [Offer!]! @derivedFrom(field: "collection") + metadata: NftContractMetadata +} + +""" +Nft Contract Metadata +""" +type NftContractMetadata @entity { + id: ID! + createdAt: BigInt! + name: String + description: String + image: String + externalLink: String + collaborators: [String!] } """ diff --git a/packages/subgraph/src/entities/metadata/handler.ts b/packages/subgraph/src/entities/metadata/handler.ts index 395725992..79ad6f3a4 100644 --- a/packages/subgraph/src/entities/metadata/handler.ts +++ b/packages/subgraph/src/entities/metadata/handler.ts @@ -5,6 +5,7 @@ import { saveBaseMetadata } from "./base"; import { getIpfsMetadataObject, parseIpfsHash } from "../../utils/ipfs"; import { convertToString } from "../../utils/json"; import { saveInnerSellerMetadata } from "./seller"; +import { saveInnerNftContractMetadata } from "./nft-contract"; // eslint-disable-next-line @typescript-eslint/ban-types export function saveMetadata(offer: Offer, timestamp: BigInt): string | null { @@ -70,3 +71,31 @@ export function saveSellerMetadata( return null; } + +export function saveCollectionMetadata( + collectionId: string, + collectionMetadataUri: string, + // eslint-disable-next-line @typescript-eslint/ban-types + timestamp: BigInt +): void { + if (collectionMetadataUri === "") { + return; + } + const ipfsHash = parseIpfsHash(collectionMetadataUri); + + if (ipfsHash === null) { + log.warning("Collection metadata URI does not contain supported CID: {}", [ + collectionMetadataUri + ]); + return; + } + const metadataObj = getIpfsMetadataObject(ipfsHash); + + if (metadataObj === null) { + log.warning("Could not load collection metadata with ipfsHash: {}", [ + ipfsHash + ]); + return; + } + saveInnerNftContractMetadata(collectionId, metadataObj, timestamp); +} diff --git a/packages/subgraph/src/entities/metadata/nft-contract/index.ts b/packages/subgraph/src/entities/metadata/nft-contract/index.ts new file mode 100644 index 000000000..72c1abc54 --- /dev/null +++ b/packages/subgraph/src/entities/metadata/nft-contract/index.ts @@ -0,0 +1,32 @@ +import { JSONValue, TypedMap, BigInt, log } from "@graphprotocol/graph-ts"; +import { convertToString, convertToStringArray } from "../../../utils/json"; +import { NftContractMetadata } from "../../../../generated/schema"; + +// source: https://docs.opensea.io/docs/contract-level-metadata + +export function saveInnerNftContractMetadata( + metadataId: string, + metadataObj: TypedMap, + // eslint-disable-next-line @typescript-eslint/ban-types + timestamp: BigInt +): void { + const name = convertToString(metadataObj.get("name")); + const description = convertToString(metadataObj.get("description")); + const image = convertToString(metadataObj.get("image")); + const externalLink = convertToString(metadataObj.get("external_link")); + const collaborators = convertToStringArray(metadataObj.get("collaborators")); + + let nftContractMetadata = NftContractMetadata.load(metadataId); + + if (!nftContractMetadata) { + nftContractMetadata = new NftContractMetadata(metadataId); + } + nftContractMetadata.createdAt = timestamp; + nftContractMetadata.name = name; + nftContractMetadata.description = description; + nftContractMetadata.image = image; + nftContractMetadata.externalLink = externalLink; + nftContractMetadata.collaborators = collaborators; + + nftContractMetadata.save(); +} diff --git a/packages/subgraph/src/mappings/account-handler.ts b/packages/subgraph/src/mappings/account-handler.ts index 18d6536d0..9d31a0d89 100644 --- a/packages/subgraph/src/mappings/account-handler.ts +++ b/packages/subgraph/src/mappings/account-handler.ts @@ -38,7 +38,10 @@ import { getAndSaveDisputeResolverFees } from "../entities/dispute-resolution"; import { saveAccountEventLog } from "../entities/event-log"; -import { saveSellerMetadata } from "../entities/metadata/handler"; +import { + saveCollectionMetadata, + saveSellerMetadata +} from "../entities/metadata/handler"; import { getSellerMetadataEntityId } from "../entities/metadata/seller"; export function checkSellerExist(sellerId: BigInt): boolean { @@ -95,6 +98,7 @@ export function handleSellerCreatedEvent(event: SellerCreated): void { const bosonVoucherContract = IBosonVoucher.bind( event.params.voucherCloneAddress ); + const collectionMetadataUri = bosonVoucherContract.contractURI(); let seller = Seller.load(sellerId); @@ -111,7 +115,7 @@ export function handleSellerCreatedEvent(event: SellerCreated): void { seller.authTokenId = authTokenFromEvent.tokenId; seller.authTokenType = authTokenFromEvent.tokenType; seller.active = true; - seller.contractURI = bosonVoucherContract.contractURI(); + seller.contractURI = collectionMetadataUri; seller.royaltyPercentage = bosonVoucherContract.getRoyaltyPercentage(); seller.metadataUri = sellerFromEvent.metadataUri || ""; seller.metadata = getSellerMetadataEntityId(seller.id.toString()); @@ -121,8 +125,14 @@ export function handleSellerCreatedEvent(event: SellerCreated): void { const externalId = "initial"; const externalIdHash = crypto.keccak256(Bytes.fromUTF8(externalId)); // save original collection + const collectionId = getOfferCollectionId(sellerId, "0"); + saveCollectionMetadata( + collectionId, + collectionMetadataUri, + event.block.timestamp + ); saveOfferCollection( - getOfferCollectionId(sellerId, "0"), + collectionId, event.params.sellerId, new BigInt(0), event.params.voucherCloneAddress, @@ -576,6 +586,7 @@ function saveOfferCollection( offerCollection.collectionAddress = collectionAddress; offerCollection.externalIdHash = externalIdHash; offerCollection.externalId = externalId; + offerCollection.metadata = offerCollectionId; offerCollection.save(); } else { log.warning("Offer collection with ID '{}' already exists!", [ @@ -610,6 +621,15 @@ export function handleCollectionCreatedEvent(event: CollectionCreated): void { externalId = collections[collectionIndex.toU32() - 1].externalId; } + const bosonVoucherContract = IBosonVoucher.bind( + event.params.collectionAddress + ); + const collectionMetadataUri = bosonVoucherContract.contractURI(); + saveCollectionMetadata( + offerCollectionId, + collectionMetadataUri, + event.block.timestamp + ); saveOfferCollection( offerCollectionId, sellerId,