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: chainHub.makeTransferRoute() #10584

Merged
merged 5 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ Generated by [AVA](https://avajs.dev).
},
],
},
CoinShape: {
amount: Object @match:string {
payload: [],
},
denom: Object @match:string {
payload: [],
},
},
CosmosAssetInfoShape: Object @match:splitRecord {
payload: [
{
Expand Down Expand Up @@ -270,6 +278,46 @@ Generated by [AVA](https://avajs.dev).
DenomShape: Object @match:string {
payload: [],
},
ForwardInfoShape: {
forward: Object @match:splitRecord {
payload: [
{
channel: Object @match:string {
payload: [],
},
port: 'transfer',
receiver: Object @match:string {
payload: [],
},
retries: Object @match:kind {
payload: 'number',
},
timeout: Object @match:string {
payload: [],
},
},
{
next: {
forward: {
channel: Object @match:string {
payload: [],
},
port: 'transfer',
receiver: Object @match:string {
payload: [],
},
retries: Object @match:kind {
payload: 'number',
},
timeout: Object @match:string {
payload: [],
},
},
},
},
],
},
},
IBCChannelIDShape: Object @match:string {
payload: [],
},
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -539,14 +539,12 @@ Generated by [AVA](https://avajs.dev).
'ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4': {
baseDenom: 'uusdc',
baseName: 'noble',
brandKey: undefined,
chainName: 'osmosis',
},
'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9': {
baseDenom: 'uusdc',
baseName: 'noble',
brand: Object @Alleged: USDC brand {},
brandKey: 'USDC',
chainName: 'agoric',
},
uusdc: {
Expand Down
Binary file not shown.
47 changes: 47 additions & 0 deletions packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { Port } from '@agoric/network';
import type {
IBCChannelID,
IBCConnectionID,
IBCPortID,
VTransferIBCEvent,
} from '@agoric/vats';
import type {
Expand All @@ -34,7 +35,9 @@ import type {
RemoteIbcAddress,
} from '@agoric/vats/tools/ibc-utils.js';
import type { QueryDelegationTotalRewardsResponse } from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/query.js';
import type { Coin } from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js';
import type { AmountArg, ChainAddress, Denom, DenomAmount } from './types.js';
import { PFM_RECEIVER } from './exos/chain-hub.js';

/** An address for a validator on some blockchain, e.g., cosmos, eth, etc. */
export type CosmosValidatorAddress = ChainAddress & {
Expand Down Expand Up @@ -352,3 +355,47 @@ export type CosmosChainAccountMethods<CCI extends CosmosChainInfo> =
export type ICQQueryFunction = (
msgs: JsonSafe<RequestQuery>[],
) => Promise<JsonSafe<ResponseQuery>[]>;

/**
* Message structure for PFM memo
*
* @see {@link https://github.com/cosmos/chain-registry/blob/58b603bbe01f70e911e3ad2bdb6b90c4ca665735/_memo_keys/ICS20_memo_keys.json#L38-L60}
*/
export interface ForwardInfo {
forward: {
receiver: ChainAddress['value'];
port: IBCPortID;
channel: IBCChannelID;
/** e.g. '10min' */
timeout: string;
/** default is 3? */
retries: number;
next?: {
forward: ForwardInfo;
};
};
}

/**
* Object used to help build MsgTransfer parameters for IBC transfers.
*
* If `forwardInfo` is present:
* - it must be stringified and provided as the `memo` field value for
* use with `MsgTransfer`.
* - `receiver` will be set to `"pfm"` - purposely invalid bech32. see {@link https://github.com/cosmos/ibc-apps/blob/26f3ad8f58e4ffc7769c6766cb42b954181dc100/middleware/packet-forward-middleware/README.md#minimal-example---chain-forward-a-b-c}
*/
export type TransferRoute = {
/** typically, `transfer` */
sourcePort: string;
sourceChannel: IBCChannelID;
token: Coin;
} & (
| {
receiver: typeof PFM_RECEIVER;
/** contains PFM forwarding info */
forwardInfo: ForwardInfo;
}
| {
receiver: string;
}
);
146 changes: 143 additions & 3 deletions packages/orchestration/src/exos/chain-hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import { BrandShape } from '@agoric/ertp/src/typeGuards.js';
import { VowShape } from '@agoric/vow';
import {
ChainAddressShape,
CoinShape,
CosmosChainInfoShape,
DenomAmountShape,
DenomDetailShape,
ForwardInfoShape,
IBCChannelIDShape,
IBCConnectionInfoShape,
} from '../typeGuards.js';
import { getBech32Prefix } from '../utils/address.js';
Expand All @@ -16,12 +20,15 @@ import { getBech32Prefix } from '../utils/address.js';
* @import {NameHub} from '@agoric/vats';
* @import {Vow, VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {CosmosAssetInfo, CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api.js';
* @import {CosmosAssetInfo, CosmosChainInfo, ForwardInfo, IBCConnectionInfo, TransferRoute} from '../cosmos-api.js';
* @import {ChainInfo, KnownChains} from '../chain-info.js';
* @import {ChainAddress, Denom} from '../orchestration-api.js';
* @import {Remote} from '@agoric/internal';
* @import {ChainAddress, Denom, DenomAmount} from '../orchestration-api.js';
* @import {Remote, TypedPattern} from '@agoric/internal';
*/

/** receiver address value for ibc transfers that involve PFM */
export const PFM_RECEIVER = /** @type {const} */ ('pfm');

/**
* If K matches a known chain, narrow the type from generic ChainInfo
*
Expand Down Expand Up @@ -167,6 +174,26 @@ const ChainIdArgShape = M.or(
),
);

// TODO #9324 determine timeout defaults
const DefaultPfmTimeoutOpts = harden(
/** @type {const} */ ({
retries: 3,
timeout: '10min',
}),
);

/** @type {TypedPattern<TransferRoute>} */
export const TransferRouteShape = M.splitRecord(
{
sourcePort: M.string(),
sourceChannel: IBCChannelIDShape,
token: CoinShape,
receiver: M.string(),
},
{ forwardInfo: ForwardInfoShape },
{},
);

const ChainHubI = M.interface('ChainHub', {
registerChain: M.call(M.string(), CosmosChainInfoShape).returns(),
getChainInfo: M.call(M.string()).returns(VowShape),
Expand All @@ -181,6 +208,12 @@ const ChainHubI = M.interface('ChainHub', {
getAsset: M.call(M.string()).returns(M.or(DenomDetailShape, M.undefined())),
getDenom: M.call(BrandShape).returns(M.or(M.string(), M.undefined())),
makeChainAddress: M.call(M.string()).returns(ChainAddressShape),
makeTransferRoute: M.call(ChainAddressShape, DenomAmountShape, M.string())
.optional({
timeout: M.string(),
retries: M.number(),
})
.returns(M.or(M.undefined(), TransferRouteShape)),
});

/**
Expand Down Expand Up @@ -454,6 +487,113 @@ export const makeChainHub = (zone, agoricNames, vowTools) => {
encoding: /** @type {const} */ ('bech32'),
});
},
/**
* Determine the transfer route for a destination and amount given the
* current holding chain.
*
* XXX consider accepting AmountArg #10449
*
* @param {ChainAddress} destination
* @param {DenomAmount} denomAmount
* @param {string} holdingChainName
* @param {Pick<ForwardInfo['forward'], 'retries' | 'timeout'>} [forwardOpts]
* @returns {TransferRoute} single hop, multi hop
* @throws {Error} if unable to determine route
*/
makeTransferRoute(destination, denomAmount, holdingChainName, forwardOpts) {
chainInfos.has(holdingChainName) ||
Fail`chain info not found for holding chain: ${q(holdingChainName)}`;

const denomDetail = chainHub.getAsset(denomAmount.denom);
denomDetail ||
Fail`no denom detail for: ${q(denomAmount.denom)}. ensure it is registered in chainHub.`;

const { baseName, chainName } = /** @type {DenomDetail} */ (denomDetail);
chainName === holdingChainName ||
Fail`cannot transfer asset ${q(denomAmount.denom)}. held on ${q(chainName)} not ${q(holdingChainName)}.`;

// currently unreachable since we can't register an asset before a chain
chainInfos.has(baseName) ||
Fail`chain info not found for issuing chain: ${q(baseName)}`;

const { chainId: baseChainId, pfmEnabled } = chainInfos.get(baseName);

const holdingChainId = chainInfos.get(holdingChainName).chainId;

// asset is transferring to or from the issuing chain, return direct route
if (
baseChainId === destination.chainId ||
baseName === holdingChainName
) {
// TODO use getConnectionInfo once its sync
const connKey = connectionKey(holdingChainId, destination.chainId);
connectionInfos.has(connKey) ||
Fail`no connection info found for ${q(connKey)}`;

const { transferChannel } = denormalizeConnectionInfo(
holdingChainId, // from chain (primary)
destination.chainId, // to chain (counterparty)
connectionInfos.get(connKey),
);
return harden({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns a fresh value; so the method name should be makeTransferRoute.

sourcePort: transferChannel.portId,
sourceChannel: transferChannel.channelId,
token: {
amount: String(denomAmount.value),
denom: denomAmount.denom,
},
receiver: destination.value,
});
}

// asset is issued on a 3rd chain, attempt pfm route
pfmEnabled || Fail`pfm not enabled on issuing chain: ${q(baseName)}`;

// TODO use getConnectionInfo once its sync
const currToIssuerKey = connectionKey(holdingChainId, baseChainId);
connectionInfos.has(currToIssuerKey) ||
Fail`no connection info found for ${q(currToIssuerKey)}`;

const issuerToDestKey = connectionKey(baseChainId, destination.chainId);
connectionInfos.has(issuerToDestKey) ||
Fail`no connection info found for ${q(issuerToDestKey)}`;

const currToIssuer = denormalizeConnectionInfo(
holdingChainId,
baseChainId,
connectionInfos.get(currToIssuerKey),
);
const issuerToDest = denormalizeConnectionInfo(
baseChainId,
destination.chainId,
connectionInfos.get(issuerToDestKey),
);

/** @type {ForwardInfo} */
const forwardInfo = harden({
forward: {
receiver: destination.value,
port: issuerToDest.transferChannel.portId,
channel: issuerToDest.transferChannel.channelId,
...DefaultPfmTimeoutOpts,
...forwardOpts,
},
});
return harden({
sourcePort: currToIssuer.transferChannel.portId,
sourceChannel: currToIssuer.transferChannel.channelId,
token: {
amount: String(denomAmount.value),
denom: denomAmount.denom,
},
/**
* purposely using invalid bech32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is nice... but I'd rather see this comment on the type.

* {@link https://github.com/cosmos/ibc-apps/blob/26f3ad8f58e4ffc7769c6766cb42b954181dc100/middleware/packet-forward-middleware/README.md#minimal-example---chain-forward-a-b-c}
*/
receiver: PFM_RECEIVER,
forwardInfo,
});
},
});

return chainHub;
Expand Down
34 changes: 33 additions & 1 deletion packages/orchestration/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { M } from '@endo/patterns';

/**
* @import {TypedPattern} from '@agoric/internal';
* @import {ChainAddress, CosmosAssetInfo, Chain, ChainInfo, CosmosChainInfo, DenomAmount, DenomInfo, AmountArg, CosmosValidatorAddress, OrchestrationPowers} from './types.js';
* @import {ChainAddress, CosmosAssetInfo, Chain, ChainInfo, CosmosChainInfo, DenomAmount, DenomInfo, AmountArg, CosmosValidatorAddress, OrchestrationPowers, ForwardInfo} from './types.js';
* @import {Any as Proto3Msg} from '@agoric/cosmic-proto/google/protobuf/any.js';
* @import {TxBody} from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js';
* @import {Coin} from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js';
* @import {TypedJson} from '@agoric/cosmic-proto';
* @import {DenomDetail} from './exos/chain-hub.js';
*/
Expand Down Expand Up @@ -112,6 +113,14 @@ export const ChainInfoShape = M.splitRecord({
});
export const DenomShape = M.string();

/** @type {TypedPattern<Coin>} */
export const CoinShape = {
/** json-safe stringified bigint */
amount: M.string(),
denom: DenomShape,
};
harden(CoinShape);

/** @type {TypedPattern<DenomInfo<any, any>>} */
export const DenomInfoShape = {
chain: M.remotable('Chain'),
Expand Down Expand Up @@ -215,3 +224,26 @@ export const OrchestrationPowersShape = {
timerService: M.remotable(),
};
harden(OrchestrationPowersShape);

const ForwardArgsShape = {
receiver: M.string(),
port: 'transfer',
channel: M.string(),
timeout: M.string(),
retries: M.number(),
};
harden(ForwardArgsShape);

/** @type {TypedPattern<ForwardInfo>} */
export const ForwardInfoShape = {
forward: M.splitRecord(ForwardArgsShape, {
/**
* Protocol allows us to recursively include `next` keys, but this only
* supports one. In practice, this is all we currently need.
*/
next: {
forward: ForwardArgsShape,
},
}),
};
harden(ForwardInfoShape);
Loading
Loading