diff --git a/contract/src/arbAssetNames.js b/contract/src/arbAssetNames.js new file mode 100644 index 0000000..cbb10a3 --- /dev/null +++ b/contract/src/arbAssetNames.js @@ -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} 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 }; +}; diff --git a/contract/src/contractMetaAux.js b/contract/src/contractMetaAux.js new file mode 100644 index 0000000..30fe681 --- /dev/null +++ b/contract/src/contractMetaAux.js @@ -0,0 +1,20 @@ +// @ts-check +import { mustMatch } from '@endo/patterns'; + +/** + * meta.customTermsShape doesn't seem to work. + * + * @template CT + * @param {ZCF} 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; +}; diff --git a/contract/src/postalSvc.js b/contract/src/postalSvc.js index 045d837..5e3c37c 100644 --- a/contract/src/postalSvc.js +++ b/contract/src/postalSvc.js @@ -1,10 +1,16 @@ // @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 @@ -12,8 +18,7 @@ const { keys, values } = Object; /** @param {ZCF} 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)); /** diff --git a/contract/src/start-arbAssetName.js b/contract/src/start-arbAssetName.js new file mode 100644 index 0000000..0b655e8 --- /dev/null +++ b/contract/src/start-arbAssetName.js @@ -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>} */ + 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); +}; diff --git a/contract/test/market-actors.js b/contract/test/market-actors.js index df5ce9b..53b4e46 100644 --- a/contract/test/market-actors.js +++ b/contract/test/market-actors.js @@ -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; @@ -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 }; +}; diff --git a/contract/test/test-arbAssetNames.js b/contract/test/test-arbAssetNames.js new file mode 100644 index 0000000..2cde003 --- /dev/null +++ b/contract/test/test-arbAssetNames.js @@ -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>>} */ +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());