Skip to content

Commit

Permalink
feat: chainHub.makeTransferRoute
Browse files Browse the repository at this point in the history
- chainHub helper to return .transfer details for ibc transfer that can have multiple hops (via pfm)
  • Loading branch information
0xpatrickdev committed Nov 28, 2024
1 parent 9a65478 commit 0215b6f
Show file tree
Hide file tree
Showing 4 changed files with 573 additions and 5 deletions.
26 changes: 26 additions & 0 deletions packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,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 @@ -373,3 +375,27 @@ export interface 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({
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
* {@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

0 comments on commit 0215b6f

Please sign in to comment.