From d2e05c43dcd5dfa3719feecc0b303b8294efeef0 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 8 Sep 2023 17:01:00 -0500 Subject: [PATCH 1/9] feat(internal): fakeStorage.getBody() supports index other than -1 --- packages/internal/src/storage-test-utils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/internal/src/storage-test-utils.js b/packages/internal/src/storage-test-utils.js index 8d4d36b2912..bca49dd3468 100644 --- a/packages/internal/src/storage-test-utils.js +++ b/packages/internal/src/storage-test-utils.js @@ -218,14 +218,15 @@ export const makeMockChainStorageRoot = () => { * * @param {string} path * @param {import('./lib-chainStorage.js').Marshaller} marshaller + * @param {number} [index] * @returns {unknown} */ - getBody: (path, marshaller = defaultMarshaller) => { + getBody: (path, marshaller = defaultMarshaller, index = -1) => { data.size || Fail`no data in storage`; /** @type {ReturnType['fromCapData']} */ const fromCapData = (...args) => Reflect.apply(marshaller.fromCapData, marshaller, args); - return unmarshalFromVstorage(data, path, fromCapData); + return unmarshalFromVstorage(data, path, fromCapData, index); }, keys: () => [...data.keys()], }); From 1fdb26d44dd2cbc78c030c0bd355633e54787fbe Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 1 Aug 2023 16:39:48 -0500 Subject: [PATCH 2/9] chore: more FarRef lint --- .../inter-protocol/test/smartWallet/test-psm-integration.js | 3 --- .../bootstrap-walletFactory-service-upgrade.js | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/inter-protocol/test/smartWallet/test-psm-integration.js b/packages/inter-protocol/test/smartWallet/test-psm-integration.js index 50a7fa7afc4..7c1bb8e9b72 100644 --- a/packages/inter-protocol/test/smartWallet/test-psm-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-psm-integration.js @@ -121,7 +121,6 @@ test('want stable', async t => { t.log('Fund the wallet'); assert(anchor.mint); const payment = anchor.mint.mintPayment(anchor.make(swapSize)); - // @ts-expect-error deposit does take a FarRef await wallet.getDepositFacet().receive(payment); t.log('Execute the swap'); @@ -167,7 +166,6 @@ test('want stable (insufficient funds)', async t => { t.log('Fund the wallet insufficiently'); assert(anchor.mint); const payment = anchor.mint.mintPayment(anchor.make(anchorFunding)); - // @ts-expect-error deposit does take a FarRef await wallet.getDepositFacet().receive(payment); t.log('Execute the swap'); @@ -380,7 +378,6 @@ test('deposit multiple payments to unknown brand', async t => { // assume that if the call succeeds then it's in durable storage. for await (const amt of [1n, 2n]) { const payment = rial.mint.mintPayment(rial.make(amt)); - // @ts-expect-error deposit does take a FarRef const result = await wallet.getDepositFacet().receive(harden(payment)); // successful request but not deposited t.deepEqual(result, { brand: rial.brand, value: 0n }); diff --git a/packages/smart-wallet/test/swingsetTests/upgradeWalletFactory/bootstrap-walletFactory-service-upgrade.js b/packages/smart-wallet/test/swingsetTests/upgradeWalletFactory/bootstrap-walletFactory-service-upgrade.js index a4a548ccb03..b041326e406 100644 --- a/packages/smart-wallet/test/swingsetTests/upgradeWalletFactory/bootstrap-walletFactory-service-upgrade.js +++ b/packages/smart-wallet/test/swingsetTests/upgradeWalletFactory/bootstrap-walletFactory-service-upgrade.js @@ -111,7 +111,6 @@ export const buildRootObject = async () => { const payment = moolaKit.mint.mintPayment( AmountMath.make(moolaKit.brand, 100n), ); - // @ts-expect-error casting far for test await E(depositFacet).receive(payment); return true; From 82ed64ec20ce7884d6cdf6caac63fab85d02af40 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 1 Aug 2023 12:54:08 -0500 Subject: [PATCH 3/9] feat(smart-wallet): withdraw payments before getting invitation This performance optimization avoids all the work of getting an invitation if there aren't sufficient funds for `proposal.give`. fixes: #7098 --- packages/smart-wallet/src/offers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/smart-wallet/src/offers.js b/packages/smart-wallet/src/offers.js index 88171861083..e685bb2dbb7 100644 --- a/packages/smart-wallet/src/offers.js +++ b/packages/smart-wallet/src/offers.js @@ -1,4 +1,5 @@ import { E, passStyleOf } from '@endo/far'; +import { deeplyFulfilledObject } from '@agoric/internal'; import { makePaymentsHelper } from './payments.js'; /** @@ -80,15 +81,15 @@ export const makeOfferExecutor = ({ // 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, ); - const paymentKeywordRecord = proposal?.give - ? paymentsManager.withdrawGive(proposal.give) - : undefined; - // 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. From dc99ca0b58af3a66778071993f0d73d099605069 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 20 Jul 2023 02:35:52 -0500 Subject: [PATCH 4/9] feat(smart-wallet): trading in non-vbank asset WIP: note limitation on depositFacet assetKind - feat(smart-wallet): purses for well-known brands - use side table for new purse storage - issuer/brand mutual check - storage faciliteates transition to decentralized issuer introduction - take care with .init() vs. .set()! - diagnosis complicated by obscure "no ordinal" diagnostic - non-vbank asset test tells story in t.log - test: demonstrate how to share brand displayInfo - test: spend before receive non-vbank asset build(smart-wallet): bundle-source devdep SQUASHME: test: bundle handling, IST_UNIT, ... SQUASHME: refactor based on review feedback - XXX move? -> TODO - mutualCheck -> assertMutual with independent failures - hoist getBrandToPurses() - rename to getPurseIfKnownBrand() - inline addIssuer (aka acceptIssuer) - throw on mutualCheck failure SQUASHME: xP fixup receive throw fixup getPurseIfKnownBrand --- packages/smart-wallet/package.json | 4 +- packages/smart-wallet/src/smartWallet.js | 172 ++++++- .../smart-wallet/test/gameAssetContract.js | 68 +++ packages/smart-wallet/test/supports.js | 4 +- packages/smart-wallet/test/test-addAsset.js | 459 +++++++++++++++++- 5 files changed, 687 insertions(+), 20 deletions(-) create mode 100644 packages/smart-wallet/test/gameAssetContract.js diff --git a/packages/smart-wallet/package.json b/packages/smart-wallet/package.json index dbf76f66781..a22fb047241 100644 --- a/packages/smart-wallet/package.json +++ b/packages/smart-wallet/package.json @@ -17,9 +17,11 @@ }, "devDependencies": { "@agoric/cosmic-proto": "^0.3.0", + "@endo/bundle-source": "^2.5.2", "@endo/captp": "^3.1.1", "@endo/init": "^0.5.56", - "ava": "^5.2.0" + "ava": "^5.2.0", + "import-meta-resolve": "^2.2.1" }, "dependencies": { "@agoric/assert": "^0.6.0", diff --git a/packages/smart-wallet/src/smartWallet.js b/packages/smart-wallet/src/smartWallet.js index c7a7e7893d6..9a7812577c1 100644 --- a/packages/smart-wallet/src/smartWallet.js +++ b/packages/smart-wallet/src/smartWallet.js @@ -1,5 +1,4 @@ import { - AmountMath, AmountShape, BrandShape, DisplayInfoShape, @@ -7,15 +6,23 @@ import { PaymentShape, PurseShape, } from '@agoric/ertp'; -import { StorageNodeShape } from '@agoric/internal'; +import { StorageNodeShape, makeTracer } from '@agoric/internal'; import { observeNotifier } from '@agoric/notifier'; import { M, mustMatch } from '@agoric/store'; -import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js'; -import { makeScalarBigMapStore, prepareExoClassKit } from '@agoric/vat-data'; import { - prepareRecorderKit, + appendToStoredArray, + provideLazy, +} from '@agoric/store/src/stores/store-utils.js'; +import { + makeScalarBigMapStore, + makeScalarBigWeakMapStore, + prepareExoClassKit, + provide, +} from '@agoric/vat-data'; +import { SubscriberShape, TopicsRecordShape, + prepareRecorderKit, } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/far'; import { makeInvitationsHelper } from './invitations.js'; @@ -25,6 +32,8 @@ import { objectMapStoragePath } from './utils.js'; const { Fail, quote: q } = assert; +const trace = makeTracer('SmrtWlt'); + /** * @file Smart wallet module * @@ -132,8 +141,8 @@ const { Fail, quote: q } = assert; * - `purseBalances` is a cache of what we've received from purses. Held so we can publish all balances on change. * * @typedef {Readonly>>, - * offerToInvitationMakers: MapStore, + * paymentQueues: MapStore>, + * offerToInvitationMakers: MapStore, * offerToPublicSubscriberPaths: MapStore>, * offerToUsedInvitation: MapStore, * purseBalances: MapStore, @@ -143,16 +152,66 @@ const { Fail, quote: q } = assert; * liveOfferSeats: WeakMapStore>, * }>} ImmutableState * + * @typedef {BrandDescriptor & { purse: Purse }} PurseRecord * @typedef {{ * }} MutableState */ +/** + * NameHub reverse-lookup, finding 0 or more names for a target value + * + * TODO: consider moving to nameHub.js? + * + * @param {unknown} target - passable Key + * @param {ERef} nameHub + */ +const namesOf = async (target, nameHub) => { + const entries = await E(nameHub).entries(); + const matches = []; + for (const [name, candidate] of entries) { + if (candidate === target) { + matches.push(name); + } + } + return harden(matches); +}; + +/** + * Check that an issuer and its brand belong to each other. + * + * TODO: move to ERTP? + * + * @param {Issuer} issuer + * @param {Brand} brand + * @returns {Promise} true iff the the brand and issuer match + */ +const checkMutual = (issuer, brand) => + Promise.all([ + E(issuer) + .getBrand() + .then(b => b === brand), + E(brand).isMyIssuer(issuer), + ]).then(checks => checks.every(Boolean)); + +export const BRAND_TO_PURSES_KEY = 'brandToPurses'; + +const getBrandToPurses = (walletPurses, key) => { + const brandToPurses = provideLazy(walletPurses, key, _k => { + /** @type {MapStore} */ + const store = makeScalarBigMapStore('purses by brand', { + durable: true, + }); + return store; + }); + return brandToPurses; +}; + /** * @param {import('@agoric/vat-data').Baggage} baggage * @param {SharedParams} shared */ export const prepareSmartWallet = (baggage, shared) => { - const { registry: _, ...passableShared } = shared; + const { registry: _r, ...passableShared } = shared; mustMatch( harden(passableShared), harden({ @@ -167,6 +226,15 @@ export const prepareSmartWallet = (baggage, shared) => { const makeRecorderKit = prepareRecorderKit(baggage, shared.publicMarshaller); + const walletPurses = provide(baggage, BRAND_TO_PURSES_KEY, () => { + trace('make purses by wallet and save in baggage at', BRAND_TO_PURSES_KEY); + /** @type {WeakMapStore>} */ + const store = makeScalarBigWeakMapStore('purses by wallet', { + durable: true, + }); + return store; + }); + /** * * @param {UniqueParams} unique @@ -245,6 +313,9 @@ export const prepareSmartWallet = (baggage, shared) => { helper: M.interface('helperFacetI', { assertUniqueOfferId: M.call(M.string()).returns(), updateBalance: M.call(PurseShape, AmountShape).optional('init').returns(), + getPurseIfKnownBrand: M.call(BrandShape) + .optional(M.eref(M.remotable())) + .returns(M.promise()), publishCurrentState: M.call().returns(), watchPurse: M.call(M.eref(PurseShape)).returns(M.promise()), }), @@ -369,6 +440,63 @@ export const prepareSmartWallet = (baggage, shared) => { }, }); }, + + /** + * Provide a purse given a NameHub of issuers and their + * brands. + * + * We current support only one NameHub, agoricNames, and + * hence one purse per brand. But we store an array of them + * to facilitate a transition to decentralized introductions. + * + * @param {Brand} brand + * @param {ERef} known - namehub with brand, issuer branches + * @returns {Promise} undefined if brand is not known + */ + async getPurseIfKnownBrand(brand, known) { + const { helper, self } = this.facets; + const brandToPurses = getBrandToPurses(walletPurses, self); + + if (brandToPurses.has(brand)) { + const purses = brandToPurses.get(brand); + if (purses.length > 0) { + // UNTIL https://github.com/Agoric/agoric-sdk/issues/6126 + // multiple purses + return purses[0].purse; + } + } + + const found = await namesOf(brand, E(known).lookup('brand')); + if (found.length === 0) { + return undefined; + } + const [edgeName] = found; + const issuer = await E(known).lookup('issuer', edgeName); + + // Even though we rely on this nameHub, double-check + // that the issuer and the brand belong to each other. + if (!(await checkMutual(issuer, brand))) { + // if they don't, it's not a "known" brand in a coherent way + return undefined; + } + + // Accept the issuer; rely on it in future offers. + const [displayInfo, purse] = await Promise.all([ + E(issuer).getDisplayInfo(), + E(issuer).makeEmptyPurse(), + ]); + + // adopt edgeName as petname + // NOTE: for decentralized introductions, qualify edgename by nameHub petname + const petname = edgeName; + const assetInfo = { petname, brand, issuer, purse, displayInfo }; + appendToStoredArray(brandToPurses, brand, assetInfo); + // NOTE: when we decentralize introduction of issuers, + // process queued payments for this brand. + + void helper.watchPurse(purse); + return purse; + }, }, /** * Similar to {DepositFacet} but async because it has to look up the purse. @@ -379,10 +507,12 @@ export const prepareSmartWallet = (baggage, shared) => { * * If the purse doesn't exist, we hold the payment in durable storage. * - * @param {import('@endo/far').FarRef} payment - * @returns {Promise} amounts for deferred deposits will be empty + * @param {Payment} payment + * @returns {Promise} + * @throws if there's not yet a purse, though the payment is held to try again when there is */ async receive(payment) { + const { helper } = this.facets; const { paymentQueues: queues, bank, invitationPurse } = this.state; const { registry, invitationBrand } = shared; const brand = await E(payment).getAllegedBrand(); @@ -390,17 +520,24 @@ export const prepareSmartWallet = (baggage, shared) => { // When there is a purse deposit into it if (registry.has(brand)) { const purse = E(bank).getPurse(brand); - // @ts-expect-error deposit does take a FarRef return E(purse).deposit(payment); } else if (invitationBrand === brand) { - // @ts-expect-error deposit does take a FarRef + // @ts-expect-error narrow assetKind to 'set' return E(invitationPurse).deposit(payment); } + const purse = await helper.getPurseIfKnownBrand( + brand, + shared.agoricNames, + ); + if (purse) { + return E(purse).deposit(payment); + } + // When there is no purse, save the payment into a queue. // It's not yet ever read but a future version of the contract can appendToStoredArray(queues, brand, payment); - return AmountMath.makeEmpty(brand); + throw Fail`cannot deposit payment with brand ${brand}: no purse`; }, }, offers: { @@ -448,6 +585,7 @@ export const prepareSmartWallet = (baggage, shared) => { * @returns {Promise} */ purseForBrand: async brand => { + const { helper } = facets; if (registry.has(brand)) { // @ts-expect-error RemotePurse cast return E(bank).getPurse(brand); @@ -455,6 +593,14 @@ export const prepareSmartWallet = (baggage, shared) => { // @ts-expect-error RemotePurse cast return invitationPurse; } + + const purse = await helper.getPurseIfKnownBrand( + brand, + shared.agoricNames, + ); + if (purse) { + return purse; + } throw Fail`cannot find/make purse for ${brand}`; }, logger, diff --git a/packages/smart-wallet/test/gameAssetContract.js b/packages/smart-wallet/test/gameAssetContract.js new file mode 100644 index 00000000000..280a4104d09 --- /dev/null +++ b/packages/smart-wallet/test/gameAssetContract.js @@ -0,0 +1,68 @@ +/** @file illustrates using non-vbank assets */ + +// deep import to avoid dependency on all of ERTP, vat-data +import { AmountShape } from '@agoric/ertp'; +import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js'; +import { makeTracer } from '@agoric/internal'; +import { M, getCopyBagEntries } from '@agoric/store'; +import { atomicRearrange } from '@agoric/zoe/src/contractSupport/index.js'; +import { E, Far } from '@endo/far'; + +const { Fail, quote: q } = assert; + +const trace = makeTracer('Game', true); + +/** @param {Amount<'copyBag'>} amt */ +const totalPlaces = amt => { + /** @type {[unknown, bigint][]} */ + const entries = getCopyBagEntries(amt.value); // XXX getCopyBagEntries returns any??? + const total = entries.reduce((acc, [_place, qty]) => acc + qty, 0n); + return total; +}; + +/** + * @param {ZCF<{joinPrice: Amount}>} zcf + */ +export const start = async zcf => { + const { joinPrice } = zcf.getTerms(); + const stableIssuer = await E(zcf.getZoeService()).getFeeIssuer(); + zcf.saveIssuer(stableIssuer, 'Price'); + + const { zcfSeat: gameSeat } = zcf.makeEmptySeatKit(); + const mint = await zcf.makeZCFMint('Place', AssetKind.COPY_BAG); + + const joinShape = harden({ + give: { Price: AmountShape }, + want: { Places: AmountShape }, + exit: M.any(), + }); + + /** @param {ZCFSeat} playerSeat */ + const joinHook = playerSeat => { + const { give, want } = playerSeat.getProposal(); + trace('join', 'give', give, 'want', want.Places.value); + + AmountMath.isGTE(give.Price, joinPrice) || + Fail`${q(give.Price)} below joinPrice of ${q(joinPrice)}}`; + + totalPlaces(want.Places) <= 3n || Fail`only 3 places allowed when joining`; + + atomicRearrange( + zcf, + harden([ + [playerSeat, gameSeat, give], + [mint.mintGains(want), playerSeat, want], + ]), + ); + playerSeat.exit(true); + return 'welcome to the game'; + }; + + const publicFacet = Far('API', { + makeJoinInvitation: () => + zcf.makeInvitation(joinHook, 'join', undefined, joinShape), + }); + + return harden({ publicFacet }); +}; +harden(start); diff --git a/packages/smart-wallet/test/supports.js b/packages/smart-wallet/test/supports.js index 50501d66289..dd51dc9c75e 100644 --- a/packages/smart-wallet/test/supports.js +++ b/packages/smart-wallet/test/supports.js @@ -94,8 +94,10 @@ export const makeMockTestSpace = async log => { /** @type { BootstrapPowers & { consume: { loadVat: (n: 'mints') => MintsVat, loadCriticalVat: (n: 'mints') => MintsVat }} } */ ( space ); - const { agoricNames, spaces } = await makeAgoricNamesAccess(); + const { agoricNames, agoricNamesAdmin, spaces } = + await makeAgoricNamesAccess(); produce.agoricNames.resolve(agoricNames); + produce.agoricNamesAdmin.resolve(agoricNamesAdmin); const { zoe, feeMintAccessP } = await setUpZoeForTest(); produce.zoe.resolve(zoe); diff --git a/packages/smart-wallet/test/test-addAsset.js b/packages/smart-wallet/test/test-addAsset.js index 2ba5aa0f58e..5a111fc21e3 100644 --- a/packages/smart-wallet/test/test-addAsset.js +++ b/packages/smart-wallet/test/test-addAsset.js @@ -1,12 +1,22 @@ // @ts-check import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { E } from '@endo/far'; +import { E, Far } from '@endo/far'; import { buildRootObject as buildBankVatRoot } from '@agoric/vats/src/vat-bank.js'; -import { makeIssuerKit } from '@agoric/ertp'; +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; -import { makeScalarMapStore } from '@agoric/store'; +import { makeCopyBag, makeScalarMapStore } from '@agoric/store'; +import { makePromiseKit } from '@endo/promise-kit'; +import bundleSource from '@endo/bundle-source'; +import { makeMarshal } from '@endo/marshal'; +import { resolve as importMetaResolve } from 'import-meta-resolve'; import { makeDefaultTestContext } from './contexts.js'; -import { makeMockTestSpace } from './supports.js'; +import { ActionType, headValue, makeMockTestSpace } from './supports.js'; +import { makeImportContext } from '../src/marshal-contexts.js'; + +const { Fail } = assert; + +const importSpec = spec => + importMetaResolve(spec, import.meta.url).then(u => new URL(u).pathname); /** @type {import('ava').TestFn>>} */ const test = anyTest; @@ -33,11 +43,14 @@ const bigIntReplacer = (_key, val) => const range = qty => [...Array(qty).keys()]; +// We use test.serial() because +// agoricNames and vstorage are shared mutable state. + /** * NOTE: this doesn't test all forms of work. * A better test would measure inter-vat messages or some such. */ -test('avoid O(wallets) storage writes for a new asset', async t => { +test.serial('avoid O(wallets) storage writes for a new asset', async t => { const bankManager = t.context.consume.bankManager; let chainStorageWrites = 0; @@ -88,3 +101,439 @@ test('avoid O(wallets) storage writes for a new asset', async t => { t.is(base.addedWrites, 0); t.is(exp.addedWrites, 0); }); + +const BOARD_AUX = 'boardAux'; +/** + * Make a storage node for auxilliary data for a value on the board. + * + * @param {ERef} chainStorage + * @param {string} boardId + */ +const makeBoardAuxNode = async (chainStorage, boardId) => { + const boardAux = E(chainStorage).makeChildNode(BOARD_AUX); + return E(boardAux).makeChildNode(boardId); +}; + +const marshalData = makeMarshal(_val => Fail`data only`); + +/** @type {(xP: Promise) => Promise} */ +const the = async xP => { + const x = await xP; + assert(x != null, 'resolution is unexpectedly nullish'); + return x; +}; + +/** @typedef {import('@agoric/internal/src/storage-test-utils.js').MockChainStorageRoot} MockVStorageRoot */ + +const IST_UNIT = 1_000_000n; +const CENT = IST_UNIT / 100n; + +/** @param {import('ava').ExecutionContext>>} t */ +const makeScenario = t => { + /** + * A player and their user agent (wallet UI, signer) + * + * @param {string} addr + * @param {PromiseKit<*>} bridgeKit - to announce UI is ready + * @param {MockVStorageRoot} vsRPC - access to vstorage via RPC + * @param {typeof t.context.sendToBridge} broadcastMsg + * @param {*} walletUpdates - access to wallet updates via RPC + */ + const aPlayer = async ( + addr, + bridgeKit, + vsRPC, + broadcastMsg, + walletUpdates, + ) => { + const ctx = makeImportContext(); + const vsGet = path => + E(vsRPC).getBody(`mockChainStorageRoot.${path}`, ctx.fromBoard); + + // Ingest well-known brands, instances. + // Wait for vstorage publication to settle first. + await eventLoopIteration(); + /** @type {Record} */ + // @ts-expect-error unsafe testing cast + const wkBrand = Object.fromEntries(await vsGet(`agoricNames.brand`)); + await vsGet(`agoricNames.instance`); + t.log(addr, 'wallet UI: ingest well-known brands', wkBrand.Place); + + assert(broadcastMsg); + /** @param {import('../src/smartWallet.js').BridgeAction} action */ + const signAndBroadcast = async action => { + const actionEncoding = ctx.fromBoard.toCapData(action); + const addIssuerMsg = { + type: ActionType.WALLET_SPEND_ACTION, + owner: addr, + spendAction: JSON.stringify(actionEncoding), + blockTime: 0, + blockHeight: 0, + }; + t.log(addr, 'walletUI: broadcast signed message', action.method); + await broadcastMsg(addIssuerMsg); + }; + + const auxInfo = async obj => { + const slot = ctx.fromBoard.toCapData(obj).slots[0]; + return vsGet(`${BOARD_AUX}.${slot}`); + }; + + const { entries } = Object; + const uiBridge = Far('UIBridge', { + /** @param {import('@endo/marshal').CapData} offerEncoding */ + proposeOffer: async offerEncoding => { + /** @type {import('../src/offers.js').OfferSpec} */ + const offer = ctx.fromBoard.fromCapData(offerEncoding); + const { give, want } = offer.proposal; + for await (const [kw, amt] of entries({ ...give, ...want })) { + // @ts-expect-error + const { displayInfo } = await auxInfo(amt.brand); + const kind = displayInfo?.assetKind === 'nat' ? 'fungible' : 'NFT'; + t.log('wallet:', kind, 'display for', kw, amt.brand); + } + t.log(addr, 'walletUI: approve offer:', offer.id); + await signAndBroadcast(harden({ method: 'executeOffer', offer })); + }, + }); + bridgeKit.resolve(uiBridge); + return walletUpdates; + }; + + return { aPlayer }; +}; + +test.serial('trading in non-vbank asset: game real-estate NFTs', async t => { + const pettyCashQty = 1000n; + const pettyCashPK = makePromiseKit(); + + const publishBrandInfo = async (chainStorage, board, brand) => { + const [id, displayInfo] = await Promise.all([ + E(board).getId(brand), + E(brand).getDisplayInfo(), + ]); + const node = makeBoardAuxNode(chainStorage, id); + const aux = marshalData.toCapData(harden({ displayInfo })); + await E(node).setValue(JSON.stringify(aux)); + }; + + /** + * @param {BootstrapPowers} powers + * @param {Record} bundles + */ + const finishBootstrap = async ({ consume }, bundles) => { + const kindAdmin = kind => E(consume.agoricNamesAdmin).lookupAdmin(kind); + + const publishAgoricNames = async () => { + const { board, chainStorage } = consume; + const pubm = E(board).getPublishingMarshaller(); + const namesNode = await E(chainStorage)?.makeChildNode('agoricNames'); + assert(namesNode); + for (const kind of ['brand', 'instance']) { + const kindNode = await E(namesNode).makeChildNode(kind); + const kindUpdater = Far('Updater', { + write: async x => { + // console.log('marshal for vstorage write', x); + const capData = await E(pubm).toCapData(x); + await E(kindNode).setValue(JSON.stringify(capData)); + }, + }); + await E(kindAdmin(kind)).onUpdate(kindUpdater); + } + t.log('bootstrap: share agoricNames updates via vstorage RPC'); + }; + + const shareIST = async () => { + const { chainStorage, bankManager, board, feeMintAccess, zoe } = consume; + const stable = await E(zoe) + .getFeeIssuer() + .then(issuer => { + return E(issuer) + .getBrand() + .then(brand => ({ issuer, brand })); + }); + await E(kindAdmin('issuer')).update('IST', stable.issuer); + await E(kindAdmin('brand')).update('IST', stable.brand); + // TODO: publishBrandInfo for all brands in agoricNames.brand + await publishBrandInfo(chainStorage, board, stable.brand); + + const invitation = await E(E(zoe).getInvitationIssuer()).getBrand(); + await E(kindAdmin('brand')).update('Zoe Invitation', invitation); + + const { creatorFacet: supplier } = await E(zoe).startInstance( + E(zoe).install(bundles.centralSupply), + {}, + { bootstrapPaymentValue: pettyCashQty * IST_UNIT }, + { feeMintAccess: await feeMintAccess }, + ); + const bootPmt = await E(supplier).getBootstrapPayment(); + const stableSupply = E(stable.issuer).makeEmptyPurse(); + await E(stableSupply).deposit(bootPmt); + pettyCashPK.resolve(stableSupply); + + await E(bankManager).addAsset( + 'uist', + 'IST', + 'Inter Stable Token', + stable, + ); + }; + + await Promise.all([publishAgoricNames(), shareIST()]); + }; + + /** + * Core eval script to start contract + * + * @param {BootstrapPowers} permittedPowers + * @param {Bundle} bundle - in prod: a bundleId + */ + const startGameContract = async ({ consume }, bundle) => { + const { agoricNames, agoricNamesAdmin, board, chainStorage, zoe } = consume; + const istBrand = await E(agoricNames).lookup('brand', 'IST'); + const ist = { + brand: istBrand, + }; + const terms = { joinPrice: AmountMath.make(ist.brand, 25n * CENT) }; + const installationP = E(zoe).install(bundle); // in prod: installBundleId + // in prod: use startUpgradeable() to save adminFacet + const { instance } = await E(zoe).startInstance(installationP, {}, terms); + t.log('CoreEval script: started game contract', instance); + const { + brands: { Place: brand }, + issuers: { Place: issuer }, + } = await E(zoe).getTerms(instance); + + t.log('CoreEval script: share via agoricNames:', brand); + const kindAdmin = kind => E(agoricNamesAdmin).lookupAdmin(kind); + await Promise.all([ + E(kindAdmin('instance')).update('game1', instance), + E(kindAdmin('issuer')).update('Place', issuer), + E(kindAdmin('brand')).update('Place', brand), + publishBrandInfo(the(chainStorage), board, brand), + ]); + }; + + /** + * @param {MockVStorageRoot} rpc - access to vstorage (in prod: via RPC) + * @param {*} walletBridge - iframe connection to wallet UI + */ + const dappFrontEnd = async (rpc, walletBridge) => { + const { fromEntries } = Object; + const ctx = makeImportContext(); + await eventLoopIteration(); + const vsGet = path => + E(rpc).getBody(`mockChainStorageRoot.${path}`, ctx.fromBoard); + + // @ts-expect-error unsafe testing cast + const wkBrand = fromEntries(await vsGet(`agoricNames.brand`)); + // @ts-expect-error unsafe testing cast + const wkInstance = fromEntries(await vsGet(`agoricNames.instance`)); + t.log('game UI: ingested well-known brands:', wkBrand.Place); + + const choices = ['Park Place', 'Boardwalk']; + const want = { + Places: AmountMath.make( + wkBrand.Place, + makeCopyBag(choices.map(name => [name, 1n])), + ), + }; + const give = { Price: AmountMath.make(wkBrand.IST, 25n * CENT) }; + /** @type {import('../src/offers.js').OfferSpec} */ + const offer1 = harden({ + id: 'joinGame1234', + invitationSpec: { + source: 'contract', + instance: wkInstance.game1, + publicInvitationMaker: 'makeJoinInvitation', + }, + proposal: { give, want }, + }); + t.log('game UI: propose offer of 0.25IST for', choices.join(', ')); + await E(walletBridge).proposeOffer(ctx.fromBoard.toCapData(offer1)); + }; + + // TODO: hoist the functions above out of the test + // so that t.context is not in scope? + const { consume, simpleProvideWallet, sendToBridge } = t.context; + + const bundles = { + game: await importSpec('./gameAssetContract.js').then(bundleSource), + centralSupply: await importSpec('@agoric/vats/src/centralSupply.js').then( + bundleSource, + ), + }; + + const fundWalletAndSubscribe = async (addr, istQty) => { + const purse = pettyCashPK.promise; + const spendBrand = await E(purse).getAllegedBrand(); + const spendingPmt = await E(purse).withdraw( + AmountMath.make(spendBrand, istQty * IST_UNIT), + ); + const wallet = simpleProvideWallet(addr); + t.log('deposit', istQty, 'IST into wallet of', addr); + await E(E(wallet).getDepositFacet()).receive(spendingPmt); + const updates = await E(wallet).getUpdatesSubscriber(); + return updates; + }; + + /** @type {BootstrapPowers} */ + // @ts-expect-error mock + const somePowers = { consume }; + + /** @type {MockVStorageRoot} */ + // @ts-expect-error mock + const mockStorage = await consume.chainStorage; + await finishBootstrap(somePowers, bundles); + t.log( + 'install game contract bundle with hash:', + bundles.game.endoZipBase64Sha512.slice(0, 24), + '...', + ); + + { + const addr1 = 'agoric1player1'; + const walletUIbridge = makePromiseKit(); + const { aPlayer } = makeScenario(t); + + const [_1, _2, updates] = await Promise.all([ + startGameContract(somePowers, bundles.game), + dappFrontEnd(mockStorage, walletUIbridge.promise), + aPlayer( + addr1, + walletUIbridge, + mockStorage, + sendToBridge, + fundWalletAndSubscribe(addr1, 10n), + ), + ]); + + /** @type {[string, bigint][]} */ + const expected = [ + ['Park Place', 1n], + ['Boardwalk', 1n], + ]; + + /** @type {import('../src/smartWallet.js').UpdateRecord} */ + const update = await headValue(updates); + assert(update.updated === 'offerStatus'); + // t.log(update.status); + t.like(update, { + updated: 'offerStatus', + status: { + id: 'joinGame1234', + invitationSpec: { publicInvitationMaker: 'makeJoinInvitation' }, + numWantsSatisfied: 1, + payouts: { Places: { value: { payload: expected } } }, + result: 'welcome to the game', + }, + }); + const { + status: { id, result, payouts }, + } = update; + // @ts-expect-error cast value to copyBag + const names = payouts?.Places.value.payload.map(([name, _qty]) => name); + t.log(id, 'result:', result, ', payouts:', names.join(', ')); + + // wallet balance was also updated + const ctx = makeImportContext(); + const vsGet = (path, ix = -1) => + mockStorage.getBody(`mockChainStorageRoot.${path}`, ctx.fromBoard, ix); + /** @type {Record} */ + const wkb = Object.fromEntries( + // @ts-expect-error unsafe testing cast + vsGet(`agoricNames.brand`), + ); + vsGet(`agoricNames.instance`); + + const balanceUpdate = vsGet(`wallet.${addr1}`, -2); + t.deepEqual(balanceUpdate, { + updated: 'balance', + currentAmount: { brand: wkb.Place, value: makeCopyBag(expected) }, + }); + } +}); + +test.serial('non-vbank asset: give before deposit', async t => { + /** + * Goofy client: proposes to give Places before we have any + * + * @param {MockVStorageRoot} rpc - access to vstorage (in prod: via RPC) + * @param {*} walletBridge - iframe connection to wallet UI + */ + const goofyClient = async (rpc, walletBridge) => { + const { fromEntries } = Object; + const ctx = makeImportContext(); + const vsGet = path => + E(rpc).getBody(`mockChainStorageRoot.${path}`, ctx.fromBoard); + // @ts-expect-error unsafe testing cast + const wkBrand = fromEntries(await vsGet(`agoricNames.brand`)); + // @ts-expect-error unsafe testing cast + const wkInstance = fromEntries(await vsGet(`agoricNames.instance`)); + + const choices = ['Disney Land']; + const give = { + Places: AmountMath.make( + wkBrand.Place, + makeCopyBag(choices.map(name => [name, 1n])), + ), + }; + const want = { Price: AmountMath.make(wkBrand.IST, 25n * CENT) }; + + /** @type {import('../src/offers.js').OfferSpec} */ + const offer1 = harden({ + id: 'joinGame2345', + invitationSpec: { + source: 'contract', + instance: wkInstance.game1, + publicInvitationMaker: 'makeJoinInvitation', + }, + proposal: { give, want }, + }); + t.log('goofy client: propose to give', choices.join(', ')); + await E(walletBridge).proposeOffer(ctx.fromBoard.toCapData(offer1)); + }; + + { + const addr2 = 'agoric1player2'; + const walletUIbridge = makePromiseKit(); + // await eventLoopIteration(); + + const { simpleProvideWallet, consume, sendToBridge } = t.context; + const wallet = simpleProvideWallet(addr2); + const updates = await E(wallet).getUpdatesSubscriber(); + /** @type {MockVStorageRoot} */ + // @ts-expect-error mock + const mockStorage = await consume.chainStorage; + const { aPlayer } = makeScenario(t); + + aPlayer(addr2, walletUIbridge, mockStorage, sendToBridge, updates); + const c2 = goofyClient(mockStorage, walletUIbridge.promise); + await t.throwsAsync(c2, { message: /Withdrawal of {.*} failed/ }); + await eventLoopIteration(); + + // wallet balance was also updated + const ctx = makeImportContext(); + const vsGet = (path, ix = -1) => + mockStorage.getBody(`mockChainStorageRoot.${path}`, ctx.fromBoard, ix); + vsGet(`agoricNames.brand`); + vsGet(`agoricNames.instance`); + + /** @type {import('../src/smartWallet.js').UpdateRecord & {updated: 'offerStatus'}} */ + let offerUpdate; + let ix = -1; + for (;;) { + /** @type {import('../src/smartWallet.js').UpdateRecord} */ + // @ts-expect-error + const update = vsGet(`wallet.${addr2}`, ix); + if (update.updated === 'offerStatus') { + offerUpdate = update; + break; + } + ix -= 1; + } + + t.regex(offerUpdate.status.error || '', /Withdrawal of {.*} failed/); + t.log(offerUpdate.status.id, offerUpdate.status.error); + } +}); From 8f7bd2f01bf4f9871224108c2815ea3fad5fc7fb Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 3 Aug 2023 15:52:47 -0500 Subject: [PATCH 5/9] test(inter-protocol): update "no purse" test --- .../inter-protocol/test/smartWallet/test-psm-integration.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/inter-protocol/test/smartWallet/test-psm-integration.js b/packages/inter-protocol/test/smartWallet/test-psm-integration.js index 7c1bb8e9b72..e5902ed20fe 100644 --- a/packages/inter-protocol/test/smartWallet/test-psm-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-psm-integration.js @@ -378,9 +378,9 @@ test('deposit multiple payments to unknown brand', async t => { // assume that if the call succeeds then it's in durable storage. for await (const amt of [1n, 2n]) { const payment = rial.mint.mintPayment(rial.make(amt)); - const result = await wallet.getDepositFacet().receive(harden(payment)); - // successful request but not deposited - t.deepEqual(result, { brand: rial.brand, value: 0n }); + await t.throwsAsync(wallet.getDepositFacet().receive(harden(payment)), { + message: /cannot deposit .*: no purse/, + }); } }); From f6ca5e215891487f90914bb38e5322a71b98afdc Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 3 Aug 2023 15:51:39 -0400 Subject: [PATCH 6/9] style: convert forEach --- packages/smart-wallet/test/test-addAsset.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/smart-wallet/test/test-addAsset.js b/packages/smart-wallet/test/test-addAsset.js index 5a111fc21e3..8480cea0d87 100644 --- a/packages/smart-wallet/test/test-addAsset.js +++ b/packages/smart-wallet/test/test-addAsset.js @@ -75,7 +75,9 @@ test.serial('avoid O(wallets) storage writes for a new asset', async t => { }; const simulate = async (qty, denom, name) => { - range(qty).forEach(startUser); + for (const idx of range(qty)) { + void startUser(idx); + } await eventLoopIteration(); const initialWrites = chainStorageWrites; From 073b450b0790d1837d59e80c0109f0923b5f0cd2 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 23 Aug 2023 16:21:04 -0500 Subject: [PATCH 7/9] feat(smart-wallet): upgrade walletFactory for non-vbank assets - mint-ist.sh to pay for publish-bundle adapted from auctioneer-private-args branch - ensure GOV1ADDR is set so we don't give agd too few args and get a weird diagnostic - chore(walletFactory): initHandler / setHandler / upgrading - chore(walletFactory): start -> prepare for compat - chore(game contract): atomicRearrange -> zcf.reallocate() for compat - feat(smart-wallet): publishAgoricBrandsDisplayInfo to vstorage - test(smart-wallet): upgraded walletFactory handles game NFT - install game contract before walletFactory upgrade - move vat-status.mjs to tools/ - move mint-ist, parseProposals to tools/ - chore: split game1 proposal from walletFactory upgrade - chore: move build-walletFactory-upgrade from builders/ to vats/ since vats/ already depends on deploy-script-support - chore: punt zoe noop actions in upgrade-11 - chore: use instancePrivateArgs in upgradeWalletFactory --- .../agoric-upgrade-11/.gitignore | 2 + .../agoric-upgrade-11/actions.sh | 28 +++- .../agoric-upgrade-11/pre_test.sh | 2 + .../agoric-upgrade-11/test.sh | 42 ++++++ .../agoric-upgrade-11/tools/mint-ist.sh | 16 +++ .../tools/parseProposals.mjs | 41 ++++++ .../wallet-all-ertp/gen-game-offer.mjs | 116 +++++++++++++++++ .../wallet-all-ertp/wf-game-propose.sh | 32 +++++ .../wallet-all-ertp/wf-install-bundles.sh | 34 +++++ .../wallet-all-ertp/wf-propose.sh | 37 ++++++ .../upgrade-walletFactory-proposal.js | 103 +++++++++++++++ packages/smart-wallet/src/walletFactory.js | 24 +++- .../smart-wallet/test/gameAssetContract.js | 16 +-- .../smart-wallet/test/start-game1-proposal.js | 122 ++++++++++++++++++ packages/vats/scripts/build-game1-start.js | 34 +++++ .../scripts/build-walletFactory-upgrade.js | 35 +++++ 16 files changed, 664 insertions(+), 20 deletions(-) create mode 100644 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/.gitignore mode change 100644 => 100755 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh create mode 100755 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/tools/mint-ist.sh create mode 100755 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/tools/parseProposals.mjs create mode 100644 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/wallet-all-ertp/gen-game-offer.mjs create mode 100755 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/wallet-all-ertp/wf-game-propose.sh create mode 100755 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/wallet-all-ertp/wf-install-bundles.sh create mode 100755 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/wallet-all-ertp/wf-propose.sh create mode 100644 packages/smart-wallet/src/proposals/upgrade-walletFactory-proposal.js create mode 100644 packages/smart-wallet/test/start-game1-proposal.js create mode 100644 packages/vats/scripts/build-game1-start.js create mode 100644 packages/vats/scripts/build-walletFactory-upgrade.js diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/.gitignore b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/.gitignore new file mode 100644 index 00000000000..781644c0395 --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/.gitignore @@ -0,0 +1,2 @@ +upgrade-walletFactory-permit.json +upgrade-walletFactory.js diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh old mode 100644 new mode 100755 index 9d5987b8ef6..cad2ee8fb01 --- a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh @@ -1,13 +1,14 @@ #!/bin/bash -. ./upgrade-test-scripts/env_setup.sh +# Dockerfile in upgrade-test sets: +# WORKDIR /usr/src/agoric-sdk/ +# Overriding it during development has occasionally been useful. +SDK=${SDK:-/usr/src/agoric-sdk} +. $SDK/upgrade-test-scripts/env_setup.sh # Enable debugging set -x -# CWD is agoric-sdk -upgrade11=./upgrade-test-scripts/agoric-upgrade-11 - # hacky restore of pruned artifacts killAgd EXPORT_DIR=$(mktemp -t -d swing-store-export-upgrade-11-XXX) @@ -45,3 +46,22 @@ agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-bac test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.vaultState') "closed" "vault3 is closed" test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.locked.value') "0" "vault3 contains no collateral" test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.debtSnapshot.debt.value') "0" "vault3 has no debt" + +upgrade11=$SDK/upgrade-test-scripts/agoric-upgrade-11 +cd $upgrade11 + +## build proposal and install bundles +./tools/mint-ist.sh +./wallet-all-ertp/wf-install-bundles.sh + +## upgrade wallet factory +./wallet-all-ertp/wf-propose.sh + +## start game1 +./wallet-all-ertp/wf-game-propose.sh + +# Pay 0.25IST join the game and get some places +node ./wallet-all-ertp/gen-game-offer.mjs Shire Mordor >/tmp/,join.json +agops perf satisfaction --from $GOV1ADDR --executeOffer /tmp/,join.json --keyring-backend=test + +cd $SDK diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/pre_test.sh b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/pre_test.sh index 1db5c34bbdc..ac0f8f4ecc6 100755 --- a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/pre_test.sh +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/pre_test.sh @@ -8,6 +8,8 @@ waitForBlock 5 # CWD is agoric-sdk upgrade11=./upgrade-test-scripts/agoric-upgrade-11 +test_val "$(agd query vstorage children published.boardAux -o json | jq .children)" "[]" "no boardAux children yet" + # validate agoric-upgrade-10 metrics after update test_val $(agd q vstorage children published.vaultFactory.managers.manager0.vaults -o json | jq -r '.children | length') 3 "we have three vaults" diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/test.sh b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/test.sh index 86dcbf2d057..53d04c4beed 100755 --- a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/test.sh +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/test.sh @@ -23,3 +23,45 @@ mv $TMP_GENESIS_DIR/priv_validator_state.json $HOME/.agoric/data mv $TMP_GENESIS_DIR/* $HOME/.agoric/config/ rm -rf $EXPORT_DIR startAgd + +testMinChildren() { + path=$1 + min=$2 + line="$(agd query vstorage children $path -o jsonlines)" + ok=$(echo $line | jq ".children | length | . > $min") + test_val "$ok" "true" "$path: more than $min children" +} + +# Check brand aux data for more than just vbank assets +testMinChildren published.boardAux 3 + +testDisplayInfo() { + name=$1 + expected=$2 + + line="$(agoric follow -lF :published.agoricNames.brand -o text)" + # find brand by name, then corresponding slot + id=$(echo $line | jq --arg name "$name" -r '.slots as $slots | .body | gsub("^#";"") | fromjson | .[] | select(.[0] == $name) | .[1] | capture("^[$](?0|[1-9][0-9]*)") | .slot | $slots[. | tonumber]') + echo $name Id: $id + + line="$(agoric follow -lF :published.boardAux.$id -o jsonlines)" + displayInfo="$(echo $line | jq -c .displayInfo)" + test_val "$displayInfo" "$expected" "$name displayInfo from boardAux" +} + +testDisplayInfo IST '{"assetKind":"nat","decimalPlaces":6}' + +testPurseValuePayload() { + addr=$1 + wkAsset=$2 + expected=$3 + + line="$(agoric follow -lF :published.wallet.$addr.current -o jsonlines)" + # HACK: selecting brand by allegedName + payload=$(echo $line | jq --arg name "$wkAsset" -c '.purses[] | select(.brand | contains($name)) | .balance.value.payload') + test_val "$payload" "$expected" "$wkAsset purse for $addr" +} + +# Smart wallet handles game Place assets? +testDisplayInfo Place '{"assetKind":"copyBag"}' +testPurseValuePayload $GOV1ADDR Place '[["Shire","1"],["Mordor","1"]]' diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/tools/mint-ist.sh b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/tools/mint-ist.sh new file mode 100755 index 00000000000..7c43a5b46d5 --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/tools/mint-ist.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +if [ -z "$GOV1ADDR" ]; then + echo run env_setup.sh to set GOV1ADDR + exit 1 +fi + +micro=000000 + +# send some collateral to gov1 +agd tx bank send validator $GOV1ADDR 20123$micro${ATOM_DENOM} \ + --keyring-backend=test --chain-id=agoriclocal --yes -bblock -o json + +export PATH=/usr/src/agoric-sdk/packages/agoric-cli/bin:$PATH +agops vaults open --giveCollateral 5000 --wantMinted 20000 > /tmp/offer.json +agops perf satisfaction --executeOffer /tmp/offer.json --from gov1 --keyring-backend=test diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/tools/parseProposals.mjs b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/tools/parseProposals.mjs new file mode 100755 index 00000000000..daab1be3636 --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/tools/parseProposals.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +import fs from 'fs'; + +const Fail = (template, ...args) => { + throw Error(String.raw(template, ...args.map(val => String(val)))); +}; + +/** + * Parse output of `agoric run proposal-builder.js` + * + * @param {string} txt + * + * adapted from packages/boot/test/bootstrapTests/supports.js + */ +const parseProposalParts = txt => { + const evals = [ + ...txt.matchAll(/swingset-core-eval (?\S+) (?