Skip to content

Commit 1f44419

Browse files
authored
feat(smartWallet): invoke offerResult without involving Zoe (#11672)
refs: - #8733 - #11256 ## Description - add a `nameHub` and `nameAdmin` to state of each offer result - add `spec.after.saveAs` option in `OfferSpec` to save an offer result - add `invokeItem` method in `BridgeAction` that uses a format somewhat like `callPipe` DRAFT until the TODOs below are addressed. ### Security / Scaling Considerations This is intended to serve as an efficient, straightforward alternative to continuing offers for the (common) case when no asset exchange is happening. The motivating usage is ymax oracles (it facitiliates optimizing Fast USDC oracles too). TODO: - [x] think thru allowing everyone, not just oracles, to use `invokeItem`. Without a suitable invitation, it's useless; but what about griefing? - [x] `callPipe` was allowed only on `agoricContract`; are we ready to drop that limitation? - [x] length limit was 3; this is currently unlimited; we haven't found motivation to extend past 3, have we? ### Documentation / Testing Considerations One happy path test shows that the feature works and how to use it. TODO - [x] test failure modes - [x] the test uses bundled code, which doesn't mix well with coverage tools. Invest in testBundle tests for smartWallet? ### Upgrade Considerations TODO: - [ ] figure out upgrade implications of adding to smartWallet exo state
2 parents 34a6123 + 579ba6f commit 1f44419

File tree

12 files changed

+734
-20
lines changed

12 files changed

+734
-20
lines changed

packages/boot/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@endo/far": "^1.1.14",
5050
"@endo/init": "^1.1.12",
5151
"@endo/marshal": "^1.8.0",
52+
"@endo/patterns": "^1.7.0",
5253
"@endo/promise-kit": "^1.1.13",
5354
"@endo/stream": "^1.2.13",
5455
"import-meta-resolve": "^4.1.0",
@@ -63,7 +64,6 @@
6364
"@agoric/store": "workspace:*",
6465
"@agoric/swingset-liveslots": "workspace:*",
6566
"@endo/base64": "^1.0.12",
66-
"@endo/patterns": "^1.7.0",
6767
"ava": "^5.3.0",
6868
"c8": "^10.1.3",
6969
"ts-blank-space": "^0.6.1"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/** @file use capabilities in smart wallet without offers */
2+
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';
3+
4+
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
5+
import type { CurrentWalletRecord } from '@agoric/smart-wallet/src/smartWallet.js';
6+
import { start as startPriceContract } from '@agoric/smart-wallet/test/wallet-fun.contract.js';
7+
import type { NameHub } from '@agoric/vats';
8+
import type { IssuerKeywordRecord } from '@agoric/zoe';
9+
import type { Installation } from '@agoric/zoe/src/zoeService/utils';
10+
import bundleSource from '@endo/bundle-source';
11+
import type { ExecutionContext, TestFn } from 'ava';
12+
import { createRequire } from 'node:module';
13+
import {
14+
makeWalletFactoryContext,
15+
type WalletFactoryTestContext as TC,
16+
} from './walletFactory.ts';
17+
18+
const nodeRequire = createRequire(import.meta.url);
19+
20+
const test = anyTest as TestFn<TC>;
21+
22+
test.before(async t => {
23+
t.context = await makeWalletFactoryContext(t);
24+
});
25+
test.after.always(t => {
26+
return t.context.shutdown && t.context.shutdown();
27+
});
28+
29+
const startContract = async <SF>(
30+
t: ExecutionContext<TC>,
31+
_startFn: SF,
32+
specifier: string,
33+
name?: string,
34+
issuers?: IssuerKeywordRecord,
35+
) => {
36+
const { runUtils, refreshAgoricNamesRemotes } = t.context;
37+
const { EV } = runUtils;
38+
const zoe = await EV.vat('bootstrap').consumeItem('zoe');
39+
const bundle = await bundleSource(nodeRequire.resolve(specifier));
40+
41+
const installation: Installation<SF> = await EV(zoe).install(bundle);
42+
const started = await EV(zoe).startInstance(installation, issuers);
43+
t.log('started', specifier, Object.keys(started));
44+
45+
if (name) {
46+
const agoricNamesAdmin =
47+
await EV.vat('bootstrap').consumeItem('agoricNamesAdmin');
48+
const instanceAdmin = await EV(agoricNamesAdmin).lookupAdmin('instance');
49+
await EV(instanceAdmin).update(name, started.instance);
50+
refreshAgoricNamesRemotes();
51+
}
52+
return started;
53+
};
54+
55+
const showInvitationBalance = (addr: string, r: CurrentWalletRecord) => [
56+
addr,
57+
'has invitations',
58+
r.purses.flatMap(p => p.balance.value),
59+
];
60+
61+
test('use offer result without zoe', async t => {
62+
const { walletFactoryDriver, agoricNamesRemotes } = t.context;
63+
64+
const makeCoreCtx = (addr: string) => {
65+
const start = async () => {
66+
const { EV } = t.context.runUtils;
67+
const namesByAddress =
68+
await EV.vat('bootstrap').consumeItem('namesByAddress');
69+
70+
const started = await startContract(
71+
t,
72+
startPriceContract,
73+
'./wallet-fun.contract.js',
74+
'walletFun',
75+
);
76+
const toSetPrices = await EV(started.creatorFacet).makeAdminInvitation();
77+
const df = await EV(namesByAddress).lookup(addr, 'depositFacet');
78+
const rxd = EV(df).receive(toSetPrices);
79+
const getPrices = async () => {
80+
const p = await EV(started.publicFacet).getPrices();
81+
return p;
82+
};
83+
return harden({ getReceived: () => rxd, getPrices });
84+
};
85+
86+
return harden({ start });
87+
};
88+
89+
// client-side presence
90+
const addr = 'agoric1admin';
91+
const wd = await walletFactoryDriver.provideSmartWallet(addr);
92+
93+
const makePriceOracle = () => {
94+
const redeemInvitation = async () => {
95+
const { walletFun } = agoricNamesRemotes.instance;
96+
t.truthy(walletFun);
97+
98+
t.log(...showInvitationBalance(addr, wd.getCurrentWalletRecord()));
99+
100+
await wd.executeOffer({
101+
id: 'redeemInvitation',
102+
invitationSpec: {
103+
source: 'purse',
104+
description: 'admin',
105+
instance: walletFun,
106+
},
107+
proposal: {},
108+
after: { saveAs: 'priceSetter' },
109+
});
110+
t.log('redeemInvitation last update', wd.getLatestUpdateRecord());
111+
};
112+
113+
const useOfferResult = () =>
114+
wd.invokeItem('priceSetter', {
115+
method: 'setPrice',
116+
args: [100n],
117+
});
118+
119+
return harden({ redeemInvitation, useOfferResult });
120+
};
121+
122+
const core = makeCoreCtx(addr);
123+
const { getPrices, getReceived } = await core.start();
124+
125+
const oracle = makePriceOracle();
126+
await oracle.redeemInvitation();
127+
await getReceived();
128+
const initialPrices = await getPrices();
129+
t.log({ initialPrices });
130+
t.deepEqual(initialPrices, [0n], 'initial price');
131+
132+
await oracle.useOfferResult();
133+
await eventLoopIteration();
134+
const actual = await getPrices();
135+
t.log('prices after', actual);
136+
t.deepEqual(actual, [100n], 'updated price');
137+
});

packages/boot/tools/drivers.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ import {
1313
} from '@agoric/internal/src/storage-test-utils.js';
1414
import { Fail } from '@endo/errors';
1515

16-
import type { OfferSpec } from '@agoric/smart-wallet/src/offers.js';
1716
import type {
17+
OfferResultStep,
18+
OfferSpec,
19+
} from '@agoric/smart-wallet/src/offers.js';
20+
import type {
21+
BridgeAction,
1822
CurrentWalletRecord,
1923
SmartWallet,
2024
UpdateRecord,
@@ -58,6 +62,15 @@ export const makeWalletFactoryDriver = async (
5862
isNew,
5963
getAddress: () => walletAddress,
6064

65+
invokeItem(name: string, ...steps: OfferResultStep[]): Promise<void> {
66+
const action: BridgeAction = harden({
67+
method: 'invokeItem',
68+
name,
69+
steps,
70+
});
71+
const offerCapData = marshaller.toCapData(action);
72+
return EV(walletPresence).handleBridgeAction(offerCapData, false);
73+
},
6174
executeOffer(offer: OfferSpec): Promise<void> {
6275
const offerCapData = myMarshaller.toCapData(
6376
harden({

packages/smart-wallet/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"prepack": "yarn run -T tsc --build tsconfig.build.json",
1111
"postpack": "git clean -f '*.d.ts*' '*.tsbuildinfo'",
1212
"test": "ava",
13+
"test:c8": "c8 --all $C8_OPTIONS ava",
1314
"test:xs": "exit 0",
1415
"lint": "yarn run -T run-s --continue-on-error 'lint:*'",
1516
"lint-fix": "yarn lint:eslint --fix",
@@ -41,6 +42,7 @@
4142
"@endo/far": "^1.1.14",
4243
"@endo/marshal": "^1.8.0",
4344
"@endo/nat": "^5.1.3",
45+
"@endo/patterns": "^1.7.0",
4446
"@endo/promise-kit": "^1.1.13"
4547
},
4648
"files": [

packages/smart-wallet/src/offerWatcher.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,20 @@ export const prepareOfferWatcher = (baggage, vowTools) => {
274274
resultWatcher: {
275275
onFulfilled(result) {
276276
const { facets } = this;
277-
facets.helper.publishResult(result);
277+
const { walletHelper } = this.state;
278+
const { after } = this.state.status;
279+
if (after) {
280+
if (after.saveAs) {
281+
const { saveAs: name } = after;
282+
// Note: result is passable and storable since it was received from another vat
283+
walletHelper.saveOfferResult(name, result);
284+
facets.helper.publishResult(
285+
harden({ [name]: { passStyle: passStyleOf(result) } }),
286+
);
287+
}
288+
} else {
289+
facets.helper.publishResult(result);
290+
}
278291
},
279292
/**
280293
* If promise disconnected, watch again. Or if there's an Error, handle

packages/smart-wallet/src/offers.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@
66
* @typedef {number | string} OfferId
77
*/
88

9+
// TODO: change saveAs to be direct part of step / offerSpec
10+
// TODO: accept missing/undefined method name for function invocation
11+
// TODO: change from multi step to a single InvokeSpec
12+
13+
/**
14+
* @typedef {{ method: string; args: unknown[] } | { saveAs: string }} OfferResultStep
15+
*/
16+
917
/**
1018
* @typedef {{
1119
* id: OfferId;
1220
* invitationSpec: import('./invitations.js').InvitationSpec;
1321
* proposal: Proposal;
1422
* offerArgs?: any;
23+
* after?: {
24+
* saveAs?: string;
25+
* };
1526
* }} OfferSpec
1627
*/
1728

0 commit comments

Comments
 (0)