const { ElvClient } = require("@eluvio/elv-client-js");
const ethers = require("ethers");
const fs = require("fs");
const path = require("path");
const Utils = require("@eluvio/elv-client-js/src/Utils.js");
const { ElvUtils } = require("./Utils");
const TOKEN_DURATION = 120000; //2 min
class ElvAccount {
/**
* Instantiate the ElvAccount Object
*
* @namedParams
* @param {string} configUrl - The Content Fabric configuration URL
* @param {string} mainObjectId - The top-level Eluvio Live object ID
* @return {ElvSpace} - New ElvAccount object connected to the specified content fabric and blockchain
*/
constructor({ configUrl, debugLogging = false }) {
this.configUrl = configUrl;
this.debug = debugLogging;
}
async Init({ privateKey }) {
this.client = await ElvClient.FromConfigurationUrl({
configUrl: this.configUrl,
});
this.wallet = this.client.GenerateWallet();
this.signer = this.wallet.AddAccount({
privateKey,
});
this.client.SetSigner({ signer:this.signer });
this.client.ToggleLogging(this.debug);
}
async InitWithId({ privateKey, id }) {
await this.Init({privateKey});
await this.SetAccountTenantContractId({ tenantId: id });
}
InitWithClient({ elvClient }) {
if (!elvClient){
throw Error("ElvAccount InitWithClient with null");
}
this.client = elvClient;
}
Address() {
if (this.client) {
return this.client.signer.address;
}
return null;
}
/**
* Creates a new account including wallet object and contract.
* Current client must be initialized and funded.
*
* @namedParams
* @param {number} funds - The amount in ETH to fund the new account.
* @param {string} accountName - The name of the account to set in it's wallet metadata (Optional)
* @param {string} tenantId - The tenant ID (iten) (Optional)
* @param {object} groupToRoles - Map of group to roles [member|manager] (Optional)
* @param {boolean} skipAddingToTenantUserGroup - skip adding to tenant user group (Optional)
* @return {Promise<Object>} - An object containing the new account mnemonic, privateKey, address, accountName, balance
*/
async Create({ funds = 0.25, accountName, tenantId, groupToRoles, skipAddingToTenantUserGroup = false }) {
const abi = fs.readFileSync(
path.resolve(__dirname, "../contracts/v3/BaseTenantSpace.abi")
);
if (!this.client) {
throw Error("ElvAccount not intialized");
}
let tenantUsersGroup;
// We don't require the key is part of a tenant (for example when creating a tenant root key)
if (tenantId){
// Validate tenant ID (make sure it is not the tenant admins ID)
const idType = await this.client.AccessType({ id: tenantId });
if (idType !== this.client.authClient.ACCESS_TYPES.TENANT) {
throw Error("Bad tenant ID");
}
// Find tenant admins address
let tenantAddr = this.client.utils.HashToAddress(tenantId);
try {
await this.client.CallContractMethod({
contractAddress: tenantAddr,
abi: JSON.parse(abi),
methodName: "groupsMapping",
methodArgs: ["tenant_admin", 0],
formatArguments: true,
});
} catch (e) {
throw Error("Bad tenant - missing tenant admins group");
}
// require tenant admin key to create new users
let owner = await this.client.CallContractMethod({
contractAddress: tenantAddr,
abi: JSON.parse(abi),
methodName: "owner",
methodArgs: [],
formatArguments: true,
});
if (this.client.CurrentAccountAddress() !== owner.toLowerCase()) {
throw Error(`Not run by Tenant Admin - ${owner.toLowerCase()}`);
}
try {
tenantUsersGroup = await this.client.CallContractMethod({
contractAddress: tenantAddr,
abi: JSON.parse(abi),
methodName: "groupsMapping",
methodArgs: ["tenant_users", 0],
formatArguments: true,
});
} catch (e) {
tenantUsersGroup = null;
console.log("WARN: missing tenant users group");
}
}
let client = await ElvClient.FromConfigurationUrl({
configUrl: this.configUrl,
});
let wallet = this.client.GenerateWallet();
const mnemonic = wallet.GenerateMnemonic();
const signer = wallet.AddAccountFromMnemonic({ mnemonic });
const privateKey = signer.privateKey;
const address = signer.address;
if (this.debug) {
console.log("privateKey: ", privateKey);
console.log("address: ", address);
}
try {
client.SetSigner({ signer });
let res = await this.client.SendFunds({
recipient: address,
ether: funds,
});
if (this.debug) {
console.log("Send Funds result: ", res);
}
await client.userProfileClient.CreateWallet();
if (tenantId) {
await client.userProfileClient.SetTenantContractId({tenantContractId: tenantId});
}
if (accountName) {
await client.userProfileClient.ReplaceUserMetadata({
metadataSubtree: "public/name",
metadata: accountName,
});
}
let balance = await wallet.GetAccountBalance({ signer });
// add new user to tenant_users group
if (tenantUsersGroup && !skipAddingToTenantUserGroup) {
await this.AddToAccessGroup({
groupAddress: tenantUsersGroup,
accountAddress: address,
isManager: false,
});
console.log("Added user to tenant users group:", tenantUsersGroup);
}
if (groupToRoles) {
for (const [group, role] of Object.entries(groupToRoles)) {
let isManager;
switch (role) {
case "manager": isManager = true; break;
default: isManager = false;
}
let groupAddress;
// convert to address format
if (group.startsWith("igrp") || group.startsWith("iten")) {
groupAddress = Utils.HashToAddress(group);
} else if (group.startsWith("0x")) {
groupAddress = group;
} else {
console.log(`WARN: User was NOT added to the group "${group}" because the provided format is invalid.
Accepted formats: igrp, iten, or address.`);
continue;
}
// add user to group provided
await this.AddToAccessGroup({
groupAddress: groupAddress,
accountAddress: address,
isManager,
});
console.log(`Added user to group: ${groupAddress} as ${role}`);
}
}
return {
accountName,
address,
mnemonic,
privateKey,
balance,
tenantId,
};
} catch (e) {
// Return funds in case of error
if (funds > 0.01) {
await client.SendFunds({
recipient: this.client.signer.address,
ether: funds - 0.01,
});
console.log("Funds returned", accountName, address, privateKey);
}
throw e;
}
}
/**
* Show info about this account.
*/
async Show() {
if (!this.client) {
throw Error("ElvAccount not intialized");
}
let address = await this.client.signer.address;
let tenantId = "";
let tenantAdminsId = "";
let userMetadata = "";
try {
tenantAdminsId = await this.client.userProfileClient.TenantId();
} catch (e){ console.log("No tenantAdminsId set."); }
try {
tenantId = await this.client.userProfileClient.TenantContractId();
} catch (e){ console.log("No tenantContractId set."); }
try {
userMetadata = await this.client.userProfileClient.UserMetadata();
} catch (e){ console.log("No User Metadata."); }
let walletAddress = await this.client.userProfileClient.WalletAddress() || "";
let userWalletObject =
await this.client.userProfileClient.UserWalletObjectInfo() || "";
let wallet = this.client.GenerateWallet();
let balance = await wallet.GetAccountBalance({ signer: this.client.signer });
let userId = ElvUtils.AddressToId({prefix:"iusr", address});
const abi = fs.readFileSync(
path.resolve(__dirname, "../contracts/v3/BaseAccessWallet.abi")
);
let userTenantIdHex = await this.client.CallContractMethod({
contractAddress: walletAddress,
abi: JSON.parse(abi),
methodName: "getMeta",
methodArgs: ["_ELV_TENANT_ID"],
});
const userTenantId = ethers.utils.toUtf8String(userTenantIdHex);
if (tenantId !== userTenantId) {
console.log("Bad user - inconsistent tenant ID", tenantId, userTenantId);
}
return { address, userId, tenantId, tenantAdminsId, walletAddress, userWalletObject, userMetadata, balance };
}
async Balance({ address }) {
if (!this.client) {
throw Error("ElvAccount not intialized");
}
let provider = this.client.ethClient.Provider();
let balance = parseFloat(ethers.utils.formatEther(await provider.getBalance(address))).toFixed(2);
return balance;
}
async SetAccountTenantAdminsAddress({ tenantAdminsAddress }) {
if (!this.client) {
throw Error("ElvAccount not intialized");
}
await this.client.userProfileClient.SetTenantId({
address: tenantAdminsAddress,
});
}
async SetAccountTenantContractId({ tenantId }) {
if (!this.client) {
throw Error("ElvAccount not intialized");
}
await this.client.userProfileClient.SetTenantContractId({
tenantContractId: tenantId,
});
let tenantContractId = await this.client.userProfileClient.TenantContractId();
if (tenantContractId !== tenantId) {
throw new Error(`User wallet has a different tenant ID: ${tenantContractId}`);
}
}
async CreateAccessGroup({ name }) {
const address = await this.client.CreateAccessGroup({
name,
});
await this.client.AddAccessGroupManager({
contractAddress: address,
memberAddress: this.client.signer.address,
});
return { name, address };
}
async SetTenantContractId({contractAddress, objectId, versionHash, tenantContractId}){
if (!this.client) {
throw Error("ElvAccount not intialized");
}
await this.client.SetTenantContractId({contractAddress, objectId, versionHash, tenantContractId});
return await this.client.TenantContractId({contractAddress, objectId, versionHash});
}
async GetTenantInfo({contractAddress, objectId, versionHash}){
if (!this.client) {
throw Error("ElvAccount not intialized");
}
const tenantContractId = await this.client.TenantContractId({contractAddress, objectId, versionHash});
const tenantId = await this.client.TenantId({contractAddress, objectId, versionHash});
return {
tenant_contract_id: tenantContractId,
tenant_id: tenantId,
};
}
async Send({ address, funds }) {
await this.client.SendFunds({
recipient: address,
ether: funds,
});
}
async ReplaceStuckTx({nonce}){
const newNonce = nonce? nonce : await this.signer.getTransactionCount("latest"); // provides confirmed nonce
console.log("nonce:", newNonce);
// get the current gas price from the Ethereum network
const gasPrice = await this.signer.getGasPrice();
// increase gas price to prioritize the transaction
const newGasPrice = gasPrice.mul(ethers.BigNumber.from("2"));
let receipt = await this.signer.sendTransaction({
to: await this.signer.getAddress(),
value: ethers.utils.parseEther("0"),
nonce: newNonce,
gasPrice: newGasPrice,
gasLimit: ethers.utils.hexlify(21000), // typical gas limit for ether transfer
});
console.log("Transaction sent:", receipt.hash);
await receipt.wait();
}
async AddToAccessGroup({ groupAddress, accountAddress, isManager = false }) {
let res = {};
if (isManager) {
res = await this.client.AddAccessGroupManager({
contractAddress: groupAddress,
memberAddress: accountAddress,
});
} else {
res = await this.client.AddAccessGroupMember({
contractAddress: groupAddress,
memberAddress: accountAddress,
});
}
return { res };
}
async RemoveFromAccessGroup({ groupAddress, accountAddress, isManager = false }) {
let res = {};
if (isManager) {
res = await this.client.RemoveAccessGroupManager({
contractAddress: groupAddress,
memberAddress: accountAddress,
});
} else {
res = await this.client.RemoveAccessGroupMember({
contractAddress: groupAddress,
memberAddress: accountAddress,
});
}
return { res };
}
/**
* Associate group with the tenant with tenant contract Id.
* @param {string} tenantId - The ID of the tenant (iten***)
* @param {string} groupAddress - Address of the group we want to remove.
*/
async SetGroupTenantConfig({ tenantId, groupAddress }) {
// check if the group has tenant contract id set
let idHex = await this.client.TenantContractId({
contractAddress: groupAddress,
});
if (idHex) {
let id = ethers.utils.toUtf8String(idHex);
if (!Utils.EqualHash(tenantId, id)) {
throw Error(`Group ${groupAddress} already has _ELV_TENANT_ID metadata set to ${id}, aborting...`);
} else {
console.log(`Group ${groupAddress} already has _ELV_TENANT_ID metadata set correctly to ${id}`);
return;
}
}
return await this.client.SetTenantContractId({
contractAddress: groupAddress,
tenantContractId: tenantId
});
}
async CreateSignedToken({
libraryId,
objectId,
versionHash,
policyId,
subject = null,
grantType,
duration = TOKEN_DURATION,
allowDecryption,
context
}) {
if (!subject) {
subject = this.client.signer.address.toLowerCase();
}
return await this.client.CreateSignedToken({
libraryId, objectId, versionHash, policyId,
subject, grantType, duration, allowDecryption, context
});
}
async CreateFabricToken({duration=TOKEN_DURATION}){
return await this.client.CreateFabricToken({duration});
}
async CreateOfferSignature({nftAddress, mintHelperAddress, tokenId, offerId}){
const nftAddressBytes = ethers.utils.arrayify(nftAddress);
const mintAddressBytes = ethers.utils.arrayify(mintHelperAddress);
const tokenIdBigInt = ethers.BigNumber.from(tokenId).toHexString();
const packedData = ethers.utils.solidityPack(
["bytes", "bytes", "uint256", "uint8"],
[nftAddressBytes, mintAddressBytes, tokenIdBigInt, offerId]
);
const encodedData = ethers.utils.keccak256(
packedData
);
let messageHashBytes = ethers.utils.arrayify(encodedData);
const signedData = await this.client.signer.signMessage(messageHashBytes);
const signature = ethers.utils.splitSignature(signedData);
return {encodedData, messageHashBytes, packedData, signedData, signature};
}
async GetBalance(){
if (!this.client) {
throw Error("ElvAccount not intialized");
}
let wallet = this.client.GenerateWallet();
let res = await wallet.GetAccountBalance({signer: this.client.signer});
return res;
}
}
ElvAccount.TOKEN_DURATION = TOKEN_DURATION;
exports.ElvAccount = ElvAccount;