Skip to content

Commit

Permalink
fix encoding levels
Browse files Browse the repository at this point in the history
  • Loading branch information
turadg committed Sep 6, 2022
1 parent 8521c6f commit 20f897d
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 146 deletions.
13 changes: 2 additions & 11 deletions packages/smart-wallet/src/offers.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ import { E, Far, passStyleOf } from '@endo/far';
* }} OfferStatus
*/

// TOOD validate at runtime
/** @type {(fromCapData: ERef<import('@endo/captp').Unserialize<unknown>>) => (capData: import('./types').WalletCapData<OfferSpec>) => OfferSpec} */
const makeOfferSpecUnmarshaller = fromCapData => capData =>
E(fromCapData)(capData);
harden(makeOfferSpecUnmarshaller);

// TODO(PS0) make into a virtual kind https://github.com/Agoric/agoric-sdk/issues/5894
// but wait for Far classes to be available https://github.com/Agoric/agoric-sdk/pull/5960
// Though maybe durability should just be on wallet object and these facets be volatile. (moving lastOfferId up to baggage)
Expand Down Expand Up @@ -66,22 +60,19 @@ export const makeOffersFacet = (
let lastOfferId = 0;

assert(fromCapData, 'missing fromCapData');
const unmarshallOfferSpec = makeOfferSpecUnmarshaller(fromCapData);

return Far('offers facet', {
/**
* Take an offer description provided in capData, augment it with payments and call zoe.offer()
*
* @param {import('./types').WalletCapData<import('./offers.js').OfferSpec>} capData
* @param {OfferSpec} offerSpec
* @returns {Promise<void>} when the offer has been sent to Zoe; payouts go into this wallet's purses
* @throws if any parts of the offer can be determined synchronously to be invalid
*/
executeOffer: async capData => {
executeOffer: async offerSpec => {
// 1. Prepare values and validate synchronously.
// Any of these may throw.

const offerSpec = await unmarshallOfferSpec(capData);

const { id, invitationSpec, proposal, offerArgs } = offerSpec;
assert(invitationSpec, 'offer missing invitationSpec');
assert(proposal, 'offer missing proposal');
Expand Down
36 changes: 36 additions & 0 deletions packages/smart-wallet/src/smartWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
withdrawPaymentsFromPurses,
} from './utils.js';

const { details: X, quote: q } = assert;

/**
* @file Smart wallet module
*
Expand All @@ -37,6 +39,16 @@ import {
* for each brand.
*/

/**
* @typedef {{
* method: 'executeOffer'
* offer: import('./offers.js').OfferSpec,
* } | {
* method: 'receive'
* payment: Payment,
* }} BridgeAction
*/

/**
* @typedef {{ updated: 'offerStatus', status: import('./offers.js').OfferStatus } |
* { updated: 'balance'; currentAmount: Amount } |
Expand Down Expand Up @@ -302,6 +314,29 @@ export const makeSmartWallet = async (
offerToInvitationMakers.init(offerId, invitationMakers),
);

/**
*
* @param {import('./types').WalletCapData<BridgeAction>} actionCapData
*/
const handleBridgeAction = actionCapData => {
assert(actionCapData.body && actionCapData.slots, 'invalid capdata');
return E.when(
E(marshaller).unserialize(actionCapData),
/** @param {BridgeAction} action */
// @ts-expect-error xxx
action => {
switch (action.method) {
case 'executeOffer':
return E(offersFacet).executeOffer(action.offer);
case 'receive':
return E(depositFacet).receive(action.payment);
default:
assert.fail(X`invalid handle bridge action ${q(action)}`);
}
},
);
};

/* Design notes:
* - an ocap to suggestIssuer() should not have authority to withdraw from purses (so no executeOffer() or applyMethod())
* XXX refactor for POLA
Expand All @@ -312,6 +347,7 @@ export const makeSmartWallet = async (
* - wallet-ui (which has key material; dapps use wallet-ui to propose actions)
*/
return Far('SmartWallet', {
handleBridgeAction,
getDepositFacet: () => depositFacet,
getOffersFacet: () => offersFacet,

Expand Down
62 changes: 8 additions & 54 deletions packages/smart-wallet/src/walletFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,53 +19,12 @@ import { makeSmartWallet } from './smartWallet.js';
* }} SmartWalletContractTerms
*/

// Client message types. These are the shapes client must use to push messages over the bridge.
/**
* @typedef {{
* target: 'deposit' | 'offers',
* method: string,
* arg: import('@endo/captp').CapData<string>,
* }} Action
* Description of action, to be encoded in a bridge messsage 'action' or 'spendAction' field.
* Only actions sent in 'spendAction' (in WalletSpendActionMsg) can spend.
*
* The `target` field maps to a getter: 'foo' --> getFooFacet()
*/

/** @type {(action: Action) => string} */
export const stringifyAction = ({ target, method, arg }) => {
switch (target) {
case 'deposit':
assert(method === 'receive', `unsupported method ${method}`);
break;
case 'offers':
assert(method === 'executeOffer', `unsupported method ${method}`);
break;
default:
assert.fail(`unsupported target ${target}`);
}
// xxx utility for validating CapData shape?
assert(arg.body && arg.slots, 'invalid arg');

return `${target}.${method} ${JSON.stringify(arg)}`;
};
/** @type {(actionStr: string) => Action} */
export const parseActionStr = str => {
const space = str.indexOf(' ');
const left = str.substring(0, space);
const argStr = str.substring(space);
const [target, method] = left.split('.');
assert(target === 'offers', 'supported action str');
const arg = JSON.parse(argStr);
return { target, method, arg };
};

// Cosmos bridge types
/**
* @typedef {{
* type: 'WALLET_ACTION',
* owner: string,
* action: string, // <target>,<method>,<argCapDataJsonStr>
* action: import('./smartWallet.js').BridgeAction,
* blockHeight: unknown, // int64
* blockTime: unknown, // int64
* }} WalletActionMsg
Expand All @@ -74,7 +33,7 @@ export const parseActionStr = str => {
* @typedef {{
* type: 'WALLET_SPEND_ACTION',
* owner: string,
* spendAction: string, // SpendAction
* spendAction: object, // XXX base on BridgeAction
* blockHeight: unknown, // int64
* blockTime: unknown, // int64
* }} WalletSpendActionMsg
Expand Down Expand Up @@ -119,19 +78,14 @@ export const start = async (zcf, privateArgs) => {
canSpend || 'action' in obj,
'missing action/spendAction property',
);
const action = parseActionStr(canSpend ? obj.spendAction : obj.action);
const actionCapData = canSpend ? obj.spendAction : obj.action;
console.log('DEBUG', { actionCapData });
// TODO validate shape before sending to wallet

const wallet = walletsByAddress.get(obj.owner); // or throw
console.log('walletFactory:', { wallet, action });
switch (action.target) {
case 'deposit':
assert(canSpend);
return E(E(wallet).getDepositFacet())[action.method](action.arg);
case 'offers':
return E(E(wallet).getOffersFacet())[action.method](action.arg);
default:
throw new Error(`unsupported action target ${action.target}`);
}

console.log('walletFactory:', { wallet, actionCapData });
return E(wallet).handleBridgeAction(actionCapData);
},
});

Expand Down
26 changes: 7 additions & 19 deletions packages/smart-wallet/test/test-psm-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
} from '@agoric/vats/tools/boot-test-utils.js';
import { eventLoopIteration } from '@agoric/zoe/tools/eventLoopIteration.js';
import { E } from '@endo/far';
import { makeDefaultTestContext } from './contexts.js';
import { coalesceUpdates } from '../src/utils.js';
import { makeDefaultTestContext } from './contexts.js';

/**
* @type {import('ava').TestFn<Awaited<ReturnType<makeDefaultTestContext>>
Expand Down Expand Up @@ -63,8 +63,7 @@ const purseBalance = (state, brand) => {

test('null swap', async t => {
const { anchor } = t.context;
const { agoricNames, board } = await E.get(t.context.consume);
const publishingMarshal = await E(board).getPublishingMarshaller();
const { agoricNames } = await E.get(t.context.consume);
const minted = await E(agoricNames).lookup('brand', 'IST');

const wallet = await t.context.simpleProvideWallet('agoric1nullswap');
Expand All @@ -90,13 +89,11 @@ test('null swap', async t => {
},
};
/** @type {import('../src/types').WalletCapData<import('../src/offers').OfferSpec>} */
// @ts-expect-error cast
const offerCapData = publishingMarshal.serialize(harden(offerSpec));

// let promises settle to notify brands and create purses
await eventLoopIteration();

offersFacet.executeOffer(offerCapData);
offersFacet.executeOffer(offerSpec);
await eventLoopIteration();

t.is(purseBalance(computedState, anchor.brand), 0n);
Expand All @@ -109,7 +106,7 @@ test('null swap', async t => {
// we test this direciton of swap because wanting anchor would require the PSM to have anchor in it first
test('want stable', async t => {
const { anchor } = t.context;
const { agoricNames, board } = await E.get(t.context.consume);
const { agoricNames } = await E.get(t.context.consume);

// no fees when wanting stable
const swapSize = 10_000n;
Expand Down Expand Up @@ -140,7 +137,6 @@ test('want stable', async t => {
instance: psmInstance,
publicInvitationMaker: 'makeWantStableInvitation',
};
const marshaller = await E(board).getPublishingMarshaller();
/** @type {import('../src/offers').OfferSpec} */
const offerSpec = {
id: 1,
Expand All @@ -151,11 +147,9 @@ test('want stable', async t => {
},
};
/** @type {import('../src/types').WalletCapData<import('../src/offers').OfferSpec>} */
// @ts-expect-error cast
const offerCapData = marshaller.serialize(harden(offerSpec));

t.log('Execute the swap');
offersFacet.executeOffer(offerCapData);
offersFacet.executeOffer(offerSpec);
await eventLoopIteration();
t.is(purseBalance(computedState, anchor.brand), 0n);
t.is(purseBalance(computedState, stableBrand), swapSize - 1n);
Expand Down Expand Up @@ -204,8 +198,6 @@ test('govern offerFilter', async t => {
const { positions, questionHandle } = await details;
const yesFilterOffers = positions[0];

const marshaller = await E(board).getPublishingMarshaller();

t.log('Prepare offer to voting invitation in purse');
{
/** @type {import('../src/invitations.js').PurseInvitationSpec} */
Expand All @@ -221,10 +213,8 @@ test('govern offerFilter', async t => {
proposal: {},
};
/** @type {import('../src/types').WalletCapData<import('../src/offers').OfferSpec>} */
// @ts-expect-error cast
const offerCapData = marshaller.serialize(harden(offerSpec));
t.log('Execute offer for the invitation');
offersFacet.executeOffer(offerCapData);
offersFacet.executeOffer(offerSpec);
}

t.log('Prepare offer to continue invitation');
Expand All @@ -244,12 +234,10 @@ test('govern offerFilter', async t => {
proposal: {},
};
/** @type {import('../src/types').WalletCapData<import('../src/offers').OfferSpec>} */
// @ts-expect-error cast
const offerCapData = marshaller.serialize(harden(offerSpec));

// wait for the previousOffer result to get into the purse
await eventLoopIteration();
offersFacet.executeOffer(offerCapData);
offersFacet.executeOffer(offerSpec);
}

t.log('Make sure vote happened');
Expand Down
76 changes: 14 additions & 62 deletions packages/smart-wallet/test/test-walletFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import '@agoric/vats/src/core/types.js';

import { makeImportContext } from '@agoric/wallet-backend/src/marshal-contexts.js';
import { E } from '@endo/far';
import { parseActionStr, stringifyAction } from '../src/walletFactory.js';
import { eventLoopIteration } from '@agoric/zoe/tools/eventLoopIteration.js';
import { makeDefaultTestContext } from './contexts.js';
import {
ActionType,
Expand Down Expand Up @@ -51,18 +51,26 @@ test('bridge', async t => {
proposal: {},
};

t.like(await lastUpdate(), {
updated: 'balance',
currentAmount: {
value: [],
},
});

assert(t.context.sendToBridge);
const res = await t.context.sendToBridge(BRIDGE_ID.WALLET, {
type: ActionType.WALLET_ACTION,
owner: mockAddress1,
action: stringifyAction({
target: 'offers',
method: 'executeOffer',
arg: ctx.fromBoard.serialize(harden(offerSpec)),
}),
// consider a helper for each action type
action: ctx.fromBoard.serialize(
harden({ method: 'executeOffer', offer: offerSpec }),
),
});
t.is(res, undefined);

await eventLoopIteration();

t.deepEqual(await lastUpdate(), {
updated: 'offerStatus',
status: {
Expand Down Expand Up @@ -99,59 +107,3 @@ test.todo(
// executeOffer to buy the junk (which can't resolve)
// exit the offer "oh I don't want to buy junk!"
);

test('wallet action encoding', t => {
const action = /** @type {const} */ ({
target: 'offers',
method: 'executeOffer',
arg: {
body: 'bogus',
slots: ['foo'],
},
});
t.is(
// @ts-expect-error CapData type should be readonly slots
stringifyAction(action),
'offers.executeOffer {"body":"bogus","slots":["foo"]}',
);

t.throws(
// @ts-expect-error
() => stringifyAction({ target: 'foo' }),
{
message: 'unsupported target foo',
},
);
t.throws(
// @ts-expect-error
() => stringifyAction({ target: 'deposit', method: 'foo' }),
{
message: 'unsupported method foo',
},
);
t.throws(
// @ts-expect-error
() => stringifyAction({ target: 'deposit', method: 'receive', arg: {} }),
{
message: 'invalid arg',
},
);
});

test('wallet action decoding', t => {
const action = /** @type {const} */ ({
target: 'offers',
method: 'executeOffer',
arg: {
body: 'bogus',
slots: ['foo'],
},
});
// @ts-expect-error CapData type should be readonly slots
const str = stringifyAction(action);
t.deepEqual(parseActionStr(str), action);

t.throws(() => parseActionStr(` ${str}`));
t.throws(() => parseActionStr(`,${str}`));
t.throws(() => parseActionStr(', '));
});

0 comments on commit 20f897d

Please sign in to comment.