From 0584ef29db6ef761872fb031fb7d8592bd59a487 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Tue, 31 Oct 2023 17:04:09 -0700 Subject: [PATCH] refactor: pull offers.js and payments.js into smartWallets Fix imports in quite a few places --- packages/agoric-cli/src/commands/auction.js | 2 +- packages/agoric-cli/src/commands/gov.js | 2 +- packages/agoric-cli/src/commands/inter.js | 8 +- packages/agoric-cli/src/commands/oracle.js | 2 +- packages/agoric-cli/src/commands/psm.js | 4 +- packages/agoric-cli/src/commands/reserve.js | 2 +- .../agoric-cli/src/commands/test-upgrade.js | 2 +- packages/agoric-cli/src/lib/wallet.js | 2 +- packages/agoric-cli/test/test-inter-cli.js | 4 +- .../boot/test/bootstrapTests/liquidation.ts | 31 +- .../test-walletSurvivesZoeRestart.ts | 272 +++--------------- packages/inter-protocol/src/clientSupport.js | 18 +- packages/smart-wallet/src/invitations.js | 2 +- packages/smart-wallet/src/offers.js | 196 ------------- packages/smart-wallet/src/payments.js | 89 ------ packages/smart-wallet/src/smartWallet.js | 33 ++- packages/smart-wallet/src/utils.js | 4 +- .../smart-wallet/test/gameAssetContract.js | 2 +- packages/smart-wallet/test/test-addAsset.js | 6 +- .../smart-wallet/test/test-walletFactory.js | 4 +- 20 files changed, 123 insertions(+), 562 deletions(-) delete mode 100644 packages/smart-wallet/src/offers.js delete mode 100644 packages/smart-wallet/src/payments.js diff --git a/packages/agoric-cli/src/commands/auction.js b/packages/agoric-cli/src/commands/auction.js index fb10b58dc2c..7672ebcbb95 100644 --- a/packages/agoric-cli/src/commands/auction.js +++ b/packages/agoric-cli/src/commands/auction.js @@ -150,7 +150,7 @@ export const makeAuctionCommand = ( path: { paramPath: { key: 'governedParams' } }, }; - /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const offer = { id: opts.offerId, invitationSpec: { diff --git a/packages/agoric-cli/src/commands/gov.js b/packages/agoric-cli/src/commands/gov.js index 12ec24da538..6e6a79a7b85 100644 --- a/packages/agoric-cli/src/commands/gov.js +++ b/packages/agoric-cli/src/commands/gov.js @@ -13,7 +13,7 @@ import { sendAction, } from '../lib/wallet.js'; -/** @typedef {import('@agoric/smart-wallet/src/offers.js').OfferSpec} OfferSpec */ +/** @typedef {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} OfferSpec */ const collectValues = (val, memo) => { memo.push(val); diff --git a/packages/agoric-cli/src/commands/inter.js b/packages/agoric-cli/src/commands/inter.js index 4ec10903c7d..6b0a5b144ed 100644 --- a/packages/agoric-cli/src/commands/inter.js +++ b/packages/agoric-cli/src/commands/inter.js @@ -101,7 +101,7 @@ const makeFormatters = assets => { /** * Dynamic check that an OfferStatus is also a BidSpec. * - * @param {import('@agoric/smart-wallet/src/offers.js').OfferStatus} offerStatus + * @param {import('@agoric/smart-wallet/src/smartWallet.js').OfferStatus} offerStatus * @param {import('../lib/wallet.js').AgoricNamesRemotes} agoricNames * @param {typeof console.warn} warn * returns null if offerStatus is not a BidSpec @@ -125,7 +125,7 @@ const coerceBid = (offerStatus, agoricNames, warn) => { } /** - * @type {import('@agoric/smart-wallet/src/offers.js').OfferStatus & + * @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferStatus & * { offerArgs: BidSpec}} */ // @ts-expect-error dynamic cast @@ -136,7 +136,7 @@ const coerceBid = (offerStatus, agoricNames, warn) => { /** * Format amounts etc. in a BidSpec OfferStatus * - * @param {import('@agoric/smart-wallet/src/offers.js').OfferStatus & + * @param {import('@agoric/smart-wallet/src/smartWallet.js').OfferStatus & * { offerArgs: BidSpec}} bid * @param {import('agoric/src/lib/format.js').AssetDescriptor[]} assets */ @@ -328,7 +328,7 @@ inter auction status /** * @param {string} from - * @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer + * @param {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} offer * @param {Awaited>} tools * @param {boolean?} dryRun */ diff --git a/packages/agoric-cli/src/commands/oracle.js b/packages/agoric-cli/src/commands/oracle.js index ccad184afbf..a576d85fb89 100644 --- a/packages/agoric-cli/src/commands/oracle.js +++ b/packages/agoric-cli/src/commands/oracle.js @@ -107,7 +107,7 @@ export const makeOracleCommand = (logger, io = {}) => { const { lookupPriceAggregatorInstance } = await rpcTools(); const instance = lookupPriceAggregatorInstance(opts.pair); - /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const offer = { id: opts.offerId, invitationSpec: { diff --git a/packages/agoric-cli/src/commands/psm.js b/packages/agoric-cli/src/commands/psm.js index 61eaedffa46..3deac00b4fa 100644 --- a/packages/agoric-cli/src/commands/psm.js +++ b/packages/agoric-cli/src/commands/psm.js @@ -198,7 +198,7 @@ export const makePsmCommand = logger => { const { lookupPsmInstance } = await rpcTools(); const psmInstance = lookupPsmInstance(opts.pair); - /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const offer = { id: opts.offerId, invitationSpec: { @@ -255,7 +255,7 @@ export const makePsmCommand = logger => { brand: istBrand, value: BigInt(opts.limit * 1_000_000), }); - /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const offer = { id: opts.offerId, invitationSpec: { diff --git a/packages/agoric-cli/src/commands/reserve.js b/packages/agoric-cli/src/commands/reserve.js index c064ec497f4..e6b4e74c100 100644 --- a/packages/agoric-cli/src/commands/reserve.js +++ b/packages/agoric-cli/src/commands/reserve.js @@ -70,7 +70,7 @@ export const makeReserveCommand = (_logger, io = {}) => { const feesToBurn = { brand: agoricNames.brand.IST, value: opts.value }; - /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const offer = { id: opts.offerId, invitationSpec: { diff --git a/packages/agoric-cli/src/commands/test-upgrade.js b/packages/agoric-cli/src/commands/test-upgrade.js index f530b05dd12..88100226ecf 100644 --- a/packages/agoric-cli/src/commands/test-upgrade.js +++ b/packages/agoric-cli/src/commands/test-upgrade.js @@ -73,7 +73,7 @@ export const makeTestCommand = ( const { home, keyringBackend: backend } = testCmd.opts(); const io = { ...networkConfig, execFileSync, delay, stdout }; - /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const offer = { id: opts.offerId, invitationSpec: { diff --git a/packages/agoric-cli/src/lib/wallet.js b/packages/agoric-cli/src/lib/wallet.js index 95e7870a806..e2bab272eb0 100644 --- a/packages/agoric-cli/src/lib/wallet.js +++ b/packages/agoric-cli/src/lib/wallet.js @@ -91,7 +91,7 @@ export const outputActionAndHint = (bridgeAction, { stdout, stderr }) => { }; /** - * @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer + * @param {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} offer * @param {Pick} [stdout] */ export const outputExecuteOfferAction = (offer, stdout = process.stdout) => { diff --git a/packages/agoric-cli/test/test-inter-cli.js b/packages/agoric-cli/test/test-inter-cli.js index ee79b967df7..69bb924cd0c 100644 --- a/packages/agoric-cli/test/test-inter-cli.js +++ b/packages/agoric-cli/test/test-inter-cli.js @@ -188,7 +188,7 @@ const makeProcess = (t, keyring, out) => { }; /** - * @type {import('@agoric/smart-wallet/src/offers.js').OfferStatus & + * @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferStatus & * { offerArgs: import('@agoric/inter-protocol/src/auction/auctionBook.js').OfferSpec}} */ const offerStatus2 = harden({ @@ -253,7 +253,7 @@ test('amount parsing', t => { test.todo('want as max collateral wanted'); /** - * @type {import('@agoric/smart-wallet/src/offers.js').OfferStatus & + * @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferStatus & * { offerArgs: import('@agoric/inter-protocol/src/auction/auctionBook.js').OfferSpec}} */ const offerStatus1 = harden({ diff --git a/packages/boot/test/bootstrapTests/liquidation.ts b/packages/boot/test/bootstrapTests/liquidation.ts index f2793746f9d..7f38e8f1af6 100644 --- a/packages/boot/test/bootstrapTests/liquidation.ts +++ b/packages/boot/test/bootstrapTests/liquidation.ts @@ -63,10 +63,9 @@ export const likePayouts = ({ Bid, Collateral }) => ({ }, }); -export const makeLiquidationTestContext = async t => { - console.time('DefaultTestContext'); +export const makeWalletFactoryContext = async t => { const swingsetTestKit = await makeSwingsetTestKit(t.log, 'bundles/vaults', { - configSpecifier: '@agoric/vm-config/decentral-main-vaults-config.json', + configSpecifier: '@agoric/vm-config/decentral-main-vaults-config.json' }); const { runUtils, storage } = swingsetTestKit; @@ -83,7 +82,7 @@ export const makeLiquidationTestContext = async t => { const refreshAgoricNamesRemotes = () => { Object.assign( agoricNamesRemotes, - makeAgoricNamesRemotesFromFakeStorage(swingsetTestKit.storage), + makeAgoricNamesRemotesFromFakeStorage(swingsetTestKit.storage) ); }; agoricNamesRemotes.brand.ATOM || Fail`ATOM missing from agoricNames`; @@ -92,8 +91,30 @@ export const makeLiquidationTestContext = async t => { const walletFactoryDriver = await makeWalletFactoryDriver( runUtils, storage, - agoricNamesRemotes, + agoricNamesRemotes ); + return { + ...swingsetTestKit, + swingsetTestKit, + agoricNamesRemotes, + refreshAgoricNamesRemotes, + walletFactoryDriver, + }; +}; + +export type WalletFactoryTestContext = Awaited< + ReturnType +>; + +export const makeLiquidationTestContext = async t => { + console.time('DefaultTestContext'); + const { + swingsetTestKit, + agoricNamesRemotes, + refreshAgoricNamesRemotes, + walletFactoryDriver + } = await makeWalletFactoryContext(t); + console.timeLog('DefaultTestContext', 'walletFactoryDriver'); const governanceDriver = await makeGovernanceDriver( diff --git a/packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts b/packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts index c1fef632c27..d126ee8198e 100644 --- a/packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts +++ b/packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts @@ -1,22 +1,20 @@ /** @file Bootstrap test of liquidation across multiple collaterals */ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { NonNullish } from '@agoric/assert'; import process from 'process'; import type { ExecutionContext, TestFn } from 'ava'; -import type { ScheduleNotification } from '@agoric/inter-protocol/src/auction/scheduler.js'; + +import { ERef } from '@endo/far'; import { BridgeHandler } from '@agoric/vats'; import { LiquidationTestContext, - likePayouts, makeLiquidationTestContext, - scale6, LiquidationSetup, } from './liquidation.ts'; +import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; const test = anyTest as TestFn; -//#region Product spec const setup: LiquidationSetup = { vaults: [ { @@ -24,30 +22,12 @@ const setup: LiquidationSetup = { ist: 100, debt: 100.5, }, - { - atom: 15, - ist: 103, - debt: 103.515, - }, - { - atom: 15, - ist: 105, - debt: 105.525, - }, ], bids: [ { give: '80IST', discount: 0.1, }, - { - give: '90IST', - price: 9.0, - }, - { - give: '150IST', - discount: 0.15, - }, ], price: { starting: 12.34, @@ -65,69 +45,15 @@ const setup: LiquidationSetup = { }, }; -const outcome = { - bids: [ - { - payouts: { - Bid: 0, - Collateral: 8.897786, - }, - }, - { - payouts: { - Bid: 0, - Collateral: 10.01001, - }, - }, - { - payouts: { - Bid: 10.46, - Collateral: 16.432903, - }, - }, - ], - reserve: { - allocations: { - ATOM: 0.309852, - STARS: 0.309852, - }, - shortfall: 0, - }, - vaultsSpec: [ - { - locked: 3.373, - }, - { - locked: 3.024, - }, - { - locked: 2.792, - }, - ], - // TODO match spec https://github.com/Agoric/agoric-sdk/issues/7837 - vaultsActual: [ - { - locked: 3.525747, - }, - { - locked: 3.181519, - }, - { - locked: 2.642185, - }, - ], -} as const; -//#endregion - test.before(async t => { t.context = await makeLiquidationTestContext(t); }); + test.after.always(t => { return t.context.shutdown && t.context.shutdown(); }); -// Reference: Flow 1 from https://github.com/Agoric/agoric-sdk/issues/7123 -const checkFlow1 = async ( +const checkFlow = async ( t: ExecutionContext, { collateralBrandKey, @@ -141,19 +67,17 @@ const checkFlow1 = async ( }); const { - advanceTimeBy, - advanceTimeTo, - check, - priceFeedDrivers, - readLatest, walletFactoryDriver, setupVaults, placeBids, controller, buildProposal, } = t.context; + const { EV } = t.context.runUtils; + const buyer = await walletFactoryDriver.provideSmartWallet('agoric1buyer'); + const buildAndExecuteProposal = async packageSpec => { const proposal = await buildProposal(packageSpec); @@ -173,163 +97,45 @@ const checkFlow1 = async ( }; const metricsPath = `published.vaultFactory.managers.manager${managerIndex}.metrics`; - await setupVaults(collateralBrandKey, managerIndex, setup); - const buyer = await walletFactoryDriver.provideSmartWallet('agoric1buyer'); - await placeBids(collateralBrandKey, 'agoric1buyer', setup); - - { - // --------------- - // Change price to trigger liquidation - // --------------- - - await priceFeedDrivers[collateralBrandKey].setPrice(9.99); - - // check nothing liquidating yet - const liveSchedule: ScheduleNotification = readLatest( - 'published.auction.schedule', - ); - t.is(liveSchedule.activeStartTime, null); - t.like(readLatest(metricsPath), { - numActiveVaults: setup.vaults.length, - numLiquidatingVaults: 0, - }); - - // advance time to start an auction - console.log(collateralBrandKey, 'step 1 of 10'); - await advanceTimeTo(NonNullish(liveSchedule.nextDescendingStepTime)); - t.like(readLatest(metricsPath), { - numActiveVaults: 0, - numLiquidatingVaults: setup.vaults.length, - liquidatingCollateral: { - value: scale6(setup.auction.start.collateral), - }, - liquidatingDebt: { value: scale6(setup.auction.start.debt) }, - lockedQuote: null, - }); - - console.log(collateralBrandKey, 'step 2 of 10'); - await advanceTimeBy(3, 'minutes'); - t.like(readLatest(`published.auction.book${managerIndex}`), { - collateralAvailable: { value: scale6(setup.auction.start.collateral) }, - startCollateral: { value: scale6(setup.auction.start.collateral) }, - startProceedsGoal: { value: scale6(setup.auction.start.debt) }, - }); - - console.log(collateralBrandKey, 'step 3 of 10'); - await advanceTimeBy(3, 'minutes'); + // restart Zoe - console.log(collateralBrandKey, 'step 4 of 10'); - await advanceTimeBy(3, 'minutes'); - // XXX updates for bid1 and bid2 are appended in the same turn so readLatest gives bid2 - // NB: console output shows 8897786n payout which matches spec 8.897ATOM - // t.like(readLatest('published.wallet.agoric1buyer'), { - // status: { - // id: `${collateralBrandKey}-bid1`, - // payouts: { - // Bid: { value: 0n }, - // Collateral: { value: scale6(outcome.bids[0].payouts.Collateral) }, - // }, - // }, - // }); - - t.like(readLatest('published.wallet.agoric1buyer'), { - status: { - id: `${collateralBrandKey}-bid2`, - payouts: likePayouts(outcome.bids[1].payouts), - }, - }); - - console.log(collateralBrandKey, 'step 5 of 10'); - await advanceTimeBy(3, 'minutes'); - - console.log(collateralBrandKey, 'step 6 of 10'); - await advanceTimeBy(3, 'minutes'); - t.like(readLatest(`published.auction.book${managerIndex}`), { - collateralAvailable: { value: 9659301n }, - }); - - console.log(collateralBrandKey, 'step 7 of 10'); - await advanceTimeBy(3, 'minutes'); - - console.log(collateralBrandKey, 'step 8 of 10'); - await advanceTimeBy(3, 'minutes'); - - console.log(collateralBrandKey, 'step 9 of 10'); - await advanceTimeBy(3, 'minutes'); - // Not part of product spec - t.like(readLatest(metricsPath), { - numActiveVaults: 0, - numLiquidationsCompleted: setup.vaults.length, - numLiquidatingVaults: 0, - retainedCollateral: { value: 0n }, - totalCollateral: { value: 0n }, - totalCollateralSold: { value: 35340699n }, - totalDebt: { value: 0n }, - totalOverageReceived: { value: 0n }, - totalProceedsReceived: { value: 309540000n }, - totalShortfallReceived: { value: 0n }, - }); - - console.log(collateralBrandKey, 'step 10 of 10'); - // continuing after now would start a new auction - { - const { nextDescendingStepTime, nextStartTime } = readLatest( - 'published.auction.schedule', - ) as Record; - t.is(nextDescendingStepTime.absValue, nextStartTime.absValue); - } - - // bid3 still live because it's not fully satisfied - const { liveOffers } = readLatest('published.wallet.agoric1buyer.current'); - t.is(liveOffers[0][1].id, `${collateralBrandKey}-bid3`); - - // restart Zoe - // /////// Upgrading //////////////////////////////// - await buildAndExecuteProposal( - '@agoric/builders/scripts/vats/null-upgrade-zoe-proposal.js', - ); - - await buyer.tryExitOffer(`${collateralBrandKey}-bid3`); - t.like(readLatest('published.wallet.agoric1buyer'), { - status: { - id: `${collateralBrandKey}-bid3`, - payouts: likePayouts(outcome.bids[2].payouts), - }, - }); + // /////// Upgrading //////////////////////////////// + const zoeUpgradeSpec = { + package: 'builders', + packageScriptName: 'build:null-upgrade-zoe-proposal', + }; + await buildAndExecuteProposal(zoeUpgradeSpec); + + // const zoe = await EV.vat('bootstrap').consumeItem('zoe'); + // const invitationIssuer = await EV(zoe).getInvitationIssuer(); + // const invitationBrand = await EV(invitationIssuer).getBrand() + t.like(await buyer.getLatestUpdateRecord(), { + currentAmount: { + // brand from EV() doesn't compare correctly + // brand: invitationBrand, + value: [], + }, + updated: 'balance', + }); - // TODO express spec up top in a way it can be passed in here - // check.vaultNotification(managerIndex, 0, { - // debt: undefined, - // vaultState: 'liquidated', - // locked: { - // value: scale6(outcome.vaultsActual[0].locked), - // }, - // }); - // check.vaultNotification(managerIndex, 1, { - // debt: undefined, - // vaultState: 'liquidated', - // locked: { - // value: scale6(outcome.vaultsActual[1].locked), - // }, - // }); - } + await buyer.executeOfferMaker(Offers.vaults.OpenVault, { + offerId: 'open1', + collateralBrandKey: 'ATOM', + wantMinted: 5.0, + giveCollateral: 9.0, + }); - // // check reserve balances - // t.like(readLatest('published.reserve.metrics'), { - // allocations: { - // [collateralBrandKey]: { - // value: scale6(outcome.reserve.allocations[collateralBrandKey]), - // }, - // }, - // shortfallBalance: { value: scale6(outcome.reserve.shortfall) }, - // }); + t.like(buyer.getLatestUpdateRecord(), { + updated: 'offerStatus', + status: { id: 'open1', numWantsSatisfied: 1 }, + }); }; -test.serial.failing( +test.serial( 'wallet survives zoe null upgrade', - checkFlow1, + checkFlow, { collateralBrandKey: 'ATOM', managerIndex: 0 }, {}, ); diff --git a/packages/inter-protocol/src/clientSupport.js b/packages/inter-protocol/src/clientSupport.js index 56e0b283498..b5c4eadbe2e 100644 --- a/packages/inter-protocol/src/clientSupport.js +++ b/packages/inter-protocol/src/clientSupport.js @@ -72,7 +72,7 @@ const makeVaultProposal = ({ brand }, opts) => { * giveCollateral: number; * collateralBrandKey: string; * }} opts - * @returns {import('@agoric/smart-wallet/src/offers.js').OfferSpec} + * @returns {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const makeOpenOffer = ({ brand }, opts) => { const proposal = makeVaultProposal({ brand }, opts); @@ -111,7 +111,7 @@ const makeOpenOffer = ({ brand }, opts) => { * wantMinted?: number; * }} opts * @param {string} previousOffer - * @returns {import('@agoric/smart-wallet/src/offers.js').OfferSpec} + * @returns {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const makeAdjustOffer = ({ brand }, opts, previousOffer) => { // NB: not really a Proposal because the brands are not remotes @@ -141,7 +141,7 @@ const makeAdjustOffer = ({ brand }, opts, previousOffer) => { * giveMinted: number; * }} opts * @param {string} previousOffer - * @returns {import('@agoric/smart-wallet/src/offers.js').OfferSpec} + * @returns {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const makeCloseOffer = ({ brand }, opts, previousOffer) => { const proposal = makeVaultProposal({ brand }, opts); @@ -218,7 +218,7 @@ const makePsmProposal = (brands, opts, fee = 0, anchor = 'AUSD') => { * | { wantMinted: number } * | { giveMinted: number } * )} opts - * @returns {import('@agoric/smart-wallet/src/offers.js').OfferSpec} + * @returns {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const makePsmSwapOffer = ({ brand }, instance, opts) => { const method = @@ -309,7 +309,7 @@ export const makeParseAmount = * discount: number; // -1 to 1. e.g. 0.10 for 10% discount, -0.05 for 5% markup * } * )} opts - * @returns {import('@agoric/smart-wallet/src/offers.js').OfferSpec} + * @returns {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const makeBidOffer = (agoricNames, opts) => { assertAllDefined({ @@ -352,7 +352,7 @@ const makeBidOffer = (agoricNames, opts) => { ), }; - /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const offerSpec = { id: opts.offerId, invitationSpec: { @@ -376,7 +376,7 @@ const makeBidOffer = (agoricNames, opts) => { * give: number; * collateralBrandKey: string; * }} opts - * @returns {import('@agoric/smart-wallet/src/offers.js').OfferSpec} + * @returns {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const makeAddCollateralOffer = ({ brand }, opts) => { /** @type {AmountKeywordRecord} */ @@ -388,7 +388,7 @@ const makeAddCollateralOffer = ({ brand }, opts) => { ), }; - /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const offerSpec = { id: opts.offerId, invitationSpec: { @@ -409,7 +409,7 @@ const makeAddCollateralOffer = ({ brand }, opts) => { * unitPrice: bigint; * }} opts * @param {string} previousOffer - * @returns {import('@agoric/smart-wallet/src/offers.js').OfferSpec} + * @returns {import('@agoric/smart-wallet/src/smartWallet.js').OfferSpec} */ const makePushPriceOffer = (_agoricNames, opts, previousOffer) => { return { diff --git a/packages/smart-wallet/src/invitations.js b/packages/smart-wallet/src/invitations.js index d905d5ff2ed..4b9822338e5 100644 --- a/packages/smart-wallet/src/invitations.js +++ b/packages/smart-wallet/src/invitations.js @@ -46,7 +46,7 @@ const MAX_PIPE_LENGTH = 2; * * @typedef {{ * source: 'continuing', - * previousOffer: import('./offers.js').OfferId, + * previousOffer: import('./smartWallet.js').OfferId, * invitationMakerName: string, * invitationArgs?: any[], * }} ContinuingInvitationSpec diff --git a/packages/smart-wallet/src/offers.js b/packages/smart-wallet/src/offers.js deleted file mode 100644 index 10b4848fa68..00000000000 --- a/packages/smart-wallet/src/offers.js +++ /dev/null @@ -1,196 +0,0 @@ -import { E, passStyleOf } from '@endo/far'; -import { deeplyFulfilledObject } from '@agoric/internal'; -import { makePaymentsHelper } from './payments.js'; - -/** - * @typedef {number | string} OfferId - */ - -/** - * @typedef {{ - * id: OfferId, - * invitationSpec: import('./invitations').InvitationSpec, - * proposal: Proposal, - * offerArgs?: unknown - * }} OfferSpec - */ - -/** Value for "result" field when the result can't be published */ -export const UNPUBLISHED_RESULT = 'UNPUBLISHED'; - -/** - * @typedef {import('./offers.js').OfferSpec & { - * error?: string, - * numWantsSatisfied?: number - * result?: unknown | typeof UNPUBLISHED_RESULT, - * payouts?: AmountKeywordRecord, - * }} OfferStatus - */ - -/* eslint-disable jsdoc/check-param-names -- bug(?) with nested objects */ -/** - * @param {object} opts - * @param {ERef} opts.zoe - * @param {{ receive: (payment: *) => Promise }} opts.depositFacet - * @param {ERef>} opts.invitationIssuer - * @param {object} opts.powers - * @param {Pick} opts.powers.logger - * @param {(spec: import('./invitations').InvitationSpec) => ERef} opts.powers.invitationFromSpec - * @param {(brand: Brand) => Promise} opts.powers.purseForBrand - * @param {(status: OfferStatus) => void} opts.onStatusChange - * @param {(offerId: string, invitationAmount: Amount<'set'>, invitationMakers: import('./types').InvitationMakers, publicSubscribers: import('./types').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord ) => Promise} opts.onNewContinuingOffer - */ -export const makeOfferExecutor = ({ - zoe, - depositFacet, - invitationIssuer, - powers, - onStatusChange, - onNewContinuingOffer, -}) => { - const { invitationFromSpec, logger, purseForBrand } = powers; - - return { - /** - * Take an offer description provided in capData, augment it with payments and call zoe.offer() - * - * @param {OfferSpec} offerSpec - * @param {(seatRef: UserSeat) => void} onSeatCreated - * @returns {Promise} when the offer has been sent to Zoe; payouts go into this wallet's purses - * @throws if any parts of the offer are determined to be invalid before calling Zoe's `offer()` - */ - async executeOffer(offerSpec, onSeatCreated) { - logger.info('starting executeOffer', offerSpec.id); - - const paymentsManager = makePaymentsHelper(purseForBrand, depositFacet); - - /** @type {OfferStatus} */ - let status = { - ...offerSpec, - }; - /** @param {Partial} changes */ - const updateStatus = changes => { - status = { ...status, ...changes }; - onStatusChange(status); - }; - - /** @type {UserSeat} */ - let seatRef; - - const tryBody = async () => { - // 1. Prepare values and validate synchronously. - const { id, invitationSpec, proposal, offerArgs } = offerSpec; - - /** @type {PaymentKeywordRecord | undefined} */ - const paymentKeywordRecord = await (proposal?.give && - deeplyFulfilledObject(paymentsManager.withdrawGive(proposal.give))); - - const invitation = invitationFromSpec(invitationSpec); - const invitationAmount = - await E(invitationIssuer).getAmountOf(invitation); - - // 2. Begin executing offer - // No explicit signal to user that we reached here but if anything above - // failed they'd get an 'error' status update. - - seatRef = await E(zoe).offer( - invitation, - proposal, - paymentKeywordRecord, - offerArgs, - ); - logger.info(id, 'seated'); - onSeatCreated(seatRef); - - const publishResult = E.when(E(seatRef).getOfferResult(), result => { - const passStyle = passStyleOf(result); - logger.info(id, 'offerResult', passStyle, result); - // someday can we get TS to type narrow based on the passStyleOf result match? - switch (passStyle) { - case 'bigint': - case 'boolean': - case 'null': - case 'number': - case 'string': - case 'symbol': - case 'undefined': - updateStatus({ result }); - break; - case 'copyRecord': - // @ts-expect-error result narrowed by passStyle - if ('invitationMakers' in result) { - // save for continuing invitation offer - void onNewContinuingOffer( - String(id), - invitationAmount, - // @ts-expect-error result narrowed by passStyle - result.invitationMakers, - // @ts-expect-error result narrowed by passStyle - result.publicSubscribers, - ); - } - // copyRecord is valid to publish but not safe as it may have private info - updateStatus({ result: UNPUBLISHED_RESULT }); - break; - default: - // drop the result - updateStatus({ result: UNPUBLISHED_RESULT }); - } - }); - - const publishWantsSatisfied = E.when( - E(seatRef).numWantsSatisfied(), - numSatisfied => { - logger.info(id, 'numSatisfied', numSatisfied); - if (numSatisfied === 0) { - updateStatus({ numWantsSatisfied: 0 }); - } - updateStatus({ - numWantsSatisfied: numSatisfied, - }); - }, - ); - - // This will block until all payouts succeed, but user will be updated - // as each payout will trigger its corresponding purse notifier. - const publishPayouts = E.when(E(seatRef).getPayouts(), payouts => - paymentsManager.depositPayouts(payouts).then(amountsOrDeferred => { - updateStatus({ payouts: amountsOrDeferred }); - }), - ); - - // The offer is complete when these promises are resolved. - // If any reject then executeOffer rejects and that must be handled. - return Promise.all([ - publishResult, - publishWantsSatisfied, - publishPayouts, - ]); - }; - - await tryBody().catch(err => { - logger.error('OFFER ERROR:', err); - // Notify the user - updateStatus({ error: err.toString() }); - // Attempt to recover payments - void paymentsManager.tryReclaimingWithdrawnPayments().then(result => { - if (result) { - updateStatus({ result }); - } - }); - if (seatRef) { - void E(seatRef) - .hasExited() - .then(hasExited => { - if (!hasExited) { - void E(seatRef).tryExit(); - } - }); - } - // propagate to caller - throw err; - }); - }, - }; -}; -harden(makeOfferExecutor); diff --git a/packages/smart-wallet/src/payments.js b/packages/smart-wallet/src/payments.js deleted file mode 100644 index cb79a3af724..00000000000 --- a/packages/smart-wallet/src/payments.js +++ /dev/null @@ -1,89 +0,0 @@ -import { Fail } from '@agoric/assert'; -import { deeplyFulfilledObject, objectMap } from '@agoric/internal'; -import { E } from '@endo/far'; - -/** - * Used in an offer execution to manage payments state safely. - * - * @param {(brand: Brand) => Promise} purseForBrand - * @param {{ receive: (payment: *) => Promise }} depositFacet - */ -export const makePaymentsHelper = (purseForBrand, depositFacet) => { - /** @type {PaymentPKeywordRecord | null} */ - let keywordPaymentPromises = null; - - /** - * Tracks from whence our payment came. - * - * @type {Map} - */ - const paymentToPurse = new Map(); - - return { - /** - * @param {AmountKeywordRecord} give - * @returns {PaymentPKeywordRecord} - */ - withdrawGive(give) { - !keywordPaymentPromises || - Fail`withdrawPayments can be called once per helper`; - keywordPaymentPromises = objectMap(give, amount => { - /** @type {Promise} */ - const purseP = purseForBrand(amount.brand); - return Promise.all([purseP, E(purseP).withdraw(amount)]).then( - ([purse, payment]) => { - paymentToPurse.set(payment, purse); - return payment; - }, - ); - }); - return keywordPaymentPromises; - }, - - /** - * Try reclaiming any of our payments that we successfully withdrew, but - * were left unclaimed. - */ - tryReclaimingWithdrawnPayments() { - if (!keywordPaymentPromises) return Promise.resolve(undefined); - const paymentPromises = Object.values(keywordPaymentPromises); - // Use allSettled to ensure we attempt all the deposits, regardless of - // individual rejections. - return Promise.allSettled( - paymentPromises.map(async paymentP => { - // Wait for the withdrawal to complete. This protects against a race - // when updating paymentToPurse. - const payment = await paymentP; - - // Find out where it came from. - const purse = paymentToPurse.get(payment); - if (purse === undefined) { - // We already tried to reclaim this payment, so stop here. - return undefined; - } - - // Now send it back to the purse. - try { - return E(purse).deposit(payment); - } finally { - // Once we've called addPayment, mark this one as done. - paymentToPurse.delete(payment); - } - }), - ); - }, - - /** - * @param {PaymentPKeywordRecord} payouts - * @returns {Promise} amounts for deferred deposits will be empty - */ - async depositPayouts(payouts) { - /** Record> */ - const amountPKeywordRecord = objectMap(payouts, paymentRef => - E.when(paymentRef, payment => depositFacet.receive(payment)), - ); - return deeplyFulfilledObject(amountPKeywordRecord); - }, - }; -}; -harden(makePaymentsHelper); diff --git a/packages/smart-wallet/src/smartWallet.js b/packages/smart-wallet/src/smartWallet.js index e38914ce631..9e2e3db00e8 100644 --- a/packages/smart-wallet/src/smartWallet.js +++ b/packages/smart-wallet/src/smartWallet.js @@ -49,17 +49,36 @@ const trace = makeTracer('SmrtWlt'); * @see {@link ../README.md}} */ +/** @typedef {number | string} OfferId */ + +/** + * @typedef {{ + * id: OfferId, + * invitationSpec: import('./invitations').InvitationSpec, + * proposal: Proposal, + * offerArgs?: unknown + * }} OfferSpec + */ + +/** + * @typedef {{ + * logger: {info: (...args: any[]) => void, error: (...args: any[]) => void}, + * makeOfferWatcher: import('./offerWatcher.js').MakeOfferWatcher, + * invitationFromSpec: import('./invitations.js').InvitationFromSpec, + * }} ExecutorPowers + */ + /** * @typedef {{ * method: 'executeOffer' - * offer: import('./offers.js').OfferSpec, + * offer: OfferSpec, * }} ExecuteOfferAction */ /** * @typedef {{ * method: 'tryExitOffer' - * offerId: import('./offers.js').OfferId, + * offerId: OfferId, * }} TryExitOfferAction */ @@ -90,12 +109,12 @@ const trace = makeTracer('SmrtWlt'); * purses: Array<{brand: Brand, balance: Amount}>, * offerToUsedInvitation: Array<[ offerId: string, usedInvitation: Amount ]>, * offerToPublicSubscriberPaths: Array<[ offerId: string, publicTopics: { [subscriberName: string]: string } ]>, - * liveOffers: Array<[import('./offers.js').OfferId, import('./offers.js').OfferStatus]>, + * liveOffers: Array<[OfferId, import('./offerWatcher.js').OfferStatus]>, * }} CurrentWalletRecord */ /** - * @typedef {{ updated: 'offerStatus', status: import('./offers.js').OfferStatus } + * @typedef {{ updated: 'offerStatus', status: import('./offerWatcher.js').OfferStatus } * | { updated: 'balance'; currentAmount: Amount } * | { updated: 'walletAction'; status: { error: string } } * } UpdateRecord Record of an update to the state of this wallet. @@ -154,8 +173,8 @@ const trace = makeTracer('SmrtWlt'); * purseBalances: MapStore, * updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit, * currentRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit, - * liveOffers: MapStore, - * liveOfferSeats: WeakMapStore>, + * liveOffers: MapStore, + * liveOfferSeats: MapStore>, * }>} ImmutableState * * @typedef {BrandDescriptor & { purse: Purse }} PurseRecord @@ -705,7 +724,7 @@ export const prepareSmartWallet = (baggage, shared) => { /** * Take an offer description provided in capData, augment it with payments and call zoe.offer() * - * @param {import('./offers.js').OfferSpec} offerSpec + * @param {OfferSpec} offerSpec * @returns {Promise} after the offer has been both seated and exited by Zoe. * @throws if any parts of the offer can be determined synchronously to be invalid */ diff --git a/packages/smart-wallet/src/utils.js b/packages/smart-wallet/src/utils.js index de4758f5bd9..22a9a88ba6b 100644 --- a/packages/smart-wallet/src/utils.js +++ b/packages/smart-wallet/src/utils.js @@ -9,7 +9,7 @@ const trace = makeTracer('WUTIL', false); /** @param {Brand<'set'>} [invitationBrand] */ export const makeWalletStateCoalescer = (invitationBrand = undefined) => { - /** @type {Map} */ + /** @type {Map} */ const offerStatuses = new Map(); /** @type {Map} */ const balances = new Map(); @@ -17,7 +17,7 @@ export const makeWalletStateCoalescer = (invitationBrand = undefined) => { /** * keyed by description; xxx assumes unique * - * @type {Map} + * @type {Map} */ const invitationsReceived = new Map(); diff --git a/packages/smart-wallet/test/gameAssetContract.js b/packages/smart-wallet/test/gameAssetContract.js index 256727bfd61..ef982ce9882 100644 --- a/packages/smart-wallet/test/gameAssetContract.js +++ b/packages/smart-wallet/test/gameAssetContract.js @@ -26,7 +26,7 @@ const totalPlaces = amt => { export const start = async zcf => { const { joinPrice } = zcf.getTerms(); const stableIssuer = await E(zcf.getZoeService()).getFeeIssuer(); - zcf.saveIssuer(stableIssuer, 'Price'); + await zcf.saveIssuer(stableIssuer, 'Price'); const { zcfSeat: gameSeat } = zcf.makeEmptySeatKit(); const mint = await zcf.makeZCFMint('Place', AssetKind.COPY_BAG); diff --git a/packages/smart-wallet/test/test-addAsset.js b/packages/smart-wallet/test/test-addAsset.js index 4cbfdad32e4..748a2a806a7 100644 --- a/packages/smart-wallet/test/test-addAsset.js +++ b/packages/smart-wallet/test/test-addAsset.js @@ -185,7 +185,7 @@ const makeScenario = t => { const uiBridge = Far('UIBridge', { /** @param {import('@endo/marshal').CapData} offerEncoding */ proposeOffer: async offerEncoding => { - /** @type {import('../src/offers.js').OfferSpec} */ + /** @type {import('../src/smartWallet.js').OfferSpec} */ const offer = ctx.fromBoard.fromCapData(offerEncoding); const { give, want } = offer.proposal; for await (const [kw, amt] of entries({ ...give, ...want })) { @@ -341,7 +341,7 @@ test.serial('trading in non-vbank asset: game real-estate NFTs', async t => { ), }; const give = { Price: AmountMath.make(wkBrand.IST, 25n * CENT) }; - /** @type {import('../src/offers.js').OfferSpec} */ + /** @type {import('../src/smartWallet.js').OfferSpec} */ const offer1 = harden({ id: 'joinGame1234', invitationSpec: { @@ -484,7 +484,7 @@ test.serial('non-vbank asset: give before deposit', async t => { }; const want = { Price: AmountMath.make(wkBrand.IST, 25n * CENT) }; - /** @type {import('../src/offers.js').OfferSpec} */ + /** @type {import('../src/smartWallet.js').OfferSpec} */ const offer1 = harden({ id: 'joinGame2345', invitationSpec: { diff --git a/packages/smart-wallet/test/test-walletFactory.js b/packages/smart-wallet/test/test-walletFactory.js index 7ad379d81ad..c03ec86eb06 100644 --- a/packages/smart-wallet/test/test-walletFactory.js +++ b/packages/smart-wallet/test/test-walletFactory.js @@ -36,7 +36,7 @@ test('bridge handler', async t => { // fund the wallet with anchor - /** @type {import('../src/offers.js').OfferSpec} */ + /** @type {import('../src/smartWallet.js').OfferSpec} */ const offerSpec = { id: 1, invitationSpec: { @@ -90,7 +90,7 @@ test('bridge with offerId string', async t => { // fund the wallet with anchor - /** @type {import('../src/offers.js').OfferSpec} */ + /** @type {import('../src/smartWallet.js').OfferSpec} */ const offerSpec = { id: 'uniqueString', invitationSpec: {