diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 7a8e230a89b..d82125b21db 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -27,7 +27,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - cli: [link-cli, local-npm] + # FIXME: Use this to enable NPM deploys... + # cli: [link-cli, local-npm] + cli: [link-cli] steps: - uses: actions/checkout@v2 diff --git a/packages/casting/src/change-follower.js b/packages/casting/src/change-follower.js index 79b2aca12f1..f8e4c1fd88d 100644 --- a/packages/casting/src/change-follower.js +++ b/packages/casting/src/change-follower.js @@ -47,5 +47,8 @@ export const makePollingChangeFollower = async leader => { return Far('polling change follower', { getLatestIterable: async () => iterable, getEachIterable: async () => iterable, + getReverseIterable: async () => { + throw Error('not implemented for polling change follower'); + }, }); }; diff --git a/packages/casting/src/follower-cosmjs.js b/packages/casting/src/follower-cosmjs.js index aab92789470..f0fd394641b 100644 --- a/packages/casting/src/follower-cosmjs.js +++ b/packages/casting/src/follower-cosmjs.js @@ -368,6 +368,21 @@ export const makeCosmjsFollower = ( } } + /** + * @param {StreamCell} streamCell + * @param {number} currentBlockHeight + * @yields {FollowerElement} + */ + function* reverseValuesFromCell(streamCell, currentBlockHeight) { + for (let i = streamCell.values.length - 1; i >= 0; i -= 1) { + yield followerElementFromStreamCellValue( + streamCell.values[i], + streamCell.blockHeight, + currentBlockHeight, + ); + } + } + /** * @param {StreamCell} streamCell * @param {number} currentBlockHeight @@ -444,7 +459,6 @@ export const makeCosmjsFollower = ( // If the block has no corresponding data, wait for the first block to // contain data. for (;;) { - cursorBlockHeight = await getBlockHeight(); cursorData = await getDataAtHeight(cursorBlockHeight); if (cursorData.length !== 0) { const cursorStreamCell = streamCellForData( @@ -457,6 +471,7 @@ export const makeCosmjsFollower = ( // TODO Long-poll for next block // https://github.com/Agoric/agoric-sdk/issues/6154 await E(leader).jitter(where); + cursorBlockHeight = await getBlockHeight(); } // For each subsequent iteration, yield every value that has been @@ -551,7 +566,27 @@ export const makeCosmjsFollower = ( } } - // Enable the periodic fetch. + /** + * @param {number} cursorBlockHeight + * @yields {FollowerElement} + */ + async function* getReverseIterableAtHeight(cursorBlockHeight) { + // Track the data for the last emitted cell (the cell at the + // cursorBlockHeight) so we know not to emit duplicates + // of that cell. + let cursorData; + while (cursorBlockHeight > 0) { + cursorData = await getDataAtHeight(cursorBlockHeight); + if (cursorData.length === 0) { + // No data at the cursor height, so signal beginning of stream. + return; + } + const cursorStreamCell = streamCellForData(cursorBlockHeight, cursorData); + yield* reverseValuesFromCell(cursorStreamCell, cursorBlockHeight); + cursorBlockHeight = cursorStreamCell.blockHeight - 1; + } + } + /** @type {Follower>} */ return Far('chain follower', { async getLatestIterable() { @@ -563,5 +598,11 @@ export const makeCosmjsFollower = ( } return getEachIterableAtHeight(height); }, + async getReverseIterable({ height = undefined } = {}) { + if (height === undefined) { + height = await getBlockHeight(); + } + return getReverseIterableAtHeight(height); + }, }); }; diff --git a/packages/casting/src/follower.js b/packages/casting/src/follower.js index 606902dfd91..8c4d227a85c 100644 --- a/packages/casting/src/follower.js +++ b/packages/casting/src/follower.js @@ -41,6 +41,12 @@ const makeSubscriptionFollower = spec => { } return mapAsyncIterable(ai, transform); }, + + getReverseIterable: async () => { + throw Error( + 'reverse iteration not implemented for subscription follower', + ); + }, }); return follower; }; diff --git a/packages/casting/src/iterable.js b/packages/casting/src/iterable.js index 271eaf452c5..69228262e30 100644 --- a/packages/casting/src/iterable.js +++ b/packages/casting/src/iterable.js @@ -98,3 +98,21 @@ export const iterateEach = (follower, options) => }); }, }); + +/** + * @template T + * @param {ERef>} follower + * @param {import('./types.js').IterateEachOptions} [options] + */ +export const iterateReverse = (follower, options) => + // For now, just pass through the iterable. + Far('iterateReverse iterable', { + /** @returns {AsyncIterator} */ + [Symbol.asyncIterator]: () => { + const eachIterable = E(follower).getReverseIterable(options); + const iterator = E(eachIterable)[Symbol.asyncIterator](); + return Far('iterateEach iterator', { + next: () => E(iterator).next(), + }); + }, + }); diff --git a/packages/casting/src/types.js b/packages/casting/src/types.js index 8179c3828c4..be3746178f9 100644 --- a/packages/casting/src/types.js +++ b/packages/casting/src/types.js @@ -36,6 +36,7 @@ export {}; * @typedef {object} Follower * @property {() => Promise>} getLatestIterable * @property {(options?: IterateEachOptions) => Promise>} getEachIterable + * @property {(options?: IterateEachOptions) => Promise>} getReverseIterable */ /** diff --git a/packages/cosmic-swingset/Makefile b/packages/cosmic-swingset/Makefile index 016b3827c06..8412cc68417 100644 --- a/packages/cosmic-swingset/Makefile +++ b/packages/cosmic-swingset/Makefile @@ -122,6 +122,7 @@ scenario2-setup-nobuild: $(AGCH) --home=t1/bootstrap keys add bootstrap --keyring-backend=test $(AGCH) --home=t1/bootstrap keys show -a bootstrap --keyring-backend=test > t1/bootstrap-address $(AGCH) --home=t1/n0 add-genesis-account `cat t1/bootstrap-address` $(BOOT_COINS) + # Create # Create the (singleton) chain node. $(AGCH) --home=t1/n0 --keyring-dir=t1/bootstrap gentx --keyring-backend=test bootstrap 73000000ubld --chain-id=$(CHAIN_ID) $(AGCH) --home=t1/n0 collect-gentxs @@ -211,7 +212,11 @@ t1-provision-one-with-powers: wait-for-cosmos $(AGCH) --home=t1/bootstrap tx swingset provision-one --keyring-backend=test --from=bootstrap \ --gas=auto --gas-adjustment=$(GAS_ADJUSTMENT) --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) \ t1/$(BASE_PORT) $$addr $(AGORIC_POWERS) -ojson | tee /dev/stderr | grep -q '"code":0'; } - + +# Send some USDC to the vbank/provision module account where it can be traded for IST. +fund-provision-pool: wait-for-cosmos + $(MAKE) ACCT_ADDR=agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346 SOLO_COINS=1234000000ibc/usdc1234 fund-acct + fund-acct: wait-for-cosmos $(AGCH) \ --home=t1/bootstrap --keyring-backend=test --from=bootstrap \ diff --git a/packages/smart-wallet/src/smartWallet.js b/packages/smart-wallet/src/smartWallet.js index b963f8680fb..8ea76ec7090 100644 --- a/packages/smart-wallet/src/smartWallet.js +++ b/packages/smart-wallet/src/smartWallet.js @@ -458,7 +458,7 @@ const finish = ({ state, facets }) => { { brand: desc.brand, issuer: desc.issuer, - petname: desc.proposedName, + petname: desc.issuerName, }, purse, ); diff --git a/packages/smart-wallet/src/utils.js b/packages/smart-wallet/src/utils.js index a2d2314bb3c..cab01acff05 100644 --- a/packages/smart-wallet/src/utils.js +++ b/packages/smart-wallet/src/utils.js @@ -9,7 +9,7 @@ import { observeIteration, subscribeEach } from '@agoric/notifier'; * 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>} updates + * @param {ERef>} updates */ export const coalesceUpdates = updates => { /** @type {Map} */ diff --git a/packages/smart-wallet/test/devices.js b/packages/smart-wallet/test/devices.js index e0160bc934d..8087a23daf2 100644 --- a/packages/smart-wallet/test/devices.js +++ b/packages/smart-wallet/test/devices.js @@ -1,6 +1,6 @@ import bundleCentralSupply from '@agoric/vats/bundles/bundle-centralSupply.js'; import bundleMintHolder from '@agoric/vats/bundles/bundle-mintHolder.js'; -import bundleWalletFactory from '@agoric/vats/bundles/bundle-legacy-walletFactory.js'; +import bundleWalletFactory from '@agoric/vats/bundles/bundle-walletFactory.js'; import bundleProvisionPool from '@agoric/vats/bundles/bundle-provisionPool.js'; export const devices = { @@ -12,7 +12,6 @@ export const devices = { return bundleCentralSupply; case 'mintHolder': return bundleMintHolder; - // TODO(PS0) replace this bundle with the non-legacy smart-wallet case 'walletFactory': return bundleWalletFactory; case 'provisionPool': diff --git a/packages/vats/decentral-core-config.json b/packages/vats/decentral-core-config.json index 8c2f4739882..df29bfa5f69 100644 --- a/packages/vats/decentral-core-config.json +++ b/packages/vats/decentral-core-config.json @@ -53,7 +53,7 @@ "sourceSpec": "@agoric/wallet/contract/src/singleWallet.js" }, "walletFactory": { - "sourceSpec": "@agoric/wallet/contract/src/walletFactory.js" + "sourceSpec": "@agoric/smart-wallet/src/walletFactory.js" }, "zoe": { "sourceSpec": "@agoric/vats/src/vat-zoe.js" diff --git a/packages/vats/decentral-demo-config.json b/packages/vats/decentral-demo-config.json index 9a01cb12e08..f846008e746 100644 --- a/packages/vats/decentral-demo-config.json +++ b/packages/vats/decentral-demo-config.json @@ -61,7 +61,7 @@ "sourceSpec": "@agoric/wallet/contract/src/singleWallet.js" }, "walletFactory": { - "sourceSpec": "@agoric/wallet/contract/src/walletFactory.js" + "sourceSpec": "@agoric/smart-wallet/src/walletFactory.js" }, "zoe": { "sourceSpec": "@agoric/vats/src/vat-zoe.js" diff --git a/packages/vats/package.json b/packages/vats/package.json index b6387e36d59..06b263c13a4 100644 --- a/packages/vats/package.json +++ b/packages/vats/package.json @@ -18,7 +18,7 @@ "test:xs": "exit 0", "lint-fix": "yarn lint:eslint --fix", "lint": "run-s --continue-on-error lint:*", - "lint:types": "tsc --maxNodeModuleJsDepth 4 -p jsconfig.json", + "lint:types": "tsc --maxNodeModuleJsDepth 5 -p jsconfig.json", "lint:eslint": "eslint ." }, "keywords": [], diff --git a/packages/vats/scripts/build-bundles.js b/packages/vats/scripts/build-bundles.js index cec39ca9f33..2d5fbd512f1 100755 --- a/packages/vats/scripts/build-bundles.js +++ b/packages/vats/scripts/build-bundles.js @@ -14,8 +14,8 @@ const sourceToBundle = [ `../bundles/bundle-singleWallet.js`, ], [ - `@agoric/wallet/contract/src/walletFactory.js`, - `../bundles/bundle-legacy-walletFactory.js`, + `@agoric/smart-wallet/src/walletFactory.js`, + `../bundles/bundle-walletFactory.js`, ], ]; diff --git a/packages/vats/src/core/startWalletFactory.js b/packages/vats/src/core/startWalletFactory.js index e4208c3605d..e10afe2bff7 100644 --- a/packages/vats/src/core/startWalletFactory.js +++ b/packages/vats/src/core/startWalletFactory.js @@ -9,7 +9,7 @@ import { Stable } from '../tokens.js'; /** * @param {ERef} zoe - * @param {Installation} inst + * @param {Installation} inst * @typedef {Awaited>} WalletFactoryStartResult */ // eslint-disable-next-line no-unused-vars diff --git a/packages/vats/src/core/types.js b/packages/vats/src/core/types.js index 6a23b68b999..07ff73183ab 100644 --- a/packages/vats/src/core/types.js +++ b/packages/vats/src/core/types.js @@ -177,7 +177,7 @@ * interchainPool: Promise>, * mintHolder: Promise>, * singleWallet: Promise>, - * walletFactory: Promise>, + * walletFactory: Promise>, * }, * }, * instance:{ diff --git a/packages/vats/test/devices.js b/packages/vats/test/devices.js index 8713adcf383..15c712cdea6 100644 --- a/packages/vats/test/devices.js +++ b/packages/vats/test/devices.js @@ -7,7 +7,7 @@ import bundlePSMCharter from '@agoric/inter-protocol/bundles/bundle-psmCharter.j 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-legacy-walletFactory.js'; +import bundleWalletFactory from '../bundles/bundle-walletFactory.js'; import bundleProvisionPool from '../bundles/bundle-provisionPool.js'; const bundles = { diff --git a/packages/wallet/api/src/marshal-contexts.js b/packages/wallet/api/src/marshal-contexts.js index 99ea9008a09..6ae66b46ac8 100644 --- a/packages/wallet/api/src/marshal-contexts.js +++ b/packages/wallet/api/src/marshal-contexts.js @@ -104,7 +104,6 @@ const initSlotVal = (table, slot, val) => { /** * Make context for exporting wallet data where brands etc. can be recognized by boardId. - * Export for use outside the smart wallet. * * When serializing wallet state for, there's a tension between * @@ -236,7 +235,7 @@ const defaultMakePresence = iface => { }; /** - * Make context for marshalling wallet or board data. To be imported into the client, which never makes objects. + * Make context for unserializing wallet or board data. * * @param {(iface: string) => unknown} [makePresence] */ @@ -295,6 +294,9 @@ export const makeImportContext = (makePresence = defaultMakePresence) => { * @param {string} iface */ fromMyWallet: (slot, iface) => { + if (!slot) { + return makePresence('dummy'); + } const { kind, id } = parseWalletSlot(walletObjects, slot); return kind ? provideVal(walletObjects[kind], id, iface) diff --git a/packages/wallet/contract/README.md b/packages/wallet/contract/README.md index 20d36cf27ed..441b49090c6 100644 --- a/packages/wallet/contract/README.md +++ b/packages/wallet/contract/README.md @@ -1,56 +1,5 @@ -# Smart Wallet contracts +# Legacy Smart Wallet contract -## Single contract +This used to have the multi-tenant wallet but that moved to `packages/smart-wallet`. Now `walletFactory` refers to that one. -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. - -# Testing -There are no automated tests yet verifying the smart wallet running on chain. Here are procedures you can use instead. - -## Notifiers - -``` -# tab 1 (chain) -cd packages/cosmic-swingset/ -make scenario2-setup scenario2-run-chain -# starts bare chain, don’t need AMM - -# tab 2 (client server) -cd packages/cosmic-swingset/ -make scenario2-run-client -# confirm no errors in logs - -# tab 3 (interactive) -agoric open --repl -# confirm in browser that `home.wallet` and `home.smartWallet` exist -agd query vstorage keys 'published.wallet' -# confirm it has a key like `published.wallet.agoric1nqxg4pye30n3trct0hf7dclcwfxz8au84hr3ht` -agoric follow :published.wallet.agoric1nqxg4pye30n3trct0hf7dclcwfxz8au84hr3ht -# confirm it has JSON data -``` +This package retains `singleWallet` which is a contract-wrapper around the solo wallet. That too is not long for this life. diff --git a/packages/wallet/contract/src/walletFactory.js b/packages/wallet/contract/src/walletFactory.js deleted file mode 100644 index a05d7299f95..00000000000 --- a/packages/wallet/contract/src/walletFactory.js +++ /dev/null @@ -1,116 +0,0 @@ -// @ts-check -/** - * @file Wallet Factory - * - * Contract to make smart wallets. - */ -import '@agoric/deploy-script-support/exported.js'; -import '@agoric/wallet-backend/src/types.js'; // TODO avoid ambient types -import '@agoric/zoe/exported.js'; - -import { makeAtomicProvider } from '@agoric/store/src/stores/store-utils.js'; -import { makeScalarBigMapStore } from '@agoric/vat-data'; -import { BridgeId } from '@agoric/internal'; -import { E, Far } from '@endo/far'; -import { makeSmartWallet } from './smartWallet.js'; - -/** - * @typedef {{ - * agoricNames: ERef, - * board: ERef, - * namesByAddress: ERef, - * }} SmartWalletContractTerms - * - * @typedef {import('@agoric/vats/src/nameHub').NameHub} NameHub - * - * @typedef {{ - * type: 'WALLET_ACTION', - * owner: string, - * action: string, - * blockHeight: unknown, // int64 - * blockTime: unknown, // int64 - * }} WalletAction - * @typedef {{ - * type: 'WALLET_SPEND_ACTION', - * owner: string, - * spendAction: string, - * blockHeight: unknown, // int64 - * blockTime: unknown, // int64 - * }} WalletSpendAction - */ - -/** - * - * @param {ZCF} zcf - * @param {{ - * storageNode: ERef, - * bridgeManager?: BridgeManager, - * }} privateArgs - */ -export const start = async (zcf, privateArgs) => { - const { agoricNames, namesByAddress, board } = zcf.getTerms(); - assert(board, 'missing board'); - assert(namesByAddress, 'missing namesByAddress'); - assert(agoricNames, 'missing agoricNames'); - const zoe = zcf.getZoeService(); - const { storageNode, bridgeManager } = privateArgs; - - /** @type {MapStore} */ - const walletsByAddress = makeScalarBigMapStore('walletsByAddress'); - const provider = makeAtomicProvider(walletsByAddress); - - const handleWalletAction = Far('walletActionHandler', { - /** - * - * @param {string} srcID - * @param {WalletAction|WalletSpendAction} obj - */ - fromBridge: async (srcID, obj) => { - console.log('walletFactory.fromBridge:', srcID, obj); - assert(obj, 'missing wallet action'); - assert.typeof(obj, 'object'); - assert.typeof(obj.owner, 'string'); - const wallet = walletsByAddress.get(obj.owner); // or throw - console.log('walletFactory:', { wallet }); - return E(wallet).performAction(obj); - }, - }); - - // NOTE: both `MsgWalletAction` and `MsgWalletSpendAction` arrive as BRIDGE_ID.WALLET - // by way of makeBlockManager() in cosmic-swingset/src/block-manager.js - await (bridgeManager && - E(bridgeManager).register(BridgeId.WALLET, handleWalletAction)); - - const shared = { - agoricNames, - board, - namesByAddress, - storageNode, - zoe, - }; - - /** - * - * @param {string} address - * @param {ERef} bank - * @param {ERef} myAddressNameAdmin - * @returns {Promise} - */ - const provideSmartWallet = async (address, bank, myAddressNameAdmin) => { - assert.typeof(address, 'string', 'invalid address'); - assert(bank, 'missing bank'); - assert(myAddressNameAdmin, 'missing myAddressNameAdmin'); - - /** @type {() => Promise} */ - const maker = () => - makeSmartWallet({ address, bank, myAddressNameAdmin }, shared); - - return provider.provideAsync(address, maker); - }; - - return { - creatorFacet: Far('walletFactoryCreator', { - provideSmartWallet, - }), - }; -}; diff --git a/packages/wallet/contract/test/devices.js b/packages/wallet/contract/test/devices.js index 47689a3f291..c5ca3f62b84 100644 --- a/packages/wallet/contract/test/devices.js +++ b/packages/wallet/contract/test/devices.js @@ -1,7 +1,7 @@ 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'; -import bundleWalletFactory from '@agoric/vats/bundles/bundle-legacy-walletFactory.js'; +import bundleWalletFactory from '@agoric/vats/bundles/bundle-walletFactory.js'; import bundleProvisionPool from '@agoric/vats/bundles/bundle-provisionPool.js'; export const devices = { diff --git a/packages/wallet/contract/test/test-walletFactory.js b/packages/wallet/contract/test/test-walletFactory.js deleted file mode 100644 index 49c5bf3bc7e..00000000000 --- a/packages/wallet/contract/test/test-walletFactory.js +++ /dev/null @@ -1,142 +0,0 @@ -// @ts-check - -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 { unsafeMakeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js'; -import { makeStorageNodeChild } from '@agoric/vats/src/lib-chainStorage.js'; -import { makeNameHubKit } from '@agoric/vats/src/nameHub.js'; -import { E, Far } from '@endo/far'; -import path from 'path'; -import { makeTestSpace, subscriptionKey } from './supports.js'; - -/** @type {import('ava').TestFn>>} */ -const test = anyTest; - -const mockAddress1 = 'mockAddress1'; -const mockAddress2 = 'mockAddress2'; -const mockAddress3 = 'mockAddress3'; - -const makeTestContext = async t => { - const { consume } = await makeTestSpace(t.log); - const { agoricNames, zoe } = consume; - - // 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, - ); - }; - - // #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/walletFactory.js`, - 'walletFactory', - ); - /** @type {Promise>} */ - const installation = E(zoe).install(bundle); - // #endregion - - // copied from makeClientBanks() - const storageNode = await makeStorageNodeChild( - consume.chainStorage, - 'wallet', - ); - - const walletFactory = E(zoe).startInstance( - installation, - {}, - { - agoricNames, - namesByAddress: consume.namesByAddress, - board: consume.board, - }, - { storageNode }, - ); - - return { - bankManager: consume.bankManager, - namesByAddressAdmin: consume.namesByAddressAdmin, - reserveAddress, - walletFactory, - zoe, - }; -}; - -test.before(async t => { - t.context = await makeTestContext(t); -}); - -test('basic', async t => { - const { - bankManager, - namesByAddressAdmin, - reserveAddress, - walletFactory, - zoe, - } = t.context; - const { creatorFacet } = await walletFactory; - - await reserveAddress(mockAddress1); - - // copied from makeClientBanks() - const bank = E(bankManager).getBankForAddress(mockAddress1); - const myAddressNameAdmin = E(namesByAddressAdmin).lookupAdmin(mockAddress1); - - const smartWallet = await E(creatorFacet).provideSmartWallet( - mockAddress1, - bank, - myAddressNameAdmin, - ); - - const bridge = await E(smartWallet).getBridge(); - t.is(await E(bridge).getZoe(), await zoe); -}); - -test('notifiers', async t => { - const { bankManager, namesByAddressAdmin, reserveAddress, walletFactory } = - t.context; - const { creatorFacet } = await walletFactory; - - async function checkAddress(address) { - await reserveAddress(address); - - const bank = E(bankManager).getBankForAddress(address); - const myAddressNameAdmin = E(namesByAddressAdmin).lookupAdmin(address); - - const smartWallet = await E(creatorFacet).provideSmartWallet( - address, - bank, - myAddressNameAdmin, - ); - - t.is( - await subscriptionKey(E(smartWallet).getSubscriber()), - `mockChainStorageRoot.wallet.${address}`, - ); - } - - await Promise.all( - [mockAddress1, mockAddress2, mockAddress3].map(checkAddress), - ); -}); diff --git a/packages/wallet/ui/src/components/ConnectionSettingsDialog.jsx b/packages/wallet/ui/src/components/ConnectionSettingsDialog.jsx index d584f556e2f..22eabcc68ae 100644 --- a/packages/wallet/ui/src/components/ConnectionSettingsDialog.jsx +++ b/packages/wallet/ui/src/components/ConnectionSettingsDialog.jsx @@ -62,7 +62,7 @@ const ConnectionSettingsDialog = ({ const classes = useStyles(); const smartConnectionHrefs = allConnectionConfigs.map(({ href }) => href); - const [config, setConfig] = useState(connectionConfig); + const [config, setConfig] = useState(connectionConfig || {}); const [selectInputId] = useState(uuid()); diff --git a/packages/wallet/ui/src/components/Proposal.jsx b/packages/wallet/ui/src/components/Proposal.jsx index cd74a716396..63bb19f65c7 100644 --- a/packages/wallet/ui/src/components/Proposal.jsx +++ b/packages/wallet/ui/src/components/Proposal.jsx @@ -15,6 +15,9 @@ const OfferEntryFromTemplate = ( ) => { const value = BigInt(stringifiedValue); const purse = purses.find(p => p.pursePetname === pursePetname); + if (!purse) { + return null; + } return (
diff --git a/packages/wallet/ui/src/service/Offers.js b/packages/wallet/ui/src/service/Offers.js index ba2279dc0fc..e28de1a0b42 100644 --- a/packages/wallet/ui/src/service/Offers.js +++ b/packages/wallet/ui/src/service/Offers.js @@ -40,7 +40,7 @@ export const getOfferService = ( const acceptOffer = async id => { const offer = offers.get(id); assert(offer, `Tried to accept undefined offer ${id}`); - const action = JSON.stringify({ type: 'acceptOffer', data: offer }); + const action = offer.spendAction; return signSpendAction(action); }; diff --git a/packages/wallet/ui/src/util/WalletBackendAdapter.js b/packages/wallet/ui/src/util/WalletBackendAdapter.js index afc59dbe5fa..d4e100e42c8 100644 --- a/packages/wallet/ui/src/util/WalletBackendAdapter.js +++ b/packages/wallet/ui/src/util/WalletBackendAdapter.js @@ -4,7 +4,7 @@ import { makeAsyncIterableFromNotifier, makeNotifierKit, } from '@agoric/notifier'; -import { iterateLatest } from '@agoric/casting'; +import { iterateEach, iterateReverse } from '@agoric/casting'; import { getScopedBridge } from '../service/ScopedBridge.js'; import { getDappService } from '../service/Dapps.js'; import { getOfferService } from '../service/Offers.js'; @@ -93,9 +93,9 @@ export const makeBackendFromWalletBridge = walletBridge => { }; /** - * @param {import('@agoric/casting').Follower} follower + * @param {import('@agoric/casting').Follower} follower * @param {import('@agoric/casting').Leader} leader - * @param {import('@agoric/casting').Unserializer} unserializer + * @param {ReturnType} marshaller * @param {string} publicAddress * @param {object} keplrConnection * @param {string} networkConfig @@ -105,7 +105,7 @@ export const makeBackendFromWalletBridge = walletBridge => { export const makeWalletBridgeFromFollower = ( follower, leader, - unserializer, + marshaller, publicAddress, keplrConnection, networkConfig, @@ -113,7 +113,7 @@ export const makeWalletBridgeFromFollower = ( // Make an unhandled rejection. throw e; }, - firstCallback, + firstCallback = () => {}, ) => { const notifiers = { getPursesNotifier: 'purses', @@ -130,15 +130,80 @@ export const makeWalletBridgeFromFollower = ( ]), ); + // We assume just one cosmos purse per brand. + const offers = {}; + const brandToPurse = new Map(); + const pursePetnameToBrand = new Map(); + + const updatePurses = () => { + const purses = []; + for (const [brand, purse] of brandToPurse.entries()) { + if (purse.currentAmount && purse.brandPetname) { + pursePetnameToBrand.set(purse.pursePetname, brand); + purses.push(purse); + } + } + notifierKits.purses.updater.updateState(harden(purses)); + }; + const followLatest = async () => { - for await (const { value: state } of iterateLatest(follower)) { + /** @type {number} */ + let firstHeight; + for await (const { blockHeight } of iterateReverse(follower)) { + // TODO: Only set firstHeight and break if the value contains all our state. + firstHeight = blockHeight; + } + for await (const { value } of iterateEach(follower, { + height: firstHeight, + })) { + /** @type {import('@agoric/smart-wallet/src/smartWallet').UpdateRecord} */ + const updateRecord = value; if (firstCallback) { firstCallback(); + Object.values(notifierKits).forEach(({ updater }) => + updater.updateState([]), + ); firstCallback = undefined; } - Object.entries(notifierKits).forEach(([stateName, { updater }]) => { - updater.updateState(state[stateName]); - }); + switch (updateRecord.updated) { + case 'brand': { + const { descriptor } = updateRecord; + const purseObj = { + ...brandToPurse.get(descriptor.brand), + brand: descriptor.brand, + brandPetname: descriptor.petname, + pursePetname: descriptor.petname, + displayInfo: descriptor.displayInfo, + }; + brandToPurse.set(descriptor.brand, purseObj); + updatePurses(); + break; + } + case 'balance': { + // TODO: Don't assume just one purse per brand. + // https://github.com/Agoric/agoric-sdk/issues/6126 + const { currentAmount } = updateRecord; + const purseObj = { + ...brandToPurse.get(currentAmount.brand), + currentAmount, + value: currentAmount.value, + }; + brandToPurse.set(currentAmount.brand, purseObj); + updatePurses(); + break; + } + case 'offerStatus': { + const { status } = updateRecord; + offers[status.id] = status; + notifierKits.offers.updater.updateState( + harden(Object.values(offers)), + ); + break; + } + default: { + throw Error(`Unknown updateRecord ${updateRecord.updated}`); + } + } } }; @@ -184,6 +249,57 @@ export const makeWalletBridgeFromFollower = ( ); const { acceptOffer, declineOffer, cancelOffer } = offerService; + // We override addOffer to adapt the old proposalTemplate format to the new + // smart-wallet format. + const addOfferPSMHack = async details => { + const { + instanceHandleBoardId: instance, // This actually is the instance handle, not an ID. + invitationMaker: { method }, + proposalTemplate: { give, want }, + } = details; + + const mapPurses = obj => + Object.fromEntries( + Object.entries(obj).map(([kw, { brand, pursePetname, value }]) => [ + kw, + { + brand: brand || pursePetnameToBrand.get(pursePetname), + value: BigInt(value), + }, + ]), + ); + const offer = { + id: new Date().getTime(), + invitationSpec: { + source: 'contract', + instance, + publicInvitationMaker: method, + }, + proposal: { + give: mapPurses(give), + want: mapPurses(want), + }, + }; + const spendAction = await E(marshaller).serialize( + harden({ + method: 'executeOffer', + offer, + }), + ); + + // Recover the instance's boardId. + const { + slots: [instanceBoardId], + } = await E(marshaller).serialize(instance); + + const fullOffer = { + ...details, + instancePetname: `instance@${instanceBoardId}`, + spendAction: JSON.stringify(spendAction), + }; + return offerService.addOffer(fullOffer); + }; + const walletBridge = Far('follower wallet bridge', { ...getNotifierMethods, getDappsNotifier: () => dappService.notifier, @@ -198,9 +314,9 @@ export const makeWalletBridgeFromFollower = ( getScopedBridge: (origin, suggestedDappPetname) => getScopedBridge(origin, suggestedDappPetname, { dappService, - offerService, + offerService: { ...offerService, addOffer: addOfferPSMHack }, leader, - unserializer, + unserializer: marshaller, publicAddress, issuerService, networkConfig, diff --git a/packages/wallet/ui/src/util/keyManagement.js b/packages/wallet/ui/src/util/keyManagement.js index abe102491b7..fdbba43ab39 100644 --- a/packages/wallet/ui/src/util/keyManagement.js +++ b/packages/wallet/ui/src/util/keyManagement.js @@ -109,7 +109,7 @@ export const zeroFee = () => { const { coinMinimalDenom: denom } = stableCurrency; const fee = { amount: [{ amount: '0', denom }], - gas: '100000', // TODO: estimate gas? + gas: '300000', // TODO: estimate gas? }; return fee; };