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

4484 multitenant smart wallet #5721

Merged
merged 2 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
66 changes: 66 additions & 0 deletions packages/store/src/stores/store-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,69 @@ export const provide = (mapStore, key, makeValue) => {
return mapStore.get(key);
};
harden(provide);

/**
* Helper for use cases in which the maker function is async. For two provide
* calls with the same key, one may be making when the other call starts and it
* would make again. (Then there'd be a collision when the second tries to store
* the key.) This prevents that race condition by immediately storing a Promise
* for the maker in an ephemeral store.
*
* Upon termination, the ephemeral store of pending makes will be lost. It's
* possible for termination to happen after the make completes and before it
* reaches durable storage.
*
* @template K
* @template V
* @param {MapStore<K, V>} durableStore
*/
export const makeAtomicProvider = durableStore => {
/** @type {Map<K, Promise<V>>} */
const pending = new Map();

/**
* Call `provideAsync` to get or make the value associated with the key,
* when the maker is asynchronous.
* If there already is one, return that. Otherwise,
* call `makeValue(key)`, remember it as the value for
* that key, and return it.
*
* @param {K} key
* @param {(key: K) => Promise<V>} makeValue
* @param {(key: K, value: V) => Promise<void>} [finishValue]
* @returns {Promise<V>}
*/
const provideAsync = (key, makeValue, finishValue) => {
if (durableStore.has(key)) {
return Promise.resolve(durableStore.get(key));
}
if (!pending.has(key)) {
const valP = makeValue(key)
.then(v => {
durableStore.init(key, v);
return v;
})
.then(v => {
if (finishValue) {
return finishValue(key, v).then(() => v);
}
return v;
})
.finally(() => {
pending.delete(key);
});
pending.set(key, valP);
}
const valP = pending.get(key);
assert(valP);
return valP;
};

return harden({ provideAsync });
};
harden(makeAtomicProvider);
/**
* @template K
* @template V
* @typedef {ReturnType<typeof makeAtomicProvider<K, V>>} AtomicProvider<K, V>
*/
74 changes: 74 additions & 0 deletions packages/store/test/test-AtomicProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// @ts-check
/* eslint-disable no-use-before-define */
// eslint-disable-next-line import/no-extraneous-dependencies
import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js';
import { Far } from '@endo/marshal';
import { setTimeout } from 'timers';

import { makeScalarMapStore } from '../src/stores/scalarMapStore.js';
import { makeAtomicProvider } from '../src/stores/store-utils.js';

import '../src/types.js';

test('race', async t => {
const store = makeScalarMapStore('foo');
const provider = makeAtomicProvider(store);
let i = 0;
const makeValue = k =>
// in Node 15+ use timers/promise
new Promise(resolve => setTimeout(() => resolve(`${k} ${(i += 1)}`), 10));

t.is(await provider.provideAsync('a', makeValue), 'a 1');
t.is(await provider.provideAsync('a', makeValue), 'a 1');

provider.provideAsync('b', makeValue);
provider.provideAsync('b', makeValue);
t.is(await provider.provideAsync('b', makeValue), 'b 2');
t.is(await provider.provideAsync('b', makeValue), 'b 2');
});

test('reject', async t => {
const store = makeScalarMapStore('foo');
const provider = makeAtomicProvider(store);
let i = 0;
const makeValue = k => Promise.reject(Error(`failure ${k} ${(i += 1)}`));

await t.throwsAsync(provider.provideAsync('a', makeValue), {
message: 'failure a 1',
});
await t.throwsAsync(provider.provideAsync('a', makeValue), {
// makeValue runs again (i += 1)
message: 'failure a 2',
});

await t.throwsAsync(provider.provideAsync('b', makeValue), {
message: 'failure b 3',
});

await t.throwsAsync(provider.provideAsync('b', makeValue), {
message: 'failure b 4',
});
});

test('far keys', async t => {
const store = makeScalarMapStore('foo');
const provider = makeAtomicProvider(store);

let i = 0;
const makeBrand = name =>
Far(`brand ${name}`, {
getAllegedName: () => `${name} ${(i += 1)}`,
});

const makeValue = brand => Promise.resolve(brand.getAllegedName());

const moola = makeBrand('moola');
const moolb = makeBrand('moolb');
t.is(await provider.provideAsync(moola, makeValue), 'moola 1');
t.is(await provider.provideAsync(moola, makeValue), 'moola 1');

provider.provideAsync(moolb, makeValue);
provider.provideAsync(moolb, makeValue);
t.is(await provider.provideAsync(moolb, makeValue), 'moolb 2');
t.is(await provider.provideAsync(moolb, makeValue), 'moolb 2');
});
3 changes: 3 additions & 0 deletions packages/vats/decentral-core-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"singleWallet": {
"sourceSpec": "@agoric/wallet/contract/src/singleWallet.js"
},
"walletFactory": {
"sourceSpec": "@agoric/wallet/contract/src/walletFactory.js"
},
"zoe": {
"sourceSpec": "@agoric/vats/src/vat-zoe.js"
}
Expand Down
3 changes: 3 additions & 0 deletions packages/vats/decentral-demo-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
"singleWallet": {
"sourceSpec": "@agoric/wallet/contract/src/singleWallet.js"
},
"walletFactory": {
"sourceSpec": "@agoric/wallet/contract/src/walletFactory.js"
},
"zoe": {
"sourceSpec": "@agoric/vats/src/vat-zoe.js"
}
Expand Down
4 changes: 4 additions & 0 deletions packages/vats/scripts/build-bundles.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const sourceToBundle = [
`@agoric/wallet/contract/src/singleWallet.js`,
`../bundles/bundle-singleWallet.js`,
],
[
`@agoric/wallet/contract/src/walletFactory.js`,
`../bundles/bundle-walletFactory.js`,
],
];

createBundles(sourceToBundle, dirname);
23 changes: 15 additions & 8 deletions packages/vats/src/core/basic-behaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,22 +236,29 @@ export const makeClientBanks = async ({
zoe,
},
installation: {
consume: { singleWallet },
consume: { walletFactory },
},
}) => {
const STORAGE_PATH = 'wallet';

const storageNode = await getChildNode(chainStorage, STORAGE_PATH);
const marshaller = E(board).getPublishingMarshaller();
const { creatorFacet } = await E(zoe).startInstance(
Copy link
Member

Choose a reason for hiding this comment

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

FYI, dropping the admin facet here is not great; but we have plans elsewhere to address it: #4548 (comment)

walletFactory,
{},
{ agoricNames, namesByAddress, board },
{ storageNode, marshaller },
);
return E(client).assignBundle([
address => {
const bank = E(bankManager).getBankForAddress(address);
/** @type {ERef<MyAddressNameAdmin>} */
const myAddressNameAdmin = E(namesByAddressAdmin).lookupAdmin(address);
const smartWallet = E(zoe).startInstance(
singleWallet,
{},
{ agoricNames, bank, namesByAddress, myAddressNameAdmin, board },
{ storageNode, marshaller },

const smartWallet = E(creatorFacet).provideSmartWallet(
address,
bank,
myAddressNameAdmin,
);

// sets these values in REPL home by way of registerWallet
Expand All @@ -267,13 +274,13 @@ export const installBootContracts = async ({
devices: { vatAdmin },
consume: { zoe },
installation: {
produce: { centralSupply, mintHolder, singleWallet },
produce: { centralSupply, mintHolder, walletFactory },
},
}) => {
for (const [name, producer] of Object.entries({
centralSupply,
mintHolder,
singleWallet,
walletFactory,
})) {
const bundleCap = D(vatAdmin).getNamedBundleCap(name);
const bundle = D(bundleCap).getBundle();
Expand Down
4 changes: 2 additions & 2 deletions packages/vats/src/core/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const SHARED_CHAIN_BOOTSTRAP_MANIFEST = harden({
chainStorage: true,
zoe: 'zoe',
},
installation: { consume: { singleWallet: 'zoe' } },
installation: { consume: { walletFactory: 'zoe' } },
home: { produce: { bank: 'bank' } },
},
installBootContracts: {
Expand All @@ -124,7 +124,7 @@ const SHARED_CHAIN_BOOTSTRAP_MANIFEST = harden({
produce: {
centralSupply: 'zoe',
mintHolder: 'zoe',
singleWallet: 'zoe',
walletFactory: 'zoe',
},
},
},
Expand Down
3 changes: 2 additions & 1 deletion packages/vats/src/core/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
* issuer: |
* 'RUN' | 'BLD' | 'Attestation' | 'AUSD',
* installation: |
* 'centralSupply' | 'mintHolder' | 'singleWallet' |
* 'centralSupply' | 'mintHolder' | 'singleWallet' | 'walletFactory' |
* 'feeDistributor' |
* 'contractGovernor' | 'committee' | 'noActionElectorate' | 'binaryVoteCounter' |
* 'amm' | 'VaultFactory' | 'liquidate' | 'runStake' |
Expand Down Expand Up @@ -171,6 +171,7 @@
* produce: Record<WellKnownName['installation'], Producer<Installation>>,
* consume: Record<WellKnownName['installation'], Promise<Installation<unknown>>> & {
* singleWallet: Promise<Installation<import('@agoric/smart-wallet/src/singleWallet.js').start>>,
* walletFactory: Promise<Installation<import('@agoric/smart-wallet/src/walletFactory.js').start>>,
* },
* },
* instance:{
Expand Down
1 change: 1 addition & 0 deletions packages/vats/src/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const agoricNamesReserved = harden({
centralSupply: 'central supply',
mintHolder: 'mint holder',
singleWallet: 'single smart wallet',
walletFactory: 'multitenant smart wallet',
contractGovernor: 'contract governor',
committee: 'committee electorate',
noActionElectorate: 'no action electorate',
Expand Down
3 changes: 3 additions & 0 deletions packages/vats/test/devices.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import bundleCentralSupply from '../bundles/bundle-centralSupply.js';
import bundleMintHolder from '../bundles/bundle-mintHolder.js';
import bundleSingleWallet from '../bundles/bundle-singleWallet.js';
import bundleWalletFactory from '../bundles/bundle-walletFactory.js';

export const devices = {
vatAdmin: {
Expand All @@ -13,6 +14,8 @@ export const devices = {
return bundleMintHolder;
case 'singleWallet':
return bundleSingleWallet;
case 'walletFactory':
return bundleWalletFactory;
default:
throw new Error(`unknown bundle ${name}`);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/wallet/api/src/lib-wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const cmp = (a, b) => {
return -1;
};

// TODO rename to makeWalletKit
/**
* @typedef {object} MakeWalletParams
* @property {ERef<ZoeService>} zoe
Expand All @@ -75,6 +76,8 @@ export function makeWallet({
inboxStateChangeHandler = noActionStateChangeHandler,
dateNow = undefined,
}) {
assert(myAddressNameAdmin, 'missing myAddressNameAdmin');

let lastId = 0;

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet/api/src/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export function buildRootObject(vatPowers) {
localTimerService,
localTimerPollInterval,
}) {
assert(myAddressNameAdmin, 'missing myAddressNameAdmin');

/** @type {ERef<() => number> | undefined} */
let dateNowP;
if (timerDevice) {
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet/contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"dependencies": {
"@agoric/wallet-backend": "0.12.1",
"@agoric/deploy-script-support": "^0.9.0",
"@agoric/store": "^0.7.2",
"@agoric/vat-data": "^0.3.1",
"@agoric/zoe": "^0.24.0",
"@agoric/notifier": "^0.4.0",
"@endo/far": "^0.2.5"
Expand Down
Loading