import { AttributeSchema, AttributeType, COLLECTION_SCHEMA_NAME, CollectionAttributesSchema, DecodedAttributes, DecodedInfixOrUrlOrCidAndHash, DecodingImageLinkOptions, LocalizedStringWithDefault, UniqueCollectionSchemaDecoded, UniqueTokenDecoded, UrlTemplateString } from "../types"; import {validateURLSafe} from "./validators"; import {CollectionProperties} from "../../substrate/extrinsics/unique/types"; import {DecodingResult} from "../schemaUtils"; import {CrossAccountId, HumanizedNftToken} from "../../types"; import type {Message, Type} from 'protobufjs' import {Root} from 'protobufjs' import {ValidationError} from "../../utils/errors"; import {getEntries, getKeys, getValues, safeJSONParse} from "../../tsUtils"; import {StringUtils, UniqueUtils} from "../../utils"; const isOffchainSchemaAValidUrl = (offchainSchema: string | undefined): offchainSchema is string => { return typeof offchainSchema === "string" && validateURLSafe(offchainSchema, 'offchainSchema') && offchainSchema.indexOf('{id}') >= 0 } export const decodeOldSchemaCollection = async (collectionId: number, properties: CollectionProperties, options: Required): Promise> => { const {imageUrlTemplate, dummyImageFullUrl} = options const propObj = properties.reduce((acc, {key, value}) => { acc[key] = value; return acc }, {} as Record) const offchainSchema: string | undefined = propObj._old_offchainSchema const constOnchainSchema: string | undefined = propObj._old_constOnChainSchema const schemaVersion: string | undefined = propObj._old_schemaVersion const variableOnchainSchema: string | undefined = propObj._old_variableOnChainSchema const offchainSchemaIsValidUrl = isOffchainSchemaAValidUrl(offchainSchema) const schema: UniqueCollectionSchemaDecoded = { schemaName: COLLECTION_SCHEMA_NAME.old, collectionId, coverPicture: { url: dummyImageFullUrl, fullUrl: null }, image: { urlTemplate: offchainSchemaIsValidUrl ? offchainSchema.replace('{id}', '{infix}') as UrlTemplateString : imageUrlTemplate }, schemaVersion: '0.0.1', attributesSchema: {}, attributesSchemaVersion: '1.0.0' } let parsedVariableOnchainSchema = null try { parsedVariableOnchainSchema = JSON.parse(variableOnchainSchema) } catch { } if (parsedVariableOnchainSchema && typeof parsedVariableOnchainSchema === 'object' && typeof parsedVariableOnchainSchema.collectionCover === 'string') { schema.coverPicture.ipfsCid = parsedVariableOnchainSchema.collectionCover delete schema.coverPicture.url schema.coverPicture.fullUrl = imageUrlTemplate.replace('{infix}', parsedVariableOnchainSchema.collectionCover) } else if (offchainSchemaIsValidUrl) { const coverUrl = offchainSchema.replace('{id}', '1') schema.coverPicture.url = coverUrl schema.coverPicture.fullUrl = coverUrl } let root: Root = {} as any let NFTMeta: Type = {} as any try { root = Root.fromJSON(JSON.parse(constOnchainSchema)) NFTMeta = root.lookupType('onChainMetaData.NFTMeta') } catch (err: any) { return { result: null, error: err as Error, } } const attributesSchema: CollectionAttributesSchema = {} let i = 0; for (const field of NFTMeta.fieldsArray) { if (field.name === 'ipfsJson') { continue } const options = !['string', 'number'].includes(field.type) && root.lookupEnum(field.type).options; const parsedOptions: LocalizedStringWithDefault[] = options ? getValues(options) .map(v => safeJSONParse<{en: string | undefined}>(v)) .filter(v => typeof v !== 'string' && typeof v.en === 'string') .map(v => { const result: any = {...(v as any)} if (typeof result._ === 'string') return result result._ = result.en || result[getKeys(result)[0]] || undefined if (typeof result._ !== 'string') return null return result; }) .filter(v => !!v) : [] const attr: AttributeSchema = { type: AttributeType.string, name: {_: field.name}, isArray: field.repeated, optional: !field.required, } if (parsedOptions.length) { attr.enumValues = parsedOptions.reduce( (acc, el, index) => { acc[index] = el return acc }, {} as {[K: number]: LocalizedStringWithDefault} ) } attributesSchema[i++] = attr } schema.attributesSchema = attributesSchema schema.attributesSchemaVersion = '1.0.0' schema.oldProperties = { _old_schemaVersion: schemaVersion, _old_offchainSchema: offchainSchema, _old_constOnChainSchema: constOnchainSchema, _old_variableOnChainSchema: variableOnchainSchema, } return {result: schema, error: null} } //todo: replace rawToken type with humanized token after core team's fix export const decodeOldSchemaToken = async (collectionId: number, tokenId: number, rawToken: { owner: any, properties: any[] }, schema: UniqueCollectionSchemaDecoded, options: Required): Promise> => { const constOnchainSchema = schema.oldProperties?._old_constOnChainSchema if (!constOnchainSchema) { return { result: null, error: new ValidationError(`collection doesn't contain _old_constOnChainSchema field`) } } let root: Root = {} as any let NFTMeta: Type = {} as any try { root = Root.fromJSON(JSON.parse(constOnchainSchema)) NFTMeta = root.lookupType('onChainMetaData.NFTMeta') } catch (err: any) { return { result: null, error: err as Error, } } if (!rawToken) { return { result: null, error: new ValidationError(`parsing token with old schema: no token passed`) } } const parsedToken: HumanizedNftToken = { owner: rawToken.owner.toHuman() as CrossAccountId, properties: rawToken.properties.map(property => { return { key: property.key.toHuman() as string, value: property.value.toJSON() as string, // valueH: property.value.toHuman() as string, } }) } const constDataProp = parsedToken.properties.find(({key}) => key === '_old_constData') if (!constDataProp) { return { result: null, error: new ValidationError('no _old_constData property found') } } const u8aToken = StringUtils.hexToU8a(constDataProp.value) let tokenDecoded: Message<{}> = {} as any let tokenDecodedHuman: Record = {} try { tokenDecoded = NFTMeta.decode(u8aToken) tokenDecodedHuman = tokenDecoded.toJSON() } catch (err: any) { return { result: null, error: err } } const tokenAttributesResult: DecodedAttributes = {} const entries = getEntries(tokenDecodedHuman) let i = 0 for (const entry of entries) { let [name, rawValue] = entry as [string, any] if (name === 'ipfsJson') { continue } let value = rawValue let isArray = false let isEnum = false const field = tokenDecoded.$type.fields[name] if (!['string', 'number'].includes(field.type)) { const enumOptions = root.lookupEnum(field.type).options isEnum = !!enumOptions; if (field.rule === 'repeated' && Array.isArray(rawValue)) { const parsedValues = rawValue .map((v: any) => { const parsed = safeJSONParse(enumOptions?.[v] || v) if (typeof parsed !== 'string') { parsed._ = parsed.en return parsed } else { return null } }) .filter(v => typeof v?._ === 'string') value = parsedValues isArray = true } else { value = safeJSONParse(enumOptions?.[rawValue] || rawValue) if (typeof value !== 'string') { value._ = value.en || getValues(value)[0] } } } if (field.type === 'string') value = {_:value} tokenAttributesResult[i++] = { name: {_: name}, type: field.type === 'number' ? AttributeType.float : AttributeType.string, value, isArray, isEnum, rawValue, } } const schemaVersion = schema.oldProperties?._old_schemaVersion const offchainSchema = schema.oldProperties?._old_offchainSchema const {imageUrlTemplate, dummyImageFullUrl} = options let image: DecodedInfixOrUrlOrCidAndHash = { url: dummyImageFullUrl, fullUrl: null, } let ipfsImageIsSet = false if (schemaVersion === 'Unique') { try { const ipfsCid = JSON.parse(tokenDecodedHuman.ipfsJson).ipfs image = { ipfsCid, fullUrl: imageUrlTemplate.replace('{infix}', ipfsCid) } ipfsImageIsSet = true } catch { } } if (!ipfsImageIsSet && isOffchainSchemaAValidUrl(offchainSchema)) { image = { urlInfix: tokenId.toString(), fullUrl: offchainSchema.replace('{id}', tokenId.toString()) } } const decodedToken: UniqueTokenDecoded = { collectionId, tokenId, owner: parsedToken.owner, image, attributes: tokenAttributesResult, } if (parsedToken.owner.Ethereum && UniqueUtils.Address.is.nestingAddress(parsedToken.owner.Ethereum)) { decodedToken.nestingParentToken = UniqueUtils.Address.nesting.addressToIds(parsedToken.owner.Ethereum) } return { result: decodedToken, error: null, } }