Skip to content

Commit

Permalink
9211 lookup denom (#9983)
Browse files Browse the repository at this point in the history
closes: #9211

## Description

Uses ChainHub for lookup of denoms and brands (from the other).

Adds some coercion utilities to get the desired shape within the exos and contract, using a ChainHub.


### Security Considerations
Contracts control their ChainHub and can specify any mapping of brand to denom.


### Scaling Considerations
If contracts import the whole Known Chains module, that will increase bundle size. I think this will be unlikely in production. We'll have on-chain lookup by then.

### Documentation Considerations
not yet

### Testing Considerations
existing coverage

### Upgrade Considerations
None of this is deployed yet
  • Loading branch information
mergify[bot] committed Aug 30, 2024
2 parents ef9ed26 + 4cac5cc commit 42d1d4f
Show file tree
Hide file tree
Showing 31 changed files with 358 additions and 233 deletions.
40 changes: 9 additions & 31 deletions multichain-testing/test/auto-stake-it.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import type { CosmosChainInfo } from '@agoric/orchestration';
import anyTest from '@endo/ses-ava/prepare-endo.js';
import type { ExecutionContext, TestFn } from 'ava';
import { useChain } from 'starshipjs';
import type { CosmosChainInfo, IBCConnectionInfo } from '@agoric/orchestration';
import type { SetupContextWithWallets } from './support.js';
import { chainConfig, commonSetup } from './support.js';
import { makeQueryClient } from '../tools/query.js';
import { makeDoOffer } from '../tools/e2e-tools.js';
import chainInfo from '../starship-chain-info.js';
import { makeDoOffer } from '../tools/e2e-tools.js';
import {
createFundedWalletAndClient,
makeIBCTransferMsg,
} from '../tools/ibc-transfer.js';
import { makeQueryClient } from '../tools/query.js';
import type { SetupContextWithWallets } from './support.js';
import { chainConfig, commonSetup } from './support.js';
import { AUTO_STAKE_IT_DELEGATIONS_TIMEOUT } from './config.js';

const test = anyTest as TestFn<SetupContextWithWallets>;

Expand Down Expand Up @@ -82,6 +83,7 @@ const makeFundAndTransfer = (t: ExecutionContext<SetupContextWithWallets>) => {
const autoStakeItScenario = test.macro({
title: (_, chainName: string) => `auto-stake-it on ${chainName}`,
exec: async (t, chainName: string) => {
// 1. setup
const {
wallets,
vstorageClient,
Expand All @@ -91,36 +93,12 @@ const autoStakeItScenario = test.macro({

const fundAndTransfer = makeFundAndTransfer(t);

// 1. Send initial tokens so denom is available (debatably necessary, but
// allows us to trace the denom until we have ibc denoms in chainInfo)
const agAdminAddr = wallets['agoricAdmin'];
console.log('Sending tokens to', agAdminAddr, `from ${chainName}`);
await fundAndTransfer(chainName, agAdminAddr);

// 2. Find 'stakingDenom' denom on agoric
const agoricConns = chainInfo['agoric'].connections as Record<
string,
IBCConnectionInfo
>;
const remoteChainInfo = (chainInfo as Record<string, CosmosChainInfo>)[
chainName
];
// const remoteChainId = remoteChainInfo.chain.chain_id;
// const agoricToRemoteConn = agoricConns[remoteChainId];
const { portId, channelId } =
agoricConns[remoteChainInfo.chainId].transferChannel;
const agoricQueryClient = makeQueryClient(
await useChain('agoric').getRestEndpoint(),
);
const stakingDenom = remoteChainInfo?.stakingTokens?.[0].denom;
if (!stakingDenom) throw Error(`staking denom found for ${chainName}`);
const { hash } = await retryUntilCondition(
() =>
agoricQueryClient.queryDenom(`/${portId}/${channelId}`, stakingDenom),
denomTrace => !!denomTrace.hash,
`local denom hash for ${stakingDenom} found`,
);
t.log(`found ibc denom hash for ${stakingDenom}:`, hash);

// 3. Find a remoteChain validator to delegate to
const remoteQueryClient = makeQueryClient(
Expand Down Expand Up @@ -161,7 +139,6 @@ const autoStakeItScenario = test.macro({
encoding: 'bech32',
chainId: remoteChainInfo.chainId,
},
localDenom: `ibc/${hash}`,
},
proposal: {},
});
Expand Down Expand Up @@ -203,7 +180,8 @@ const autoStakeItScenario = test.macro({
const { delegation_responses } = await retryUntilCondition(
() => remoteQueryClient.queryDelegations(icaAddress),
({ delegation_responses }) => !!delegation_responses.length,
`delegations visible on ${chainName}`,
`auto-stake-it delegations visible on ${chainName}`,
AUTO_STAKE_IT_DELEGATIONS_TIMEOUT,
);
t.log('delegation balance', delegation_responses[0]?.balance);
t.like(
Expand Down
32 changes: 32 additions & 0 deletions multichain-testing/test/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { RetryOptions } from '../tools/sleep.js';

/**
* Wait 90 seconds to ensure staking rewards are available.
*
* While we expect staking rewards to be available after a
* single block (~5-12 seconds for most chains), this provides additional
* padding after observed failures in CI
* (https://github.com/Agoric/agoric-sdk/issues/9934).
*
* A more robust approach might consider Distribution params and the
* {@link FAUCET_POUR} constant to determine how many blocks it should take for
* rewards to be available.
*/
export const STAKING_REWARDS_TIMEOUT: RetryOptions = {
retryIntervalMs: 5000,
maxRetries: 18,
};

/**
* Wait 2 minutes to ensure:
* - IBC Transfer from LocalAccount -> ICA Account Completes
* - Delegation from ICA Account (initiated from SwingSet) Completes
* - Delegations are visible via LCD (API Endpoint)
*
* Most of the time this finishes in <7 seconds, but other times it
* appears to take much longer.
*/
export const AUTO_STAKE_IT_DELEGATIONS_TIMEOUT: RetryOptions = {
retryIntervalMs: 5000,
maxRetries: 24,
};
20 changes: 2 additions & 18 deletions multichain-testing/test/stake-ica.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,13 @@ import {
} from './support.js';
import { makeDoOffer } from '../tools/e2e-tools.js';
import { makeQueryClient } from '../tools/query.js';
import { sleep, type RetryOptions } from '../tools/sleep.js';
import { sleep } from '../tools/sleep.js';
import { STAKING_REWARDS_TIMEOUT } from './config.js';

const test = anyTest as TestFn<SetupContextWithWallets>;

const accounts = ['user1', 'user2'];

/**
* Wait 90 seconds to ensure staking rewards are available.
*
* While we expect staking rewards to be available after a
* single block (~5-12 seconds for most chains), this provide additional
* padding after observed failures in CI
* (https://github.com/Agoric/agoric-sdk/issues/9934).
*
* A more robust approach might consider Distribution params and the
* {@link FAUCET_POUR} constant to determine how many blocks it should take for
* rewards to be available.
*/
export const STAKING_REWARDS_TIMEOUT: RetryOptions = {
retryIntervalMs: 5000,
maxRetries: 18,
};

test.before(async t => {
const { deleteTestKeys, setupTestKeys, ...rest } = await commonSetup(t);
// XXX not necessary for CI, but helpful for unexpected failures in
Expand Down
6 changes: 4 additions & 2 deletions multichain-testing/tools/ibc-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ type SimpleChainAddress = {
chainName: string;
};

export const DEFAULT_TIMEOUT_NS = 1893456000000000000n;
// 2030-01-01T00:00:00Z
export const DEFAULT_TIMEOUT_NS =
1893456000n * NANOSECONDS_PER_MILLISECOND * MILLISECONDS_PER_SECOND;

/**
* @param {number} [ms] current time in ms (e.g. Date.now())
* @param {bigint} [minutes=5n] number of minutes in the future
* @returns {bigint} nanosecond timestamp 5 mins in the future */
* @returns {bigint} nanosecond timestamp absolute since Unix epoch */
export const getTimeout = (ms: number = 0, minutes = 5n) => {
// UNTIL #9200. timestamps are getting clobbered somewhere along the way
// and we are observing failed transfers with timeouts years in the past.
Expand Down
3 changes: 1 addition & 2 deletions packages/boot/test/bootstrapTests/orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,8 @@ test.serial('stakeAtom - smart wallet', async t => {
proposal: {},
}),
{
message: 'Brands not currently supported.',
message: 'No denomination for brand [object Alleged: ATOM brand]',
},
'brands not currently supported',
);
});

Expand Down
11 changes: 4 additions & 7 deletions packages/orchestration/src/examples/auto-stake-it.flows.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Fail } from '@endo/errors';
import { denomHash } from '../utils/denomHash.js';

/**
* @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js';
Expand All @@ -21,19 +22,13 @@ import { Fail } from '@endo/errors';
* @param {{
* chainName: string;
* validator: CosmosValidatorAddress;
* localDenom: Denom;
* }} offerArgs
*/
export const makeAccounts = async (
orch,
{ makeStakingTap, makePortfolioHolder, chainHub },
seat,
{
chainName,
validator,
// TODO localDenom is user supplied, until #9211
localDenom,
},
{ chainName, validator },
) => {
seat.exit(); // no funds exchanged
const [agoric, remoteChain] = await Promise.all([
Expand Down Expand Up @@ -65,6 +60,8 @@ export const makeAccounts = async (
);
assert(transferChannel.counterPartyChannelId, 'unable to find sourceChannel');

const localDenom = `ibc/${denomHash({ denom: remoteDenom, channelId: transferChannel.channelId })}`;

// Every time the `localAccount` receives `remoteDenom` over IBC, delegate it.
const tap = makeStakingTap({
localAccount,
Expand Down
18 changes: 15 additions & 3 deletions packages/orchestration/src/examples/stakeBld.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
* @file Stake BLD contract
*/
import { makeTracer } from '@agoric/internal';
import { heapVowE as E, prepareVowTools } from '@agoric/vow/vat.js';
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
import { makeDurableZone } from '@agoric/zone/durable.js';
import { prepareVowTools, heapVowE as E } from '@agoric/vow/vat.js';
import { deeplyFulfilled } from '@endo/marshal';
import { M } from '@endo/patterns';
import { prepareLocalOrchestrationAccountKit } from '../exos/local-orchestration-account.js';
import { makeChainHub } from '../exos/chain-hub.js';
import { prepareLocalOrchestrationAccountKit } from '../exos/local-orchestration-account.js';
import fetchedChainInfo from '../fetched-chain-info.js';

/**
* @import {NameHub} from '@agoric/vats';
Expand Down Expand Up @@ -41,13 +42,15 @@ export const start = async (zcf, privateArgs, baggage) => {
);
const vowTools = prepareVowTools(zone.subZone('vows'));

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

const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit(
zone,
makeRecorderKit,
zcf,
privateArgs.timerService,
vowTools,
makeChainHub(privateArgs.agoricNames, vowTools),
chainHub,
);

// ----------------
Expand All @@ -56,6 +59,15 @@ export const start = async (zcf, privateArgs, baggage) => {
const BLD = zcf.getTerms().brands.In;
const bldAmountShape = await E(BLD).getAmountShape();

// XXX big dependency (59KB) but in production will probably already be registered in agoricNames
chainHub.registerChain('agoric', fetchedChainInfo.agoric);
chainHub.registerAsset('ubld', {
baseName: 'agoric',
baseDenom: 'ubld',
brand: BLD,
chainName: 'agoric',
});

async function makeLocalAccountKit() {
const account = await E(privateArgs.localchain).makeAccount();
const address = await E(account).getAddress();
Expand Down
21 changes: 18 additions & 3 deletions packages/orchestration/src/exos/chain-hub-admin.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/* we expect promises to resolved promptly, */
/* eslint-disable no-restricted-syntax */
import { M } from '@endo/patterns';
import { heapVowE } from '@agoric/vow/vat.js';
import { M } from '@endo/patterns';
import { CosmosChainInfoShape } from '../typeGuards.js';
import { DenomDetailShape } from './chain-hub.js';

/**
* @import {Zone} from '@agoric/zone';
* @import {CosmosChainInfo, IBCConnectionInfo} from '@agoric/orchestration';
* @import {ChainHub} from './chain-hub.js';
* @import {CosmosChainInfo, Denom, IBCConnectionInfo} from '@agoric/orchestration';
* @import {ChainHub, DenomDetail} from './chain-hub.js';
*/

/**
Expand All @@ -28,6 +29,7 @@ export const prepareChainHubAdmin = (zone, chainHub) => {
CosmosChainInfoShape,
ConnectionInfoShape,
).returns(M.undefined()),
registerAsset: M.call(M.string(), DenomDetailShape).returns(M.promise()),
}),
{
/**
Expand All @@ -48,6 +50,19 @@ export const prepareChainHubAdmin = (zone, chainHub) => {
connectionInfo,
);
},
/**
* Register an asset that may be held on a chain other than the issuing
* chain.
*
* @param {Denom} denom - on the holding chain, whose name is given in
* `detail.chainName`
* @param {DenomDetail} detail - chainName and baseName must be registered
*/
async registerAsset(denom, detail) {
// XXX async work necessary before the synchronous call
await heapVowE.when(chainHub.getChainInfo('agoric'));
chainHub.registerAsset(denom, detail);
},
},
);
return makeCreatorFacet;
Expand Down
24 changes: 23 additions & 1 deletion packages/orchestration/src/exos/chain-hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js';
* @property {string} baseName - name of issuing chain; e.g. cosmoshub
* @property {Denom} baseDenom - e.g. uatom
* @property {string} chainName - name of holding chain; e.g. agoric
* @property {Brand} [brand] - vbank brand, if registered
* @property {Brand<'nat'>} [brand] - vbank brand, if registered
* @see {ChainHub} `registerAsset` method
*/
/** @type {TypedPattern<DenomDetail>} */
Expand Down Expand Up @@ -168,6 +168,7 @@ const ChainHubI = M.interface('ChainHub', {
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())),
});

/**
Expand Down Expand Up @@ -199,6 +200,11 @@ export const makeChainHub = (agoricNames, vowTools) => {
keyShape: M.string(),
valueShape: DenomDetailShape,
});
/** @type {MapStore<Brand, string>} */
const brandDenoms = zone.mapStore('brandDenom', {
keyShape: BrandShape,
valueShape: M.string(),
});

const lookupChainInfo = vowTools.retriable(
zone,
Expand Down Expand Up @@ -380,15 +386,31 @@ export const makeChainHub = (agoricNames, vowTools) => {
chainInfos.has(baseName) ||
Fail`must register chain ${q(baseName)} first`;
denomDetails.init(denom, detail);
if (detail.brand) {
brandDenoms.init(detail.brand, denom);
}
},
/**
* Retrieve holding, issuing chain names etc. for a denom.
*
* @param {Denom} denom
* @returns {DenomDetail}
*/
lookupAsset(denom) {
return denomDetails.get(denom);
},
/**
* Retrieve holding, issuing chain names etc. for a denom.
*
* @param {Brand} brand
* @returns {string | undefined}
*/
lookupDenom(brand) {
if (brandDenoms.has(brand)) {
return brandDenoms.get(brand);
}
return undefined;
},
});

return chainHub;
Expand Down
Loading

0 comments on commit 42d1d4f

Please sign in to comment.