diff --git a/contract/package.json b/contract/package.json index 414c0c3..909da8e 100644 --- a/contract/package.json +++ b/contract/package.json @@ -22,9 +22,9 @@ }, "devDependencies": { "@agoric/deploy-script-support": "^0.10.4-u12.0", + "@agoric/eslint-config": "dev", + "@agoric/inter-protocol": "0.16.2-u12.0", "@agoric/smart-wallet": "^0.5.4-u12.0", - "@agoric/store": "^0.9.3-u12.0", - "@agoric/vats": "^0.15.2-u12.0", "@endo/bundle-source": "^2.8.0", "@endo/eslint-plugin": "^0.5.2", "@endo/init": "^0.5.60", @@ -57,11 +57,16 @@ "dependencies": { "@agoric/ertp": "^0.16.3-u12.0", "@agoric/time": "^0.3.3-u12.0", + "@agoric/store": "^0.9.3-u12.0", + "@agoric/vat-data": "0.5.2", + "@agoric/vats": "^0.15.2-u12.0", "@agoric/zoe": "^0.26.3-u12.0", "@agoric/zone": "^0.2.3-u12.0", + "@endo/captp": "3.1.1", "@endo/far": "^0.2.22", "@endo/marshal": "^0.8.9", - "@endo/patterns": "^0.2.5" + "@endo/patterns": "^0.2.5", + "@endo/promise-kit": "0.2.56" }, "ava": { "files": [ @@ -81,6 +86,11 @@ }, "homepage": "https://github.com/agoric-labs/dapp-join-game#readme", "eslintConfig": { + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2021 + }, + "ignorePatterns": "bundles/**.js", "extends": [ "@agoric" ] @@ -90,4 +100,4 @@ "arrowParens": "avoid", "singleQuote": true } -} +} \ No newline at end of file diff --git a/contract/src/@types/inter-types.js b/contract/src/@types/inter-types.js new file mode 100644 index 0000000..d8a9a8d --- /dev/null +++ b/contract/src/@types/inter-types.js @@ -0,0 +1,40 @@ +// @ts-check +export {}; + +/** + * @typedef {object} VaultManagerParamValues + * @property {Ratio} liquidationMargin - margin below which collateral will be + * liquidated to satisfy the debt. + * @property {Ratio} liquidationPenalty - penalty charged upon liquidation as + * proportion of debt + * @property {Ratio} interestRate - annual interest rate charged on debt + * positions + * @property {Ratio} mintFee - The fee (in BasisPoints) charged when creating or + * increasing a debt position. + * @property {Amount<'nat'>} debtLimit + * @property {Ratio} [liquidationPadding] - vault must maintain this in order to + * remove collateral or add debt + */ + +/** + * @typedef {object} InterchainAssetOptions + * @property {string} denom + * @property {number} decimalPlaces + * @property {string} keyword - used in regstering with reserve, vaultFactory + * @property {string} [issuerName] - used in agoricNames for compatibility: + * defaults to `keyword` if not provided + * @property {string} [proposedName] - defaults to `issuerName` if not provided + * @property {string} [oracleBrand] - defaults to `issuerName` if not provided + */ + +/** + * @typedef {{ + * addIssuer: (issuer: Issuer, keyword: string) => Promise + * }} ReserveCreator + */ + +/** + * @typedef {{ + * addVaultType(collateralIssuer: Issuer<'nat'>, collateralKeyword: Keyword, initialParamValues: VaultManagerParamValues) + * }} VaultFactoryCreator + */ diff --git a/contract/src/types.d.ts b/contract/src/@types/zoe-contract-facet.d.ts similarity index 81% rename from contract/src/types.d.ts rename to contract/src/@types/zoe-contract-facet.d.ts index 0c7361a..4de2f47 100644 --- a/contract/src/types.d.ts +++ b/contract/src/@types/zoe-contract-facet.d.ts @@ -1,4 +1,4 @@ -type CopyRecord = import('@endo/pass-style').CopyRecord; +import type { CopyRecord } from '@endo/pass-style'; // export type {ContractMeta} from '@agoric/zoe/src/contractFacet/types-ambient'; export type ContractMeta = { diff --git a/contract/src/interAssets.js b/contract/src/interAssets.js new file mode 100644 index 0000000..71922b0 --- /dev/null +++ b/contract/src/interAssets.js @@ -0,0 +1,184 @@ +// @ts-check +import { E } from '@endo/far'; +import { ToFarFunction } from '@endo/captp'; +import { makePromiseKit } from '@endo/promise-kit'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { provide } from '@agoric/vat-data'; +import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js'; +import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; + +import { whenQuiescent } from './when-quiescent.js'; + +console.log('TODO: createPriceFeed from price-feed-proposal'); +console.log('TODO: ensureOracleBrands from price-feed-proposal'); +console.log('TODO: scaledPriceAuthority - or skip it?'); + +console.log('TODO: startPSM'); + +/** + * @typedef {{ + * startUpgradable: StartUpgradable + * }} UpgradeTools + */ + +export const oracleBrandFeedName = (inBrandName, outBrandName) => + `${inBrandName}-${outBrandName} price feed`; + +/** + * @param {ZCF<{ agoricNames: NameHub }>} zcf + * @param {{ + * tools: UpgradeTools, + * chainTimerService: ERef, + * contractAdmin: { + * reserve: import('./@types/inter-types.js').ReserveCreator, + * vaultFactory: import('./@types/inter-types.js').VaultFactoryCreator, + * auctioneer: GovernanceFacetKit['creatorFacet'], + * }, + * nameAdmin: { + * issuer: import('@agoric/vats/src/types').NameAdmin, + * brand: import('@agoric/vats/src/types').NameAdmin, + * }, + * bankManager: BankManager, + * }} privateArgs + * + * @param {import('@agoric/vat-data').Baggage} baggage + */ +export const start = async (zcf, privateArgs, baggage) => { + /** @type {import('@agoric/zone').Zone} */ + const zone = makeDurableZone(baggage); + const { agoricNames } = zcf.getTerms(); + const { tools, contractAdmin, nameAdmin, bankManager, chainTimerService } = + privateArgs; + + const installation = { + /** @type {Promise>} */ + mintHolder: E(agoricNames).lookup('installation', 'mintHolder'), + }; + + const lookupInstance = name => E(agoricNames).lookup('instance', name); + + /** + * @param {string} name + * @param {{ issuer: Issuer, brand: Brand}} kit + */ + const publishAsset = (name, { issuer, brand }) => + Promise.all([ + E(nameAdmin.issuer).update(name, issuer), + E(nameAdmin.brand).update(name, brand), + ]); + + const stable = await E(agoricNames).lookup('brand', 'IST'); + + /** @type {import('./@types/inter-types.js').VaultManagerParamValues} */ + const initialVaultParams = { + debtLimit: AmountMath.make(stable, 1_000n * 1_000_000n), + interestRate: makeRatio(1n, stable), + liquidationPadding: makeRatio(25n, stable), + liquidationMargin: makeRatio(150n, stable), + mintFee: makeRatio(50n, stable, 10_000n), + liquidationPenalty: makeRatio(1n, stable), + }; + + const makeInterAssetKit = zone.exoClassKit( + 'InterAsset', + undefined, // TODO: interface guards + () => ({}), + { + creator: { + /** + * @param {Pick} assetOpts + * @returns {Promise, 'mintRecoveryPurse'>>} + */ + async startMintHolder(assetOpts) { + const { keyword, issuerName = keyword, decimalPlaces } = assetOpts; + /** @type {DisplayInfo<'nat'>} */ + const displayInfo = { + decimalPlaces, + assetKind: AssetKind.NAT, + }; + const terms = { + keyword: issuerName, // "keyword" is a misnomer in mintHolder terms + assetKind: AssetKind.NAT, + displayInfo, + }; + + const facets = await E(tools).startUpgradable({ + installation: installation.mintHolder, + label: issuerName, + privateArgs: undefined, + terms, + }); + const { creatorFacet: mint, publicFacet: issuer } = facets; + const brand = await E(issuer).getBrand(); + + // @ts-expect-error AssetKind NAT guaranteed by construction + return { mint, issuer, brand, displayInfo }; + }, + + /** @param {import('./@types/inter-types.js').InterchainAssetOptions} assetOpts */ + async makeVBankAsset(assetOpts) { + const { + keyword, + issuerName = keyword, + proposedName = issuerName, + denom, + } = assetOpts; + const { creator } = this.facets; + const { mint, issuer, brand } = + await creator.startMintHolder(assetOpts); + const kit = { mint, issuer, brand }; + + await Promise.all([ + E(bankManager).addAsset(denom, issuerName, proposedName, kit), + E(contractAdmin.reserve).addIssuer(issuer, keyword), + publishAsset(issuerName, { issuer, brand }), + ]); + return harden({ issuer, brand }); + }, + + /** @param {import('./@types/inter-types.js').InterchainAssetOptions} assetOpts */ + async addVaultCollateral(assetOpts) { + const { + keyword, + issuerName = keyword, + oracleBrand = issuerName, + } = assetOpts; + const { creator } = this.facets; + const { issuer: interchainIssuer } = + await creator.makeVBankAsset(assetOpts); + + // don't add the collateral offering to vaultFactory until its price feed is available + // eslint-disable-next-line no-restricted-syntax -- allow this computed property + await lookupInstance(oracleBrandFeedName(oracleBrand, 'USD')); + + const auctioneerCreator = contractAdmin.auctioneer; + const schedules = await E(auctioneerCreator).getSchedule(); + + const finishPromiseKit = makePromiseKit(); + const addBrandThenResolve = ToFarFunction( + 'addBrandThenResolve', + async () => { + await E(auctioneerCreator).addBrand(interchainIssuer, keyword); + finishPromiseKit.resolve(undefined); + }, + ); + + // schedules actions on a timer (or does it immediately). + // finishPromiseKit signals completion. + void whenQuiescent(schedules, chainTimerService, addBrandThenResolve); + await finishPromiseKit.promise; + + await E(contractAdmin.vaultFactory).addVaultType( + interchainIssuer, + keyword, + initialVaultParams, + ); + }, + }, + public: {}, + }, + ); + const kit = provide(baggage, 'interAssetKit', makeInterAssetKit); + return kit; +}; diff --git a/contract/src/interchainMints.deploy.js b/contract/src/interchainMints.deploy.js new file mode 100644 index 0000000..09d7cb5 --- /dev/null +++ b/contract/src/interchainMints.deploy.js @@ -0,0 +1,79 @@ +// @ts-check +import { E } from '@endo/far'; +import { allValues } from './objectTools.js'; +import { + installContract, + startContract, +} from './platform-goals/start-contract.js'; + +const { Fail } = assert; + +const contractName = 'interchainMints'; + +/** + * @param {BootstrapPowers} powers + * @param {{ options: { interchainMints: { bundleID: string }}}} config + */ +export const installMintsContract = async (powers, config) => { + const { + // must be supplied by caller or template-replaced + bundleID = Fail`no bundleID`, + } = config?.options?.[contractName] ?? {}; + + return installContract(powers, { + name: contractName, + bundleID, + }); +}; + +/** + * @param {BootstrapPowers} powers + */ +export const deployInterchainMints = async powers => { + /** @type {Installation} */ + const installation = powers.installation.consume.interchainMints; + const { agoricNamesAdmin, bankManager } = powers.consume; + const { interchainMintsKit } = powers.produce; + + const privateArgs = await allValues({ + nameAdmins: allValues({ + brand: E(agoricNamesAdmin).lookupAdmin('brand'), + issuer: E(agoricNamesAdmin).lookupAdmin('issuer'), + }), + bankManager, + }); + + const kit = await startContract(powers, { + name: contractName, + startArgs: { installation, privateArgs }, + }); + interchainMintsKit.reset(); + interchainMintsKit.resolve(kit); +}; + +/** + * @param {BootstrapPowers} powers + * @param {{ options: { interchainMints: { bundleID: string }}}} config + */ +export const main = (powers, config) => + Promise.all([ + installMintsContract(powers, config), + deployInterchainMints(powers), + ]); + +export const permit = { + consume: { + agoricNamesAdmin: true, + bankManager: true, + zoe: true, + startUpgradable: true, + }, + produce: { interchainMintsKit: true }, + installation: { + produce: { interchainMints: true }, + consume: { interchainMints: true }, + }, + instance: { + produce: { interchainMints: true }, + }, +}; diff --git a/contract/src/interchainMints.js b/contract/src/interchainMints.js new file mode 100644 index 0000000..fe64af8 --- /dev/null +++ b/contract/src/interchainMints.js @@ -0,0 +1,116 @@ +/** @file main entry point is {@link prepare} */ + +// @jessie-check +// @ts-check + +import { E } from '@endo/far'; +import { M } from '@endo/patterns'; +import { makeDurableIssuerKit, AssetKind, BrandShape } from '@agoric/ertp'; +import { makeDurableZone } from '@agoric/zone/durable.js'; + +/** @import { Baggage } from '@agoric/vat-data' */ +/** @import { ContractMeta } from './@types/zoe-contract-facet'; */ +/** @import { NameAdmin } from '@agoric/vats'; */ +/** @import { InterchainAssetOptions } from './@types/inter-types'; */ + +/** @see {@link InterchainAssetOptions} */ +const InterchainAssetOptionsShape = M.splitRecord( + { + // issuerBoardId not supported + denom: M.string(), + decimalPlaces: M.number(), + // TODO: time to fix the keyword goofiness? + keyword: M.string(), + }, + { + issuerName: M.string(), + proposedName: M.string(), + oracleBrand: M.string(), + }, +); + +/** @type {ContractMeta} */ +export const meta = { + upgradability: 'canUpgrade', + privateArgsShape: { + nameAdmins: { + brand: M.remotable('Brand NameAdmin'), + issuer: M.remotable('Issuer NameAdmin'), + }, + bankManager: M.remotable('BankManager'), + }, +}; +harden(meta); +export const privateArgsShape = meta.privateArgsShape; + +/** + * Create and register IssuerKits for interchain / vbank assets. + * + * @param {unknown} _zcf + * @param {{ + * nameAdmins: { + * brand: NameAdmin, + * issuer: NameAdmin, + * }, + * bankManager: BankManager, + * }} privateArgs + * @param {Baggage} baggage + */ +export const prepare = (_zcf, { nameAdmins, bankManager }, baggage) => { + const zone = makeDurableZone(baggage); + + const publicFacet = zone.exo('NoPublicMethods', undefined, {}); + + /** @type {MapStore} */ + const brandToBaggage = zone.mapStore('brandToBaggage', { + keyShape: BrandShape, + }); + + const creatorFacet = zone.exo( + 'InterchainMints', + M.interface('InterchainMints', { + addAsset: M.callWhen(InterchainAssetOptionsShape).returns(), + }), + { + /** + * @param {InterchainAssetOptions} interchainAssetOptions + * @throws if the denom or issuerName is already registered in the vbank + */ + async addAsset(interchainAssetOptions) { + const issuerBaggage = zone.detached().mapStore('issuerBaggage'); + const { + denom, + decimalPlaces, + keyword, + issuerName = keyword, + proposedName = keyword, + } = interchainAssetOptions; + + // We need a mint we can pass to the bankManager, + // so a ZCFMint does not suffice. + const kit = makeDurableIssuerKit( + issuerBaggage, + issuerName, + AssetKind.NAT, + { decimalPlaces }, + ); + + await E(bankManager).addAsset(denom, issuerName, proposedName, kit); + // XXX if it throws, issuerBaggage is un-reclaimed storage + + brandToBaggage.init(kit.brand, issuerBaggage); + + await Promise.all([ + E(nameAdmins.issuer).update(issuerName, kit.issuer), + E(nameAdmins.brand).update(issuerName, kit.brand), + ]); + }, + }, + ); + + return { + publicFacet, + creatorFacet, + }; +}; +harden(prepare); diff --git a/contract/src/platform-goals/start-contract.js b/contract/src/platform-goals/start-contract.js new file mode 100644 index 0000000..f72bb25 --- /dev/null +++ b/contract/src/platform-goals/start-contract.js @@ -0,0 +1,123 @@ +/** @file utilities to start typical contracts in core eval scripts. */ +// @ts-check + +import { E } from '@endo/far'; + +const { Fail } = assert; + +/** + * Given a bundleID and a permitted name, install a bundle and "produce" + * the installation, which also publishes it via agoricNames. + * + * @param {BootstrapPowers} powers - zoe, installation.produce[name] + * @param {{ name: string, bundleID: string }} opts + */ +export const installContract = async ( + { consume: { zoe }, installation: { produce: produceInstallation } }, + { name, bundleID }, +) => { + const installation = await E(zoe).installBundleID(bundleID); + produceInstallation[name].reset(); + produceInstallation[name].resolve(installation); + console.log(name, '(re-)installed as', bundleID.slice(0, 8)); + return installation; +}; + +/** + * Given a permitted name, start a contract instance; save upgrade info; publish instance. + * Optionally: publish issuers/brands. + * + * Note: publishing brands requires brandAuxPublisher from board-aux.core.js. + * + * @param {BootstrapPowers} powers - consume.startUpgradable, installation.consume[name], instance.produce[name] + * @param {{ + * name: string; + * startArgs?: StartArgs; + * issuerNames?: string[]; + * }} opts + * + * @typedef {Partial>[0]>} StartArgs + */ +export const startContract = async ( + powers, + { name, startArgs, issuerNames }, +) => { + const { + consume: { startUpgradable }, + installation: { consume: consumeInstallation }, + instance: { produce: produceInstance }, + } = powers; + + const installation = await consumeInstallation[name]; + + console.log(name, 'start args:', startArgs); + const started = await E(startUpgradable)({ + ...startArgs, + installation, + label: name, + }); + const { instance } = started; + produceInstance[name].reset(); + produceInstance[name].resolve(instance); + + console.log(name, 'started'); + + if (issuerNames) { + /** @type {BootstrapPowers & import('./board-aux.core').BoardAuxPowers} */ + // @ts-expect-error cast + const auxPowers = powers; + + const { zoe, brandAuxPublisher } = auxPowers.consume; + const { produce: produceIssuer } = auxPowers.issuer; + const { produce: produceBrand } = auxPowers.brand; + const { brands, issuers } = await E(zoe).getTerms(instance); + + await Promise.all( + issuerNames.map(async issuerName => { + const brand = brands[issuerName]; + const issuer = issuers[issuerName]; + console.log('CoreEval script: share via agoricNames:', brand); + + produceBrand[issuerName].reset(); + produceIssuer[issuerName].reset(); + produceBrand[issuerName].resolve(brand); + produceIssuer[issuerName].resolve(issuer); + + await E(brandAuxPublisher).publishBrandInfo(brand); + }), + ); + } + + return started; +}; + +/** + * In order to avoid linking from other packages, we + * provide a work-alike for AmountMath.make and use tests to check equivalence. + * + * Note that this version doesn't do as much input validation. + */ +export const AmountMath = { + /** + * @template {AssetKind} K + * @param {Brand} brand + * @param {*} value + */ + make: (brand, value) => harden({ brand, value }), +}; + +const pathSegmentPattern = /^[a-zA-Z0-9_-]{1,100}$/; + +/** @type {(name: string) => void} */ +export const assertPathSegment = name => { + pathSegmentPattern.test(name) || + Fail`Path segment names must consist of 1 to 100 characters limited to ASCII alphanumerics, underscores, and/or dashes: ${name}`; +}; +harden(assertPathSegment); + +/** @type {(name: string) => string} */ +export const sanitizePathSegment = name => { + const candidate = name.replace(/[ ,]/g, '_'); + assertPathSegment(candidate); + return candidate; +}; diff --git a/contract/src/types-ambient.js b/contract/src/types-ambient.js new file mode 100644 index 0000000..903238b --- /dev/null +++ b/contract/src/types-ambient.js @@ -0,0 +1,2 @@ +import '@agoric/store/exported.js'; +import '@agoric/vats/src/core/types.js'; diff --git a/contract/src/when-quiescent.js b/contract/src/when-quiescent.js new file mode 100644 index 0000000..0bb46e4 --- /dev/null +++ b/contract/src/when-quiescent.js @@ -0,0 +1,75 @@ +// @ts-check + +import { TimeMath } from '@agoric/time'; +import { E, Far } from '@endo/far'; + +const { quote: q } = assert; + +// wait a short while after end to allow things to settle +const BUFFER = 5n * 60n; +// let's insist on 20 minutes leeway for running the scripts +const COMPLETION = 20n * 60n; + +/** + * This function works around an issue identified in #8307 and #8296, and fixed + * in #8301. The fix is needed until #8301 makes it into production. + * + * If there is a liveSchedule, 1) run now if start is far enough away, + * otherwise, 2) run after endTime. If neither liveSchedule nor nextSchedule is + * defined, 3) run now. If there is only a nextSchedule, 4) run now if startTime + * is far enough away, else 5) run after endTime + * + * @param {import('@agoric/inter-protocol/src/auction/scheduler.js').FullSchedule} schedules + * @param {ERef} timer + * @param {() => void} thunk + */ +export const whenQuiescent = async (schedules, timer, thunk) => { + const { nextAuctionSchedule, liveAuctionSchedule } = schedules; + const now = await E(timer).getCurrentTimestamp(); + + const waker = Far('addAssetWaker', { wake: () => thunk() }); + + if (liveAuctionSchedule) { + const safeStart = TimeMath.subtractAbsRel( + liveAuctionSchedule.startTime, + COMPLETION, + ); + + if (TimeMath.compareAbs(safeStart, now) < 0) { + // case 2 + console.warn( + `Add Asset after live schedule's endtime: ${q( + liveAuctionSchedule.endTime, + )}`, + ); + + return E(timer).setWakeup( + TimeMath.addAbsRel(liveAuctionSchedule.endTime, BUFFER), + waker, + ); + } + } + + if (!liveAuctionSchedule && nextAuctionSchedule) { + const safeStart = TimeMath.subtractAbsRel( + nextAuctionSchedule.startTime, + COMPLETION, + ); + if (TimeMath.compareAbs(safeStart, now) < 0) { + // case 5 + console.warn( + `Add Asset after next schedule's endtime: ${q( + nextAuctionSchedule.endTime, + )}`, + ); + return E(timer).setWakeup( + TimeMath.addAbsRel(nextAuctionSchedule.endTime, BUFFER), + waker, + ); + } + } + + // cases 1, 3, and 4 fall through to here. + console.warn(`Add Asset immediately`, thunk); + return thunk(); +}; diff --git a/contract/test/boot-tools.js b/contract/test/boot-tools.js index 8e92171..a253325 100644 --- a/contract/test/boot-tools.js +++ b/contract/test/boot-tools.js @@ -13,7 +13,7 @@ export const getBundleId = b => `b1-${b.endoZipBase64Sha512}`; export const makeBootstrapPowers = async ( log, - spaceNames = ['installation', 'instance', 'issuer', 'brand'], + spaceNames = ['installation', 'instance', 'issuer', 'brand', 'vbankAsset'], ) => { const { produce, consume } = makePromiseSpace(); @@ -49,7 +49,23 @@ export const makeBootstrapPowers = async ( return auxData; }; + const startUpgradable = ({ + installation, + issuerKeywordRecord, + terms, + privateArgs, + label, + }) => + E(zoe).startInstance( + installation, + issuerKeywordRecord, + terms, + privateArgs, + label, + ); + produce.zoe.resolve(zoe); + produce.startUpgradable.resolve(startUpgradable); produce.feeMintAccess.resolve(feeMintAccess); produce.agoricNamesAdmin.resolve(agoricNamesAdmin); produce.agoricNames.resolve(agoricNames); @@ -84,12 +100,9 @@ export const makeBundleCacheContext = async (_t, dest = 'bundles/') => { return { bundleCache, shared }; }; -export const bootAndInstallBundles = async (t, bundleRoots) => { - t.log('bootstrap'); - const powersKit = await makeBootstrapPowers(t.log); - const { vatAdminState } = powersKit; - +export const installBundles = async (t, vatAdminState, bundleRoots) => { const { bundleCache } = t.context; + await null; /** @type {Record} */ const bundles = {}; for (const [name, rootModulePath] of Object.entries(bundleRoots)) { @@ -99,6 +112,15 @@ export const bootAndInstallBundles = async (t, bundleRoots) => { vatAdminState.installBundle(bundleID, bundle); bundles[name] = bundle; } - harden(bundles); + return harden(bundles); +}; + +export const bootAndInstallBundles = async (t, bundleRoots) => { + t.log('bootstrap'); + const powersKit = await makeBootstrapPowers(t.log); + const { vatAdminState } = powersKit; + + const bundles = await installBundles(t, vatAdminState, bundleRoots); + return { ...powersKit, bundles }; }; diff --git a/contract/test/market-actors.js b/contract/test/market-actors.js index df5ce9b..2f25500 100644 --- a/contract/test/market-actors.js +++ b/contract/test/market-actors.js @@ -1,7 +1,7 @@ // @ts-check import { E, getInterfaceOf } from '@endo/far'; import { AmountMath } from '@agoric/ertp/src/amountMath.js'; -import { allValues, mapValues } from './wallet-tools.js'; +import { allValues, mapValues, seatLike } from './wallet-tools.js'; const { entries, fromEntries, keys } = Object; const { Fail } = assert; @@ -193,11 +193,10 @@ export const starterSam = async (t, mine, wellKnown) => { invitationArgs: [{ bundleID, customTerms }], }, }); + const seat = seatLike(updates); const expected = { - result: { - invitationMakers: true, - }, + result: 'UNPUBLISHED', payouts: { Started: { brand: brand.Invitation, @@ -228,33 +227,21 @@ export const starterSam = async (t, mine, wellKnown) => { return it; }; - const todo = new Map(entries(expected)); - let done; - for await (const update of updates) { - // t.log('Sam offer update', update); - if (update.updated !== 'offerStatus') continue; - const { result, payouts } = update.status; - if (result && todo.has('result')) { - checkKeys('Sam gets creatorFacet', result, expected.result); - todo.delete('result'); - } - if (payouts) { - checkKeys(undefined, payouts, expected.payouts); - const { Started } = payouts; - t.is(Started.brand, expected.payouts.Started.brand); - const details = first(Started.value); - const [details0] = expected.payouts.Started.value; - checkKeys(undefined, details, details0); - t.is(details.instance, details0.instance); - checkKeys( - 'Sam gets instance etc.', - details.customDetails, - details0.customDetails, - ); - done = details.customDetails; - todo.delete('payouts'); - } - } - t.deepEqual([...todo.keys()], []); - return done; + const result = await E(seat).getOfferResult(); + t.is(result, expected.result, 'Sam gets creatorFacet'); + + const payouts = await E(seat).getPayouts(); + checkKeys(undefined, payouts, expected.payouts); + const { Started } = payouts; + t.is(Started.brand, expected.payouts.Started.brand); + const details = first(Started.value); + const [details0] = expected.payouts.Started.value; + checkKeys(undefined, details, details0); + t.is(details.instance, details0.instance); + checkKeys( + 'Sam gets instance etc.', + details.customDetails, + details0.customDetails, + ); + return details.customDetails; }; diff --git a/contract/test/test-interchainMints.js b/contract/test/test-interchainMints.js new file mode 100644 index 0000000..cb6370d --- /dev/null +++ b/contract/test/test-interchainMints.js @@ -0,0 +1,156 @@ +// @ts-check + +// eslint-disable-next-line import/order +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { createRequire } from 'module'; + +import { E } from '@endo/far'; +import { buildRootObject as buildBankVatRoot } from '@agoric/vats/src/vat-bank.js'; +import { extractPowers } from '@agoric/vats/src/core/utils.js'; +import { makeScalarMapStore } from '@agoric/store'; +import { + installBundles, + getBundleId, + makeBootstrapPowers, + makeBundleCacheContext, +} from './boot-tools.js'; +import { + main as deploy, + permit as mintsPermit, +} from '../src/interchainMints.deploy.js'; + +/** @import { StartedInstanceKit } from '@agoric/zoe/src/zoeService/utils.js'; */ + +/** @type {import('ava').TestFn>>} */ +const test = anyTest; + +const nodeRequire = createRequire(import.meta.url); + +const bundleRoots = { + interchainMints: nodeRequire.resolve('../src/interchainMints.js'), +}; + +const makeTestContext = async t => { + const bc = await makeBundleCacheContext(t); + t.log('bootstrap'); + const { powers, vatAdminState } = await makeBootstrapPowers(t.log); + + const shared = {}; + return { ...bc, powers, vatAdminState, shared }; +}; + +test.before(async t => (t.context = await makeTestContext(t))); + +test.serial('install interchainMints bundle', async t=>{ + const { vatAdminState, shared } = t.context; + + const bundles = await installBundles(t, vatAdminState, bundleRoots); + + t.truthy(bundles.interchainMints); + Object.assign(shared, { bundles }); +}); + +test.serial('interchainMints contract adds IBC tokens to vbank', async t => { + const { powers, shared } = t.context; + const { bundles } = shared; + const { agoricNamesAdmin } = powers.consume; + + // produce bankManager + { + const noBridge = undefined; + const baggage = makeScalarMapStore('baggage'); + const bankManager = E( + buildBankVatRoot(undefined, undefined, baggage), + ).makeBankManager(noBridge, E(agoricNamesAdmin).lookupAdmin('vbankAsset')); + powers.produce.bankManager.resolve(bankManager); + } + + const bundleID = getBundleId(bundles.interchainMints); + const permittedPowers = extractPowers(mintsPermit, powers); + await deploy(permittedPowers, { + options: { interchainMints: { bundleID } }, + }); + + /** @type {StartedInstanceKit} */ + const kit = await powers.consume.interchainMintsKit; + t.log('interchainMintsKit', kit); + t.is(typeof kit.adminFacet, 'object', 'adminFacet is available'); + t.is(typeof kit.instance, 'object'); + t.is( + kit.instance, + await powers.instance.consume.interchainMints, + 'instance matches agoricNames', + ); + + const tokens = [ + { + issuerName: 'Asset1', + issuerKeyword: 'Asset1', + denom: + 'ibc/1234567890123456789012345678901234567890123456789012345678901234', + }, + { + issuerName: 'Asset2', + issuerKeyword: 'Asset2', + denom: + 'ibc/2234567890123456789012345678901234567890123456789012345678901234', + }, + ]; + + const { agoricNames } = powers.consume; + const { creatorFacet: mintsAdmin } = kit; + await Promise.all( + tokens.map(async ({ issuerName, ...details }) => { + await E(mintsAdmin).addAsset({ + issuerName, + ...details, + keyword: issuerName, + decimalPlaces: 6, + }); + + const issuerP = E(agoricNames).lookup('issuer', issuerName); + const brand = await E(agoricNames).lookup('brand', issuerName); + const p1 = await E(issuerP).makeEmptyPurse(); + const balance = await E(p1).getCurrentAmount(); + t.log(`E(${issuerName}).makeEmptyPurse() works:`, balance); + t.deepEqual( + balance, + { brand, value: 0n }, + `${issuerName} provides working purses`, + ); + }), + ); + + const vbankAsset = await E(E(agoricNames).lookup('vbankAsset')) + .entries() + .then(es => Object.fromEntries(es)); + t.log('agoricNames.vbankAsset:', vbankAsset); + const expected = { + 'ibc/1234567890123456789012345678901234567890123456789012345678901234': { + // brand: ..., + denom: + 'ibc/1234567890123456789012345678901234567890123456789012345678901234', + displayInfo: { + assetKind: 'nat', + decimalPlaces: 6, + }, + // issuer: ..., + issuerName: 'Asset1', + proposedName: 'Asset1', + }, + 'ibc/2234567890123456789012345678901234567890123456789012345678901234': { + // brand: ..., + denom: + 'ibc/2234567890123456789012345678901234567890123456789012345678901234', + displayInfo: { + assetKind: 'nat', + decimalPlaces: 6, + }, + // issuer: ..., + issuerName: 'Asset2', + proposedName: 'Asset2', + }, + }; + t.like(vbankAsset, expected, 'vbankAsset has registered assets'); +}); diff --git a/yarn.lock b/yarn.lock index 02770a4..a3124aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -167,6 +167,11 @@ "@endo/nat" "4.1.27" "@endo/promise-kit" "0.2.56" +"@agoric/eslint-config@dev": + version "0.4.1-dev-00cccfc.0" + resolved "https://registry.yarnpkg.com/@agoric/eslint-config/-/eslint-config-0.4.1-dev-00cccfc.0.tgz#27748ff4581de89381dfa6538b7b2b4bddf30839" + integrity sha512-jGTMk9PsPPqtgvvKcxx+5+D2SH13TfSGzTtZ2VBbVzW0KtelCC1lpggal8dcMXtMwqIDg6eKp2ukY2Jqtl6zQw== + "@agoric/eventual-send@^0.14.1": version "0.14.1" resolved "https://registry.yarnpkg.com/@agoric/eventual-send/-/eventual-send-0.14.1.tgz#b414888bed67cf003a61bd22da30a70f79b8f9dc" @@ -221,6 +226,29 @@ resolved "https://registry.yarnpkg.com/@agoric/import-manager/-/import-manager-0.3.12-u12.0.tgz#0c3e5144ce35c64e2208c2373eafff72ba512f9d" integrity sha512-p5Cyf8uEpUwsHlRCUSQUnM7rVdn/VBk0vMceo2A4ESKT127HmhYhbggx/4KzmcweU1miFvONglIRcdCyN52EHQ== +"@agoric/inter-protocol@0.16.2-u12.0", "@agoric/inter-protocol@^0.16.2-u12.0": + version "0.16.2-u12.0" + resolved "https://registry.yarnpkg.com/@agoric/inter-protocol/-/inter-protocol-0.16.2-u12.0.tgz#7ebcf5e9d8d5d0aa08306ef44413e6e0a5f2704e" + integrity sha512-1MI1FScafG0tzvkLa3HoxlToq7LiQMvw+u4//+Ufbix0e3vwjRvLT2M8vt44Q0+/+23nSV9DjaCUGGL4nzIYeQ== + dependencies: + "@agoric/assert" "^0.6.1-u11wf.0" + "@agoric/ertp" "^0.16.3-u12.0" + "@agoric/governance" "^0.10.4-u12.0" + "@agoric/internal" "^0.4.0-u12.0" + "@agoric/notifier" "^0.6.3-u12.0" + "@agoric/store" "^0.9.3-u12.0" + "@agoric/time" "^0.3.3-u12.0" + "@agoric/vat-data" "^0.5.3-u12.0" + "@agoric/vats" "^0.15.2-u12.0" + "@agoric/zoe" "^0.26.3-u12.0" + "@endo/captp" "3.1.1" + "@endo/eventual-send" "0.17.2" + "@endo/far" "0.2.18" + "@endo/marshal" "0.8.5" + "@endo/nat" "4.1.27" + agoric "^0.21.2-u12.0" + jessie.js "^0.3.2" + "@agoric/inter-protocol@^0.16.1": version "0.16.1" resolved "https://registry.yarnpkg.com/@agoric/inter-protocol/-/inter-protocol-0.16.1.tgz#ace188e046bb63b0afd2d79065671d26dab53868" @@ -244,29 +272,6 @@ agoric "^0.21.1" jessie.js "^0.3.2" -"@agoric/inter-protocol@^0.16.2-u12.0": - version "0.16.2-u12.0" - resolved "https://registry.yarnpkg.com/@agoric/inter-protocol/-/inter-protocol-0.16.2-u12.0.tgz#7ebcf5e9d8d5d0aa08306ef44413e6e0a5f2704e" - integrity sha512-1MI1FScafG0tzvkLa3HoxlToq7LiQMvw+u4//+Ufbix0e3vwjRvLT2M8vt44Q0+/+23nSV9DjaCUGGL4nzIYeQ== - dependencies: - "@agoric/assert" "^0.6.1-u11wf.0" - "@agoric/ertp" "^0.16.3-u12.0" - "@agoric/governance" "^0.10.4-u12.0" - "@agoric/internal" "^0.4.0-u12.0" - "@agoric/notifier" "^0.6.3-u12.0" - "@agoric/store" "^0.9.3-u12.0" - "@agoric/time" "^0.3.3-u12.0" - "@agoric/vat-data" "^0.5.3-u12.0" - "@agoric/vats" "^0.15.2-u12.0" - "@agoric/zoe" "^0.26.3-u12.0" - "@endo/captp" "3.1.1" - "@endo/eventual-send" "0.17.2" - "@endo/far" "0.2.18" - "@endo/marshal" "0.8.5" - "@endo/nat" "4.1.27" - agoric "^0.21.2-u12.0" - jessie.js "^0.3.2" - "@agoric/internal@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@agoric/internal/-/internal-0.3.2.tgz#a1242947083ab46cbd34613add8bacbd0c9dc443" @@ -613,7 +618,7 @@ "@endo/nat" "^4.1.27" clsx "^1.1.1" -"@agoric/vat-data@^0.5.2": +"@agoric/vat-data@0.5.2", "@agoric/vat-data@^0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@agoric/vat-data/-/vat-data-0.5.2.tgz#abafab83279552466cf4ca946faa175a0a1423dc" integrity sha512-j71bSl7oPcWikR4bP15KMu67D3BLGLhEOcqgewC1cArcE99rhxDU19ALN0OITD0F0KkNCahRNifoIr73n/fBng==