Skip to content

Commit

Permalink
feat: localOrchAccount.getBalance (non-vbank assets) and `localOrch…
Browse files Browse the repository at this point in the history
…Account.getBalances` (#10029)

closes: #9610

## Description
- updates `LocalOrchestrationAccount.getBalance()` to support non-vbank assets (using `localchain.query()` to ask cosmos bank)
- implements `LocalOrchestrationAccount.getBalances()`
- BREAKING: renames `ChainHub.lookupDenom` and `ChainHub.lookupAsset` to `getDenom` and `getAsset`
- BREAKING: `ChainHub.getAsset` returns undefined instead of throwing when lookup fails

### Security Considerations
n/a

### Scaling Considerations
n/a

### Documentation Considerations
Updates pertinent jsodc and `exos/README.md`

### Testing Considerations
Includes unit and e2e tests

### Upgrade Considerations
n/a, unreleased code
  • Loading branch information
mergify[bot] authored Sep 6, 2024
2 parents fb3af6c + 687adf1 commit b31bd09
Show file tree
Hide file tree
Showing 20 changed files with 395 additions and 131 deletions.
17 changes: 9 additions & 8 deletions multichain-testing/test/account-balance-queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,15 @@ const queryAccountBalances = test.macro({
const { icqEnabled } = (chainInfo as Record<string, CosmosChainInfo>)[
chainName
];
const expectValidResult = icqEnabled || chainName === 'agoric';
t.log(
icqEnabled
? 'ICQ Enabled expecting offer result.'
: 'ICQ Disabled expecting offer error',
`Expecting offer ${expectValidResult ? 'result' : 'error'} for ${chainName}`,
);

const {
status: { result, error },
} = offerResult;
if (icqEnabled) {
if (expectValidResult) {
t.is(error, undefined, 'No error observed for supported chain');
const balances = JSON.parse(result);
t.truthy(balances, 'Result is parsed successfully');
Expand Down Expand Up @@ -156,16 +155,16 @@ const queryAccountBalance = test.macro({
const { icqEnabled } = (chainInfo as Record<string, CosmosChainInfo>)[
chainName
];

const expectValidResult = icqEnabled || chainName === 'agoric';
t.log(
icqEnabled
? 'ICQ Enabled, expecting offer result.'
: 'ICQ Disabled, expecting offer error',
`Expecting offer ${expectValidResult ? 'result' : 'error'} for ${chainName}`,
);

const {
status: { result, error },
} = offerResult;
if (icqEnabled) {
if (expectValidResult) {
t.is(error, undefined, 'No error observed for supported chain');
const parsedBalance = JSON.parse(result);
t.truthy(parsedBalance, 'Result is parsed successfully');
Expand All @@ -186,5 +185,7 @@ const queryAccountBalance = test.macro({

test.serial(queryAccountBalances, 'osmosis');
test.serial(queryAccountBalances, 'cosmoshub');
test.serial(queryAccountBalances, 'agoric');
test.serial(queryAccountBalance, 'osmosis');
test.serial(queryAccountBalance, 'cosmoshub');
test.serial(queryAccountBalance, 'agoric');
2 changes: 1 addition & 1 deletion packages/boot/test/bootstrapTests/orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ test.serial('stakeAtom - smart wallet', async t => {
proposal: {},
}),
{
message: 'No denomination for brand [object Alleged: ATOM brand]',
message: 'No denom for brand [object Alleged: ATOM brand]',
},
);
});
Expand Down
14 changes: 9 additions & 5 deletions packages/orchestration/src/examples/stakeBld.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ export const start = async (zcf, privateArgs, baggage) => {

const chainHub = makeChainHub(privateArgs.agoricNames, vowTools);

const { localchain, timerService } = privateArgs;
const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit(
zone,
makeRecorderKit,
zcf,
privateArgs.timerService,
vowTools,
chainHub,
{
makeRecorderKit,
zcf,
timerService,
vowTools,
chainHub,
localchain,
},
);

// ----------------
Expand Down
1 change: 1 addition & 0 deletions packages/orchestration/src/exos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ classDiagram
executeTx()
getAddress()
getBalance()
getBalances()
getPublicTopics()
monitorTransfers()
send()
Expand Down
19 changes: 11 additions & 8 deletions packages/orchestration/src/exos/chain-hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ const ChainHubI = M.interface('ChainHub', {
getConnectionInfo: M.call(ChainIdArgShape, ChainIdArgShape).returns(VowShape),
getChainsAndConnection: M.call(M.string(), M.string()).returns(VowShape),
registerAsset: M.call(M.string(), DenomDetailShape).returns(),
lookupAsset: M.call(M.string()).returns(DenomDetailShape),
lookupDenom: M.call(BrandShape).returns(M.or(M.string(), M.undefined())),
getAsset: M.call(M.string()).returns(M.or(DenomDetailShape, M.undefined())),
getDenom: M.call(BrandShape).returns(M.or(M.string(), M.undefined())),
});

/**
Expand Down Expand Up @@ -394,18 +394,21 @@ export const makeChainHub = (agoricNames, vowTools) => {
* Retrieve holding, issuing chain names etc. for a denom.
*
* @param {Denom} denom
* @returns {DenomDetail}
* @returns {DenomDetail | undefined}
*/
lookupAsset(denom) {
return denomDetails.get(denom);
getAsset(denom) {
if (denomDetails.has(denom)) {
return denomDetails.get(denom);
}
return undefined;
},
/**
* Retrieve holding, issuing chain names etc. for a denom.
* Retrieve denom (string) for a Brand.
*
* @param {Brand} brand
* @returns {string | undefined}
* @returns {Denom | undefined}
*/
lookupDenom(brand) {
getDenom(brand) {
if (brandDenoms.has(brand)) {
return brandDenoms.get(brand);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ import {
IBCTransferOptionsShape,
} from '../typeGuards.js';
import { coerceCoin, coerceDenom } from '../utils/amounts.js';
import { maxClockSkew, tryDecodeResponse } from '../utils/cosmos.js';
import {
maxClockSkew,
tryDecodeResponse,
toDenomAmount,
} from '../utils/cosmos.js';
import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js';
import { makeTimestampHelper } from '../utils/time.js';

Expand Down Expand Up @@ -107,9 +111,6 @@ const PUBLIC_TOPICS = {
account: ['Staking Account holder status', M.any()],
};

/** @type {(c: { denom: string; amount: string }) => DenomAmount} */
const toDenomAmount = c => ({ denom: c.denom, value: BigInt(c.amount) });

/**
* @param {Zone} zone
* @param {object} powers
Expand Down
135 changes: 96 additions & 39 deletions packages/orchestration/src/exos/local-orchestration-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Shape as NetworkShape } from '@agoric/network';
import { M } from '@agoric/vat-data';
import { VowShape } from '@agoric/vow';
import { E } from '@endo/far';
import { Fail, q } from '@endo/errors';

import {
AmountArgShape,
Expand All @@ -14,8 +15,9 @@ import {
DenomShape,
IBCTransferOptionsShape,
TimestampProtoShape,
TypedJsonShape,
} from '../typeGuards.js';
import { maxClockSkew } from '../utils/cosmos.js';
import { maxClockSkew, toDenomAmount } from '../utils/cosmos.js';
import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js';
import { makeTimestampHelper } from '../utils/time.js';
import { preparePacketTools } from './packet-tools.js';
Expand All @@ -24,25 +26,23 @@ import { coerceCoin, coerceDenomAmount } from '../utils/amounts.js';

/**
* @import {HostOf} from '@agoric/async-flow';
* @import {LocalChainAccount} from '@agoric/vats/src/localchain.js';
* @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, ChainInfo, IBCConnectionInfo, OrchestrationAccountI} from '@agoric/orchestration';
* @import {LocalChain, LocalChainAccount} from '@agoric/vats/src/localchain.js';
* @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, IBCConnectionInfo, OrchestrationAccountI} from '@agoric/orchestration';
* @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'.
* @import {Zone} from '@agoric/zone';
* @import {Remote} from '@agoric/internal';
* @import {Bytes} from '@agoric/network';
* @import {InvitationMakers} from '@agoric/smart-wallet/src/types.js';
* @import {TimerService, TimestampRecord} from '@agoric/time';
* @import {PromiseVow, EVow, Vow, VowTools} from '@agoric/vow';
* @import {Vow, VowTools} from '@agoric/vow';
* @import {TypedJson, JsonSafe, ResponseTo} from '@agoric/cosmic-proto';
* @import {Coin} from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js';
* @import {Matcher, Pattern} from '@endo/patterns';
* @import {Matcher} from '@endo/patterns';
* @import {ChainHub} from './chain-hub.js';
* @import {PacketTools} from './packet-tools.js';
*/

const trace = makeTracer('LOA');

const { Fail } = assert;
const { Vow$ } = NetworkShape; // TODO #9611

const EVow$ = shape => M.or(Vow$(shape), M.promise(/* shape */));
Expand Down Expand Up @@ -82,19 +82,17 @@ const PUBLIC_TOPICS = {

/**
* @param {Zone} zone
* @param {MakeRecorderKit} makeRecorderKit
* @param {ZCF} zcf
* @param {Remote<TimerService>} timerService
* @param {VowTools} vowTools
* @param {ChainHub} chainHub
* @param {object} powers
* @param {MakeRecorderKit} powers.makeRecorderKit
* @param {ZCF} powers.zcf
* @param {Remote<TimerService>} powers.timerService
* @param {VowTools} powers.vowTools
* @param {ChainHub} powers.chainHub
* @param {Remote<LocalChain>} powers.localchain
*/
export const prepareLocalOrchestrationAccountKit = (
zone,
makeRecorderKit,
zcf,
timerService,
vowTools,
chainHub,
{ makeRecorderKit, zcf, timerService, vowTools, chainHub, localchain },
) => {
const { watch, allVows, asVow, when } = vowTools;
const { makeIBCTransferSender } = prepareIBCTools(
Expand Down Expand Up @@ -140,9 +138,15 @@ export const prepareLocalOrchestrationAccountKit = (
onFulfilled: M.call(M.any()).optional(M.any()).returns(M.undefined()),
}),
getBalanceWatcher: M.interface('getBalanceWatcher', {
onFulfilled: M.call(AmountShape)
.optional(DenomShape)
.returns(DenomAmountShape),
onFulfilled: M.call(AmountShape, DenomShape).returns(DenomAmountShape),
}),
queryBalanceWatcher: M.interface('queryBalanceWatcher', {
onFulfilled: M.call(TypedJsonShape).returns(DenomAmountShape),
}),
queryBalancesWatcher: M.interface('queryBalancesWatcher', {
onFulfilled: M.call(TypedJsonShape).returns(
M.arrayOf(DenomAmountShape),
),
}),
invitationMakers: M.interface('invitationMakers', {
Delegate: M.call(M.string(), AmountShape).returns(M.promise()),
Expand Down Expand Up @@ -356,6 +360,44 @@ export const prepareLocalOrchestrationAccountKit = (
return harden({ denom, value: natAmount.value });
},
},
/**
* handles a QueryBalanceRequest from localchain.query and returns the
* balance as a DenomAmount
*/
queryBalanceWatcher: {
/**
* @param {ResponseTo<
* TypedJson<'/cosmos.bank.v1beta1.QueryBalanceRequest'>
* >} result
* @returns {DenomAmount}
*/
onFulfilled(result) {
const { balance } = result;
if (!balance || !balance?.denom) {
throw Fail`Expected balance ${q(result)};`;
}
return harden(toDenomAmount(balance));
},
},
/**
* handles a QueryAllBalancesRequest from localchain.query and returns the
* balances as a DenomAmounts
*/
queryBalancesWatcher: {
/**
* @param {ResponseTo<
* TypedJson<'/cosmos.bank.v1beta1.QueryAllBalancesRequest'>
* >} result
* @returns {DenomAmount[]}
*/
onFulfilled(result) {
const { balances } = result;
if (!balances || !Array.isArray(balances)) {
throw Fail`Expected balances ${q(result)};`;
}
return harden(balances.map(toDenomAmount));
},
},
holder: {
/** @type {HostOf<OrchestrationAccountI['asContinuingOffer']>} */
asContinuingOffer() {
Expand All @@ -380,33 +422,48 @@ export const prepareLocalOrchestrationAccountKit = (
});
},
/**
* TODO: balance lookups for non-vbank assets
*
* @type {HostOf<OrchestrationAccountI['getBalance']>}
*/
getBalance(denomArg) {
const [brand, denom] =
typeof denomArg === 'string'
? [chainHub.lookupAsset(denomArg).brand, denomArg]
: [denomArg, chainHub.lookupDenom(denomArg)];
return asVow(() => {
const [brand, denom] =
typeof denomArg === 'string'
? [chainHub.getAsset(denomArg)?.brand, denomArg]
: [denomArg, chainHub.getDenom(denomArg)];

if (!brand) {
throw Fail`No brand for ${denomArg}`;
}
if (!denom) {
throw Fail`No denom for ${denomArg}`;
}
if (!denom) {
throw Fail`No denom for brand: ${denomArg}`;
}

return watch(
E(this.state.account).getBalance(brand),
this.facets.getBalanceWatcher,
denom,
);
if (brand) {
return watch(
E(this.state.account).getBalance(brand),
this.facets.getBalanceWatcher,
denom,
);
}

return watch(
E(localchain).query(
typedJson('/cosmos.bank.v1beta1.QueryBalanceRequest', {
address: this.state.address.value,
denom,
}),
),
this.facets.queryBalanceWatcher,
);
});
},
/** @type {HostOf<OrchestrationAccountI['getBalances']>} */
getBalances() {
// TODO https://github.com/Agoric/agoric-sdk/issues/9610
return asVow(() => Fail`not yet implemented`);
return watch(
E(localchain).query(
typedJson('/cosmos.bank.v1beta1.QueryAllBalancesRequest', {
address: this.state.address.value,
}),
),
this.facets.queryBalancesWatcher,
);
},

/**
Expand Down
5 changes: 3 additions & 2 deletions packages/orchestration/src/exos/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ const prepareOrchestratorKit = (
},
/** @type {HostOf<Orchestrator['getDenomInfo']>} */
getDenomInfo(denom) {
const { chainName, baseName, baseDenom, brand } =
chainHub.lookupAsset(denom);
const denomDetail = chainHub.getAsset(denom);
if (!denomDetail) throw Fail`No denom detail for ${q(denom)}`;
const { chainName, baseName, baseDenom, brand } = denomDetail;
chainByName.has(chainName) ||
Fail`use getChain(${q(chainName)}) before getDenomInfo(${q(denom)})`;
const chain = chainByName.get(chainName);
Expand Down
Loading

0 comments on commit b31bd09

Please sign in to comment.