Skip to content
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

feat: localOrchAccount.getBalance (non-vbank assets) and localOrchAccount.getBalances #10029

Merged
merged 11 commits into from
Sep 6, 2024
Merged
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
Loading