// @ts-check

import '../../exported.js';

import { AmountMath } from '@agoric/ertp';
import '@agoric/governance/exported.js';
import '@agoric/vats/exported.js';
import '@agoric/vats/src/core/types.js';
import { makeStorageNodeChild } from '@agoric/vats/src/lib-chainStorage.js';
import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js';
import { E, Far } from '@endo/far';
import { Stable, Stake } from '@agoric/vats/src/tokens.js';
import { deeplyFulfilledObject } from '@agoric/internal';
import * as Collect from '../collect.js';
import { makeTracer } from '../makeTracer.js';
import { makeStakeReporter } from '../my-lien.js';
import { makeReserveTerms } from '../reserve/params.js';
import { makeStakeFactoryTerms } from '../stakeFactory/params.js';
import { liquidationDetailTerms } from '../vaultFactory/liquidation.js';
import { makeGovernedTerms } from '../vaultFactory/params.js';
import { makeAmmTerms } from '../vpool-xyk-amm/params.js';

const trace = makeTracer('RunEconBehaviors', false);

const { details: X } = assert;

const SECONDS_PER_HOUR = 60n * 60n;
const SECONDS_PER_DAY = 24n * SECONDS_PER_HOUR;

const BASIS_POINTS = 10_000n;
const MILLI = 1_000_000n;

/**
 * @typedef {GovernedCreatorFacet<import('../stakeFactory/stakeFactory.js').StakeFactoryCreator>} StakeFactoryCreator
 * @typedef {import('../stakeFactory/stakeFactory.js').StakeFactoryPublic} StakeFactoryPublic
 * @typedef {import('../reserve/assetReserve.js').GovernedAssetReserveFacetAccess} GovernedAssetReserveFacetAccess
 */

/**
 * @typedef {object} PSMFacets
 * @property {Instance} psm
 * @property {Instance} psmGovernor
 * @property {Awaited<ReturnType<import('../psm/psm.js').start>>['creatorFacet']} psmCreatorFacet
 * @property {GovernedContractFacetAccess<{},{}>} psmGovernorCreatorFacet
 * @property {AdminFacet} psmAdminFacet
 */

/**
 * @typedef { WellKnownSpaces & ChainBootstrapSpace & EconomyBootstrapSpace
 * } EconomyBootstrapPowers
 * @typedef {PromiseSpaceOf<{
 *   ammInstanceWithoutReserve: Instance,
 *   ammCreatorFacet: XYKAMMCreatorFacet,
 *   ammGovernorCreatorFacet: GovernedContractFacetAccess<XYKAMMPublicFacet,XYKAMMCreatorFacet>,
 *   economicCommitteeCreatorFacet: CommitteeElectorateCreatorFacet,
 *   feeDistributorCreatorFacet: import('../feeDistributor.js').FeeDistributorCreatorFacet,
 *   feeDistributorPublicFacet: import('../feeDistributor.js').FeeDistributorPublicFacet,
 *   periodicFeeCollectors: import('../feeDistributor.js').PeriodicFeeCollector[],
 *   bankMints: Mint[],
 *   psmFacets: MapStore<Brand, PSMFacets>,
 *   psmCharterCreatorFacet: Awaited<ReturnType<import('../psm/psmCharter.js').start>>['creatorFacet'],
 *   psmCharterAdminFacet: AdminFacet,
 *   reservePublicFacet: import('../reserve/assetReserve.js').AssetReservePublicFacet,
 *   reserveCreatorFacet: import('../reserve/assetReserve.js').AssetReserveLimitedCreatorFacet,
 *   reserveGovernorCreatorFacet: GovernedAssetReserveFacetAccess,
 *   stakeFactoryCreatorFacet: StakeFactoryCreator,
 *   vaultFactoryCreator: VaultFactoryCreatorFacet,
 *   vaultFactoryGovernorCreator: GovernedContractFacetAccess<{},{}>,
 *   vaultFactoryVoteCreator: unknown,
 *   minInitialDebt: NatValue,
 * }>} EconomyBootstrapSpace
 */

/**
 * @file A collection of productions, each of which declares inputs and outputs.
 * Each function is passed a set of powers for reading from and writing to the vat config.
 *
 * Each of the things they produce they're responsible for resolving or setting.
 *
 * In production called by @agoric/vats to bootstrap.
 */

/**
 * @param {EconomyBootstrapPowers} powers
 * @param {{
 *   interchainPoolOptions?: { minimumCentral?: bigint }
 * }} [options]
 */
export const startInterchainPool = async (
  {
    consume: { bankManager: mgrP, zoe, agoricNamesAdmin },
    installation: {
      consume: { interchainPool: installation },
    },
    instance: {
      consume: { amm },
      produce: { interchainPool: _viaAgoricNamesAdmin },
    },
    brand: {
      consume: { [Stable.symbol]: centralBrandP },
    },
    issuer: {
      consume: { [Stable.symbol]: centralIssuerP },
    },
  },
  { interchainPoolOptions = {} } = {},
) => {
  trace('startInterchainPool');
  // TODO: get minimumCentral dynamically from the AMM
  const { minimumCentral = 100n * MILLI } = interchainPoolOptions;
  const [centralIssuer, centralBrand, bankManager] = await Promise.all([
    centralIssuerP,
    centralBrandP,
    mgrP,
  ]);

  const terms = await deeplyFulfilledObject(
    harden({
      minimumCentral: AmountMath.make(centralBrand, minimumCentral),
      amm,
    }),
  );
  const { instance } = await E(zoe).startInstance(
    installation,
    { Central: centralIssuer },
    terms,
    {
      bankManager,
    },
  );

  const instanceAdmin = E(agoricNamesAdmin).lookupAdmin('instance');
  await E(instanceAdmin).update('interchainPool', instance);
};
harden(startInterchainPool);

const AMM_STORAGE_PATH = 'amm'; // TODO: share with agoricNames?

/**
 * @param {EconomyBootstrapPowers} powers
 * @param {{ options?: { minInitialPoolLiquidity?: bigint }}} opts
 */
export const setupAmm = async (
  {
    consume: {
      board,
      chainTimerService,
      zoe,
      economicCommitteeCreatorFacet: committeeCreator,
      chainStorage,
    },
    produce: {
      ammCreatorFacet,
      ammInstanceWithoutReserve,
      ammGovernorCreatorFacet,
    },
    brand: {
      consume: { [Stable.symbol]: runBrandP },
    },
    issuer: {
      consume: { [Stable.symbol]: centralIssuer },
    },
    instance: {
      produce: { ammGovernor },
    },
    installation: {
      consume: { contractGovernor: governorInstallation, amm: ammInstallation },
    },
  },
  { options = {} } = {},
) => {
  trace('setupAmm');

  const poserInvitationP = E(committeeCreator).getPoserInvitation();
  const [poserInvitation, runBrand] = await Promise.all([
    poserInvitationP,
    runBrandP,
  ]);
  const { minInitialPoolLiquidity = 1_000_000_000n } = options;

  const ammTerms = makeAmmTerms(
    chainTimerService,
    E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitation),
    AmountMath.make(runBrand, minInitialPoolLiquidity),
  );

  const storageNode = await makeStorageNodeChild(
    chainStorage,
    AMM_STORAGE_PATH,
  );
  const marshaller = await E(board).getPublishingMarshaller();

  const ammGovernorTerms = await deeplyFulfilledObject(
    harden({
      timer: chainTimerService,
      governedContractInstallation: ammInstallation,
      governed: {
        terms: ammTerms,
        issuerKeywordRecord: { Central: centralIssuer },
      },
    }),
  );

  /** @type {{ creatorFacet: GovernedContractFacetAccess<XYKAMMPublicFacet,XYKAMMCreatorFacet>, publicFacet: GovernorPublic, instance: Instance }} */
  const g = await E(zoe).startInstance(
    governorInstallation,
    {},
    ammGovernorTerms,
    {
      electorateCreatorFacet: committeeCreator,
      governed: {
        initialPoserInvitation: poserInvitation,
        storageNode,
        marshaller,
      },
    },
  );

  const [creatorFacet, ammPublicFacet, instance] = await Promise.all([
    E(g.creatorFacet).getCreatorFacet(),
    E(g.creatorFacet).getPublicFacet(),
    E(g.publicFacet).getGovernedContract(),
  ]);
  ammGovernorCreatorFacet.resolve(g.creatorFacet);
  ammCreatorFacet.resolve(creatorFacet);

  // Confirm that the amm was indeed setup
  assert(ammPublicFacet, X`ammPublicFacet broken  ${ammPublicFacet}`);

  ammInstanceWithoutReserve.resolve(instance);
  ammGovernor.resolve(g.instance);
  return ammInstallation;
};

/** @param {EconomyBootstrapPowers} powers */
export const setupReserve = async ({
  consume: {
    ammCreatorFacet,
    ammInstanceWithoutReserve,
    board,
    feeMintAccess: feeMintAccessP,
    chainStorage,
    chainTimerService,
    zoe,
    economicCommitteeCreatorFacet: committeeCreator,
  },
  produce: {
    reserveCreatorFacet,
    reserveGovernorCreatorFacet,
    reservePublicFacet,
  },
  issuer: {
    consume: { [Stable.symbol]: centralIssuer },
  },
  instance: {
    produce: {
      amm: ammInstanceProducer,
      reserve: reserveInstanceProducer,
      reserveGovernor,
    },
  },
  installation: {
    consume: {
      contractGovernor: governorInstallation,
      reserve: reserveInstallation,
    },
  },
}) => {
  const STORAGE_PATH = 'reserve';
  trace('setupReserve');
  const poserInvitationP = E(committeeCreator).getPoserInvitation();
  const [poserInvitation, poserInvitationAmount, feeMintAccess] =
    await Promise.all([
      poserInvitationP,
      E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP),
      feeMintAccessP,
    ]);

  const reserveTerms = makeReserveTerms(
    poserInvitationAmount,
    ammInstanceWithoutReserve,
  );

  const storageNode = await makeStorageNodeChild(chainStorage, STORAGE_PATH);
  const marshaller = await E(board).getReadonlyMarshaller();

  const reserveGovernorTerms = await deeplyFulfilledObject(
    harden({
      timer: chainTimerService,
      governedContractInstallation: reserveInstallation,
      governed: {
        terms: reserveTerms,
        issuerKeywordRecord: { Central: centralIssuer },
      },
    }),
  );
  /** @type {{ creatorFacet: GovernedAssetReserveFacetAccess, publicFacet: GovernorPublic, instance: Instance }} */
  const g = await E(zoe).startInstance(
    governorInstallation,
    {},
    reserveGovernorTerms,
    {
      electorateCreatorFacet: committeeCreator,
      governed: {
        feeMintAccess,
        initialPoserInvitation: poserInvitation,
        marshaller,
        storageNode,
      },
    },
  );

  const [creatorFacet, publicFacet, instance] = await Promise.all([
    E(g.creatorFacet).getCreatorFacet(),
    E(g.creatorFacet).getPublicFacet(),
    E(g.publicFacet).getGovernedContract(),
  ]);

  reserveGovernorCreatorFacet.resolve(g.creatorFacet);
  reserveCreatorFacet.resolve(creatorFacet);
  reservePublicFacet.resolve(publicFacet);

  reserveInstanceProducer.resolve(instance);
  reserveGovernor.resolve(g.instance);

  // AMM requires Reserve in order to add pools, but we can't provide it at startInstance
  // time because Reserve requires AMM itself in order to be started.
  // Now that we have the reserve, provide it to the AMM.
  trace('Resolving the reserve public facet on the AMM');
  await E(ammCreatorFacet).resolveReserveFacet(publicFacet);
  // it now has the reserve
  ammInstanceProducer.resolve(ammInstanceWithoutReserve);

  return reserveInstallation;
};

/**
 * @param {EconomyBootstrapPowers} powers
 * @param {object} config
 * @param {LoanTiming} [config.loanParams]
 * @param {bigint} minInitialDebt
 */
export const startVaultFactory = async (
  {
    consume: {
      board,
      chainStorage,
      chainTimerService,
      priceAuthority,
      zoe,
      feeMintAccess: feeMintAccessP,
      economicCommitteeCreatorFacet: electorateCreatorFacet,
      reserveCreatorFacet,
    },
    produce, // {  vaultFactoryCreator }
    brand: {
      consume: { [Stable.symbol]: centralBrandP },
    },
    instance: {
      produce: instanceProduce,
      consume: { amm: ammInstance, reserve: reserveInstance },
    },
    installation: {
      consume: {
        VaultFactory: vaultFactoryInstallation,
        liquidate: liquidateInstallationP,
        contractGovernor: contractGovernorInstallation,
      },
    },
  },
  {
    loanParams = {
      chargingPeriod: SECONDS_PER_HOUR,
      recordingPeriod: SECONDS_PER_DAY,
    },
  } = {},
  minInitialDebt = 5_000_000n,
) => {
  const STORAGE_PATH = 'vaultFactory';
  trace('startVaultFactory');

  const poserInvitationP = E(electorateCreatorFacet).getPoserInvitation();
  const shortfallInvitationP =
    E(reserveCreatorFacet).makeShortfallReportingInvitation();
  const [
    initialPoserInvitation,
    poserInvitationAmount,
    initialShortfallInvitation,
    shortfallInvitationAmount,
    liquidateInstallation,
    feeMintAccess,
  ] = await Promise.all([
    poserInvitationP,
    E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP),
    shortfallInvitationP,
    E(E(zoe).getInvitationIssuer()).getAmountOf(shortfallInvitationP),
    liquidateInstallationP,
    feeMintAccessP,
  ]);

  const centralBrand = await centralBrandP;

  const ammPublicFacet = await E(zoe).getPublicFacet(ammInstance);
  const reservePublicFacet = await E(zoe).getPublicFacet(reserveInstance);
  const storageNode = await makeStorageNodeChild(chainStorage, STORAGE_PATH);
  const marshaller = await E(board).getReadonlyMarshaller();

  const vaultFactoryTerms = makeGovernedTerms(
    { storageNode, marshaller },
    {
      priceAuthority,
      reservePublicFacet,
      loanTiming: loanParams,
      liquidationInstall: liquidateInstallation,
      timer: chainTimerService,
      electorateInvitationAmount: poserInvitationAmount,
      ammPublicFacet,
      liquidationTerms: liquidationDetailTerms(centralBrand),
      minInitialDebt: AmountMath.make(centralBrand, minInitialDebt),
      bootstrapPaymentValue: 0n,
      shortfallInvitationAmount,
    },
  );

  const governorTerms = await deeplyFulfilledObject(
    harden({
      timer: chainTimerService,
      governedContractInstallation: vaultFactoryInstallation,
      governed: {
        terms: vaultFactoryTerms,
        issuerKeywordRecord: {},
      },
    }),
  );

  const { creatorFacet: governorCreatorFacet, instance: governorInstance } =
    await E(zoe).startInstance(
      contractGovernorInstallation,
      undefined,
      governorTerms,
      harden({
        electorateCreatorFacet,
        governed: {
          feeMintAccess,
          initialPoserInvitation,
          initialShortfallInvitation,
          marshaller,
          storageNode,
        },
      }),
    );

  const [vaultFactoryInstance, vaultFactoryCreator] = await Promise.all([
    E(governorCreatorFacet).getInstance(),
    E(governorCreatorFacet).getCreatorFacet(),
  ]);

  const voteCreator = Far('vaultFactory vote creator', {
    /** @type {VoteOnParamChanges} */
    voteOnParamChanges: (...args) =>
      E(governorCreatorFacet).voteOnParamChanges(...args),
  });

  produce.vaultFactoryCreator.resolve(vaultFactoryCreator);
  produce.vaultFactoryGovernorCreator.resolve(governorCreatorFacet);
  produce.vaultFactoryVoteCreator.resolve(voteCreator);

  // Advertise the installations, instances in agoricNames.
  instanceProduce.VaultFactory.resolve(vaultFactoryInstance);
  instanceProduce.Treasury.resolve(vaultFactoryInstance);
  instanceProduce.VaultFactoryGovernor.resolve(governorInstance);
};

/**
 * Grant access to the VaultFactory creatorFacet
 * to up to one user based on address.
 *
 * @param {EconomyBootstrapPowers} powers
 * @param {object} [root0]
 * @param {object} [root0.options]
 * @param {string} [root0.options.vaultFactoryControllerAddress]
 */
export const grantVaultFactoryControl = async (
  { consume: { client, priceAuthorityAdmin, vaultFactoryCreator } },
  { options: { vaultFactoryControllerAddress } = {} } = {},
) => {
  await E(client).assignBundle([
    addr => ({
      vaultFactoryCreatorFacet:
        typeof vaultFactoryControllerAddress === 'string' &&
        addr === vaultFactoryControllerAddress
          ? vaultFactoryCreator
          : undefined,
      priceAuthorityAdminFacet:
        typeof vaultFactoryControllerAddress === 'string' &&
        addr === vaultFactoryControllerAddress
          ? priceAuthorityAdmin
          : undefined,
    }),
  ]);
};
harden(grantVaultFactoryControl);

/** @param {BootstrapPowers} powers */
export const configureVaultFactoryUI = async ({
  consume: { board, zoe },
  issuer: {
    consume: { [Stable.symbol]: stableIssuerP },
  },
  brand: {
    consume: { [Stable.symbol]: stableBrandP },
  },
  installation: {
    consume: {
      amm: ammInstallation,
      VaultFactory: vaultInstallation,
      contractGovernor,
      binaryVoteCounter,
      liquidate,
    },
  },
  instance: {
    consume: { amm: ammInstance, VaultFactory: vaultInstance },
  },
  uiConfig,
}) => {
  const installs = await Collect.allValues({
    vaultFactory: vaultInstallation,
    amm: ammInstallation,
    contractGovernor,
    binaryVoteCounter,
    liquidate,
  });
  const instances = await Collect.allValues({
    amm: ammInstance,
    vaultFactory: vaultInstance,
  });
  const [stableIssuer, stableBrand] = await Promise.all([
    stableIssuerP,
    stableBrandP,
  ]);

  const invitationIssuer = await E(zoe).getInvitationIssuer();

  const vaultFactoryUiDefaults = {
    CONTRACT_NAME: 'VaultFactory',
    AMM_NAME: 'amm',
    BRIDGE_URL: 'http://127.0.0.1:8000',
    // Avoid setting API_URL, so that the UI uses the same origin it came from,
    // if it has an api server.
    // API_URL: 'http://127.0.0.1:8000',
  };

  // Look up all the board IDs.
  const boardIdValue = [
    ['INSTANCE_BOARD_ID', instances.vaultFactory],
    ['INSTALLATION_BOARD_ID', installs.vaultFactory],
    // @deprecated
    ['RUN_ISSUER_BOARD_ID', stableIssuer],
    // @deprecated
    ['RUN_BRAND_BOARD_ID', stableBrand],
    ['STABLE_ISSUER_BOARD_ID', stableIssuer],
    ['STABLE_BRAND_BOARD_ID', stableBrand],
    ['AMM_INSTALLATION_BOARD_ID', installs.amm],
    ['LIQ_INSTALLATION_BOARD_ID', installs.liquidate],
    ['BINARY_COUNTER_INSTALLATION_BOARD_ID', installs.binaryVoteCounter],
    ['CONTRACT_GOVERNOR_INSTALLATION_BOARD_ID', installs.contractGovernor],
    ['AMM_INSTANCE_BOARD_ID', instances.amm],
    ['INVITE_BRAND_BOARD_ID', E(invitationIssuer).getBrand()],
  ];
  await Promise.all(
    boardIdValue.map(async ([key, valP]) => {
      const val = await valP;
      const boardId = await E(board).getId(val);
      vaultFactoryUiDefaults[key] = boardId;
    }),
  );

  // Stash the defaults where the UI can find them.
  harden(vaultFactoryUiDefaults);

  // Install the names in agoricNames.
  uiConfig.produce[vaultFactoryUiDefaults.CONTRACT_NAME].resolve(
    vaultFactoryUiDefaults,
  );
  uiConfig.produce.Treasury.resolve(vaultFactoryUiDefaults); // compatibility
};
harden(configureVaultFactoryUI);

/**
 * Start the reward distributor.
 *
 * @param {EconomyBootstrapPowers} powers
 */
export const startRewardDistributor = async ({
  consume: {
    chainTimerService,
    bankManager,
    vaultFactoryCreator,
    periodicFeeCollectors,
    ammCreatorFacet,
    stakeFactoryCreatorFacet,
    reservePublicFacet,
    zoe,
  },
  produce: {
    feeDistributorCreatorFacet: feeDistributorCreatorFacetP,
    periodicFeeCollectors: periodicFeeCollectorsP,
  },
  instance: {
    produce: { feeDistributor: feeDistributorP },
  },
  installation: {
    consume: { feeDistributor },
  },
  issuer: {
    consume: { [Stable.symbol]: centralIssuerP },
  },
  brand: {
    consume: { [Stable.symbol]: centralBrandP },
  },
}) => {
  trace('startRewardDistributor');
  const timerService = await chainTimerService;
  const feeDistributorTerms = await deeplyFulfilledObject(
    harden({
      timerService,
      collectionInterval: 60n * 60n, // 1 hour
      keywordShares: {
        RewardDistributor: 1n,
        Reserve: 1n,
      },
    }),
  );

  const [centralIssuer, centralBrand] = await Promise.all([
    centralIssuerP,
    centralBrandP,
  ]);

  const rewardDistributorDepositFacet = await E(bankManager)
    .getRewardDistributorDepositFacet(Stable.denom, {
      issuer: centralIssuer,
      brand: centralBrand,
    })
    .catch(e => {
      console.error('Cannot create fee collector deposit facet', e);
      return undefined;
    });

  const feeDistributorFacets = await E(zoe).startInstance(
    feeDistributor,
    { Fee: centralIssuer },
    feeDistributorTerms,
  );
  await E(feeDistributorFacets.creatorFacet).setDestinations({
    RewardDistributor:
      rewardDistributorDepositFacet &&
      E(feeDistributorFacets.creatorFacet).makeDepositFacetDestination(
        rewardDistributorDepositFacet,
      ),
    Reserve: E(feeDistributorFacets.creatorFacet).makeOfferDestination(
      'Collateral',
      reservePublicFacet,
      'makeAddCollateralInvitation',
    ),
  });

  feeDistributorCreatorFacetP.resolve(feeDistributorFacets.creatorFacet);
  feeDistributorP.resolve(feeDistributorFacets.instance);

  // Initialize the periodic collectors list if we don't have one.
  periodicFeeCollectorsP.resolve([]);
  const periodicCollectors = await periodicFeeCollectors;

  const collectorFacets = {
    vaultFactory: vaultFactoryCreator,
    amm: ammCreatorFacet,
    runStake: stakeFactoryCreatorFacet,
  };
  await Promise.all(
    Object.entries(collectorFacets).map(async ([debugName, collectorFacet]) => {
      const collector = E(
        feeDistributorFacets.creatorFacet,
      ).makeContractFeeCollector(zoe, collectorFacet);
      const periodicCollector = await E(
        feeDistributorFacets.creatorFacet,
      ).startPeriodicCollection(debugName, collector);
      periodicCollectors.push(periodicCollector);
    }),
  );
};
harden(startRewardDistributor);

/**
 * @param {BootstrapPowers & PromiseSpaceOf<{
 *   lienBridge: StakingAuthority,
 * }>} powers
 */
export const startLienBridge = async ({
  consume: { bridgeManager: bridgeOptP },
  produce: { lienBridge },
  brand: {
    consume: { [Stake.symbol]: bldP },
  },
}) => {
  trace('lienBridge');

  const bridgeManager = await bridgeOptP;
  if (!bridgeManager) {
    return;
  }
  const bldBrand = await bldP;
  const reporter = makeStakeReporter(bridgeManager, bldBrand);
  lienBridge.resolve(reporter);
};

/**
 * @typedef {EconomyBootstrapPowers & PromiseSpaceOf<{
 *   client: ClientManager,
 *   lienBridge: StakingAuthority,
 * }>} StakeFactoryBootstrapPowers
 */

/**
 * @param {StakeFactoryBootstrapPowers } powers
 * @param {object} config
 * @param {bigint} [config.debtLimit] count of Minted
 * @param {Rational} [config.mintingRatio] ratio of Minted to BLD
 * @param {bigint} [config.interestRateBP]
 * @param {bigint} [config.loanFeeBP]
 * @param {bigint} [config.chargingPeriod]
 * @param {bigint} [config.recordingPeriod]
 * @typedef {[bigint, bigint]} Rational
 * @typedef {Awaited<ReturnType<typeof import('../stakeFactory/stakeFactory.js').start>>} StartStakeFactory
 */
export const startStakeFactory = async (
  {
    consume: {
      board,
      zoe,
      feeMintAccess: feeMintAccessP,
      lienBridge,
      client,
      chainTimerService,
      chainStorage,
      economicCommitteeCreatorFacet,
    },
    // @ts-expect-error TODO: add to BootstrapPowers
    produce: { stakeFactoryCreatorFacet, stakeFactoryGovernorCreatorFacet },
    installation: {
      consume: {
        contractGovernor: contractGovernorInstallation,
        stakeFactory: stakeFactoryInstallation,
      },
    },
    instance: {
      produce: { stakeFactory: stakeFactoryinstanceR },
    },
    brand: {
      consume: { [Stake.symbol]: bldBrandP, [Stable.symbol]: runBrandP },
      produce: { Attestation: attestationBrandR },
    },
    issuer: {
      consume: { [Stake.symbol]: bldIssuer },
      produce: { Attestation: attestationIssuerR },
    },
  },
  {
    debtLimit = 1_000_000_000_000n,
    mintingRatio = [1n, 4n],
    interestRateBP = 250n,
    loanFeeBP = 200n,
    chargingPeriod = SECONDS_PER_HOUR,
    recordingPeriod = SECONDS_PER_DAY,
  } = {},
) => {
  const STORAGE_PATH = 'stakeFactory';

  const [bldBrand, runBrand] = await Promise.all([bldBrandP, runBrandP]);

  const poserInvitationP = E(
    economicCommitteeCreatorFacet,
  ).getPoserInvitation();
  const [initialPoserInvitation, electorateInvitationAmount, feeMintAccess] =
    await Promise.all([
      poserInvitationP,
      E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP),
      feeMintAccessP,
    ]);

  const stakeFactoryTerms = makeStakeFactoryTerms(
    {
      timerService: chainTimerService,
      chargingPeriod,
      recordingPeriod,
    },
    {
      debtLimit: AmountMath.make(runBrand, debtLimit),
      mintingRatio: makeRatio(
        mintingRatio[0],
        runBrand,
        mintingRatio[1],
        bldBrand,
      ),
      interestRate: makeRatio(interestRateBP, runBrand, BASIS_POINTS),
      loanFee: makeRatio(loanFeeBP, runBrand, BASIS_POINTS),
      electorateInvitationAmount,
    },
  );

  const storageNode = await makeStorageNodeChild(chainStorage, STORAGE_PATH);
  const marshaller = await E(board).getReadonlyMarshaller();

  const stakeTerms = await deeplyFulfilledObject(
    harden({
      timer: chainTimerService,
      governedContractInstallation: stakeFactoryInstallation,
      governed: {
        terms: stakeFactoryTerms,
        issuerKeywordRecord: { Stake: bldIssuer },
      },
    }),
  );

  /** @type {{ publicFacet: GovernorPublic, creatorFacet: GovernedContractFacetAccess<StakeFactoryPublic,StakeFactoryCreator>}} */
  const governorFacets = await E(zoe).startInstance(
    contractGovernorInstallation,
    {},
    stakeTerms,
    {
      governed: {
        feeMintAccess,
        initialPoserInvitation,
        lienBridge,
        storageNode,
        marshaller,
      },
    },
  );
  const governedInstance = await E(governorFacets.creatorFacet).getInstance();
  const creatorFacet = E(governorFacets.creatorFacet).getCreatorFacet();

  const {
    issuers: { Attestation: attIssuer },
    brands: { Attestation: attBrand },
  } = await E(zoe).getTerms(governedInstance);

  stakeFactoryCreatorFacet.resolve(creatorFacet);
  stakeFactoryGovernorCreatorFacet.resolve(governorFacets.creatorFacet);
  stakeFactoryinstanceR.resolve(governedInstance);
  attestationBrandR.resolve(attBrand);
  attestationIssuerR.resolve(attIssuer);
  return Promise.all([
    E(client).assignBundle([
      address => ({
        // @ts-expect-error ??? creatorFacet is a StakeFactoryCreator; it has the method
        attMaker: E(creatorFacet).provideAttestationMaker(address),
      }),
    ]),
  ]);
};
harden(startStakeFactory);