Skip to content

Commit

Permalink
chore(launchIt): put mint+pool back together again
Browse files Browse the repository at this point in the history
handling issuers when paying out the tokens was messy
  • Loading branch information
dckc committed Jan 3, 2024
1 parent 5f23a85 commit 3541e1a
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 114 deletions.
129 changes: 59 additions & 70 deletions contract/src/launchIt.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,105 +20,90 @@ const { Fail, quote: q } = assert;

const KeywordShape = M.string();

const MintOptsShape = M.splitRecord(
/**
* @typedef {{
* name: Keyword,
* assetKind?: AssetKind,
* displayInfo?: DisplayInfo,
* }} LaunchOpts
*/
const LaunchOptShape = M.splitRecord(
{ name: KeywordShape, supplyQty: M.bigint() },
{ assetKind: AssetKindShape, displayInfo: DisplayInfoShape },
);

const PoolProposalShape = harden({
give: { Base: AmountShape },
const LaunchProposalShape = harden({
give: {},
want: { Deposit: AmountShape },
exit: {
afterDeadline: { timer: TimerServiceShape, deadline: TimestampRecordShape },
},
});

/** @type {import('./types').ContractMeta} */
export const meta = {
privateArgsShape: { timerService: TimerServiceShape },
};

/**
* Deposits are limited to fungible assets.
* This contract is limited to fungible assets.
*
* TODO: charge for launching?
*
* @typedef {{
* timerBrand: unknown,
* }} LaunchItTerms
*
* @typedef {{
* name: Keyword,
* supplyQty: bigint,
* assetKind?: AssetKind,
* displayInfo?: DisplayInfo,
* }} MintOpts
*
* @typedef {{
* proposal: Proposal,
* seats: { creator: ZCFSeat, lockup: ZCFSeat, deposits: ZCFSeat }
* }} PoolOpts
*
* @param {ZCF<LaunchItTerms>} zcf
* @param {{ timerService: import('@agoric/time/src/types').TimerService }} privateArgs
* @param {ZCF} zcf
* @param {unknown} _privateArgs
* @param {import('@agoric/vat-data').Baggage} baggage
*/
export const start = async (zcf, privateArgs, baggage) => {
export const start = async (zcf, _privateArgs, baggage) => {
// TODO: consider moving minting to separate contract
// though... then we have the add issuer problem.

/**
* @param {ZCFSeat} seat
* @param {MintOpts} opts
* @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 mintHandler = async (seat, opts) => {
mustMatch(opts, MintOptsShape);
const { name, supplyQty, assetKind = 'nat', displayInfo = {} } = opts;
// TODO: charge for launching?
const mint = await zcf.makeZCFMint(name, assetKind, displayInfo);
const { brand } = await E(mint).getIssuerRecord();
const supplyAmt = AmountMath.make(brand, supplyQty);
mint.mintGains({ [name]: supplyAmt }, seat); // and throw away the mint
// 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 name;
};
const launchHandler = async (creator, opts) => {
mustMatch(opts, LaunchOptShape);
const { name, assetKind = 'nat', displayInfo = {} } = opts;

const { timerService } = privateArgs;
const { timerBrand } = zcf.getTerms();
const svcBrand = await E(timerService).getTimerBrand();
timerBrand === svcBrand ||
Fail`timerBrand of ${q(timerService)} must match ${q(timerBrand)}}`;
const proposal = creator.getProposal();

const zone = makeDurableZone(baggage);
const timestampShape = { timerBrand, absValue: TimestampValueShape };
const pools = zone.mapStore('pools', {
keyShape: M.nat(),
// valueShape: poolOptsShape,
});
// TODO: charge for launching?
const mint = await zcf.makeZCFMint(name, assetKind, displayInfo);

/** @type {OfferHandler} */
const launchHandler = async creator => {
const proposal = creator.getProposal();
const { give, exit } = proposal;
assert('afterDeadline' in exit, 'guaranteed by shape');
// const { afterDeadline } = exit;
const { zcfSeat: lockup } = zcf.makeEmptySeatKit();
atomicRearrange(zcf, [[creator, lockup, give]]);
const { zcfSeat: deposits } = zcf.makeEmptySeatKit();
const key = pools.size();
/** @type {PoolOpts} */
const detail = { proposal, seats: { creator, lockup, deposits } };

/** @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 createSubscribeInvitation = poolKey => {
/** @type {PoolOpts} */
const zone = makeDurableZone(baggage);
const pools = zone.mapStore('pools', {
keyShape: M.number(),
// valueShape: PoolDetailShape,
});

const makeSubscribeInvitation = poolKey => {
/** @type {PoolDetail} */
const pool = pools.get(poolKey);
const { deposits } = pool.seats;

/** @type {OfferHandler} */
const subscribeHandler = subscriber => {
const { give } = subscriber.getProposal();
Expand All @@ -129,20 +114,24 @@ export const start = async (zcf, privateArgs, baggage) => {
const proposalShape = harden({
give: { Deposit: { brand: Deposit.brand, value: M.nat() } },
});
return zcf.makeInvitation(subscribeHandler, 'subscribe', {}, proposalShape);
return zcf.makeInvitation(
subscribeHandler,
'subscribe',
undefined,
proposalShape,
);
};

return {
publicFacet: Far('PF', {
makeMintInvitation: () => zcf.makeInvitation(mintHandler, 'mint'),
makeCreatePoolInvitation: () =>
makeLaunchInvitation: () =>
zcf.makeInvitation(
launchHandler,
'launch',
undefined,
PoolProposalShape,
LaunchProposalShape,
),
createSubscribeInvitation,
makeSubscribeInvitation,
}),
};
};
105 changes: 61 additions & 44 deletions contract/test/test-launchIt.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { makeBundleCacheContext, 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 } from '@agoric/ertp/src/amountMath.js';
import { makeIssuerKit } from '@agoric/ertp';

const nodeRequire = createRequire(import.meta.url);

Expand Down Expand Up @@ -39,19 +41,14 @@ const startLaunchIt = async (powers, config) => {
produce: { [contractName]: produceInstance },
},
} = powers;
const { bundleID = Fail`no bundleID` } = config.options?.[contractName] ?? {};
const { bundleID = Fail`no bundleID`, issuers = {} } =
config.options?.[contractName] ?? {};
/** @type {Installation<import('../src/launchIt.js').start>} */
const installation = await E(zoe).installBundleID(bundleID);
produceInstallation.resolve(installation);

const timerBrand = await E(chainTimerService).getTimerBrand();
// TODO: use startUpgradeable
const started = await E(zoe).startInstance(
installation,
{},
{ timerBrand },
{ timerService: await chainTimerService },
);
const started = await E(zoe).startInstance(installation, issuers);
produceInstance.resolve(started.instance);
};

Expand All @@ -64,15 +61,17 @@ test.serial('start contract', async t => {
t.log('publish bundle', bundleID.slice(0, 8));
vatAdminState.installBundle(bundleID, bundle);

const MNY = makeIssuerKit('MNY');

await startLaunchIt(powers, {
options: { [contractName]: { bundleID } },
options: { [contractName]: { bundleID, issuers: { MNY: MNY.issuer } } },
});

const { agoricNames } = powers.consume;
const instance = await E(agoricNames).lookup('instance', contractName);
t.is(typeof instance, 'object');

t.context.shared.powers = powers;
Object.assign(t.context.shared, { powers, MNY });
});

test.serial('launch a token', async t => {
Expand All @@ -84,46 +83,59 @@ test.serial('launch a token', async t => {
* @param {import('./wallet-tools.js').MockWallet} wallet
*/
const cathy = async (wellKnown, wallet) => {
/** @type {import('@agoric/smart-wallet').OfferSpec} */
const mintOfferSpec = {
id: 'mint-1',
invitationSpec: {
source: 'contract',
instance: await wellKnown.instance.consume[contractName],
publicInvitationMaker: 'makeMintInvitation',
},
proposal: { give: {} },
offerArgs: { name: 'CDOG', supplyQty: 1_000_000n },
};

t.log('1,000,000 CDOG tokens are minted');
const updates = await E(wallet.offers).executeOffer(mintOfferSpec);

const expected = [
{ status: { id: mintOfferSpec.id } },
{ status: { result: 'CDOG' } },
];
for await (const selector of expected) {
const { value } = await updates.next();
// t.log('update ##NN', value);
t.log('update ##NN', selector);
t.like(value, selector);
const instance = await wellKnown.instance[contractName];
const { timerService: timer } = wellKnown;
const timerBrand = await wellKnown.brand.timer;
const MNYbrand = await wellKnown.brand.MNY;

{
const deadline = harden({ timerBrand, absValue: 10n });

/** @type {import('@agoric/smart-wallet').OfferSpec} */
const launchOfferSpec = {
id: 'mint-1',
invitationSpec: {
source: 'contract',
instance,
publicInvitationMaker: 'makeLaunchInvitation',
},
proposal: {
give: {},
want: { Deposit: AmountMath.makeEmpty(MNYbrand) },
exit: { afterDeadline: { timer, deadline } },
},
offerArgs: { name: 'CDOG', supplyQty: 1_000_000n },
};

t.log('1,000,000 CDOG tokens are minted');
const updates = await E(wallet.offers).executeOffer(launchOfferSpec);

const expected = [
{ id: launchOfferSpec.id },
{ result: 0 },
// { status: { numWantsSatisfied: 1 } },
// { status: { payouts: '@@' } },
];
for await (const selector of expected) {
const {
value: { status },
} = await updates.next();
t.log('expecting ##NN', selector);
// t.log('update ##NN', value);
t.like(status, selector);
}
}

t.log('CDOG tokens are locked up in a pool');
const timerBrand = await wellKnown.brand.consume.timerBrand;
const deadline = harden({ timerBrand, absValue: 10n });
};

const albert = async (wellKnown, wallet) => {
const instance = await wellKnown.instance.consume[contractName];
const instance = await wellKnown.instance[contractName];
/** @type {import('@agoric/smart-wallet').OfferSpec} */
const offerSpec = {
id: 'contribute-2',
invitationSpec: {
source: 'contract',
instance,
publicInvitationMaker: 'makeContributeInvitation',
publicInvitationMaker: 'createSubscribeInvitation',
invitationArgs: [
{
name: 'CDOG',
Expand All @@ -135,11 +147,15 @@ test.serial('launch a token', async t => {
proposal: { give: {} },
};
};
const powers = t.context.shared.powers;
const { powers, MNY } = t.context.shared;

powers.brand.produce.MNY.resolve(MNY.brand);
powers.issuer.produce.MNY.resolve(MNY.issuer);
const wellKnown = {
instance: powers.instance,
issuer: powers.issuer,
brand: powers.brand,
timerService: await powers.consume.chainTimerService, // XXX
instance: powers.instance.consume,
issuer: powers.issuer.consume,
brand: powers.brand.consume,
};

const { zoe } = powers.consume;
Expand All @@ -151,6 +167,7 @@ test.serial('launch a token', async t => {
{ zoe, namesByAddressAdmin, chainStorage },
{ Invitation: invitationIssuer },
);
assert(await wellKnown.brand.timer, 'no timer brand???');
await cathy(wellKnown, await walletFactory.makeSmartWallet('agoric1cathy'));
t.log('TODO: pool is open for contributions');
t.log('TODO: boostrap time is up. swap contributions');
Expand Down

0 comments on commit 3541e1a

Please sign in to comment.