ElvSpace.js

Back
const { ElvUtils } = require("./Utils");
const { ElvAccount } = require("./ElvAccount");
const constants = require("./Constants");

const { ElvClient } = require("@eluvio/elv-client-js");

const Ethers = require("ethers");
const CBOR = require("cbor-x");
const { ElvTenant } = require("./ElvTenant");

class ElvSpace {
  /**
   * Instantiate the ElvSpace Object
   *
   * @namedParams
   * @param {string} configUrl - The Content Fabric configuration URL
   * @param {string} mainObjectId - The top-level Eluvio Live object ID
   * @return {ElvSpace} - New ElvSpace object connected to the specified content fabric and blockchain
   */
  constructor({ configUrl, spaceAddress, kmsAddress, debugLogging = false }) {
    this.configUrl = configUrl;
    (this.spaceAddress = spaceAddress), (this.kmsAddress = kmsAddress);
    this.debug = debugLogging;
  }

  async Init({ spaceOwnerKey }) {
    this.client = await ElvClient.FromConfigurationUrl({
      configUrl: this.configUrl,
    });
    let wallet = this.client.GenerateWallet();
    let signer = wallet.AddAccount({
      privateKey: spaceOwnerKey,
    });
    this.client.SetSigner({ signer });
    this.client.ToggleLogging(this.debug);
  }

  InitWithClient({ elvClient }) {
    this.client = elvClient;
  }

  async TenantCreate({ tenantName, funds = 51 }) {
    let account = null;
    let elvAccount = null;
    try {
      // Create ElvAccount
      elvAccount = new ElvAccount({
        configUrl: this.configUrl,
      });

      await elvAccount.InitWithClient({
        elvClient: this.client,
      });

      let initialBalance = await elvAccount.GetBalance();
      if (initialBalance < funds){
        throw Error(`Signer ${elvAccount.client.signer.address.toString()} has insufficient balance: ${initialBalance} Elv, require ${funds} Elv`) ;
      }

      const tenantSlug = tenantName.toLowerCase().replace(/ /g, "-");
      account = await elvAccount.Create({
        funds: funds,
        accountName: `${tenantSlug}-elv-admin`,
      });

      if (this.debug){
        console.log("create account - done");
      }

      await elvAccount.Init({ privateKey: account.privateKey });

      if (this.debug){
        console.log("create admin groups");
      }

      // Create Tenant Admin Access Group
      let tenantAdminGroup = await elvAccount.CreateAccessGroup({
        name: `${tenantName} Tenant Admins`,
      });

      await elvAccount.AddToAccessGroup({
        groupAddress: tenantAdminGroup.address,
        accountAddress: account.address,
        isManager: true,
      });

      // Create Content Admin Access Group
      let contentAdminGroup = await elvAccount.CreateAccessGroup({
        name: `${tenantName} Content Admins`,
      });

      await elvAccount.AddToAccessGroup({
        groupAddress: contentAdminGroup.address,
        accountAddress: account.address,
        isManager: true,
      });

      // Create Tenant Users Access Group
      let tenantUsersGroup = await elvAccount.CreateAccessGroup({
        name: `${tenantName} Tenant Users`,
      });

      await elvAccount.AddToAccessGroup({
        groupAddress: tenantUsersGroup.address,
        accountAddress: account.address,
        isManager: true,
      });

      if (this.debug){
        console.log("tenant admins:", tenantAdminGroup);
        console.log("content admins:", contentAdminGroup);
        console.log("tenant users:", tenantUsersGroup);
      }
      let adminGroups = {
        tenantAdminGroup,
        contentAdminGroup,
        tenantUsersGroup
      };

      let tenant = await this.TenantDeploy({
        tenantName: "", // not used
        ownerAddress: account.address,
        tenantAdminGroupAddress: tenantAdminGroup.address,
        contentAdminGroupAddress: contentAdminGroup.address,
        tenantUsersGroupAddress: tenantUsersGroup.address,
      });

      // Assign the created tenant to account
      await elvAccount.SetAccountTenantContractId({tenantId: tenant.id});
      account.tenantAdminsId = await elvAccount.client.userProfileClient.TenantId();
      account.tenantContractId = await elvAccount.client.userProfileClient.TenantContractId();

      // Add _ELV_TENANT_ID to groups' metadata so we can identify the tenant these groups belong to
      await elvAccount.SetGroupTenantConfig({
        tenantId: tenant.id,
        groupAddress: tenantAdminGroup.address
      });

      await elvAccount.SetGroupTenantConfig({
        tenantId: tenant.id,
        groupAddress: contentAdminGroup.address
      });

      await elvAccount.SetGroupTenantConfig({
        tenantId: tenant.id,
        groupAddress: tenantUsersGroup.address
      });

      return {
        account,
        adminGroups,
        tenant,
      };
    } catch (e){
      throw {error:e, account};
    }
  }

  async TenantDeploy({ tenantName, ownerAddress, tenantAdminGroupAddress, contentAdminGroupAddress, tenantUsersGroupAddress }) {
    let tenantContract;
    try {
      tenantContract = await ElvUtils.DeployContractFile({
        client: this.client,
        fileName: "BaseTenantSpace",
        args: [this.spaceAddress, tenantName, this.kmsAddress],
      });
    } catch (e) {
      console.log("[Error]: TenantDeploy can only be called by the space owner.");
      throw (e);
    }

    // Create ElvTenant
    const elvTenant = new ElvTenant({
      configUrl: this.configUrl,
    });
    await elvTenant.Init({
      privateKey: process.env.PRIVATE_KEY,
    });

    let res = {};

    let tenantFuncsContract = await ElvUtils.DeployContractFile({
      client: this.client,
      fileName: "TenantFuncsBase",
    });

    var tt4Bytes = ElvUtils.GetFunc4Bytes(
      "transferToken(bytes,uint256,address)"
    );

    var ag4Bytes = ElvUtils.GetFunc4Bytes("applyGroups(bytes,uint256,address)");

    var array4Bytes = [tt4Bytes, ag4Bytes];

    res = await this.client.CallContractMethodAndWait({
      contractAddress: tenantContract.address,
      abi: JSON.parse(tenantContract.abi),
      methodName: "addFuncs",
      methodArgs: [array4Bytes, tenantFuncsContract.address],
      formatArguments: false,
    });

    if (this.debug){
      console.log("Result addFuncs", res);
    }

    if (tenantAdminGroupAddress) {
      let contractAdminGroup = await this.client.CallContractMethod({
        contractAddress: tenantContract.address,
        abi: JSON.parse(tenantContract.abi),
        methodName: "GROUP_ID_ADMIN",
      });

      contractAdminGroup = Ethers.utils.parseBytes32String(contractAdminGroup);

      res = await this.client.CallContractMethodAndWait({
        contractAddress: tenantContract.address,
        abi: JSON.parse(tenantContract.abi),
        methodName: "addGroup",
        methodArgs: [contractAdminGroup, tenantAdminGroupAddress],
        formatArguments: true,
      });

      if (this.debug){
        console.log("Result set tenant admin group", res);
      }
    }

    if (contentAdminGroupAddress) {
      res = await this.client.CallContractMethodAndWait({
        contractAddress: tenantContract.address,
        abi: JSON.parse(tenantContract.abi),
        methodName: "addGroup",
        methodArgs: ["content_admin", contentAdminGroupAddress],
        formatArguments: true,
      });

      if (this.debug) {
        console.log("Result set content admin group", res);
      }
    }

    if (tenantUsersGroupAddress) {
      res = await this.client.CallContractMethodAndWait({
        contractAddress: tenantContract.address,
        abi: JSON.parse(tenantContract.abi),
        methodName: "addGroup",
        methodArgs: ["tenant_users", tenantUsersGroupAddress],
        formatArguments: true,
      });

      if (this.debug) {
        console.log("Result set tenant users group", res);
      }
    }

    let tenantContractId = ElvUtils.AddressToId({prefix:"iten", address:tenantContract.address});
    // set tenant status to active
    await elvTenant.TenantSetStatus({
      tenantContractId,
      tenantStatus: constants.TENANT_STATE_ACTIVE,
    });
    const tenantStatus = await elvTenant.TenantStatus({tenantContractId});

    if (ownerAddress) {
      res = await this.client.CallContractMethodAndWait({
        contractAddress: tenantContract.address,
        abi: JSON.parse(tenantContract.abi),
        methodName: "transferOwnership",
        methodArgs: [ownerAddress],
        formatArguments: true,
      });

      if (this.debug){
        console.log("Result transferOwnership", res);
      }

      let owner = await this.client.CallContractMethod({
        contractAddress: tenantContract.address,
        abi: JSON.parse(tenantContract.abi),
        methodName: "owner",
      });

      let creator = await this.client.CallContractMethod({
        contractAddress: tenantContract.address,
        abi: JSON.parse(tenantContract.abi),
        methodName: "creator",
      });

      if (this.debug){
        console.log("New owner", owner, "creator", creator);
      }
    }

    return {
      name: tenantName,
      id: ElvUtils.AddressToId({prefix:"iten", address:tenantContract.address}),
      address: tenantContract.address,
      tenantStatus,
      tenantAdminGroupAddress,
      contentAdminGroupAddress,
      tenantUsersGroupAddress,
    };
  }

  /**
   * Return a list of nodes in the content space, optionally filtered by node ID or endpoint.
   *
   * @param {string} matchEndpoint Only return nodes that have endpoints matching this string
   * @param {string} matchNodeId Only return the node matching this nodeId
   */
  async SpaceNodes({matchEndpoint, matchNodeId}) {

    // List all nodes in the space
    let bign = await this.client.CallContractMethod({
      contractAddress: this.spaceAddress,
      methodName: "numActiveNodes",
    });
    let n = bign.toNumber();

    let nodes = [];

    for (let i = 0; i < n; i ++) {

      let bigi = Ethers.BigNumber.from(i);
      let addr = await this.client.CallContractMethod({
        contractAddress: this.spaceAddress,
        methodName: "activeNodeAddresses",
        methodArgs: [bigi],
        formatArguments: true
      });

      let locatorsHex = await this.client.CallContractMethod({
        contractAddress: this.spaceAddress,
        methodName: "activeNodeLocators",
        methodArgs: [bigi]
      });

      let nodeId = this.client.utils.AddressToNodeId(addr);

      if (matchNodeId != undefined && nodeId != matchNodeId) {
        continue; // Not a match
      }

      let node = {id: nodeId, endpoints: []};

      // Parse locators CBOR
      let buffer = locatorsHex.slice(2, locatorsHex.length); // Skip "0x"
      let hex = buffer.toString("hex");
      let locators = CBOR.decodeAllSync(hex);

      let match = false;
      if (locators.length >= 5) {
        let fabArray = locators[4].fab;
        if (fabArray != undefined) {
          for (let i = 0; i < fabArray.length; i ++) {
            let host = fabArray[i].host;
            if (matchEndpoint != undefined && !host.includes(matchEndpoint)) {
              continue; // Not a match
            }
            match = true;
            let endpoint = fabArray[i].scheme + "://" + host;
            if (fabArray[i].port != "") {
              endpoint = endpoint + ":" + fabArray[i].port;
            }
            endpoint = endpoint + "/" + fabArray[i].path;
            node.endpoints.push(endpoint);
          }
        }
      }
      if (match) {
        nodes.push(node);
      }
    }

    return nodes;
  }

}

exports.ElvSpace = ElvSpace;