-
Notifications
You must be signed in to change notification settings - Fork 7
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
Query bank balances #51
Changes from 4 commits
ad6530d
bb1b673
83ccf67
c22b225
f26e38e
3fbf3fd
38c2ea2
d312e60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,6 @@ import { | |
makeAsyncIterableFromNotifier, | ||
} from '@agoric/notifier'; | ||
import { E } from '@endo/eventual-send'; | ||
|
||
import { | ||
loadOffers as load, | ||
removeOffer as remove, | ||
|
@@ -17,11 +16,10 @@ import { | |
|
||
import type { SmartWalletKey } from '../store/Dapps'; | ||
import type { OfferSpec, OfferStatus } from '@agoric/smart-wallet/src/offers'; | ||
import { Marshal } from '@endo/marshal'; | ||
|
||
import type { Marshal } from '@endo/marshal'; | ||
import type { Notifier } from '@agoric/notifier/src/types'; | ||
import { Petname } from '@agoric/smart-wallet/src/types'; | ||
import { Brand } from '@agoric/ertp/src/types'; | ||
import type { Petname } from '@agoric/smart-wallet/src/types'; | ||
import type { Brand } from '@agoric/ertp/src/types'; | ||
|
||
export const getOfferService = ( | ||
smartWalletKey: SmartWalletKey, | ||
|
@@ -41,33 +39,43 @@ export const getOfferService = ( | |
id, | ||
instanceHandle, | ||
publicInvitationMaker, | ||
proposalTemplate: { give, want }, | ||
proposalTemplate: { give: giveTemplate, want: wantTemplate }, | ||
} = offer; | ||
|
||
const mapPursePetnamesToBrands = paymentProposals => | ||
Object.fromEntries( | ||
const convertProposals = async paymentProposals => { | ||
const entries = await Promise.all( | ||
Object.entries(paymentProposals).map( | ||
// @ts-expect-error | ||
([kw, { pursePetname, value }]) => { | ||
const brand = pursePetnameToBrand.get(pursePetname); | ||
if (!brand) { | ||
async ([kw, { pursePetname, value, amount: serializedAmount }]) => { | ||
/// XXX test e2e with dapp inter once feasible. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for the documenting. consider TODO. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
const amount = serializedAmount | ||
? await E(boardIdMarshaller).unserialize(serializedAmount) | ||
: { brand: pursePetnameToBrand.get(pursePetname), value }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we're cool with open-coding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, you caught a bug, I wasn't converting value to BigInt. Just changed to use AmountMath and confirmed it works. |
||
|
||
if (!(amount.brand && amount.value)) { | ||
return []; | ||
} | ||
return [ | ||
kw, | ||
{ | ||
brand, | ||
value: BigInt(value), | ||
}, | ||
]; | ||
return [kw, amount]; | ||
}, | ||
), | ||
); | ||
return Object.fromEntries(entries); | ||
}; | ||
|
||
const instance = await E(boardIdMarshaller).unserialize(instanceHandle); | ||
const { | ||
slots: [instanceBoardId], | ||
} = await E(boardIdMarshaller).serialize(instance); | ||
const deconstructInstance = async () => { | ||
const instance = await E(boardIdMarshaller).unserialize(instanceHandle); | ||
const { | ||
slots: [instanceBoardId], | ||
} = await E(boardIdMarshaller).serialize(instance); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i had to remember there's no promise pipelining in this app so the two awaits are fine. it wasn't obvious to me why you wanted to wrap these in a function. It looks like you want consider,
and then just above the first use of
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I'm not sure about promise pipelining in the rest of the app, but we await here so that the card doesn't show up before it has the data needed to render. |
||
|
||
return { instance, instanceBoardId }; | ||
}; | ||
|
||
const [{ instance, instanceBoardId }, give, want] = await Promise.all([ | ||
deconstructInstance(), | ||
convertProposals(giveTemplate), | ||
convertProposals(wantTemplate), | ||
]); | ||
|
||
const offerForAction: OfferSpec = { | ||
id, | ||
|
@@ -77,8 +85,8 @@ export const getOfferService = ( | |
publicInvitationMaker, | ||
}, | ||
proposal: { | ||
give: mapPursePetnamesToBrands(give), | ||
want: mapPursePetnamesToBrands(want), | ||
give, | ||
want, | ||
}, | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -5,6 +5,7 @@ import { objectMap } from '@agoric/internal'; | |||||||
import { | ||||||||
makeAsyncIterableFromNotifier, | ||||||||
makeNotifierKit, | ||||||||
observeNotifier, | ||||||||
} from '@agoric/notifier'; | ||||||||
import { | ||||||||
assertHasData, | ||||||||
|
@@ -29,8 +30,11 @@ import { KeplrUtils } from '../contexts/Provider.jsx'; | |||||||
import type { PurseInfo } from '@agoric/web-components/src/keplr-connection/fetchCurrent'; | ||||||||
import { HttpEndpoint } from '@cosmjs/tendermint-rpc'; | ||||||||
import type { ValueFollowerElement } from '@agoric/casting/src/types'; | ||||||||
import { queryBankBalances } from './queryBankBalances'; | ||||||||
import type { Coin } from '@cosmjs/stargate'; | ||||||||
|
||||||||
const newId = kind => `${kind}${Math.random()}`; | ||||||||
const POLL_INTERVAL_MS = 6000; | ||||||||
|
||||||||
export type BackendSchema = { | ||||||||
actions: object; | ||||||||
|
@@ -108,6 +112,7 @@ export const makeBackendFromWalletBridge = ( | |||||||
|
||||||||
const cancel = e => { | ||||||||
backendUpdater.fail(e); | ||||||||
walletBridge.cleanup(); | ||||||||
}; | ||||||||
|
||||||||
return { backendIt, cancel }; | ||||||||
|
@@ -120,13 +125,20 @@ export const makeWalletBridgeFromFollowers = ( | |||||||
currentFollower: ValueFollower<CurrentWalletRecord>, | ||||||||
updateFollower: ValueFollower<UpdateRecord>, | ||||||||
beansOwingFollower: ValueFollower<string>, | ||||||||
vbankAssetsFollower: ValueFollower<unknown>, | ||||||||
agoricBrandsFollower: ValueFollower<unknown>, | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please include the types. if they can't be imported just write out what this code is assuming. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also this is getting very long for a positional arguments list. very much worth turning into a named options argument, though not necessarily this PR There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added types here and a bit below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was going to say something about the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes but we were getting away with it because |
||||||||
keplrConnection: KeplrUtils, | ||||||||
errorHandler = e => { | ||||||||
// Make an unhandled rejection. | ||||||||
throw e; | ||||||||
}, | ||||||||
firstCallback: () => void | undefined = () => {}, | ||||||||
) => { | ||||||||
let isHalted = false; | ||||||||
let isBankLoaded = false; | ||||||||
let isSmartWalletLoaded = false; | ||||||||
let isOfferServiceStarted = false; | ||||||||
|
||||||||
const notifiers = { | ||||||||
getPursesNotifier: 'purses', | ||||||||
getContactsNotifier: 'contacts', | ||||||||
|
@@ -142,14 +154,12 @@ export const makeWalletBridgeFromFollowers = ( | |||||||
]), | ||||||||
); | ||||||||
|
||||||||
const { notifier: beansOwingNotifier, updater: beansOwingUpdater } = | ||||||||
makeNotifierKit<Number | null>(null); | ||||||||
|
||||||||
// We assume just one cosmos purse per brand. | ||||||||
const brandToPurse = new Map<Brand, PurseInfo>(); | ||||||||
const pursePetnameToBrand = new Map<Petname, Brand>(); | ||||||||
|
||||||||
const updatePurses = () => { | ||||||||
console.debug('brandToPurse map', brandToPurse); | ||||||||
const purses = [] as PurseInfo[]; | ||||||||
for (const [brand, purse] of brandToPurse.entries()) { | ||||||||
if (purse.currentAmount && purse.brandPetname) { | ||||||||
|
@@ -159,6 +169,13 @@ export const makeWalletBridgeFromFollowers = ( | |||||||
} | ||||||||
} | ||||||||
notifierKits.purses.updater.updateState(harden(purses)); | ||||||||
|
||||||||
// Make sure the offer service has all purses from both bank and smart | ||||||||
// wallet chainstorage. | ||||||||
if (!isOfferServiceStarted && isBankLoaded && isSmartWalletLoaded) { | ||||||||
isOfferServiceStarted = true; | ||||||||
offerService.start(pursePetnameToBrand); | ||||||||
} | ||||||||
}; | ||||||||
|
||||||||
const signSpendAction = async (data: string) => { | ||||||||
|
@@ -188,15 +205,79 @@ export const makeWalletBridgeFromFollowers = ( | |||||||
marshaller, | ||||||||
); | ||||||||
|
||||||||
const { notifier: beansOwingNotifier, updater: beansOwingUpdater } = | ||||||||
makeNotifierKit<Number | null>(null); | ||||||||
|
||||||||
const watchBeansOwing = async () => { | ||||||||
for await (const { value } of iterateLatest(beansOwingFollower)) { | ||||||||
if (isHalted) return; | ||||||||
beansOwingUpdater.updateState(Number(value)); | ||||||||
} | ||||||||
}; | ||||||||
|
||||||||
// Reads purses from the cosmos bank module. These are not necessarily real | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider "Infer purses from Cosmos bank module balances" since, as you say, purses can't be read from Cosmos. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||||||
// "purses" in the smart wallet contract, because those are lazily | ||||||||
// instantiated, but we still need to show the balances. | ||||||||
const watchChainBalances = () => { | ||||||||
let vbankAssets; | ||||||||
let bank; | ||||||||
|
||||||||
const possiblyUpdateBankPurses = () => { | ||||||||
if (!vbankAssets || !bank) return; | ||||||||
|
||||||||
const bankMap = new Map<string, string>( | ||||||||
bank.map(({ denom, amount }) => [denom, amount]), | ||||||||
); | ||||||||
|
||||||||
vbankAssets.forEach(([denom, info]) => { | ||||||||
const amount = bankMap.get(denom) ?? 0n; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If an asset exists in the vbank but not in the user's account we want to show it as a purse with 0 balance so they can click the eventual "deposit" button for ibc transfer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very helpful note. Please include in the code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||||||
const purseInfo: PurseInfo = { | ||||||||
brand: info.brand, | ||||||||
currentAmount: AmountMath.make(info.brand, BigInt(amount)), | ||||||||
brandPetname: info.issuerName, | ||||||||
pursePetname: info.issuerName, | ||||||||
displayInfo: info.displayInfo, | ||||||||
}; | ||||||||
brandToPurse.set(info.brand, purseInfo); | ||||||||
}); | ||||||||
|
||||||||
isBankLoaded = true; | ||||||||
updatePurses(); | ||||||||
}; | ||||||||
|
||||||||
const watchBank = async () => { | ||||||||
if (isHalted) return; | ||||||||
const balances = await queryBankBalances(keplrConnection.address, rpc); | ||||||||
bank = balances; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the const?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, done |
||||||||
possiblyUpdateBankPurses(); | ||||||||
setTimeout(watchBank, POLL_INTERVAL_MS); | ||||||||
}; | ||||||||
|
||||||||
const watchVbankAssets = async () => { | ||||||||
for await (const { value } of iterateLatest(vbankAssetsFollower)) { | ||||||||
if (isHalted) return; | ||||||||
vbankAssets = value; | ||||||||
possiblyUpdateBankPurses(); | ||||||||
} | ||||||||
}; | ||||||||
|
||||||||
void watchVbankAssets(); | ||||||||
void watchBank(); | ||||||||
}; | ||||||||
|
||||||||
const fetchAgoricBrands = async () => { | ||||||||
for await (const { value } of iterateLatest(agoricBrandsFollower)) { | ||||||||
if (value) { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when would the iterator return an object without a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I was afraid it might have an initial value of null or something, but that's more a concern for notifiers, and it would throw anyway in that case. Just removed the conditional |
||||||||
return new Map(value.map(([k, v]) => [v, k])); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This took me some time to read through without type info. I think it's making a map with the keys/values inverted. Consider https://lodash.com/docs/4.17.15#invert to make that clear, or a code comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added types and left a comment |
||||||||
} | ||||||||
} | ||||||||
}; | ||||||||
|
||||||||
const fetchCurrent = async () => { | ||||||||
await assertHasData(currentFollower); | ||||||||
void watchBeansOwing(); | ||||||||
watchChainBalances(); | ||||||||
|
||||||||
const latestIterable = await E(currentFollower).getLatestIterable(); | ||||||||
const iterator = latestIterable[Symbol.asyncIterator](); | ||||||||
const latest = await iterator.next(); | ||||||||
|
@@ -210,13 +291,29 @@ export const makeWalletBridgeFromFollowers = ( | |||||||
} | ||||||||
const currentEl: ValueFollowerElement<CurrentWalletRecord> = latest.value; | ||||||||
const wallet = currentEl.value; | ||||||||
console.log('wallet current', wallet); | ||||||||
console.debug('wallet current', wallet); | ||||||||
|
||||||||
const agoricBrands = await fetchAgoricBrands(); | ||||||||
assert(agoricBrands, 'Failed to fetch agoric brands'); | ||||||||
|
||||||||
for (const purse of wallet.purses) { | ||||||||
console.debug('registering purse', purse); | ||||||||
const brandDescriptor = wallet.brands.find( | ||||||||
bd => purse.brand === bd.brand, | ||||||||
); | ||||||||
assert(brandDescriptor, `missing descriptor for brand ${purse.brand}`); | ||||||||
// We only care about the zoe invite purse, which has asset kind 'set'. | ||||||||
// Non 'set' amounts need to be fetched from vbank to know their | ||||||||
// decimalPlaces. | ||||||||
// | ||||||||
// If we have add non 'set' amount purses that aren't in the vbank, it's | ||||||||
// not currently possible to read their decimalPlaces, so this code | ||||||||
// will need updating. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This documentation is good. Ideally we'd import some I leave it to @turadg to judge the cost-effectiveness of that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know of any runtime features of an invitation issuer that distinguishes it from any other Set kind issuer. You could make this "set"-ness check more apparent using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops I meant '@agoric/store'. But yeah let's wait for it to be in Endo |
||||||||
if ( | ||||||||
!Array.isArray(purse.balance.value) || | ||||||||
!agoricBrands.has(purse.brand) | ||||||||
) { | ||||||||
continue; | ||||||||
} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider distinguishing these conditions:
Likely to pay off debugging someday. And in the meantime it's code documentation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||||||
const brandDescriptor = { | ||||||||
petname: agoricBrands.get(purse.brand), | ||||||||
displayInfo: { assetKind: 'set' }, | ||||||||
}; | ||||||||
const purseInfo: PurseInfo = { | ||||||||
brand: purse.brand, | ||||||||
currentAmount: purse.balance, | ||||||||
|
@@ -226,9 +323,8 @@ export const makeWalletBridgeFromFollowers = ( | |||||||
}; | ||||||||
brandToPurse.set(purse.brand, purseInfo); | ||||||||
} | ||||||||
console.debug('brandToPurse map', brandToPurse); | ||||||||
isSmartWalletLoaded = true; | ||||||||
updatePurses(); | ||||||||
offerService.start(pursePetnameToBrand); | ||||||||
return currentEl.blockHeight; | ||||||||
}; | ||||||||
|
||||||||
|
@@ -340,6 +436,7 @@ export const makeWalletBridgeFromFollowers = ( | |||||||
makeEmptyPurse, | ||||||||
addContact, | ||||||||
addIssuer, | ||||||||
cleanup: () => (isHalted = true), | ||||||||
}); | ||||||||
|
||||||||
return walletBridge; | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { QueryClient, createProtobufRpcClient } from '@cosmjs/stargate'; | ||
import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; | ||
import { QueryClientImpl } from 'cosmjs-types/cosmos/bank/v1beta1/query'; | ||
import type { Coin } from '@cosmjs/stargate'; | ||
import type { HttpEndpoint } from '@cosmjs/tendermint-rpc'; | ||
|
||
export const queryBankBalances = async ( | ||
address: string, | ||
rpc: HttpEndpoint, | ||
): Promise<Coin[]> => { | ||
const tendermint = await Tendermint34Client.connect(rpc); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ambient authority. hm.
I suppose we have agreed on an exception to that for UI code? I'd like to see that in the README or the like. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noted, I don't know if I have a clear way to explain the philosophy as it pertains to UI code at the moment though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we have agreed agreed on an exception to that for UI code and I agree it would be valuable to have in the README of this repo to refer to. Not a blocker imo There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreed, not a blocker. meanwhile, I realize pointing to the wiki doesn't automatically make a link the other way, where citing an issue does: Agoric/agoric-sdk#2160 |
||
const queryClient = new QueryClient(tendermint); | ||
const rpcClient = createProtobufRpcClient(queryClient); | ||
const bankQueryService = new QueryClientImpl(rpcClient); | ||
|
||
const { balances } = await bankQueryService.AllBalances({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. scaling consideration: How often do we do this? I gather from @arirubinstein that we're supposed to make no more than 2 RPC queries per second. I gather @michaelfig plans to provide rate limiting in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea my hope is this could unblock us for now and we can swap in the improved casting stuff later, just added a comment here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
address, | ||
}); | ||
|
||
return balances; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12164,7 +12164,7 @@ rollup@^2.43.1: | |
optionalDependencies: | ||
fsevents "~2.3.2" | ||
|
||
rollup@endojs/endo#rollup-2.7.1-patch-1, "rollup@github:endojs/endo#rollup-2.7.1-patch-1": | ||
rollup@endojs/endo#rollup-2.7.1-patch-1: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. curious. |
||
version "2.70.1-endo.1" | ||
resolved "https://codeload.github.com/endojs/endo/tar.gz/54060e784a4dbe77b6692f17344f4d84a198530d" | ||
optionalDependencies: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a little hard to follow. consider filtering and naming the value transformer.
Then it's a small step to using
objectMap
anddeeplyFulfilled
;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I gave this a shot but I got a little confused with how to use those utility functions and the code wasn't looking much simpler, I'd prefer to leave this as is.