Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: publish issuer/brand under arbitrary name #11

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
107 changes: 107 additions & 0 deletions contract/src/arbAssetNames.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* @file Prototype Decentralized Asset Naming
* @see {start}
*/
// @ts-check
import { E, Far } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';
import { AmountShape } from '@agoric/ertp/src/typeGuards.js';
import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js';
import { getTerms } from './contractMetaAux.js';

const { Fail, quote: q } = assert;

const AssetInfoShape = harden({
issuer: M.remotable('Issuer'),
brand: M.remotable('Brand'),
});

const PublishProposalShape = harden({
give: { Pay: AmountShape },
want: {},
exit: M.any(),
});

/** @type {import("./types").ContractMeta} */
export const meta = harden({
privateArgsShape: {
nameAdmins: { issuer: M.remotable(), brand: M.remotable() },
},
customTermsShape: {
board: M.remotable('board'),
price: AmountShape,
},
});

/**
*
* @typedef {{
* board: import('@agoric/vats/src/types').Board,
* price: Amount,
* }} BoardTerms
* @param {ZCF<BoardTerms>} zcf
* @param {{ nameAdmins: {
* issuer: import('@agoric/vats/src/types').NameAdmin,
* brand: import('@agoric/vats/src/types').NameAdmin
* }}} privateArgs
* @param {*} baggage
*/
export const start = async (zcf, privateArgs, baggage) => {
const { board, price } = getTerms(zcf, meta);
mustMatch(privateArgs, meta.privateArgsShape);
const { nameAdmins } = privateArgs;

/**
* @param {Issuer} issuer
* @param {Brand} brand
*/
const prepare = async (issuer, brand) => {
mustMatch(harden({ issuer, brand }), AssetInfoShape);
// Check: issuer and brand recognize each other
const issuerBrand = await E(issuer).getBrand();
issuerBrand === brand ||
Fail`issuer ${q(issuer)}'s brand is ${q(issuerBrand)} not ${q(brand)}`;
const issuerOk = await E(brand).isMyIssuer(issuer);
issuerOk || Fail`brand ${q(brand)} does not recognize ${q(issuer)}`;

// Check: issuer makes purses with the same brand
const aPurse = await E(issuer).makeEmptyPurse();
const purseBrand = await E(aPurse).getAllegedBrand();
purseBrand === brand ||
Fail`issuer ${q(issuer)}'s purse's brand is ${q(purseBrand)} not ${q(
brand,
)}`;
};

/**
* @param {Issuer} issuer
* @param {Brand} brand
*/
const commit = async (issuer, brand) => {
const name = await E(board).getId(brand);
await Promise.all([
E(nameAdmins.issuer).update(name, issuer),
E(nameAdmins.brand).update(name, brand),
]);
return name;
};

const { zcfSeat: proceeds } = zcf.makeEmptySeatKit();
const publishHandler = async (seat, offerArgs) => {
mustMatch(harden(offerArgs), AssetInfoShape);
atomicRearrange(zcf, harden([[seat, proceeds, { Pay: price }]]));
const { issuer, brand } = offerArgs;
await prepare(issuer, brand);
const name = await commit(issuer, brand);
return name;
};
const makePublishAssetInvitation = () =>
zcf.makeInvitation(
publishHandler,
'publishAsset',
undefined,
PublishProposalShape,
);
const publicFacet = Far('Arb Asset Naming', { makePublishAssetInvitation });
return { publicFacet };
};
20 changes: 20 additions & 0 deletions contract/src/contractMetaAux.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @ts-check
import { mustMatch } from '@endo/patterns';

/**
* meta.customTermsShape doesn't seem to work.
*
* @template CT
* @param {ZCF<CT>} zcf
* @param {import('./types').ContractMeta} meta
* @returns {CT & StandardTerms}
*/
export const getTerms = (zcf, meta) => {
const terms = zcf.getTerms();
const { customTermsShape } = meta;
if (customTermsShape) {
const { issuers: _i, brands: _b, ...customTerms } = terms;
mustMatch(harden(customTerms), customTermsShape);
}
return terms;
};
11 changes: 8 additions & 3 deletions contract/src/postalSvc.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
// @ts-check
import { E, Far } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';
import { M } from '@endo/patterns';
import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import { getTerms } from './contractMetaAux.js';

const { keys, values } = Object;

/** @type {import("./types").ContractMeta} */
export const meta = harden({
customTermsShape: { namesByAddress: M.remotable('namesByAddress') },
});

/**
* @typedef {object} PostalSvcTerms
* @property {import('@agoric/vats').NameHub} namesByAddress
*/

/** @param {ZCF<PostalSvcTerms>} zcf */
export const start = zcf => {
const { namesByAddress, issuers } = zcf.getTerms();
mustMatch(namesByAddress, M.remotable('namesByAddress'));
const { namesByAddress, issuers } = getTerms(zcf, meta);
console.log('postalSvc issuers', Object.keys(issuers));

/**
Expand Down
51 changes: 51 additions & 0 deletions contract/src/start-arbAssetName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @ts-check
import { E } from '@endo/far';
import { AmountMath } from '@agoric/ertp/src/amountMath.js';

/** @typedef {typeof import('./arbAssetNames.js').start} ContractFn */

/**
* @param {BootstrapPowers} powers
* @param {{ assetNamingOptions: {
* bundleID: string
* stable?: string
* price: number | bigint
* unit?: number | bigint
* }}} config
*/
export const startArbAssetName = async (powers, config) => {
const {
consume: { zoe, board, agoricNamesAdmin },
instance: {
produce: { arbAssetName },
},
brand: { consume: brandConsume },
issuer: { consume: issuerConsume },
} = powers;
const {
assetNamingOptions: { bundleID, stable = 'IST', price, unit = 1n },
} = config;
/** @type {ERef<Installation<ContractFn>>} */
const installation = E(zoe).installBundleID(bundleID);
const issuers = { Price: await issuerConsume[stable] };
const terms = {
board: await board,
price: AmountMath.make(
await brandConsume[stable],
BigInt(price) * BigInt(unit),
),
};
const privateArgs = {
nameAdmins: {
issuer: await E(agoricNamesAdmin).lookupAdmin('issuer'),
brand: await E(agoricNamesAdmin).lookupAdmin('brand'),
},
};
const startedKit = await E(zoe).startInstance(
installation,
issuers,
terms,
privateArgs,
);
arbAssetName.resolve(startedKit.instance);
};
49 changes: 49 additions & 0 deletions contract/test/market-actors.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// @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 } from './wallet-tools.js';
import { makeIssuerKit } from '@agoric/ertp';

const { entries, fromEntries, keys } = Object;
const { Fail } = assert;
Expand Down Expand Up @@ -258,3 +260,50 @@ export const starterSam = async (t, mine, wellKnown) => {
t.deepEqual([...todo.keys()], []);
return done;
};

const seatLike = updates => {
const sync = {
result: makePromiseKit(),
payouts: makePromiseKit(),
};
(async () => {
for await (const update of updates) {
if (update.updated !== 'offerStatus') continue;
const { result, payouts } = update.status;
if (result) sync.result.resolve(result);
if (payouts) sync.payouts.resolve(payouts);
}
})();
return harden({
getOfferResult: () => sync.result.promise,
getPayouts: () => sync.payouts.promise,
});
};
/**
* @param {import('ava').ExecutionContext} t
* @param {{ wallet: import('./wallet-tools.js').MockWallet }} mine
* @param { WellKnown & { terms: { arbAssetName: { price }}} } wellKnown
*/
export const launcherLarry = async (t, { wallet }, wellKnown) => {
const { price } = wellKnown.terms.arbAssetName;
const BRD = makeIssuerKit('BRD');
const launched = { issuer: BRD.issuer, brand: BRD.brand };
t.log('Larry launched', launched);
await E(wallet.offers).addIssuer(BRD.issuer);
const instance = await wellKnown.instance.arbAssetName;
t.log('Larry offers', price, 'to publish');
const updates = await E(wallet.offers).executeOffer({
id: 'larry-brd-publish-1',
invitationSpec: {
source: 'contract',
instance,
publicInvitationMaker: 'makePublishAssetInvitation',
},
proposal: { give: { Pay: price } },
offerArgs: launched,
});
const seat = seatLike(updates);
const id = await E(seat).getOfferResult();
t.log('Larry published as', id);
return { ...launched, id };
};
74 changes: 74 additions & 0 deletions contract/test/test-arbAssetNames.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// @ts-check
/* eslint-disable import/order -- https://github.com/endojs/endo/issues/1235 */
// import { test } from './prepare-test-env-ava.js';
import { test as anyTest } from './prepare-test-env-ava.js';
import { createRequire } from 'module';

import { E } from '@endo/far';
import {
bootAndInstallBundles,
getBundleId,
makeBundleCacheContext,
} from './boot-tools.js';
import { startArbAssetName } from '../src/start-arbAssetName.js';
import { mockWalletFactory } from './wallet-tools.js';
import { launcherLarry } from './market-actors.js';
import { makeStableFaucet } from './mintStable.js';

/** @type {import('ava').TestFn<Awaited<ReturnType<makeBundleCacheContext>>>} */
const test = anyTest;

test.before(async t => (t.context = await makeBundleCacheContext(t)));

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

const contractName = 'arbAssetNames';
const bundleRoots = {
[contractName]: nodeRequire.resolve('../src/arbAssetNames.js'),
};

test('launcher Larry publishes his asset type', async t => {
const { powers, bundles } = await bootAndInstallBundles(t, bundleRoots);

const config = {
bundleID: getBundleId(bundles[contractName]),
price: 100n,
unit: 1_000_000n,
};
await startArbAssetName(powers, { assetNamingOptions: config });
const instance = await powers.instance.consume.arbAssetName;
t.log(instance);
t.is(typeof instance, 'object');

const { agoricNames, zoe, namesByAddressAdmin, chainStorage, feeMintAccess } =
powers.consume;
const { price } = await E(zoe).getTerms(instance);
const wellKnown = {
installation: {},
instance: powers.instance.consume,
issuer: powers.issuer.consume,
brand: powers.brand.consume,
terms: { arbAssetName: { price } },
};

const walletFactory = mockWalletFactory(
{ zoe, namesByAddressAdmin, chainStorage },
{
Invitation: await wellKnown.issuer.Invitation,
IST: await wellKnown.issuer.IST,
},
);
const { bundleCache } = t.context;
const { faucet } = makeStableFaucet({ feeMintAccess, zoe, bundleCache });
const funds = await E(faucet)(price.value * 2n);
const wallet = await walletFactory.makeSmartWallet('agoric1launcherLarry');
await E(wallet.deposit).receive(funds.withdraw(funds.getCurrentAmount()));
const info = await launcherLarry(t, { wallet }, wellKnown);
const publishedBrand = await E(agoricNames).lookup('brand', info.id);
t.log(info.brand, 'at', info.id);
t.is(publishedBrand, info.brand);
});

test.todo('publish an issuer / brand');

test('ok?', t => t.pass());