Skip to content

Commit

Permalink
Merge pull request #5721 from Agoric/4484-multitenant-smart-wallet
Browse files Browse the repository at this point in the history
4484 multitenant smart wallet
  • Loading branch information
mergify[bot] authored Jul 7, 2022
2 parents 21eb21d + 57eac62 commit 9d42a8e
Show file tree
Hide file tree
Showing 17 changed files with 577 additions and 11 deletions.
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(
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

0 comments on commit 9d42a8e

Please sign in to comment.