Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6044 wallet voting #6077

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/deploy-script-support/src/offer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { E } from '@endo/far';
import { assert } from '@agoric/assert';
import { AmountMath } from '@agoric/ertp';

// XXX this module has hidden dependency on the walletAdmin type
/**
* @param {ERef<any>} walletAdmin - an internal type of the
* wallet, not defined here
Expand Down
11 changes: 10 additions & 1 deletion packages/governance/src/committee.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import { QuorumRule } from './question.js';

const { ceilDivide } = natSafeMath;

/**
* @typedef {{
* castBallotFor: (questionHandle: Handle<'Question'>, positions: Position[]) => Promise<void>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Chris-Hibbert I just realized that in order for committee members to be able to put questions as well, the "charter" contract should be a different shape:

Our design for raising debt limits involved using a method on a public facet, which assumed users had access to zoe and E(zoe).getPublicFacet().
https://github.com/Agoric/inter-protocol-bootstrap/blob/56de1ed1821e19828a6f74ff10b7fce27b3f2cb7/vote-raise-debt-limit.js#L177

I think life will be simpler if the new charter contract you're building uses methods on an offer result instead of methods on a public facet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... if the new charter contract you're building ...

There isn't a task corresponding to that description on my radar. Can we make an issue?
I presume we want a variant of econCommitteeCharter that serves PSM only. If the capability to invoke votes is carried by an offer result, should the various committee members each invoke a separate invitation, or is an invitation exercised once in bootstrap, and the offer result handed out from there?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make an issue?

Yes, we can; I more or less expected that was part of "Do The Right Thing" that you said what you had what you need for. The capability to vote is taken care of by the committee contract. My concern here is for posing questions. You're right that this raises a question of how the invitations get to the committee members. I suppose we can use the same mechanism as we did for the voting invitations, but it's worth some thought / discussion.

* }} CommitteeVoter
*/

/**
* Each Committee (an Electorate) represents a particular set of voters. The
* number of voters is visible in the terms.
Expand Down Expand Up @@ -46,7 +52,10 @@ const start = (zcf, privateArgs) => {
);

const makeCommitteeVoterInvitation = index => {
/** @type {OfferHandler} */
/**
* @param {ZCFSeat} seat
* @returns {CommitteeVoter}
*/
const offerHandler = seat => {
const voterHandle = makeHandle('Voter');
seat.exit();
Expand Down
2 changes: 1 addition & 1 deletion packages/vats/src/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Far } from '@endo/far';

/**
* @typedef {object} BridgeHandler An object that can receive messages from the bridge device
* @property {(srcId: string, obj: any) => Promise<void>} fromBridge Handle an inbound message
* @property {(srcId: string, obj: any) => Promise<unknown>} fromBridge Handle an inbound message
*
* @typedef {object} BridgeManager The object to manage this bridge
* @property {(dstID: string, obj: any) => any} toBridge
Expand Down
1 change: 1 addition & 0 deletions packages/wallet/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dependencies": {
"@agoric/assert": "^0.4.0",
"@agoric/cache": "^0.1.0",
"@agoric/cosmic-proto": "^0.1.0",
"@agoric/ertp": "^0.14.2",
"@agoric/internal": "^0.1.0",
"@agoric/nat": "^4.1.0",
Expand Down
23 changes: 23 additions & 0 deletions packages/wallet/api/src/findOrMakeInvitation.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ const findByKeyValuePairs = async (invitationPurseBalance, kvs) => {
return harden([matchingValue]);
};

/**
*
* @param {ERef<Purse>} invitationPurse
* @param {Brand} invitationBrand
* @returns {Invitation}
*/
const makeFindInvitation = (invitationPurse, invitationBrand) => {
const findInvitation = async (queryFn, queryParams) => {
const purseBalance = await E(invitationPurse).getCurrentAmount();
Expand All @@ -72,6 +78,13 @@ const makeFindInvitation = (invitationPurse, invitationBrand) => {
return findInvitation;
};

/**
*
* @param {LegacyMap<string, PromiseRecord<unknown>>} idToOfferResultPromiseKit
* @param {string} dappOrigin
* @param {{ description: string, priorOfferId: string }} opts
* @returns {Promise<Invitation>}
*/
const makeContinuingInvitation = async (
idToOfferResultPromiseKit,
dappOrigin,
Expand Down Expand Up @@ -115,6 +128,16 @@ const makeInvitation = async (
return E(publicFacet)[method](...args);
};

/**
*
* @param {LegacyMap<string, PromiseRecord<unknown>>} idToOfferResultPromiseKit
* @param {ERef<Board>} board
* @param {ERef<ZoeService>} zoe
* @param {Purse} invitationPurse
* @param {Brand} invitationBrand
* @param {unknown} offer
* @returns {Invitation}
*/
export const findOrMakeInvitation = async (
idToOfferResultPromiseKit,
board,
Expand Down
68 changes: 60 additions & 8 deletions packages/wallet/api/src/lib-wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { objectMap } from '@agoric/internal';
import { makeLegacyMap, makeScalarMap, makeScalarWeakMap } from '@agoric/store';
import { makeScalarBigMapStore } from '@agoric/vat-data';
import { AmountMath } from '@agoric/ertp';
import { E } from '@endo/eventual-send';
import { E, HandledPromise } from '@endo/eventual-send';

import { makeMarshal, passStyleOf, Far, mapIterable } from '@endo/marshal';
import { Nat } from '@agoric/nat';
Expand All @@ -43,6 +43,24 @@ import '@agoric/inter-protocol/exported.js';
import './internal-types.js';
import './types.js';

/**
* @typedef {{
* type: 'WALLET_ACTION',
* owner: string, // address of signer of the action
* action: string, // JSON-serialized SOMETHING {type: 'applyMethod' | 'suggestIssuer' }
* blockHeight: unknown, // int64
* blockTime: unknown, // int64
* }} WalletAction
* @typedef {{
* type: 'WALLET_SPEND_ACTION',
* owner: string,
* spendAction: string, // JSON-serialized SOMETHING including acceptOffer (which can spend) {type: 'applyMethod' | 'acceptOffer' | 'suggestIssuer' }
* blockHeight: unknown, // int64
* blockTime: unknown, // int64
* }} WalletSpendAction
* @typedef {WalletAction | WalletSpendAction} WalletBridgeAction
*/

// does nothing
const noActionStateChangeHandler = _newState => {};

Expand Down Expand Up @@ -171,8 +189,15 @@ export function makeWalletRoot({
/** @type {Store<Brand, Purse>} */
const brandToAutoDepositPurse = makeScalarMap('brand');

// Offers that the wallet knows about (the inbox).
/**
* Offers that the wallet knows about (the inbox)
*
* @type {MapStore<string, Record<string, any>>}
*/
const idToOffer = makeScalarMap('offerId');
/**
* @type {MapStore<string, ERef<Notifier<unknown>>>}
*/
const idToNotifierP = makeScalarMap('offerId');
/** @type {LegacyMap<string, PromiseRecord<any>>} */
// Legacy because promise kits are not passables
Expand Down Expand Up @@ -1319,6 +1344,7 @@ export function makeWalletRoot({

const offerResultP = E(seat).getOfferResult();
idToOfferResultPromiseKit.get(id).resolve(offerResultP);
E.when(offerResultP, result => context.saveOfferResultActions(result));

ret = {
depositedP,
Expand Down Expand Up @@ -1649,10 +1675,17 @@ export function makeWalletRoot({
return issuerManager;
}

/**
* @param {Handle<'invitation'>} invitationHandle
* @param {unknown} offerResult
*/
async function saveOfferResult(invitationHandle, offerResult) {
invitationHandleToOfferResult.init(invitationHandle, offerResult);
}

/**
* @param {Handle<'invitation'>} invitationHandle
*/
async function getOfferResult(invitationHandle) {
return invitationHandleToOfferResult.get(invitationHandle);
}
Expand Down Expand Up @@ -1800,21 +1833,40 @@ export function makeWalletRoot({
return acceptOffer(`${dappOrigin}#${rawId}`);
};

// TODO throw if capData doesn't match a currently supported use case
/** @param {import('@endo/marshal').CapData<string>} capData */
const handleApplyMethodAction = async capData => {
// TODO validate shape before destructuring
// unserialize=fromCapData (consider a fromCapData abstraction that takes a pattern)
const [receiver, methodName, args] = marshaller.unserialize(capData);
// equivalent to: E(receiver)[methodName](...args);
HandledPromise.applyMethod(receiver, methodName, args);
};

const handleSuggestIssuerAction = ({ petname, boardId }) =>
suggestIssuer(petname, boardId);

/** @typedef {{spendAction: string}} Action */
/**
* @param {Action} obj
* @returns {Promise<any>}
* @param {WalletBridgeAction} obj
* @returns {Promise<unknown>}
*/
const performAction = obj => {
const { type, data } = JSON.parse(obj.spendAction);
const { type = null, canSpend, ...rest } =
// XXX branching on canSpend probably belongs somewhere else
'spendAction' in obj
? { ...JSON.parse(obj.spendAction), canSpend: true }
: { ...JSON.parse(obj.action), canSpend: false };

switch (type) {
case 'acceptOffer':
return handleAcceptOfferAction(data);
assert(canSpend);
return handleAcceptOfferAction(rest.data);
/** @see ApplyMethodPayload */
case 'applyMethod':
assert(!canSpend);
return handleApplyMethodAction(rest);
case 'suggestIssuer':
return handleSuggestIssuerAction(data);
return handleSuggestIssuerAction(rest.data);
default:
throw new Error(`Unknown wallet action ${type}`);
}
Expand Down
8 changes: 8 additions & 0 deletions packages/wallet/api/src/marshal-contexts.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ const initSlotVal = (table, slot, val) => {
*/
export const makeExportContext = () => {
const walletObjects = {
/** @type {IdTable<number, unknown>} */
offerResult: {
bySlot: makeScalarMap(),
byVal: makeScalarMap(),
},
/** @type {IdTable<number, Purse>} */
purse: {
bySlot: makeScalarMap(),
Expand Down Expand Up @@ -187,6 +192,8 @@ export const makeExportContext = () => {
* @param {IdTable<number, V>} table
*/
const makeSaver = (kind, table) => {
// XXX table passed in to simplify type inference
assert.equal(table, walletObjects[kind]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea.

let nonce = 0;
/** @param {V} val */
const saver = val => {
Expand All @@ -197,6 +204,7 @@ export const makeExportContext = () => {
};

return harden({
saveOfferResultActions: makeSaver('offerResult', walletObjects.offerResult),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Actions part of this / these names is a little iffy. hm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I opted for consistency with the others, but I don't understand why "Actions" is part of any of them. Cool to remove from all?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. @michaelfig ?

are the other 2 used? @samsiegart ?

savePurseActions: makeSaver('purse', walletObjects.purse),
savePaymentActions: makeSaver('payment', walletObjects.payment),
/**
Expand Down
48 changes: 46 additions & 2 deletions packages/wallet/api/test/test-lib-wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';

// eslint-disable-next-line import/no-extraneous-dependencies
import bundleSource from '@endo/bundle-source';
import { makeCache } from '@agoric/cache';
import { makeIssuerKit, AmountMath, AssetKind } from '@agoric/ertp';
import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp';
import bundleSource from '@endo/bundle-source';

import { M } from '@agoric/store';

Expand Down Expand Up @@ -1196,6 +1196,47 @@ test('lib-wallet addOffer for autoswap swap', async t => {
);
});

function makeCounter() {
let count = 0;
return Far('counter', {
add(delta) {
count += delta;
},
read() {
return count;
},
});
}

test('lib-wallet performAction applyMethod', async t => {
const { wallet } = await setupTest(t);

const counter = makeCounter();

const capData = wallet
.getMarshaller()
.serialize(harden([counter, 'add', [3]]));

const action = JSON.stringify({
...capData,
type: 'applyMethod',
});
// @ts-expect-error cast
await wallet.performAction({ action });

t.is(counter.read(), 3);

t.throws(
() =>
wallet.performAction(
// @ts-expect-error cast
{ spendAction: action },
),
undefined,
'applyMethod type should not allow spendAction',
);
});

test('lib-wallet performAction suggestIssuer', async t => {
const { board, wallet } = await setupTest(t);
const { issuer: bucksIssuer } = makeIssuerKit('bucks');
Expand All @@ -1205,6 +1246,7 @@ test('lib-wallet performAction suggestIssuer', async t => {
type: 'suggestIssuer',
data: { petname: 'bucksIssuer', boardId: bucksIssuerBoardId },
});
// @ts-expect-error cast
const done = await wallet.performAction({ spendAction: action });

t.truthy(done);
Expand Down Expand Up @@ -1329,8 +1371,10 @@ test('lib-wallet performAction acceptOffer', async t => {
type: 'acceptOffer',
data: offer,
});
// @ts-expect-error cast
const accepted = await wallet.performAction({ spendAction: action });
assert(accepted);
// @ts-expect-error cast
const { depositedP } = accepted;
await t.throwsAsync(
() => wallet.getUINotifier(rawId, 'http://localhost:3001'),
Expand Down
19 changes: 17 additions & 2 deletions packages/wallet/api/test/test-wallet-marshal.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const makeOnChainWallet = board => {
});
};

test('makeImportContext preserves identity across AMM and wallet', t => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we still want a test that makeImportContext preserves identity across the AMM and wallet; that was its original motivation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to revert. I thought I was executing your suggestion in #5883 (comment)

Or are you suggesting to keep this change and add something like test.todo('makeImportContext preserves identity across AMM and wallet') ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops... that "better phrasing" suggestion wasn't all that clear; it was about the t.throws() message, not the test() description.

test('contract cannot forge references to purses', t => {
const context = makeImportContext();

const board = makeBoard(0, { prefix: 'board' });
Expand Down Expand Up @@ -118,7 +118,7 @@ test('ensureBoardId allows re-registration; initBoardId does not', t => {
t.throws(() => context.initBoardId('board12', brandM));
});

test('makeExportContext.serialize handles unregistered identites', t => {
test('makeExportContext.serialize handles unregistered identities', t => {
const brand = Far('Zoe invitation brand', {});
const instance = Far('amm instance', {});
const invitationAmount = harden({ brand, value: [{ instance }] });
Expand Down Expand Up @@ -163,4 +163,19 @@ test('makeExportContext.serialize handles unregistered identites', t => {
slots: ['payment:1'],
});
t.deepEqual(context.unserialize(cap2), myPayment);

{
const val = Far('someOfferResult', {});
context.saveOfferResultActions(val);
const cap = context.serialize(val);
t.deepEqual(cap, {
body: JSON.stringify({
'@qclass': 'slot',
iface: 'Alleged: someOfferResult',
index: 0,
}),
slots: ['offerResult:1'],
});
t.deepEqual(context.unserialize(cap), val);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's important that unserialize preserves identity, so this test should be t.is(...)

}
});
3 changes: 3 additions & 0 deletions packages/wallet/contract/src/smartWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export const makeSmartWallet = async (
return Far('SmartWallet', {
...wallet,
getSubscriber: () => storedSubscriber,
/**
* @param {import('@agoric/wallet-backend/src/lib-wallet.js').WalletBridgeAction} obj
*/
performAction: obj => E(admin).performAction(obj),
});
};
Expand Down
Loading