diff --git a/contract/src/postalSvc.js b/contract/src/postalSvc.js new file mode 100644 index 0000000..d784cde --- /dev/null +++ b/contract/src/postalSvc.js @@ -0,0 +1,62 @@ +// @ts-check +import { E, Far } from '@endo/far'; +import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js'; + +const { keys, values } = Object; + +/** + * @typedef {object} PostalSvcTerms + * @property {import('@agoric/vats').NameHub} namesByAddress + */ + +/** @param {ZCF} zcf */ +export const start = zcf => { + const { namesByAddress, issuers } = zcf.getTerms(); + console.log('postalSvc issuers', Object.keys(issuers)); + + /** + * @param {string} addr + * @returns {ERef} + */ + const getDepositFacet = addr => { + assert.typeof(addr, 'string'); + return E(namesByAddress).lookup(addr, 'depositFacet'); + }; + + /** + * @param {string} addr + * @param {Payment} pmt + */ + const sendTo = (addr, pmt) => E(getDepositFacet(addr)).receive(pmt); + + /** @param {string} recipient */ + const makeSendInvitation = recipient => { + assert.typeof(recipient, 'string'); + + /** @type {OfferHandler} */ + const handleSend = async seat => { + const { give } = seat.getProposal(); + const depositFacet = await getDepositFacet(recipient); + const payouts = await withdrawFromSeat(zcf, seat, give); + + // XXX partial failure? return payments? + await Promise.all( + values(payouts).map(pmtP => + Promise.resolve(pmtP).then(pmt => E(depositFacet).receive(pmt)), + ), + ); + seat.exit(); + return `sent ${keys(payouts).join(', ')}`; + }; + + return zcf.makeInvitation(handleSend, 'send'); + }; + + const publicFacet = Far('postalSvc', { + lookup: (...path) => E(namesByAddress).lookup(...path), + getDepositFacet, + sendTo, + makeSendInvitation, + }); + return { publicFacet }; +}; diff --git a/contract/src/start-postalSvc.js b/contract/src/start-postalSvc.js new file mode 100644 index 0000000..f03b5d9 --- /dev/null +++ b/contract/src/start-postalSvc.js @@ -0,0 +1,106 @@ +/** + * @file core eval script* to start the postalSvc contract. + * + * * see test-gimix-proposal.js to make a script from this file. + * + * The `permit` export specifies the corresponding permit. + */ +// @ts-check + +import { E, Far } from '@endo/far'; + +const { Fail } = assert; + +const trace = (...args) => console.log('start-postalSvc', ...args); + +const fail = msg => { + throw Error(msg); +}; + +/** + * ref https://github.com/Agoric/agoric-sdk/issues/8408#issuecomment-1741445458 + * + * @param {ERef} namesByAddressAdmin + */ +const fixHub = async namesByAddressAdmin => { + /** @type {import('@agoric/vats').NameHub} */ + const hub = Far('Hub work-around', { + lookup: async (addr, ...rest) => { + await E(namesByAddressAdmin).reserve(addr); + const addressAdmin = await E(namesByAddressAdmin).lookupAdmin(addr); + assert(addressAdmin, 'no admin???'); + const addressHub = E(addressAdmin).readonly(); + if (rest.length === 0) return addressHub; + await E(addressAdmin).reserve(rest[0]); + return E(addressHub).lookup(...rest); + }, + has: _key => Fail`key space not well defined`, + entries: () => Fail`enumeration not supported`, + values: () => Fail`enumeration not supported`, + keys: () => Fail`enumeration not supported`, + }); + return hub; +}; + +/** + * @param {BootstrapPowers} powers + * @param {{ options?: { postalSvc: { + * bundleID: string; + * }}}} config + */ +export const startPostalSvc = async (powers, config) => { + const { + consume: { zoe, namesByAddressAdmin }, + installation: { + // @ts-expect-error not statically known at genesis + produce: { postalSvc: produceInstallation }, + }, + instance: { + // @ts-expect-error not statically known at genesis + produce: { postalSvc: produceInstance }, + }, + } = powers; + const { bundleID = fail(`no bundleID; try test-gimix-proposal.js?`) } = + config.options?.postalSvc ?? {}; + + /** @type {Installation} */ + const installation = await E(zoe).installBundleID(bundleID); + produceInstallation.resolve(installation); + + const namesByAddress = await fixHub(namesByAddressAdmin); + + const [IST, Invitation] = await Promise.all([ + E(zoe).getFeeIssuer(), + E(zoe).getInvitationIssuer(), + ]); + const { instance } = await E(zoe).startInstance( + installation, + { IST, Invitation }, + { namesByAddress }, + ); + produceInstance.resolve(instance); + + trace('postalSvc started'); +}; + +export const manifest = /** @type {const} */ ({ + [startPostalSvc.name]: { + consume: { + agoricNames: true, + namesByAddress: true, + namesByAddressAdmin: true, + zoe: true, + }, + installation: { + produce: { postalSvc: true }, + }, + instance: { + produce: { postalSvc: true }, + }, + }, +}); + +export const permit = JSON.stringify(Object.values(manifest)[0]); + +// script completion value +startPostalSvc; diff --git a/contract/test/test-postalSvc.js b/contract/test/test-postalSvc.js new file mode 100644 index 0000000..2840174 --- /dev/null +++ b/contract/test/test-postalSvc.js @@ -0,0 +1,124 @@ +// @ts-check +// XXX what's the state-of-the-art in ava setup? +// 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, Far } from '@endo/far'; +import { unsafeMakeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js'; +import { makeNameHubKit, makePromiseSpace } from '@agoric/vats'; +import { makeWellKnownSpaces } from '@agoric/vats/src/core/utils.js'; +import { AmountMath } from '@agoric/ertp/src/amountMath.js'; +import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; +import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js'; +import { startPostalSvc } from '../src/start-postalSvc.js'; + +/** @type {import('ava').TestFn>>} */ +const test = anyTest; + +const myRequire = createRequire(import.meta.url); + +const assets = { + postalSvc: myRequire.resolve('../src/postalSvc.js'), +}; + +const makeTestContext = async t => { + const bundleCache = await unsafeMakeBundleCache('bundles/'); + + return { bundleCache }; +}; + +test.before(async t => (t.context = await makeTestContext(t))); + +const bootstrap = async log => { + const { produce, consume } = makePromiseSpace(); + + const { admin, vatAdminState } = makeFakeVatAdmin(); + const { zoeService: zoe, feeMintAccess } = makeZoeKitForTest(admin); + + const { nameHub: agoricNames, nameAdmin: agoricNamesAdmin } = + makeNameHubKit(); + const spaces = await makeWellKnownSpaces(agoricNamesAdmin, log, [ + 'installation', + 'instance', + ]); + + const { nameAdmin: namesByAddressAdmin } = makeNameHubKit(); + + produce.zoe.resolve(zoe); + produce.feeMintAccess.resolve(feeMintAccess); + produce.agoricNames.resolve(agoricNames); + produce.namesByAddressAdmin.resolve(namesByAddressAdmin); + + /** @type {BootstrapPowers}} */ + // @ts-expect-error mock + const powers = { produce, consume, ...spaces }; + + return { powers, vatAdminState }; +}; + +test('deliver payment using address', async t => { + t.log('bootstrap'); + const { powers, vatAdminState } = await bootstrap(t.log); + + const { bundleCache } = t.context; + const bundle = await bundleCache.load(assets.postalSvc, 'postalSvc'); + const bundleID = `b1-${bundle.endoZipBase64Sha512}`; + t.log('publish bundle', bundleID.slice(0, 8)); + vatAdminState.installBundle(bundleID, bundle); + + await startPostalSvc(powers, { + options: { postalSvc: { bundleID } }, + }); + + const { agoricNames, zoe, namesByAddressAdmin } = powers.consume; + + const instance = await E(agoricNames).lookup('instance', 'postalSvc'); + + const addr1 = 'agoric1receiver'; + + const rxd = []; + const depositFacet = Far('DepositFacet', { + /** @param {Payment} pmt */ + receive: async pmt => { + rxd.push(pmt); + // XXX should return amount of pmt + }, + }); + + const my = makeNameHubKit(); + my.nameAdmin.update('depositFacet', depositFacet); + await E(namesByAddressAdmin).update(addr1, my.nameHub, my.nameAdmin); + + const { issuers, brands } = await E(zoe).getTerms(instance); + const postalSvc = E(zoe).getPublicFacet(instance); + const purse = await E(issuers.IST).makeEmptyPurse(); + + const pmt1 = await E(purse).withdraw(AmountMath.make(brands.IST, 0n)); + + // XXX should test that return value is amount + t.log('send IST with public facet to', addr1); + await E(postalSvc).sendTo(addr1, pmt1); + t.deepEqual(rxd, [pmt1]); + + { + const Payment = AmountMath.make(brands.IST, 0n); + const pmt2 = await E(postalSvc).makeSendInvitation(addr1); + const pmt3 = await E(purse).withdraw(Payment); + const Invitation = await E(issuers.Invitation).getAmountOf(pmt2); + const proposal = { give: { Payment, Invitation } }; + t.log('make offer to send IST, Invitation to', addr1); + const seat = E(zoe).offer( + E(postalSvc).makeSendInvitation(addr1), + proposal, + { Payment: pmt3, Invitation: pmt2 }, + ); + // XXX test is overly sensitive to order? + const result = await E(seat).getOfferResult(); + t.is(result, 'sent Invitation, Payment'); + t.deepEqual(rxd, [pmt1, pmt2, pmt3]); + const done = await E(seat).getPayouts(); + } +}); +test.todo('partial failure: send N+1 payments where >= 1 delivery fails');