From 23ca000f9a5ae4c57abbba5ca870bf0cfa691723 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sun, 14 Jan 2024 23:15:48 -0600 Subject: [PATCH] WIP: launch using contractStarter failing with: Error#1: unsupported source: continuing at Alleged: Offers.executeOffer (file:///home/connolly/projects/ag-power-tools/contract/test/wallet-tools.js:107:15) - contractStarter: add install method, separate from starting - require give: { Fee } amount in proposal hapes - add label support - payouts: Handles rather than Started (factor out depositHandles) - test: starterSam can install - launchIt: contract per launch, using creatorFacet - test: launcherLarry subsumes creatorCathy --- contract/src/contractStarter.js | 100 +++++++++++++++++---- contract/src/launchIt.js | 154 +++++++++++++------------------- contract/test/market-actors.js | 107 +++++++++++++++++----- contract/test/test-launchIt.js | 84 +++++++---------- 4 files changed, 263 insertions(+), 182 deletions(-) diff --git a/contract/src/contractStarter.js b/contract/src/contractStarter.js index 905afa9..f815251 100644 --- a/contract/src/contractStarter.js +++ b/contract/src/contractStarter.js @@ -80,6 +80,18 @@ export const privateArgsShape = meta.privateArgsShape; * }} LimitedAccess */ +export const InstallOptsShape = M.splitRecord( + { bundleID: M.string() }, + { label: M.string() }, +); + +/** + * @typedef {{ + * bundleID: string, + * label?: string // XXX call it bundleLabel? + * }} InstallOpts + */ + /** * @see {ZoeService.startInstance} */ @@ -109,7 +121,7 @@ export const StartOptionsShape = M.and( * issuerKeywordRecord: Record, * customTerms: StartParams['terms'], * privateArgs: StartParams['privateArgs'], - * instanceLabel: string, + * instanceLabel: string, // XXX add bundleLabel? * permit: Record, * }>} StartOptions */ @@ -137,6 +149,60 @@ export const start = (zcf, limitedPowers, _baggage) => { const pubMarshaller = E(board).getPublishingMarshaller(); + const InstallProposalShape = M.splitRecord({ + give: { Fee: M.gte(prices.installBundleID) }, + // TODO: want: { Started: StartedAmountPattern } + }); + + /** + * @param {ZCFSeat} seat + * @param {string} description + * @param {{ installation: unknown, instance?: unknown }} handles + */ + const depositHandles = async (seat, description, handles) => { + const handlesInDetails = zcf.makeInvitation( + noHandler, + description, + handles, + NoProposalShape, + ); + const amt = await E(invitationIssuerP).getAmountOf(handlesInDetails); + await depositToSeat( + zcf, + seat, + { Handles: amt }, + { Handles: handlesInDetails }, + ); + }; + + /** + * @param {ZCFSeat} seat + * @param {InstallOpts} opts + */ + const installHandler = async (seat, opts) => { + mustMatch(opts, InstallOptsShape); + + atomicRearrange( + zcf, + harden([[seat, fees, { Fee: prices.installBundleID }]]), + ); + + const { bundleID, label } = opts; + const installation = await E(zoe).installBundleID(bundleID, label); + + await depositHandles(seat, 'installed', { installation }); + seat.exit(); + return harden(`${opts.label} installed`); + }; + + const makeInstallInvitation = () => + zcf.makeInvitation( + installHandler, + 'install', + undefined, + InstallProposalShape, + ); + // NOTE: opts could be moved to offerArgs to // save one layer of closure, but // this way makes the types more discoverable via publicFacet @@ -161,12 +227,17 @@ export const start = (zcf, limitedPowers, _baggage) => { ...keys(opts.permit || {}).map(k => prices[k]), ]); + const StartProposalShape = M.splitRecord({ + give: { Fee: M.gte(Fee) }, + // TODO: want: { Started: StartedAmountPattern } + }); + /** @param {ZCFSeat} seat */ const handleStart = async seat => { atomicRearrange(zcf, harden([[seat, fees, { Fee }]])); const installation = await ('installation' in opts ? opts.installation - : E(zoe).installBundleID(opts.bundleID)); + : E(zoe).installBundleID(opts.bundleID, opts.instanceLabel)); const { issuerKeywordRecord, customTerms, privateArgs, instanceLabel } = opts; @@ -182,7 +253,7 @@ export const start = (zcf, limitedPowers, _baggage) => { { ...privateArgs, ...powers }, instanceLabel, ); - // WARNING: adminFacet is dropped + // TODO: WARNING: adminFacet is dropped const { instance, creatorFacet } = it; const itsTerms = await E(zoe).getTerms(instance); @@ -196,26 +267,21 @@ export const start = (zcf, limitedPowers, _baggage) => { await E(creatorFacet).initStorageNode(itsStorage); } - const handlesInDetails = zcf.makeInvitation( - noHandler, - 'started', - { instance, installation }, - NoProposalShape, - ); - const amt = await E(invitationIssuerP).getAmountOf(handlesInDetails); - await depositToSeat( - zcf, - seat, - { Started: amt }, - { Started: handlesInDetails }, - ); + await depositHandles(seat, 'started', { instance, installation }); seat.exit(); return harden({ invitationMakers: creatorFacet }); }; - return zcf.makeInvitation(handleStart, 'start'); + + return zcf.makeInvitation( + handleStart, + 'start', + undefined, + StartProposalShape, + ); }; const publicFacet = Far('PublicFacet', { + makeInstallInvitation, makeStartInvitation, }); diff --git a/contract/src/launchIt.js b/contract/src/launchIt.js index f19b68c..d4b6edb 100644 --- a/contract/src/launchIt.js +++ b/contract/src/launchIt.js @@ -18,120 +18,92 @@ import { atomicRearrange } from '@agoric/zoe/src/contractSupport/index.js'; const { Fail, quote: q } = assert; -const KeywordShape = M.string(); - +/** @type {import('./types').ContractMeta} */ +export const meta = { + customTermsShape: M.splitRecord( + { name: M.string(), supplyQty: M.bigint() }, + { assetKind: AssetKindShape, displayInfo: DisplayInfoShape }, + ), +}; +export const { customTermsShape } = meta; /** * @typedef {{ - * name: Keyword, + * name: string, * assetKind?: AssetKind, * displayInfo?: DisplayInfo, - * }} LaunchOpts + * }} LaunchTerms */ -const LaunchOptShape = M.splitRecord( - { name: KeywordShape, supplyQty: M.bigint() }, - { assetKind: AssetKindShape, displayInfo: DisplayInfoShape }, -); - -const LaunchProposalShape = harden({ - give: {}, - want: { Deposit: AmountShape }, - exit: { - afterDeadline: { timer: TimerServiceShape, deadline: TimestampRecordShape }, - }, -}); /** * This contract is limited to fungible assets. * - * TODO: charge for launching? - * - * @param {ZCF} zcf + * @param {ZCF} zcf * @param {unknown} _privateArgs * @param {import('@agoric/vat-data').Baggage} baggage */ export const start = async (zcf, _privateArgs, baggage) => { - // TODO: consider moving minting to separate contract - // though... then we have the add issuer problem. - - /** - * @typedef {{ - * proposal: Proposal, - * mint: ZCFMint, - * seats: { creator: ZCFSeat, lockup: ZCFSeat, deposits: ZCFSeat }, - * }} PoolDetail - * - */ - - /** - * @param {ZCFSeat} creator - * @param {LaunchOpts} opts - * @throws if name is already used (ISSUE: how are folks supposed to know???) - */ - const launchHandler = async (creator, opts) => { - mustMatch(opts, LaunchOptShape); - const { name, assetKind = 'nat', displayInfo = {} } = opts; - - const proposal = creator.getProposal(); - - // TODO: charge for launching? - const mint = await zcf.makeZCFMint(name, assetKind, displayInfo); - - const { zcfSeat: lockup } = zcf.makeEmptySeatKit(); - const { zcfSeat: deposits } = zcf.makeEmptySeatKit(); + const { name, assetKind = 'nat', displayInfo = {}, brands } = zcf.getTerms(); + mustMatch(brands, M.splitRecord({ Deposit: BrandShape })); + const { zcfSeat: deposits } = zcf.makeEmptySeatKit(); - /** @type {PoolDetail} */ - const detail = harden({ - proposal, - mint, - seats: { creator, lockup, deposits }, - }); - const key = pools.getSize(); - pools.init(key, detail); - // const invitationMakers = { TODO: {} }; - // ISSUE: how does the brand get to the board so clients can make offers? - // ISSUE: how can clients make offers if issuer is not in agoricNames? - return key; - }; - - const zone = makeDurableZone(baggage); - const pools = zone.mapStore('pools', { - keyShape: M.number(), - // valueShape: PoolDetailShape, + const DepositAmountShape = { brand: brands.Deposit, value: M.nat() }; + const SubscribeProposalShape = harden({ + give: { Deposit: DepositAmountShape }, }); - const makeSubscribeInvitation = poolKey => { - /** @type {PoolDetail} */ - const pool = pools.get(poolKey); - const { deposits } = pool.seats; - - /** @type {OfferHandler} */ - const subscribeHandler = subscriber => { - const { give } = subscriber.getProposal(); - atomicRearrange(zcf, [[subscriber, deposits, give]]); - }; + /** @type {OfferHandler} */ + const subscribeHandler = subscriber => { + const { give } = subscriber.getProposal(); + atomicRearrange(zcf, [[subscriber, deposits, give]]); + }; - const { Deposit } = pool.proposal.want; - const proposalShape = harden({ - give: { Deposit: { brand: Deposit.brand, value: M.nat() } }, - }); + const makeSubscribeInvitation = () => { return zcf.makeInvitation( subscribeHandler, 'subscribe', undefined, - proposalShape, + SubscribeProposalShape, ); }; - return { - publicFacet: Far('PF', { - makeLaunchInvitation: () => - zcf.makeInvitation( - launchHandler, - 'launch', - undefined, - LaunchProposalShape, - ), - makeSubscribeInvitation, - }), + const publicFacet = Far('PF', { makeSubscribeInvitation }); + + const mint = await zcf.makeZCFMint(name, assetKind, displayInfo); + const { zcfSeat: lockup } = zcf.makeEmptySeatKit(); + + const LaunchProposalShape = M.splitRecord( + { + exit: { + afterDeadline: { + timer: TimerServiceShape, + deadline: TimestampRecordShape, + }, + }, + }, + { + want: { Deposit: DepositAmountShape }, + }, + ); + + /** + * @param {ZCFSeat} creator + */ + const launchHandler = async creator => { + const proposal = creator.getProposal(); + + // const invitationMakers = { TODO: {} }; + return name; }; + + const creatorFacet = Far('CF', { + Launch: () => + zcf.makeInvitation( + launchHandler, + 'launch', + undefined, + LaunchProposalShape, + ), + }); + + return { publicFacet, creatorFacet }; }; diff --git a/contract/test/market-actors.js b/contract/test/market-actors.js index 499b217..22a0997 100644 --- a/contract/test/market-actors.js +++ b/contract/test/market-actors.js @@ -1,5 +1,6 @@ // @ts-check import { E, getInterfaceOf } from '@endo/far'; +import { makePromiseKit } from '@endo/promise-kit'; import { AmountMath } from '@agoric/ertp/src/amountMath.js'; import { allValues, mapValues, seatLike } from './wallet-tools.js'; @@ -221,6 +222,34 @@ export const starterSam = async (t, mine, wellKnown) => { }, }; + let offerSeq = 0; + /** @param {{ bundleID: string, label?: string}} opts */ + const install = async opts => { + const starterAux = await wellKnown.boardAux(instance.contractStarter); + const { installBundleID } = starterAux.terms.prices; + t.log('sam gives', installBundleID, 'to install', opts.label); + const updates = await E(wallet.offers).executeOffer({ + id: `install-${(offerSeq += 1)}`, + invitationSpec: { + source: 'contract', + instance: instance.contractStarter, + publicInvitationMaker: 'makeInstallInvitation', + }, + proposal: { give: { Fee: installBundleID } }, + offerArgs: opts, + }); + const payouts = await E(seatLike(updates)).getPayouts(); + t.log('sam install paid', payouts); + const { + value: [ + { + customDetails: { installation }, + }, + ], + } = payouts.Handles; + return installation; + }; + const getPostalSvcTerms = async () => { const { terms: { namesByAddress }, @@ -240,14 +269,17 @@ export const starterSam = async (t, mine, wellKnown) => { 'from', (opts.bundleID || '').slice(0, 8), ); + const starterAux = await wellKnown.boardAux(instance.contractStarter); + const { startInstance } = starterAux.terms.prices; const updates = await E(wallet.offers).executeOffer({ - id: 'samStart-1', + id: `samStart-${(offerSeq += 1)}`, invitationSpec: { source: 'contract', instance: instance.contractStarter, publicInvitationMaker: 'makeStartInvitation', invitationArgs: [opts], }, + proposal: { give: { Fee: startInstance } }, }); const seat = seatLike(updates); @@ -272,47 +304,78 @@ export const starterSam = async (t, mine, wellKnown) => { return details.customDetails; }; - return { getPostalSvcTerms, installAndStart }; + return { install, getPostalSvcTerms, installAndStart }; }; /** * @param {import('ava').ExecutionContext} t * @param {{ * wallet: import('./wallet-tools.js').MockWallet - * instance: Instance * }} mine * @param {*} wellKnown */ -export const creatorCathy = async (t, { wallet, instance }, wellKnown) => { +export const launcherLarry = async (t, { wallet }, wellKnown) => { const { timerService: timer } = wellKnown; const timerBrand = await wellKnown.brand.timer; - const MNYbrand = await wellKnown.brand.MNY; + const MNY = { + brand: await wellKnown.brand.MNY, + issuer: await wellKnown.issuer.MNY, + }; + + const instance = { + contractStarter: await wellKnown.instance.contractStarter, + }; + + let offerSeq = 0; - { + const launch = async ( + customTerms = { name: 'CDOG', supplyQty: 1_000_000n }, + installation, + ) => { + const starterAux = await wellKnown.boardAux(instance.contractStarter); + const { startInstance } = starterAux.terms.prices; const deadline = harden({ timerBrand, absValue: 10n }); + const startOpts = { + label: 'CDOG-launch', + installation, + issuerKeywordRecord: { Deposit: MNY.issuer }, + customTerms, + }; /** @type {import('@agoric/smart-wallet').OfferSpec} */ - const launchOfferSpec = { - id: 'mint-1', + + const launchOfferId = `launch-${(offerSeq += 1)}`; + const updates = await E(wallet.offers).executeOffer({ + id: launchOfferId, invitationSpec: { source: 'contract', - instance, - publicInvitationMaker: 'makeLaunchInvitation', + instance: instance.contractStarter, + publicInvitationMaker: 'makeStartInvitation', + invitationArgs: [startOpts], }, proposal: { - give: {}, - want: { Deposit: AmountMath.makeEmpty(MNYbrand) }, - exit: { afterDeadline: { timer, deadline } }, + give: { Fee: startInstance }, }, - offerArgs: { name: 'CDOG', supplyQty: 1_000_000n }, - }; + }); - t.log('1,000,000 CDOG tokens are minted'); - const updates = await E(wallet.offers).executeOffer(launchOfferSpec); + t.log('TODO: 1,000,000 CDOG tokens are minted'); - const expected = { result: 0 }; - const seat = seatLike(updates); - const result = await E(seat).getOfferResult(); - t.is(result, expected.result); - } + const result = await E(seatLike(updates)).getOfferResult(); + t.log('larry launch result', result); + const up2 = await E(wallet.offers).executeOffer({ + id: `kickoff-${(offerSeq += 1)}`, + invitationSpec: { + source: 'continuing', + previousOffer: launchOfferId, + invitationMakerName: 'Launch', + }, + proposal: { + exit: { afterDeadline: { timer, deadline } }, + }, + }); + const kickoffResult = await E(seatLike(up2)).getOfferResult(); + t.is(result, '@@@'); + }; + + return { launch }; }; diff --git a/contract/test/test-launchIt.js b/contract/test/test-launchIt.js index 92d7022..2bda66b 100644 --- a/contract/test/test-launchIt.js +++ b/contract/test/test-launchIt.js @@ -7,14 +7,11 @@ import { makeBundleCacheContext, bootAndInstallBundles, getBundleId, - makeBootstrapPowers, } from './boot-tools.js'; -import { mockWalletFactory } from './wallet-tools.js'; -import { makeNameHubKit } from '@agoric/vats'; -import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; -import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js'; +import { makeWalletFactory } from './wallet-tools.js'; +import { AssetKind } from '@agoric/ertp/src/amountMath.js'; import { makeIssuerKit } from '@agoric/ertp'; -import { creatorCathy, starterSam } from './market-actors.js'; +import { launcherLarry, starterSam } from './market-actors.js'; import { installContractStarter, startContractStarter, @@ -33,21 +30,11 @@ const test = anyTest; test.before(async t => (t.context = await makeBundleCacheContext(t))); -const makeWalletFactory = async (powers, issuers) => { - const { zoe, namesByAddressAdmin, chainStorage } = powers.consume; - - const invitationIssuer = await E(zoe).getInvitationIssuer(); - const walletFactory = mockWalletFactory( - { zoe, namesByAddressAdmin, chainStorage }, - { Invitation: invitationIssuer, ...issuers }, - ); - return walletFactory; -}; - -test.serial('boot walletFactory, contractStarter', async t => { +test.serial('boot, walletFactory, contractStarter', async t => { const bootKit = await bootAndInstallBundles(t, bundleRoots); const { powers, bundles } = bootKit; - const walletFactory = await makeWalletFactory(powers, {}); + const { IST } = powers.issuer.consume; + const walletFactory = await makeWalletFactory(powers.consume, { IST }); const bundleID = getBundleId(bundles.contractStarter); await installContractStarter(powers, { @@ -60,7 +47,7 @@ test.serial('boot walletFactory, contractStarter', async t => { Object.assign(t.context.shared, { ...bootKit, walletFactory }); // XXX untyped }); -test.serial('start contract', async t => { +test.serial('start launchIt instance to launch token', async t => { const { shared } = t.context; const { powers, boardAux, bundles } = shared; @@ -78,6 +65,7 @@ test.serial('start contract', async t => { * import('./market-actors').BoardAux} */ const wellKnown = { + timerService: await powers.consume.chainTimerService, // XXX installation: powers.installation.consume, instance: powers.instance.consume, issuer: powers.issuer.consume, @@ -87,21 +75,38 @@ test.serial('start contract', async t => { ), boardAux, }; + const sam = starterSam( t, { wallet: await walletFactory.makeSmartWallet('agoric1sam') }, wellKnown, ); - const { instance } = await E(sam).installAndStart({ - bundleID, - issuerKeywordRecord: { MNY: MNY.issuer }, - instanceLabel: 'launchIt', - }); + const installation = await E(sam).install({ bundleID, label: 'launchIt' }); + + // issuerKeywordRecord: { MNY: MNY.issuer }, - t.is(typeof instance, 'object'); + t.is(typeof installation, 'object'); - Object.assign(t.context.shared, { powers, MNY, instance }); + t.log('Creator Cathy chooses to launch a new token, CDOG, paired with MNY'); + + powers.brand.produce.MNY.resolve(MNY.brand); + powers.issuer.produce.MNY.resolve(MNY.issuer); + + assert(await wellKnown.brand.timer, 'no timer brand???'); + const larry = await launcherLarry( + t, + { wallet: await walletFactory.makeSmartWallet('agoric1cathy') }, + wellKnown, + ); + const x = await E(larry).launch( + { name: 'CDOG', supplyQty: 1_000_000n }, + installation, + ); + t.deepEqual(x, '@@@'); + t.log('TODO: pool is open for contributions'); + t.log('TODO: boostrap time is up. swap contributions'); + t.log('TODO Cathy withdraws proceeds'); }); const albert = async (wellKnown, wallet) => { @@ -124,28 +129,3 @@ const albert = async (wellKnown, wallet) => { proposal: { give: {} }, }; }; - -test.serial('launch a token', async t => { - t.log('Creator Cathy chooses to launch a new token, CDOG, paired with MNY'); - - const { powers, MNY, walletFactory, instance } = t.context.shared; - - powers.brand.produce.MNY.resolve(MNY.brand); - powers.issuer.produce.MNY.resolve(MNY.issuer); - const wellKnown = { - timerService: await powers.consume.chainTimerService, // XXX - instance: powers.instance.consume, - issuer: powers.issuer.consume, - brand: powers.brand.consume, - }; - - assert(await wellKnown.brand.timer, 'no timer brand???'); - await creatorCathy( - t, - { wallet: await walletFactory.makeSmartWallet('agoric1cathy'), instance }, - wellKnown, - ); - t.log('TODO: pool is open for contributions'); - t.log('TODO: boostrap time is up. swap contributions'); - t.log('TODO Cathy withdraws proceeds'); -});