Skip to content

Commit

Permalink
feat(smart-wallet): publish possibly exitable offers in current
Browse files Browse the repository at this point in the history
  • Loading branch information
samsiegart committed Feb 18, 2023
1 parent 638c2b6 commit 94443a5
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 27 deletions.
35 changes: 14 additions & 21 deletions packages/inter-protocol/test/smartWallet/test-psm-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { NonNullish } from '@agoric/assert';

import {
coalesceUpdates,
sequenceUpdates,
sequenceCurrents,
} from '@agoric/smart-wallet/src/utils.js';
import { INVITATION_MAKERS_DESC } from '../../src/econCommitteeCharter.js';
import {
Expand Down Expand Up @@ -66,7 +66,8 @@ test('null swap', async t => {
const { getBalanceFor, wallet } = await t.context.provideWalletAndBalances(
'agoric1nullswap',
);
const updates = sequenceUpdates(E(wallet).getUpdatesSubscriber());
const computedState = coalesceUpdates(E(wallet).getUpdatesSubscriber());
const currents = sequenceCurrents(E(wallet).getCurrentSubscriber());

/** @type {import('@agoric/smart-wallet/src/invitations').AgoricContractInvitationSpec} */
const invitationSpec = {
Expand All @@ -75,37 +76,29 @@ test('null swap', async t => {
callPipe: [['makeGiveMintedInvitation']],
};

await wallet.getOffersFacet().executeOffer({
const offer = {
id: 'nullSwap',
invitationSpec,
proposal: {
// empty amounts
give: { In: AmountMath.makeEmpty(mintedBrand) },
want: { Out: anchor.makeEmpty() },
},
});

await eventLoopIteration();

t.like(updates[0], {
updated: 'balance',
});

const statusUpdateHasKeys = (updateIndex, result, numWants, payouts) => {
const { status } = updates[updateIndex];
t.is('result' in status, result, 'result');
t.is('numWantsSatisfied' in status, numWants, 'numWantsSatisfied');
t.is('payouts' in status, payouts, 'payouts');
t.false('error' in status);
};

statusUpdateHasKeys(1, false, false, false);
statusUpdateHasKeys(2, true, false, false);
statusUpdateHasKeys(3, true, true, false);
statusUpdateHasKeys(4, true, true, true);
await wallet.getOffersFacet().executeOffer(harden(offer));

await eventLoopIteration();

const status = computedState.offerStatuses.get('nullSwap');
t.is(status?.id, 'nullSwap');
t.false('error' in NonNullish(status), 'should not have an error');
t.is(await E.get(getBalanceFor(anchor.brand)).value, 0n);
t.is(await E.get(getBalanceFor(mintedBrand)).value, 0n);

t.deepEqual(currents[0].possiblyExitableOffers, {});
t.deepEqual(currents[1].possiblyExitableOffers, { nullSwap: offer });
t.deepEqual(currents[2].possiblyExitableOffers, {});
});

// we test this direciton of swap because wanting anchor would require the PSM to have anchor in it first
Expand Down
35 changes: 34 additions & 1 deletion packages/smart-wallet/src/smartWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const mapToRecord = map => Object.fromEntries(map.entries());
* purseBalances: MapStore<RemotePurse, Amount>,
* updatePublishKit: PublishKit<UpdateRecord>,
* currentPublishKit: PublishKit<CurrentWalletRecord>,
* possiblyExitableOffers: MapStore<import('./offers.js').OfferId, import('./offers.js').OfferStatus>,
* }>} ImmutableState
*
* @typedef {{
Expand Down Expand Up @@ -233,6 +234,10 @@ export const prepareSmartWallet = (baggage, shared) => {
/** @type {PublishKit<CurrentWalletRecord>} */
currentPublishKit,
walletStorageNode,
possiblyExitableOffers: makeScalarBigMapStore(
'possibly exitable offers',
{ durable: true },
),
};

return {
Expand Down Expand Up @@ -304,12 +309,14 @@ export const prepareSmartWallet = (baggage, shared) => {
offerToUsedInvitation,
offerToPublicSubscriberPaths,
purseBalances,
possiblyExitableOffers,
} = this.state;
currentPublishKit.publisher.publish({
purses: [...purseBalances.values()].map(a => ({
brand: a.brand,
balance: a,
})),
possiblyExitableOffers: mapToRecord(possiblyExitableOffers),
offerToUsedInvitation: mapToRecord(offerToUsedInvitation),
offerToPublicSubscriberPaths: mapToRecord(
offerToPublicSubscriberPaths,
Expand Down Expand Up @@ -399,7 +406,7 @@ export const prepareSmartWallet = (baggage, shared) => {
* @throws if any parts of the offer can be determined synchronously to be invalid
*/
async executeOffer(offerSpec) {
const { facets } = this;
const { facets, state } = this;
const {
address,
bank,
Expand Down Expand Up @@ -446,6 +453,32 @@ export const prepareSmartWallet = (baggage, shared) => {
},
onStatusChange: offerStatus => {
logger.info('offerStatus', offerStatus);

const isOfferPossiblyExitable = !(
'error' in offerStatus || 'numWantsSatisfied' in offerStatus
);

if (
isOfferPossiblyExitable &&
!state.possiblyExitableOffers.has(offerStatus.id)
) {
state.possiblyExitableOffers.init(
offerStatus.id,
harden(offerStatus),
);
facets.helper.publishCurrentState();
// Don't need to publish an update because the offer is new.
return;
}

if (
!isOfferPossiblyExitable &&
state.possiblyExitableOffers.has(offerStatus.id)
) {
state.possiblyExitableOffers.delete(offerStatus.id);
facets.helper.publishCurrentState();
}

updatePublishKit.publisher.publish({
updated: 'offerStatus',
status: offerStatus,
Expand Down
25 changes: 20 additions & 5 deletions packages/smart-wallet/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,33 @@ export const coalesceUpdates = (updates, invitationBrand) => {
};

/**
* Sequence updates from a wallet UpdateRecord publication feed. Note that local
* state may not reflect the wallet's state if the initial updates are missed.
* @param {import('@agoric/casting').Follower<any>} follower
* @throws if there is no first height
*/
export const assertHasData = async follower => {
const eachIterable = E(follower).getReverseIterable();
const iterator = await E(eachIterable)[Symbol.asyncIterator]();
const el = await iterator.next();

// done before we started
if (el.done && !el.value) {
assert.fail(NO_SMART_WALLET_ERROR);
}
};

/**
* Sequence currents from a wallet UpdateRecord publication feed. Note that local
* state may not reflect the wallet's state if the initial currents are missed.
*
* If this proves to be a problem we can add an option to this or a related
* utility to reset state from RPC.
*
* @param {ERef<Subscriber<import('./smartWallet').UpdateRecord>>} updates
* @param {ERef<Subscriber<import('./smartWallet').CurrentWalletRecord>>} currents
*/
export const sequenceUpdates = updates => {
export const sequenceCurrents = currents => {
const sequence = [];

void observeIteration(subscribeEach(updates), {
void observeIteration(subscribeEach(currents), {
updateState: updateRecord => {
sequence.push(updateRecord);
},
Expand Down

0 comments on commit 94443a5

Please sign in to comment.