diff --git a/.github/workflows/test-all-packages.yml b/.github/workflows/test-all-packages.yml index c09879dfea05..f8441bde93a4 100644 --- a/.github/workflows/test-all-packages.yml +++ b/.github/workflows/test-all-packages.yml @@ -93,6 +93,8 @@ jobs: run: cd packages/assert && yarn ${{ steps.vars.outputs.test }} - name: yarn test (wallet/api) run: cd packages/wallet/api && yarn ${{ steps.vars.outputs.test }} + - name: yarn test (wallet/contract) + run: cd packages/wallet/contract && yarn ${{ steps.vars.outputs.test }} - name: yarn test (deployment) run: cd packages/deployment && yarn ${{ steps.vars.outputs.test }} - name: yarn test (ERTP) diff --git a/package.json b/package.json index e6b7bb186abc..09f80c7fb50b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "golang/cosmos", "packages/*", "packages/wallet/api", + "packages/wallet/contract", "packages/wallet/ui" ], "type": "module", diff --git a/packages/agoric-cli/src/sdk-package-names.js b/packages/agoric-cli/src/sdk-package-names.js index e4aaac411189..b271bf1c0a69 100644 --- a/packages/agoric-cli/src/sdk-package-names.js +++ b/packages/agoric-cli/src/sdk-package-names.js @@ -19,6 +19,7 @@ export default [ "@agoric/run-protocol", "@agoric/same-structure", "@agoric/sharing-service", + "@agoric/smart-wallet", "@agoric/solo", "@agoric/sparse-ints", "@agoric/spawner", diff --git a/packages/casting/src/follower-cosmjs.js b/packages/casting/src/follower-cosmjs.js index 6bca5494eaa1..0a95a278f6e7 100644 --- a/packages/casting/src/follower-cosmjs.js +++ b/packages/casting/src/follower-cosmjs.js @@ -317,6 +317,10 @@ export const makeCosmjsFollower = ( const getProvenValue = () => getProvenValueAtHeight(allegedBlockHeight); const buf = await queryVerifier(getProvenValue, crash, getAllegedValue); + if (buf.length === 0) { + fail(Error('No query results')); + return; + } attempt = 0; if (!committer.isValid()) { return; diff --git a/packages/vats/decentral-core-config.json b/packages/vats/decentral-core-config.json index 963112eff013..679270179d24 100644 --- a/packages/vats/decentral-core-config.json +++ b/packages/vats/decentral-core-config.json @@ -42,12 +42,12 @@ "sharing": { "sourceSpec": "@agoric/vats/src/vat-sharing.js" }, - "walletManager": { - "sourceSpec": "@agoric/vats/src/vat-walletManager.js" + "singleWallet": { + "sourceSpec": "@agoric/wallet/contract/src/singleWallet.js" }, "zoe": { "sourceSpec": "@agoric/vats/src/vat-zoe.js" } }, "defaultManagerType": "xs-worker" -} +} \ No newline at end of file diff --git a/packages/vats/decentral-demo-config.json b/packages/vats/decentral-demo-config.json index 7494dcaffca9..114646988467 100644 --- a/packages/vats/decentral-demo-config.json +++ b/packages/vats/decentral-demo-config.json @@ -49,12 +49,12 @@ "sharing": { "sourceSpec": "@agoric/vats/src/vat-sharing.js" }, - "walletManager": { - "sourceSpec": "@agoric/vats/src/vat-walletManager.js" + "singleWallet": { + "sourceSpec": "@agoric/wallet/contract/src/singleWallet.js" }, "zoe": { "sourceSpec": "@agoric/vats/src/vat-zoe.js" } }, "defaultManagerType": "xs-worker" -} +} \ No newline at end of file diff --git a/packages/vats/scripts/build-bundles.js b/packages/vats/scripts/build-bundles.js index 5c9f1d982fa5..071508733894 100755 --- a/packages/vats/scripts/build-bundles.js +++ b/packages/vats/scripts/build-bundles.js @@ -8,6 +8,10 @@ const dirname = url.fileURLToPath(new URL('.', import.meta.url)); const sourceToBundle = [ [`../src/centralSupply.js`, `../bundles/bundle-centralSupply.js`], [`../src/mintHolder.js`, `../bundles/bundle-mintHolder.js`], + [ + `@agoric/wallet/contract/src/singleWallet.js`, + `../bundles/bundle-singleWallet.js`, + ], ]; createBundles(sourceToBundle, dirname); diff --git a/packages/vats/src/core/basic-behaviors.js b/packages/vats/src/core/basic-behaviors.js index 2473334ed843..fd351f18a9c2 100644 --- a/packages/vats/src/core/basic-behaviors.js +++ b/packages/vats/src/core/basic-behaviors.js @@ -224,9 +224,39 @@ export const makeAddressNameHubs = async ({ harden(makeAddressNameHubs); /** @param {BootstrapSpace} powers */ -export const makeClientBanks = async ({ consume: { client, bankManager } }) => { +export const makeClientBanks = async ({ + consume: { + agoricNames, + board, + namesByAddress, + namesByAddressAdmin, + client, + chainStorage, + bankManager, + zoe, + }, + installation: { + consume: { singleWallet }, + }, +}) => { + const STORAGE_PATH = 'wallet'; + + const storageNode = await getChildNode(chainStorage, STORAGE_PATH); + const marshaller = E(board).getPublishingMarshaller(); return E(client).assignBundle([ - address => ({ bank: E(bankManager).getBankForAddress(address) }), + address => { + const bank = E(bankManager).getBankForAddress(address); + const myAddressNameAdmin = E(namesByAddressAdmin).lookupAdmin(address); + const smartWallet = E(zoe).startInstance( + singleWallet, + {}, + { agoricNames, bank, namesByAddress, myAddressNameAdmin, board }, + { storageNode, marshaller }, + ); + + // sets these values in REPL home by way of registerWallet + return { bank, smartWallet }; + }, ]); }; harden(makeClientBanks); @@ -237,12 +267,13 @@ export const installBootContracts = async ({ devices: { vatAdmin }, consume: { zoe }, installation: { - produce: { centralSupply, mintHolder }, + produce: { centralSupply, mintHolder, singleWallet }, }, }) => { for (const [name, producer] of Object.entries({ centralSupply, mintHolder, + singleWallet, })) { const bundleCap = D(vatAdmin).getNamedBundleCap(name); const bundle = D(bundleCap).getBundle(); diff --git a/packages/vats/src/core/manifest.js b/packages/vats/src/core/manifest.js index ef4cc9717a79..bb2ded15e193 100644 --- a/packages/vats/src/core/manifest.js +++ b/packages/vats/src/core/manifest.js @@ -104,9 +104,16 @@ const SHARED_CHAIN_BOOTSTRAP_MANIFEST = harden({ }, makeClientBanks: { consume: { + agoricNames: true, + namesByAddress: true, + namesByAddressAdmin: true, bankManager: 'bank', + board: 'board', client: true, + chainStorage: true, + zoe: 'zoe', }, + installation: { consume: { singleWallet: 'zoe' } }, home: { produce: { bank: 'bank' } }, }, installBootContracts: { @@ -117,6 +124,7 @@ const SHARED_CHAIN_BOOTSTRAP_MANIFEST = harden({ produce: { centralSupply: 'zoe', mintHolder: 'zoe', + singleWallet: 'zoe', }, }, }, diff --git a/packages/vats/src/core/types.js b/packages/vats/src/core/types.js index 16a02cdb3d20..c56045291e64 100644 --- a/packages/vats/src/core/types.js +++ b/packages/vats/src/core/types.js @@ -136,7 +136,8 @@ * issuer: | * 'RUN' | 'BLD' | 'Attestation' | 'AUSD', * installation: | - * 'centralSupply' | 'mintHolder' | 'feeDistributor' | + * 'centralSupply' | 'mintHolder' | 'singleWallet' | + * 'feeDistributor' | * 'contractGovernor' | 'committee' | 'noActionElectorate' | 'binaryVoteCounter' | * 'amm' | 'VaultFactory' | 'liquidate' | 'runStake' | * 'Pegasus' | 'reserve' | 'psm' | 'interchainPool', diff --git a/packages/vats/src/core/utils.js b/packages/vats/src/core/utils.js index c3faa0c964db..cd4285b8ae77 100644 --- a/packages/vats/src/core/utils.js +++ b/packages/vats/src/core/utils.js @@ -35,6 +35,7 @@ export const agoricNamesReserved = harden({ installation: { centralSupply: 'central supply', mintHolder: 'mint holder', + singleWallet: 'single smart wallet', contractGovernor: 'contract governor', committee: 'committee electorate', noActionElectorate: 'no action electorate', diff --git a/packages/vats/src/vat-walletManager.js b/packages/vats/src/vat-walletManager.js index e5645a9f707f..1d80496b8cb3 100644 --- a/packages/vats/src/vat-walletManager.js +++ b/packages/vats/src/vat-walletManager.js @@ -1,7 +1,10 @@ import { E, Far } from '@endo/far'; import walletBundle from '@agoric/wallet-backend/bundles/bundle-wallet.js'; -export const buildRootObject = _vatPowers => { +/** + * @deprecated + */ +export const buildRootObject = () => { return Far('walletManager root', { buildWalletManager: vatAdminSvc => Far('walletManager', { diff --git a/packages/vats/test/devices.js b/packages/vats/test/devices.js index 347cc4029205..dcaf0c074012 100644 --- a/packages/vats/test/devices.js +++ b/packages/vats/test/devices.js @@ -1,16 +1,21 @@ import bundleCentralSupply from '../bundles/bundle-centralSupply.js'; import bundleMintHolder from '../bundles/bundle-mintHolder.js'; +import bundleSingleWallet from '../bundles/bundle-singleWallet.js'; export const devices = { vatAdmin: { getNamedBundleCap: name => ({ getBundle: () => { - if (name === 'centralSupply') { - return bundleCentralSupply; - } else if (name === 'mintHolder') { - return bundleMintHolder; + switch (name) { + case 'centralSupply': + return bundleCentralSupply; + case 'mintHolder': + return bundleMintHolder; + case 'singleWallet': + return bundleSingleWallet; + default: + throw new Error(`unknown bundle ${name}`); } - throw new Error(`unknown bundle ${name}`); }, }), }, diff --git a/packages/vats/test/test-clientBundle.js b/packages/vats/test/test-clientBundle.js index 58480555acf4..831baf375cf6 100644 --- a/packages/vats/test/test-clientBundle.js +++ b/packages/vats/test/test-clientBundle.js @@ -14,9 +14,12 @@ import { } from '@agoric/run-protocol/src/proposals/demoIssuers.js'; import { makeClientManager } from '../src/core/chain-behaviors.js'; import { makeAgoricNamesAccess, makePromiseSpace } from '../src/core/utils.js'; -import { buildRootObject as bldMintRoot } from '../src/vat-mints.js'; +import { buildRootObject as mintsRoot } from '../src/vat-mints.js'; +import { buildRootObject as boardRoot } from '../src/vat-board.js'; import { installBootContracts, + makeAddressNameHubs, + makeBoard, makeClientBanks, } from '../src/core/basic-behaviors.js'; @@ -37,12 +40,16 @@ const setUpZoeForTest = async () => { }; harden(setUpZoeForTest); +/** + * @typedef {{ + * (n: 'board'): import('../src/core/basic-behaviors.js').BoardVat + * (n: 'mint'): MintsVat + * }} LoadVat + */ test('connectFaucet produces payments', async t => { const space = /** @type {any} */ (makePromiseSpace(t.log)); const { consume, produce } = - /** @type { BootstrapPowers & { consume: { loadVat: (n: 'mints') => MintsVat }} } */ ( - space - ); + /** @type { BootstrapPowers & { consume: { loadVat: LoadVat }} } */ (space); const { agoricNames, spaces } = makeAgoricNamesAccess(); produce.agoricNames.resolve(agoricNames); @@ -51,14 +58,22 @@ test('connectFaucet produces payments', async t => { produce.feeMintAccess.resolve(feeMintAccess); produce.loadVat.resolve(name => { - assert.equal(name, 'mints'); - return bldMintRoot(); + switch (name) { + case 'mints': + return mintsRoot(); + case 'board': + return boardRoot(); + default: + throw Error('unknown loadVat name'); + } }); t.plan(4); // be sure bank.deposit() gets called const bldKit = makeIssuerKit('BLD'); produce.bldIssuerKit.resolve(bldKit); + produce.chainStorage.resolve(undefined); + const runIssuer = E(zoe).getFeeIssuer(); produce.bankManager.resolve( Promise.resolve( @@ -76,6 +91,8 @@ test('connectFaucet produces payments', async t => { return amt; }, }), + // @ts-expect-error mock + getAssetSubscription: () => null, }), }), ), @@ -98,6 +115,9 @@ test('connectFaucet produces payments', async t => { }; await Promise.all([ + // @ts-expect-error missing keys: devices, vats, vatPowers, vatParameters, and 2 more. + makeBoard({ consume, produce, ...spaces }), + makeAddressNameHubs({ consume, produce, ...spaces }), installBootContracts({ vatPowers, devices, consume, produce, ...spaces }), makeClientManager({ consume, produce, ...spaces }), connectFaucet({ consume, produce, ...spaces }), diff --git a/packages/wallet/contract/README.md b/packages/wallet/contract/README.md new file mode 100644 index 000000000000..23ae74a292c0 --- /dev/null +++ b/packages/wallet/contract/README.md @@ -0,0 +1,31 @@ +# Smart Wallet contracts + +## Single contract + +The `singleWalet` contract manages a single smart wallet. + +# Multi-tenant contract + +The `walletFactory` contract provisions and manages smart wallets. + +# Common + +There can be zero or one wallets per Cosmos address. + +lib-wallet has makeWallet but that's really makeWalletKit + +1. Generate an address (off-chain) +2. Provision an account using that address, which causes a Bank to get created + ??? What happens if you try to provision again using the same address? It's a Cosmos level transaction; maybe that fails. +3. Create a Wallet using the Bank (it includes the implementation of Virtual Purses so when you getAmount it goes down to the Golang layer) + ??? What happens if you try to create another wallet using that bank? + +1 Address : 0/1 Bank +1 Address : 1 `myAddressNamesAdmin` +1 Bank : 0/1 Wallet + +By design there's a 1:1 across all four. + +`namesByAddress` and `board` are shared by everybody. + +`myAddressNamesAdmin` is from the account you provision. diff --git a/packages/wallet/contract/jsconfig.json b/packages/wallet/contract/jsconfig.json new file mode 100644 index 000000000000..0f3b5a4bfc9b --- /dev/null +++ b/packages/wallet/contract/jsconfig.json @@ -0,0 +1,25 @@ +// This file can contain .js-specific Typescript compiler config. +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + + "noEmit": true, +/* + // The following flags are for creating .d.ts files: + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, +*/ + "downlevelIteration": true, + "strictNullChecks": true, + "moduleResolution": "node", + }, + "include": [ + "*.js", + "scripts/**/*.js", + "src/**/*.js", + "test/**/*.js", + "tools/**/*.js", + ], +} diff --git a/packages/wallet/contract/package.json b/packages/wallet/contract/package.json new file mode 100644 index 000000000000..a1bf48905032 --- /dev/null +++ b/packages/wallet/contract/package.json @@ -0,0 +1,49 @@ +{ + "name": "@agoric/smart-wallet", + "version": "0.1.1", + "description": "Wallet contract", + "type": "module", + "scripts": { + "build": "exit 0", + "test": "ava", + "test:xs": "exit 0", + "lint": "run-s --continue-on-error lint:*", + "lint-fix": "yarn lint:eslint --fix", + "lint:types": "tsc --maxNodeModuleJsDepth 5 -p jsconfig.json", + "lint:eslint": "eslint ." + }, + "devDependencies": { + "ava": "^3.12.1", + "@agoric/ertp": "^0.14.2", + "@agoric/run-protocol": "^0.11.0", + "@agoric/vats": "^0.10.0", + "@endo/captp": "^2.0.9" + }, + "dependencies": { + "@agoric/wallet-backend": "0.12.1", + "@agoric/deploy-script-support": "^0.9.0", + "@agoric/zoe": "^0.24.0", + "@agoric/notifier": "^0.4.0", + "@endo/far": "^0.2.5" + }, + "keywords": [], + "repository": { + "type": "git", + "url": "git+https://github.com/Agoric/agoric" + }, + "author": "Agoric", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/Agoric/agoric/issues" + }, + "homepage": "https://github.com/Agoric/agoric#readme", + "ava": { + "files": [ + "test/**/test-*.js" + ], + "timeout": "2m" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/wallet/contract/src/singleWallet.js b/packages/wallet/contract/src/singleWallet.js new file mode 100644 index 000000000000..bb63d13c1883 --- /dev/null +++ b/packages/wallet/contract/src/singleWallet.js @@ -0,0 +1,90 @@ +// @ts-check +import { E, Far } from '@endo/far'; +import { + makeStoredSubscription, + makeSubscriptionKit, + observeIteration, +} from '@agoric/notifier'; +import '@agoric/deploy-script-support/exported.js'; +import '@agoric/zoe/exported.js'; +import spawn from '@agoric/wallet-backend/src/wallet.js'; + +import '@agoric/wallet-backend/src/types.js'; // TODO avoid ambient types + +const { assign, entries, keys, fromEntries } = Object; + +/** + * @typedef {{ + * agoricNames: NameHub, + * bank: import('@agoric/vats/src/vat-bank').Bank, + * board: Board, + * myAddressNameAdmin: MyAddressNameAdmin, + * namesByAddress: NameHub, + * }} SmartWalletContractTerms + */ + +/** + * @param {ZCF} zcf + * @param {{ + * storageNode?: ERef, + * marshaller?: ERef, + * }} privateArgs + */ +export const start = async (zcf, privateArgs) => { + const { agoricNames, bank, namesByAddress, myAddressNameAdmin, board } = + zcf.getTerms(); + assert(board, 'missing board'); + assert(myAddressNameAdmin, 'missing myAddressNameAdmin'); + assert(namesByAddress, 'missing namesByAddress'); + assert(agoricNames, 'missing agoricNames'); + const zoe = zcf.getZoeService(); + + const walletVat = spawn({ + agoricNames, + namesByAddress, + myAddressNameAdmin, + zoe, + board, + localTimerService: undefined, + }); + + const wallet = await E(walletVat).getWallet(bank); + const address = await E(myAddressNameAdmin).getMyAddress(); + const { storageNode, marshaller } = privateArgs; + + const myWalletStorageNode = + storageNode && E(storageNode).getChildNode(address); + const admin = E(wallet).getAdminFacet(); + + /** @type {Record>>} */ + const notifierParts = { + contacts: E(admin).getContactsNotifier(), + dapps: E(admin).getDappsNotifier(), + issuers: E(admin).getIssuersNotifier(), + offers: E(admin).getOffersNotifier(), + payments: E(admin).getPaymentsNotifier(), + purses: E(admin).getPursesNotifier(), + }; + const mutableState = fromEntries(keys(notifierParts).map(key => [key, []])); + const { subscription, publication } = makeSubscriptionKit(); + publication.updateState({ ...mutableState }); + + entries(notifierParts).forEach(([key, notifier]) => { + void observeIteration(notifier, { + updateState: value => + publication.updateState({ ...assign(mutableState, { [key]: value }) }), + }); + }); + + const storedSubscription = makeStoredSubscription( + subscription, + myWalletStorageNode, + marshaller, + ); + return { + creatorFacet: Far('SingleWallet', { + ...wallet, + getSubscription: () => storedSubscription, + }), + }; +}; diff --git a/packages/wallet/contract/test/devices.js b/packages/wallet/contract/test/devices.js new file mode 100644 index 000000000000..dde3c23906ae --- /dev/null +++ b/packages/wallet/contract/test/devices.js @@ -0,0 +1,22 @@ +import bundleCentralSupply from '@agoric/vats/bundles/bundle-centralSupply.js'; +import bundleMintHolder from '@agoric/vats/bundles/bundle-mintHolder.js'; +import bundleSingleWallet from '@agoric/vats/bundles/bundle-singleWallet.js'; + +export const devices = { + vatAdmin: { + getNamedBundleCap: name => ({ + getBundle: () => { + switch (name) { + case 'centralSupply': + return bundleCentralSupply; + case 'mintHolder': + return bundleMintHolder; + case 'singleWallet': + return bundleSingleWallet; + default: + throw new Error(`unknown bundle ${name}`); + } + }, + }), + }, +}; diff --git a/packages/wallet/contract/test/test-singleWallet.js b/packages/wallet/contract/test/test-singleWallet.js new file mode 100644 index 000000000000..c72d268e8035 --- /dev/null +++ b/packages/wallet/contract/test/test-singleWallet.js @@ -0,0 +1,198 @@ +// @ts-nocheck FIXME + +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import '@agoric/vats/src/core/types.js'; +import '@agoric/zoe/exported.js'; + +import { makeIssuerKit } from '@agoric/ertp'; +import { connectFaucet } from '@agoric/run-protocol/src/proposals/demoIssuers.js'; +import { unsafeMakeBundleCache } from '@agoric/run-protocol/test/bundleTool.js'; +import { subscriptionKey } from '@agoric/run-protocol/test/supports.js'; +import { + installBootContracts, + makeAddressNameHubs, + makeBoard, +} from '@agoric/vats/src/core/basic-behaviors.js'; +import { makeClientManager } from '@agoric/vats/src/core/chain-behaviors.js'; +import { + makeAgoricNamesAccess, + makePromiseSpace, +} from '@agoric/vats/src/core/utils.js'; +import { + getChildNode, + makeChainStorageRoot, +} from '@agoric/vats/src/lib-chainStorage.js'; +import { makeNameHubKit } from '@agoric/vats/src/nameHub.js'; +import { buildRootObject as boardRoot } from '@agoric/vats/src/vat-board.js'; +import { buildRootObject as mintsRoot } from '@agoric/vats/src/vat-mints.js'; +import { makeZoeKit } from '@agoric/zoe'; +import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; +import { makeLoopback } from '@endo/captp'; +import { E, Far } from '@endo/far'; +import path from 'path'; +import { devices } from './devices.js'; + +/** @type {import('ava').TestInterface>>} */ +// @ts-expect-error cast +const test = anyTest; + +const setUpZoeForTest = async () => { + const { makeFar } = makeLoopback('zoeTest'); + const { zoeService, feeMintAccess: nonFarFeeMintAccess } = makeZoeKit( + makeFakeVatAdmin(() => {}).admin, + ); + /** @type {import('@endo/far').ERef} */ + const zoe = makeFar(zoeService); + const feeMintAccess = await makeFar(nonFarFeeMintAccess); + return { + zoe, + feeMintAccess, + }; +}; +harden(setUpZoeForTest); + +export const mockChainStorageRoot = () => { + const toStorage = v => v; + return makeChainStorageRoot(toStorage, 'swingset', 'mockChainStorageRoot'); +}; + +const makeTestContext = async t => { + const space = /** @type {any} */ (makePromiseSpace(t.log)); + const { consume, produce } = + /** @type { BootstrapPowers & { consume: { loadVat: (n: 'mints') => MintsVat }} } */ ( + space + ); + const { agoricNames, spaces } = makeAgoricNamesAccess(); + produce.agoricNames.resolve(agoricNames); + + const { zoe, feeMintAccess } = await setUpZoeForTest(); + produce.zoe.resolve(zoe); + produce.feeMintAccess.resolve(feeMintAccess); + + produce.loadVat.resolve(name => { + switch (name) { + case 'mints': + return mintsRoot(); + case 'board': + return boardRoot(); + default: + throw Error('unknown loadVat name'); + } + }); + + const bldKit = makeIssuerKit('BLD'); + produce.bldIssuerKit.resolve(bldKit); + produce.chainStorage.resolve(mockChainStorageRoot()); + + produce.bankManager.resolve( + Promise.resolve( + Far('mockBankManager', { + getBankForAddress: _a => + Far('mockBank', { + getPurse: () => ({ + deposit: async (_, _x) => { + return null; + }, + }), + getAssetSubscription: () => null, + }), + }), + ), + ); + + const vatPowers = { + D: x => x, + }; + + await Promise.all([ + // @ts-expect-error + makeBoard({ consume, produce, ...spaces }), + makeAddressNameHubs({ consume, produce, ...spaces }), + installBootContracts({ vatPowers, devices, consume, produce, ...spaces }), + makeClientManager({ consume, produce, ...spaces }), + connectFaucet({ consume, produce, ...spaces }), + ]); + + // Adapted from perAddress in makeAddressNameHubs() + const reserveAddress = address => { + // Create a name hub for this address. + const { nameHub: myAddressNameHub, nameAdmin: rawMyAddressNameAdmin } = + makeNameHubKit(); + + /** @type {MyAddressNameAdmin} */ + const myAddressNameAdmin = Far('myAddressNameAdmin', { + ...rawMyAddressNameAdmin, + getMyAddress: () => address, + }); + // reserve space for deposit facet + myAddressNameAdmin.reserve('depositFacet'); + // Register it with the namesByAddress hub. + return E(consume.namesByAddressAdmin).update( + address, + myAddressNameHub, + myAddressNameAdmin, + ); + }; + + const address = 'bogusAddress'; + await reserveAddress(address); + + // #region Installs + const pathname = new URL(import.meta.url).pathname; + const dirname = path.dirname(pathname); + + const bundleCache = await unsafeMakeBundleCache('bundles/'); + const bundle = await bundleCache.load( + `${dirname}/../src/singleWallet.js`, + 'singleWallet', + ); + /** @type {Installation} */ + const installation = E(zoe).install(bundle); + // #endregion + + // copied from makeClientBanks() + const bank = E(consume.bankManager).getBankForAddress(address); + const myAddressNameAdmin = E(consume.namesByAddressAdmin).lookupAdmin( + address, + ); + + // copied from makeClientBanks() + const storageNode = await getChildNode(consume.chainStorage, 'wallet'); + const marshaller = E(consume.board).getPublishingMarshaller(); + + const singleWallet = E(zoe).startInstance( + installation, + {}, + { + agoricNames, + bank, + namesByAddress: consume.namesByAddress, + myAddressNameAdmin, + board: consume.board, + }, + { storageNode, marshaller }, + ); + + return { + singleWallet, + zoe, + }; +}; + +test.before(async t => { + t.context = await makeTestContext(t); +}); + +test('basic', async t => { + const { singleWallet, zoe } = t.context; + const { creatorFacet } = await singleWallet; + + const bridge = await E(creatorFacet).getBridge(); + t.is(await E(bridge).getZoe(), await zoe); + + t.is( + await subscriptionKey(E(creatorFacet).getSubscription()), + 'mockChainStorageRoot.wallet.bogusAddress', + ); +});