dcl-catalyst-client
Version:
A client to query and perform changes on Decentraland's catalyst servers
162 lines • 7.12 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildEntityWithoutNewFiles = exports.buildEntity = exports.buildEntityAndFile = void 0;
const hashing_1 = require("@dcl/hashing");
const schemas_1 = require("@dcl/schemas");
/**
* Take all the entity's data, build the entity file with it, and calculate its id
*/
async function buildEntityAndFile({ type, pointers, timestamp, content, metadata }) {
var _a;
// Make sure that there is at least one pointer
if (pointers.length === 0)
throw new Error(`All entities must have at least one pointer.`);
const entity = {
// default version is V3
version: 'v3',
type,
pointers,
timestamp,
content,
metadata
};
// prevent duplicated file names
if (content) {
const usedFilenames = new Set();
for (const a of content) {
const lowerCasedFileName = a.file.toLowerCase();
if (usedFilenames.has(lowerCasedFileName)) {
throw new Error(`Error creating the deployable entity: Decentraland's file system is case insensitive, the file ${JSON.stringify(a.file)} is repeated`);
}
usedFilenames.add(lowerCasedFileName);
}
}
const entityFile = new TextEncoder().encode(JSON.stringify(entity));
const entityId = await (0, hashing_1.hashV1)(entityFile);
const entityWithId = {
id: entityId,
...entity
};
if (!schemas_1.Entity.validate(entityWithId)) {
throw new Error('Generated entity is not valid:\n' + ((_a = schemas_1.Entity.validate.errors) === null || _a === void 0 ? void 0 : _a.map(($) => $.message).join('\n')));
}
return { entity: entityWithId, entityFile };
}
exports.buildEntityAndFile = buildEntityAndFile;
/**
* As part of the deployment process, an entity has to be built. In this method, we are building it, based on the data provided.
* After the entity is built, the user will have to sign the entity id, to prove they are actually who they say they are.
*/
async function buildEntity({ type, pointers, files, metadata, timestamp }) {
// Reorder input
const contentFiles = Array.from(files ? files : []).map(([key, content]) => ({
key,
content
}));
// Calculate hashes
const allInfo = await Promise.all(contentFiles.map(async ({ key, content }) => ({ key, content, hash: await (0, hashing_1.hashV1)(content) })));
const hashesByKey = new Map(allInfo.map(({ hash, key }) => [key, hash]));
const filesByHash = new Map(allInfo.map(({ hash, content }) => [hash, content]));
return buildEntityInternal(type, pointers, {
hashesByKey,
filesByHash,
metadata,
timestamp
});
}
exports.buildEntity = buildEntity;
/**
* In cases where we don't need upload content files, we can simply generate the new entity.
* We can still use already uploaded hashes on this new entity.
*/
async function buildEntityWithoutNewFiles(fetcher, { contentUrl, type, pointers, hashesByKey, metadata, timestamp }) {
const givenFilesMaps = (hashesByKey !== null && hashesByKey !== void 0 ? hashesByKey : metadata) ? getHashesByKey(metadata) : undefined;
// When the old entity has the old hashing algorithm, then the full entity with new hash will need to be deployed.
if (!!givenFilesMaps && isObsoleteProfile(type, givenFilesMaps)) {
const files = await downloadAllFiles(fetcher, contentUrl, givenFilesMaps);
const metadataWithNewHash = await updateMetadata(files, metadata);
return buildEntity({
type,
pointers,
files,
metadata: metadataWithNewHash,
timestamp
});
}
return buildEntityInternal(type, pointers, { hashesByKey, metadata, timestamp });
}
exports.buildEntityWithoutNewFiles = buildEntityWithoutNewFiles;
async function buildEntityInternal(type, pointers, options) {
// Make sure that there is at least one pointer
if (pointers.length === 0) {
throw new Error(`All entities must have at least one pointer.`);
}
// Re-organize the hashes
const hashesByKey = (options === null || options === void 0 ? void 0 : options.hashesByKey) ? options === null || options === void 0 ? void 0 : options.hashesByKey : new Map();
const entityContent = Array.from(hashesByKey.entries()).map(([key, hash]) => ({
file: key,
hash
}));
// Calculate timestamp if necessary
const timestamp = (options === null || options === void 0 ? void 0 : options.timestamp) ? options === null || options === void 0 ? void 0 : options.timestamp : Date.now();
// Build entity file
const { entity, entityFile } = await buildEntityAndFile({
type,
pointers,
timestamp,
content: entityContent,
metadata: options === null || options === void 0 ? void 0 : options.metadata
});
// Add entity file to content files
const filesByHash = (options === null || options === void 0 ? void 0 : options.filesByHash) ? options.filesByHash : new Map();
filesByHash.set(entity.id, entityFile);
return { files: filesByHash, entityId: entity.id };
}
function isObsoleteProfile(type, hashesByKey) {
return type === schemas_1.EntityType.PROFILE && Array.from(hashesByKey).some(([, hash]) => hash.toLowerCase().startsWith('qm'));
}
async function fetchArrayBuffer(fetcher, url) {
const response = await fetcher.fetch(url);
const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
}
async function downloadAllFiles(fetcher, contentUrl, hashes) {
const oldBodyHash = hashes.get('body.png');
const oldFaceHash = hashes.get('face256.png');
const bodyUrl = new URL(`${contentUrl}/contents/${oldBodyHash}`).toString();
const faceUrl = new URL(`${contentUrl}/contents/${oldFaceHash}`).toString();
const [bodyFileContent, faceFileContent] = await Promise.all([
fetchArrayBuffer(fetcher, bodyUrl),
fetchArrayBuffer(fetcher, faceUrl)
]);
return new Map([
['body.png', bodyFileContent],
['face256.png', faceFileContent]
]);
}
async function updateMetadata(files, metadata) {
if (!metadata)
return metadata;
metadata.avatars = await Promise.all(metadata.avatars.map(async (avatar) => {
const newSnapshots = { face256: '', body: '' };
const face256Content = files.get('face256.png');
if (!!face256Content) {
newSnapshots['face256'] = await (0, hashing_1.hashV1)(face256Content);
}
const bodyContent = files.get('body.png');
if (!!bodyContent) {
newSnapshots['body'] = await (0, hashing_1.hashV1)(bodyContent);
}
avatar.avatar.snapshots = newSnapshots;
return avatar;
}));
return metadata;
}
function getHashesByKey(metadata) {
const avatar = metadata.avatars[0];
return new Map([
['body.png', avatar.avatar.snapshots.body],
['face256.png', avatar.avatar.snapshots.face256]
]);
}
//# sourceMappingURL=DeploymentBuilder.js.map