/**
* Methods for deploying and interacting with contracts
*
* @module ElvClient/Contracts
*/
const Ethers = require("ethers");
//const ContentContract = require("../contracts/BaseContent");
const {
ValidateAddress,
ValidateParameters,
ValidatePresence,
ValidateObject,
ValidateVersion
} = require("../Validation");
const Utils=require("../Utils");
/**
* Return the name of the contract, as specified in the contracts "version" string
*
* @methodGroup Contracts
*
* @namedParams
* @param {string} contractAddress - Address of the contract
*
* @return {Promise<string>} - Name of the contract
*/
exports.ContractName = async function({contractAddress}) {
contractAddress = ValidateAddress(contractAddress);
return await this.ethClient.ContractName(contractAddress);
};
/**
* Retrieve the ABI for the given contract via its address or a Fabric ID. Contract must be a standard Eluvio contract
*
* @methodGroup Contracts
* @namedParams
* @param {string=} contractAddress - The address of the contract
* @param {string=} id - The Fabric ID of the contract
*
* @return {Promise<Object>} - The ABI for the given contract
*
* @throws If ABI is not able to be determined, throws an error
*/
exports.ContractAbi = async function({contractAddress, id}) {
const contractInfo = await this.authClient.ContractInfo({address: contractAddress, id});
if(!contractInfo) {
throw Error(`Unable to determine contract info for ${contractAddress}`);
}
return contractInfo.abi;
};
/**
* Retrieve the ABI, access type, and whether V3 is used for a given contract via its address or a Fabric ID.
*
* @methodGroup Contracts
* @namedParams
* @param {string=} id - The Fabric ID of the contract
* @param {string=} address - The address of the contract
*
* @return {Promise<Object>} - The ABI, access type, and isV3 for the given contract
*/
exports.ContractInfo = async function({id, address}) {
return this.authClient.ContractInfo({id, address});
};
/**
* Format the arguments to be used for the specified method of the contract
*
* @methodGroup Contracts
* @namedParams
* @param {Object} abi - ABI of contract
* @param {string} methodName - Name of method for which arguments will be formatted
* @param {Array<string>} args - List of arguments
*
* @returns {Array<string>} - List of formatted arguments
*/
exports.FormatContractArguments = function({abi, methodName, args}) {
return this.ethClient.FormatContractArguments({abi, methodName, args});
};
/**
* Deploy a contract from ABI and bytecode. This client's signer will be the owner of the contract.
*
* @methodGroup Contracts
* @namedParams
* @param {Object} abi - ABI of contract
* @param {string} bytecode - Bytecode of the contract
* @param {Array<string>} constructorArgs - List of arguments to the contract constructor
* @param {Object=} overrides - Change default gasPrice or gasLimit used for this action
*
* @returns {Promise<Object>} - Response containing the deployed contract address and the transaction hash of the deployment
*/
exports.DeployContract = async function({abi, bytecode, constructorArgs, overrides={}}) {
return await this.ethClient.DeployContract({abi, bytecode, constructorArgs, overrides, signer: this.signer});
};
/**
* Get all events on the specified contract
*
* @methodGroup Contracts
* @namedParams
* @param {string} contractAddress - The address of the contract
* @param {Object=} abi - ABI of contract - If the contract is a standard Eluvio contract, this can be determined automatically if not specified
* @param {number=} fromBlock - Limit results to events after the specified block (inclusive)
* @param {number=} toBlock - Limit results to events before the specified block (inclusive)
* @param {number=} count=1000 - Maximum range of blocks to search (unless both toBlock and fromBlock are specified)
* @param {boolean=} includeTransaction=false - If specified, more detailed transaction info will be included.
* Note: This requires one extra network call per block, so it should not be used for very large ranges
* @returns {Promise<Array<Array<Object>>>} - List of blocks, in ascending order by block number, each containing a list of the events in the block.
*/
exports.ContractEvents = async function({
contractAddress,
abi,
fromBlock=0,
toBlock,
count=1000,
topics,
includeTransaction=false
}) {
contractAddress = ValidateAddress(contractAddress);
if(!abi) { abi = await this.ContractAbi({contractAddress}); }
const blocks = await this.FormatBlockNumbers({fromBlock, toBlock, count});
this.Log(`Querying contract events ${contractAddress} - Blocks ${blocks.fromBlock} to ${blocks.toBlock}`);
return await this.ethClient.ContractEvents({
contractAddress,
abi,
fromBlock: blocks.fromBlock,
toBlock: blocks.toBlock,
topics,
includeTransaction
});
};
/**
* Call the specified method on a deployed contract. This action will be performed by this client's signer.
*
* Use this method to call constant methods and contract attributes, as well as transaction-performing methods
* for which the transaction does not need to be awaited.
*
* @methodGroup Contracts
* @namedParams
* @param {string} contractAddress - Address of the contract to call the specified method on
* @param {Object=} abi - ABI of contract - If the contract is a standard Eluvio contract, this can be determined automatically if not specified
* @param {string} methodName - Method to call on the contract
* @param {Array=} methodArgs - List of arguments to the contract constructor
* @param {(number | BigNumber)=} value - Amount of ether to include in the transaction
* @param {boolean=} formatArguments=true - If specified, the arguments will automatically be formatted to the ABI specification
* @param {Object=} overrides - Change default gasPrice or gasLimit used for this action
*
* @returns {Promise<*>} - Response containing information about the transaction
*/
exports.CallContractMethod = async function({
contractAddress,
abi,
methodName,
methodArgs=[],
value,
overrides={},
formatArguments=true,
cacheContract=true,
overrideCachedContract=false
}) {
contractAddress = ValidateAddress(contractAddress);
// Delete cached visibility value if it is being changed
contractAddress = this.utils.FormatAddress(contractAddress);
if(methodName === "setVisibility" && this.visibilityInfo[contractAddress]) {
delete this.visibilityInfo[contractAddress];
}
if(!abi) { abi = await this.ContractAbi({contractAddress}); }
return await this.ethClient.CallContractMethod({
contractAddress,
abi,
methodName,
methodArgs,
value,
overrides,
formatArguments,
cacheContract,
overrideCachedContract
});
};
/**
* Call the specified method on a deployed contract and wait for the transaction to be mined.
* This action will be performed by this client's signer.
*
* Use this method to call transaction-performing methods and wait for the transaction to complete.
*
* @methodGroup Contracts
* @namedParams
* @param {string} contractAddress - Address of the contract to call the specified method on
* @param {Object=} abi - ABI of contract - If the contract is a standard Eluvio contract, this can be determined automatically if not specified
* @param {string} methodName - Method to call on the contract
* @param {Array<string>=} methodArgs=[] - List of arguments to the contract constructor
* @param {(number | BigNumber)=} value - Amount of ether to include in the transaction
* @param {Object=} overrides - Change default gasPrice or gasLimit used for this action
* @param {boolean=} formatArguments=true - If specified, the arguments will automatically be formatted to the ABI specification
*
* @see Utils.WeiToEther
*
* @returns {Promise<*>} - The event object of this transaction. See the ExtractEventFromLogs method for parsing
* the resulting event(s)
*/
exports.CallContractMethodAndWait = async function({
contractAddress,
abi,
methodName,
methodArgs,
value,
overrides={},
formatArguments=true,
cacheContract=true,
overrideCachedContract=false
}) {
contractAddress = ValidateAddress(contractAddress);
// Delete cached visibility value if it is being changed
contractAddress = this.utils.FormatAddress(contractAddress);
if(methodName === "setVisibility" && this.visibilityInfo[contractAddress]) {
delete this.visibilityInfo[contractAddress];
}
if(!abi) { abi = await this.ContractAbi({contractAddress}); }
return await this.ethClient.CallContractMethodAndWait({
contractAddress,
abi,
methodName,
methodArgs,
value,
overrides,
formatArguments,
cacheContract,
overrideCachedContract
});
};
/**
* Retrieve metadata from the specified contract
*
* @methodGroup Contracts
* @namedParams
* @param {string} contractAddress - The address of the contract
* @param {string} metadataKey - The metadata key to retrieve
*
* @return {Promise<Object|string>}
*/
exports.ContractMetadata = async function({contractAddress, metadataKey, }) {
ValidatePresence("contractAddress", contractAddress);
ValidatePresence("metadataKey", metadataKey);
try {
const metadata = await this.CallContractMethod({
contractAddress,
methodName: "getMeta",
methodArgs: [metadataKey]
});
const data = Buffer.from((metadata || "").replace("0x", ""), "hex").toString("utf-8");
try {
return JSON.parse(data);
} catch(error) {
return data;
}
} catch(error) {
return "";
}
};
/**
* Merge contract metadata at the specified key.
*
* @methodGroup Contracts
* @namedParams
* @param {string} contractAddress - The address of the contract
* @param {string} metadataKey - The metadata key to retrieve
* @param {string} metadata
*/
exports.MergeContractMetadata = async function({contractAddress, metadataKey, metadata}) {
ValidatePresence("contractAddress", contractAddress);
ValidatePresence("metadataKey", metadataKey);
const existingMetadata = await this.ContractMetadata({contractAddress, metadataKey}) || {};
if(typeof existingMetadata === "object") {
metadata = {
...existingMetadata,
...metadata
};
}
await this.CallContractMethodAndWait({
contractAddress,
methodName: "putMeta",
methodArgs: [
metadataKey,
JSON.stringify(metadata)
]
});
};
/**
* Replace the contract metadata at the specified key
*
* @methodGroup Contracts
* @namedParams
* @param {string} contractAddress - The address of the contract
* @param {string} metadataKey - The metadata key to retrieve
* @param {string|Object} metadata - The metadata to insert
*/
exports.ReplaceContractMetadata = async function({contractAddress, metadataKey, metadata}) {
ValidatePresence("contractAddress", contractAddress);
ValidatePresence("metadataKey", metadataKey);
if(typeof metadata === "object") {
metadata = JSON.stringify(metadata);
}
await this.CallContractMethodAndWait({
contractAddress,
methodName: "putMeta",
methodArgs: [
metadataKey,
metadata
]
});
};
/**
* Get the custom contract of the specified object
*
* @methodGroup Contracts
* @namedParams
* @param {string=} libraryId - ID of the library
* @param {string=} objectId - ID of the object
* @param {string=} versionHash - Version hash of the object
*
* @returns {Promise<string> | undefined} - If the object has a custom contract, this will return the address of the custom contract
*/
exports.CustomContractAddress = async function({libraryId, objectId, versionHash}) {
ValidateParameters({libraryId, objectId, versionHash});
if(versionHash) {
objectId = this.utils.DecodeVersionHash(versionHash).objectId;
}
if(libraryId === this.contentSpaceLibraryId || this.utils.EqualHash(libraryId, objectId)) {
// Content type or content library object - no custom contract
return;
}
this.Log(`Retrieving custom contract address: ${objectId}`);
const abi = await this.ContractAbi({id: objectId});
const customContractAddress = await this.ethClient.CallContractMethod({
contractAddress: this.utils.HashToAddress(objectId),
abi,
methodName: "contentContractAddress",
methodArgs: []
});
if(customContractAddress === this.utils.nullAddress) { return; }
return this.utils.FormatAddress(customContractAddress);
};
/**
* Set the custom contract of the specified object with the contract at the specified address
*
* Note: This also updates the content object metadata with information about the contract - particularly the ABI
*
* @methodGroup Contracts
* @namedParams
* @param {string} libraryId - ID of the library
* @param {string} objectId - ID of the object
* @param {string} customContractAddress - Address of the deployed custom contract
* @param {string=} name - Optional name of the custom contract
* @param {string=} description - Optional description of the custom contract
* @param {Object} abi - ABI of the custom contract
* @param {Object=} factoryAbi - If the custom contract is a factory, the ABI of the contract it deploys
* @param {Object=} overrides - Change default gasPrice or gasLimit used for this action
*
* @returns {Promise<Object>} - Result transaction of calling the setCustomContract method on the content object contract
*/
exports.SetCustomContentContract = async function({
libraryId,
objectId,
customContractAddress,
name,
description,
abi,
factoryAbi,
overrides={}
}) {
ValidateParameters({libraryId, objectId});
customContractAddress = ValidateAddress(customContractAddress);
customContractAddress = this.utils.FormatAddress(customContractAddress);
this.Log(`Setting custom contract address: ${objectId} ${customContractAddress}`);
const setResult = await this.ethClient.SetCustomContentContract({
contentContractAddress: this.utils.HashToAddress(objectId),
customContractAddress,
overrides,
signer: this.signer
});
const writeToken = (await this.EditContentObject({libraryId, objectId})).write_token;
await this.ReplaceMetadata({
libraryId,
objectId,
writeToken,
metadataSubtree: "custom_contract",
metadata: {
name,
description,
address: customContractAddress,
abi,
factoryAbi
}
});
await this.FinalizeContentObject({libraryId, objectId, writeToken, commitMessage: "Set custom contract"});
return setResult;
};
/**
* Extract the specified event log from the given event obtained from the
* CallContractAndMethodAndWait method
*
* @methodGroup Contracts
* @namedParams
* @param {string} contractAddress - Address of the contract to call the specified method on
*
* @param {Object} event - Event of the transaction from CallContractMethodAndWait
* @param {string} eventName - Name of the event to parse
*
* @see Utils.WeiToEther
*
* @returns {Promise<Object>} - The parsed event log from the event
*/
exports.ExtractEventFromLogs = function({abi, event, eventName}) {
return this.ethClient.ExtractEventFromLogs({abi, event, eventName});
};
/**
* Extract the specified value from the specified event log from the given event obtained
* from the CallContractAndMethodAndWait method
*
* @methodGroup Contracts
* @namedParams
* @param {string} contractAddress - Address of the contract to call the specified method on
* @param {Object} abi - ABI of contract
* @param {Object} event - Event of the transaction from CallContractMethodAndWait
* @param {string} eventName - Name of the event to parse
* @param {string} eventValue - Name of the value to extract from the event
*
* @returns {Promise<string>} The value extracted from the event
*/
exports.ExtractValueFromEvent = function({abi, event, eventName, eventValue}) {
const eventLog = this.ethClient.ExtractEventFromLogs({abi, event, eventName, eventValue});
return eventLog ? eventLog.args[eventValue] : undefined;
};
exports.FormatBlockNumbers = async function({fromBlock, toBlock, count=10}) {
const latestBlock = await this.BlockNumber();
if(!toBlock) {
if(!fromBlock) {
toBlock = latestBlock;
fromBlock = toBlock - count + 1;
} else {
toBlock = fromBlock + count - 1;
}
} else if(!fromBlock) {
fromBlock = toBlock - count + 1;
}
// Ensure block numbers are valid
if(toBlock > latestBlock) {
toBlock = latestBlock;
}
if(fromBlock < 0) {
fromBlock = 0;
}
return { fromBlock, toBlock };
};
/**
* Get events from the blockchain in reverse chronological order, starting from toBlock. This will also attempt
* to identify and parse any known Eluvio contract methods. If successful, the method name, signature, and input
* values will be included in the log entry.
*
* @methodGroup Blockchain
* @namedParams
* @param {number=} toBlock - Limit results to events before the specified block (inclusive) - If not specified, will start from latest block
* @param {number=} fromBlock - Limit results to events after the specified block (inclusive)
* @param {number=} count=10 - Max number of events to include (unless both toBlock and fromBlock are specified)
* @param {boolean=} includeTransaction=false - If specified, more detailed transaction info will be included.
* Note: This requires two extra network calls per transaction, so it should not be used for very large ranges
* @returns {Promise<Array<Array<Object>>>} - List of blocks, in ascending order by block number, each containing a list of the events in the block.
*/
exports.Events = async function({toBlock, fromBlock, count=10, includeTransaction=false}={}) {
const blocks = await this.FormatBlockNumbers({fromBlock, toBlock, count});
this.Log(`Querying events - Blocks ${blocks.fromBlock} to ${blocks.toBlock}`);
return await this.ethClient.Events({
fromBlock: blocks.fromBlock,
toBlock: blocks.toBlock,
includeTransaction
});
};
/**
* Retrieve the latest block number on the blockchain
*
* @methodGroup Blockchain
*
* @returns {Promise<number>} - The latest block number
*/
exports.BlockNumber = async function() {
return await this.ethClient.MakeProviderCall({methodName: "getBlockNumber"});
};
/**
* Get the balance (in ether) of the specified address
*
* @methodGroup Blockchain
* @namedParams
* @param {string} address - Address to query
*
* @returns {Promise<string>} - Balance of the account, in ether (as string)
*/
exports.GetBalance = async function({address}) {
address = ValidateAddress(address);
const balance = await this.ethClient.MakeProviderCall({methodName: "getBalance", args: [address]});
return Ethers.utils.formatEther(balance);
};
/**
* Send ether from this client's current signer to the specified recipient address
*
* @methodGroup Blockchain
* @namedParams
* @param {string} recipient - Address of the recipient
* @param {number} ether - Amount of ether to send
*
* @returns {Promise<Object>} - The transaction receipt
*/
exports.SendFunds = async function({recipient, ether}) {
recipient = ValidateAddress(recipient);
const transaction = await this.signer.sendTransaction({
to: recipient,
value: Ethers.utils.parseEther(ether.toString())
});
return await transaction.wait();
};
const GetObjectIDAndContractAddress = async function({contractAddress, objectId, versionHash}){
if(contractAddress){
ValidateAddress(contractAddress);
objectId = Utils.AddressToObjectId(contractAddress);
} else if(versionHash){
ValidateVersion(versionHash);
objectId = this.utils.DecodeVersionHash(versionHash).objectId;
contractAddress = Utils.HashToAddress(objectId);
} else if(objectId){
ValidateObject(objectId);
contractAddress=Utils.HashToAddress(objectId);
} else {
throw Error("contractAddress or objectId or versionHash not specified");
}
return {
contractAddress,
objectId
};
};
/**
* Retrieve the ID of the tenant admin group set for the specified object
*
* @methodGroup Tenant
* @namedParams
* @param {string=} contractAddress - The address of the object
* @param {string=} objectId - The ID of the object
* @param {string=} versionHash - A version hash of the object
*
* @returns {Promise<string|undefined>}
*/
exports.TenantId = async function({contractAddress, objectId, versionHash}) {
objectInfo = await GetObjectIDAndContractAddress({contractAddress, objectId, versionHash});
contractAddress = objectInfo.contractAddress;
objectId = objectInfo.objectId;
let tenantId;
try {
const hasGetMetaMethod = await this.authClient.ContractHasMethod({
contractAddress: contractAddress,
methodName: "getMeta"
});
if(hasGetMetaMethod) {
tenantId = await this.ContractMetadata({
contractAddress:contractAddress,
metadataKey:"_tenantId"
});
}
// If the getMeta method does not exist or is not set in the contract, check the fabric metadata.
if(tenantId === undefined) {
const libraryId = await this.ContentObjectLibraryId({ objectId });
tenantId = await this.ContentObjectMetadata({
libraryId,
objectId,
metadataSubtree: "tenantId",
});
}
return tenantId;
} catch(e) {
return "";
}
};
/**
* Retrieve the ID of the tenant contract for the specified object
*
* @methodGroup Tenant
* @namedParams
* @param {string=} contractAddress - The address of the object
* @param {string=} objectId - The ID of the object
* @param {string=} versionHash - A version hash of the object
*
* @returns {Promise<string|undefined>}
*/
exports.TenantContractId = async function({contractAddress, objectId, versionHash}) {
objectInfo = await GetObjectIDAndContractAddress({contractAddress, objectId, versionHash});
contractAddress = objectInfo.contractAddress;
objectId = objectInfo.objectId;
try {
const hasGetMetaMethod = await this.authClient.ContractHasMethod({
contractAddress: contractAddress,
methodName: "getMeta"
});
let tenantContractId;
if(hasGetMetaMethod) {
tenantContractId = await this.ContractMetadata({
contractAddress:contractAddress,
metadataKey:"_ELV_TENANT_ID"
});
}
// If the getMeta method does not exist or is not set in the contract, check the fabric metadata.
if(tenantContractId === undefined) {
const libraryId = await this.ContentObjectLibraryId({ objectId });
tenantContractId = await this.ContentObjectMetadata({
libraryId,
objectId,
metadataSubtree: "tenantContractId",
});
}
return tenantContractId;
} catch(e) {
return "";
}
};
/**
* Set the tenant contract ID and tenant admin group ID for the specified object
* when tenant admin group ID is provided
*
* @methodGroup Tenant
* @namedParams
* @param {string=} contractAddress - The address of the object
* @param {string=} objectId - The ID of the object
* @param {string=} versionHash - A version hash of the object
* @param {string} tenantContractId - The tenant contract ID to set
* @param {string} tenantId - The tenant ID to set
*
* @returns {Promise<{tenantId: (undefined|string), tenantContractId}>}
*/
exports.SetTenantId = async function({contractAddress, objectId, versionHash, tenantId}) {
objectInfo = await GetObjectIDAndContractAddress({contractAddress, objectId, versionHash});
contractAddress = objectInfo.contractAddress;
objectId = objectInfo.objectId;
const objectVersion = await this.authClient.AccessType(objectId);
if(objectVersion !== this.authClient.ACCESS_TYPES.GROUP &&
objectVersion !== this.authClient.ACCESS_TYPES.WALLET &&
objectVersion !== this.authClient.ACCESS_TYPES.LIBRARY &&
objectVersion !== this.authClient.ACCESS_TYPES.TYPE &&
objectVersion !== this.authClient.ACCESS_TYPES.TENANT) {
throw Error(`Invalid object ID: ${objectId},
applicable only for wallet,group, library or content_type object.`);
}
ValidateObject(tenantId);
if(!tenantId.startsWith("iten") || !Utils.ValidHash(tenantId)) {
throw Error(`Invalid tenant ID: ${tenantId}`);
}
const version = await this.authClient.AccessType(tenantId);
if(version !== this.authClient.ACCESS_TYPES.GROUP) {
throw Error("Invalid tenant ID: " + tenantId);
}
// get tenantContractId set for the tenant admin group
tenantContractId = await this.TenantContractId({
objectId: tenantId
});
if(tenantContractId){
return await this.SetTenantContractId({
contractAddress,
objectId,
versionHash,
tenantContractId
});
} else {
throw Error("Invalid tenantId: tenant contract id not found");
}
};
/**
* Set the tenant contract ID and tenant admin group ID for the specified object
* when tenant contract ID is provided
*
* @methodGroup Tenant
* @namedParams
* @param {string=} contractAddress - The address of the object
* @param {string=} objectId - The ID of the object
* @param {string=} versionHash - A version hash of the object
* @param {string} tenantContractId - The tenant contract ID to set
*
* @returns {Promise<{tenantId: (undefined|string), tenantContractId}>}
*/
exports.SetTenantContractId = async function({contractAddress, objectId, versionHash, tenantContractId}) {
objectInfo = await GetObjectIDAndContractAddress({contractAddress, objectId, versionHash});
contractAddress = objectInfo.contractAddress;
objectId = objectInfo.objectId;
const objectVersion = await this.authClient.AccessType(objectId);
if(objectVersion !== this.authClient.ACCESS_TYPES.GROUP &&
objectVersion !== this.authClient.ACCESS_TYPES.WALLET &&
objectVersion !== this.authClient.ACCESS_TYPES.LIBRARY &&
objectVersion !== this.authClient.ACCESS_TYPES.TYPE &&
objectVersion !== this.authClient.ACCESS_TYPES.TENANT) {
throw Error(`Invalid object ID: ${objectId},
applicable only for wallet,group, library or content_type object.`);
}
ValidateObject(tenantContractId);
if(tenantContractId && (!tenantContractId.startsWith("iten") || !Utils.ValidHash(tenantContractId))) {
throw Error(`Invalid tenant ID: ${tenantContractId}`);
}
const tenantAddress = Utils.HashToAddress(tenantContractId);
const version = await this.authClient.AccessType(tenantContractId);
if(version !== this.authClient.ACCESS_TYPES.TENANT) {
throw Error("Invalid tenant ID: " + tenantContractId);
}
// get tenant admin group
const tenantAdminGroupAddress = await this.CallContractMethod({
contractAddress: tenantAddress,
methodName: "groupsMapping",
methodArgs: ["tenant_admin", 0],
formatArguments: true,
});
const hasPutMetaMethod = await this.authClient.ContractHasMethod({
contractAddress: contractAddress,
methodName: "putMeta"
});
if(hasPutMetaMethod) {
await this.ReplaceContractMetadata({
contractAddress: contractAddress,
metadataKey: "_ELV_TENANT_ID",
metadata: tenantContractId
});
if(tenantAdminGroupAddress){
await this.ReplaceContractMetadata({
contractAddress: contractAddress,
metadataKey: "_tenantId",
metadata: `iten${Utils.AddressToHash(tenantAdminGroupAddress)}`
});
} else {
// eslint-disable-next-line no-console
console.warn("No tenant ID associated with current tenant.");
}
} else {
const libraryId = await this.ContentObjectLibraryId({ objectId });
const editRequest = await this.EditContentObject({libraryId, objectId});
await this.MergeMetadata({
libraryId,
objectId,
writeToken: editRequest.write_token,
metadata: {
tenantContractId,
tenantId: !tenantAdminGroupAddress ? undefined : `iten${Utils.AddressToHash(tenantAdminGroupAddress)}`
},
});
await this.FinalizeContentObject({
libraryId,
objectId,
writeToken: editRequest.write_token,
commitMessage: "set tenant_contract_id"
});
}
return {
tenantContractId: tenantContractId,
tenantId: !tenantAdminGroupAddress ? undefined : `iten${Utils.AddressToHash(tenantAdminGroupAddress)}`
};
};
/**
* Remove the tenant contract ID and tenant admin group ID for the specified object
*
* @methodGroup Tenant
* @namedParams
* @param {string=} contractAddress - The address of the object
* @param {string=} objectId - The ID of the object
* @param {string=} versionHash - A version hash of the object
*
* @returns {Promise<void>}
*/
exports.ResetTenantId = async function({contractAddress, objectId, versionHash}) {
objectInfo = await GetObjectIDAndContractAddress({contractAddress, objectId, versionHash});
contractAddress = objectInfo.contractAddress;
objectId = objectInfo.objectId;
const objectVersion = await this.authClient.AccessType(objectId);
if(objectVersion !== this.authClient.ACCESS_TYPES.GROUP &&
objectVersion !== this.authClient.ACCESS_TYPES.WALLET &&
objectVersion !== this.authClient.ACCESS_TYPES.LIBRARY &&
objectVersion !== this.authClient.ACCESS_TYPES.TYPE &&
objectVersion !== this.authClient.ACCESS_TYPES.TENANT) {
throw Error(`Invalid object ID: ${objectId},
applicable only for wallet,group, library or content_type object.`);
}
let tenantContractId = this.TenantContractId({objectId});
let tenantId = this.TenantId({objectId});
if(tenantContractId || tenantId){
const hasPutMetaMethod = await this.authClient.ContractHasMethod({
contractAddress: contractAddress,
methodName: "putMeta"
});
if(hasPutMetaMethod) {
await this.ReplaceContractMetadata({
contractAddress: contractAddress,
metadataKey: "_ELV_TENANT_ID",
metadata: ""
});
await this.ReplaceContractMetadata({
contractAddress: contractAddress,
metadataKey: "_tenantId",
metadata: ""
});
} else {
const libraryId = await this.ContentObjectLibraryId({ objectId });
const editRequest = await this.EditContentObject({libraryId, objectId});
await this.MergeMetadata({
libraryId,
objectId,
writeToken: editRequest.write_token,
metadata: {
tenantContractId: undefined,
tenantId: undefined
},
});
await this.FinalizeContentObject({
libraryId,
objectId,
writeToken: editRequest.write_token,
commitMessage: "remove tenant_contract_id"
});
}
} else {
// eslint-disable-next-line no-console
console.warn("No tenant ID associated with current tenant.");
}
};
/**
* Enum for object types that can be cleaned up after object deletion.
* Used by the ObjectCleanup method to determine which associated objects to clean.
*
* @property {string=} LIBRARY - Cleanup libraries
* @property {string=} CONTENT_OBJECT - Cleanup content objects
* @property {string=} GROUP - Cleanup access groups
* @property {string=} CONTENT_TYPE - Cleanup content types
* @property {string=} ALL - Cleanup all of the above
*/
const ObjectTypesToClean = Object.freeze({
LIBRARY: "library",
CONTENT_OBJECT: "content_object",
GROUP: "group",
CONTENT_TYPE: "content_type",
ALL: "all"
});
/**
* Cleans up deleted objects pointed to by the access index of a given "access group" or "user wallet"
* Contracts of type "access group" and "user wallet" contain an "access index" - a list of objects that they have access to.
*
* There are 4 specific indexes, one for each object type:
* - content
* - library
* - access groups
* - content types
*
* If an object gets deleted and the access index still points to it, it will cause errors in API calls for the access
* group or user wallet.
*
* This function checks each index for objects that are deleted, and removes them from the index (either all indexes or
* just the one specified by parameter 'objectTypeToClean')
*
* For user, the cleanup is performed on the user wallet and on all its access group
*
* @methodGroup Contracts
* @namedParams
* @param {string=} contractAddress - The address of the object
* @param {string=} objectId - The ID of the object
* @param {string=} versionHash - A version hash of the object
* @param {string=} objectTypeToClean - The type of object to clean: one of "library", "content_object", "group", "content_type", or "all"
* @returns {Promise<Object>} - Resolves with an object showing the count of items before and after cleanup.
*
* Example return value:
* {
* "0x123...": {
* beforeCleanup: {
* librariesLength: 2,
* contentObjectsLength: 4,
* accessGroupsLength: 1,
* contentTypesLength: 3
* },
* afterCleanup: {
* librariesLength: 0,
* contentObjectsLength: 0,
* accessGroupsLength: 0,
* contentTypesLength: 0
* }
* },
* "groups": {
* "0x123...": {
* beforeCleanup: {
* contentObjectsLength: 1
* },
* afterCleanup: {
* contentObjectsLength: 0
* }
* }
* }
* }
*/
exports.ObjectCleanup = async function ({
contractAddress,
objectId,
versionHash,
objectTypeToClean = ObjectTypesToClean.ALL
}) {
objectInfo = await GetObjectIDAndContractAddress({contractAddress, objectId, versionHash});
contractAddress = objectInfo.contractAddress;
let isUserWallet = false;
let userAddress;
// Check if the contract is a user wallet address
try {
await this.CallContractMethod({
contractAddress,
methodName: "getLibrariesLength",
formatArguments: false,
});
} catch(e) {
try {
userAddress = contractAddress;
contractAddress = await this.userProfileClient.UserWalletAddress({address: contractAddress});
isUserWallet = true;
} catch(walletError) {
throw new Error(`Invalid object: ${walletError.message}`);
}
}
const allowedTypes = Object.values(ObjectTypesToClean);
if(!allowedTypes.includes(objectTypeToClean)) {
throw Error(`Invalid objectType '${objectTypeToClean}'. Allowed types: ${allowedTypes.join(", ")}`);
}
const cleanupTasks = {
[ObjectTypesToClean.LIBRARY]: async ({contractAddress, res}) => {
const before = await this.CallContractMethod({
contractAddress,
methodName: "getLibrariesLength",
formatArguments: false,
});
res.beforeCleanup.librariesLength = before.toNumber();
await this.CallContractMethodAndWait({
contractAddress,
methodName: "cleanUpLibraries",
formatArguments: true,
});
const after = await this.CallContractMethod({
contractAddress,
methodName: "getLibrariesLength",
formatArguments: false,
});
res.afterCleanup.librariesLength = after.toNumber();
},
[ObjectTypesToClean.CONTENT_OBJECT]: async ({contractAddress, res}) => {
const before = await this.CallContractMethod({
contractAddress,
methodName: "getContentObjectsLength",
formatArguments: false,
});
res.beforeCleanup.contentObjectsLength = before.toNumber();
await this.CallContractMethodAndWait({
contractAddress,
methodName: "cleanUpContentObjects",
formatArguments: true,
});
const after = await this.CallContractMethod({
contractAddress,
methodName: "getContentObjectsLength",
formatArguments: false,
});
res.afterCleanup.contentObjectsLength = after.toNumber();
},
[ObjectTypesToClean.GROUP]: async ({contractAddress, res}) => {
let before = await this.CallContractMethod({
contractAddress,
methodName: "getAccessGroupsLength",
formatArguments: false,
});
res.beforeCleanup.accessGroupsLength = before.toNumber();
await this.CallContractMethodAndWait({
contractAddress,
methodName: "cleanUpAccessGroups",
formatArguments: true,
});
const after = await this.CallContractMethod({
contractAddress,
methodName: "getAccessGroupsLength",
formatArguments: false,
});
res.afterCleanup.accessGroupsLength = after.toNumber();
},
[ObjectTypesToClean.CONTENT_TYPE]: async ({contractAddress, res}) => {
const before = await this.CallContractMethod({
contractAddress,
methodName: "getContentTypesLength",
formatArguments: false,
});
res.beforeCleanup.contentTypesLength = before.toNumber();
await this.CallContractMethodAndWait({
contractAddress,
methodName: "cleanUpContentTypes",
formatArguments: true,
});
const after = await this.CallContractMethod({
contractAddress,
methodName: "getContentTypesLength",
formatArguments: false,
});
res.afterCleanup.contentTypesLength = after.toNumber();
}
};
const runCleanupTasks = async ({contractAddress}) => {
try {
const res = {
beforeCleanup: {},
afterCleanup: {}
};
if(objectTypeToClean === ObjectTypesToClean.ALL) {
for(const type of Object.keys(cleanupTasks)) {
await cleanupTasks[type]({contractAddress, res});
}
} else {
await cleanupTasks[objectTypeToClean]({contractAddress, res});
}
return res;
} catch(e) {
throw new Error(`Error during '${objectTypeToClean}' cleanup for ${contractAddress}: ${e.message}`);
}
};
let results = {};
// run cleanup on main contract
const res = await runCleanupTasks({contractAddress});
if(isUserWallet){
results[userAddress] = res;
} else {
results[contractAddress] = res;
}
// run cleanup on access group contracts if this is a user wallet
if(isUserWallet) {
const groupsLength = await this.CallContractMethod({
contractAddress,
methodName: "getAccessGroupsLength",
formatArguments: false,
});
if(groupsLength > 0) {
results["groups"] = {};
}
const groupAddressPromises = [];
for(let i=0; i<groupsLength; i++) {
groupAddressPromises.push(
this.CallContractMethod({
contractAddress,
methodName: "getAccessGroup",
methodArgs: [i],
formatArguments: false,
})
);
}
const groupAddresses = await Promise.all(groupAddressPromises);
const cleanupResults = await Promise.all(
groupAddresses.map(addr =>
runCleanupTasks({contractAddress: addr}).then(res => [addr, res]))
);
for(const [addr, res] of cleanupResults) {
results["groups"][addr] = res;
}
}
return results;
};