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

spike: fusdc to evm and solana #10967

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
131 changes: 106 additions & 25 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const AdvancerVowCtxShape = M.splitRecord(
destination: ChainAddressShape,
forwardingAddress: M.string(),
txHash: EvmHashShape,
toIntermediate: M.opt(M.boolean()),
},
{ tmpSeat: M.remotable() },
);
Expand Down Expand Up @@ -81,21 +82,45 @@ const AdvancerKitI = harden({
}),
});

/**
* Address hooks only deal in strings, so see if we can parse
* chainId to an integer.
* @param {string} chainId
*/
const formatChainId = chainId => {
Copy link
Member

Choose a reason for hiding this comment

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

why not just leave it as a string? there's no arithmetic being performed.

Copy link
Member Author

Choose a reason for hiding this comment

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

For eip155 chains, chain ID is uint, so the thinking was to preserve that

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know of a current use case, but could imagine a smart contract function signature that expects an integer for a chainId parameter. Perhaps these types of functions can handle this concern themselves instead of the orchestration api.

For eth ecosystem chain ids, let's please at least use {number} for the type value.

const asInt = parseInt(chainId, 10);
return !Number.isNaN(asInt) ? asInt : chainId;
};

/**
* @typedef {{
* fullAmount: NatAmount;
* advanceAmount: NatAmount;
* destination: ChainAddress;
* forwardingAddress: NobleAddress;
* txHash: EvmHash;
* toIntermediate?: boolean;
* }} AdvancerVowCtx
*/

/**
* @typedef {{
* notifier: import('./settler.js').SettlerKit['notifier'];
* borrower: LiquidityPoolKit['borrower'];
* poolAccount: HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>;
* settlementAddress: ChainAddress;
* intermediateRecipientAddress?: ChainAddress;
* }} AdvancerConfig
*/

/** @typedef {AdvancerConfig & { intermediateRecipient: OrchestrationAccount<{chainId: 'noble-1'}> | undefined }} AdvancerState */

export const stateShape = harden({
notifier: M.remotable(),
borrower: M.remotable(),
poolAccount: M.remotable(),
intermediateRecipient: M.opt(ChainAddressShape),
intermediateRecipient: M.opt(M.remotable()),
intermediateRecipientAddress: M.opt(ChainAddressShape),
settlementAddress: M.opt(ChainAddressShape),
});

Expand Down Expand Up @@ -131,20 +156,17 @@ export const prepareAdvancerKit = (
'Fast USDC Advancer',
AdvancerKitI,
/**
* @param {{
* notifier: import('./settler.js').SettlerKit['notifier'];
* borrower: LiquidityPoolKit['borrower'];
* poolAccount: HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>;
* settlementAddress: ChainAddress;
* intermediateRecipient?: ChainAddress;
* }} config
* @param {AdvancerConfig} config
*/
config =>
harden({
...config,
// make sure the state record has this property, perhaps with an undefined value
intermediateRecipient: config.intermediateRecipient,
}),
/** @type {AdvancerState}*/ (
harden({
...config,
intermediateRecipient: undefined,
// make sure the state record has this property, perhaps with an undefined value
intermediateRecipientAddress: config.intermediateRecipientAddress,
})
),
{
advancer: {
/**
Expand Down Expand Up @@ -177,10 +199,19 @@ export const prepareAdvancerKit = (
if (decoded.baseAddress !== settlementAddress.value) {
throw Fail`⚠️ baseAddress of address hook ${q(decoded.baseAddress)} does not match the expected address ${q(settlementAddress.value)}`;
}
const { EUD } = /** @type {AddressHook['query']} */ (decoded.query);
log(`decoded EUD: ${EUD}`);
// throws if the bech32 prefix is not found
const destination = chainHub.makeChainAddress(EUD);
const { EUD, CID } = /** @type {AddressHook['query']} */ (
decoded.query
);
log(`decoded EUD: ${EUD}, CID: ${CID}`);

const destination = CID
? harden({
value: EUD,
chainId: formatChainId(CID),
// note: omitting encoding
})
: // only works for bech32 addrs; throws if prefix is not found in ChainHub
chainHub.makeChainAddress(EUD);

const fullAmount = toAmount(evidence.tx.amount);
const { borrower, notifier, poolAccount } = this.state;
Expand Down Expand Up @@ -220,9 +251,16 @@ export const prepareAdvancerKit = (
statusManager.observe(evidence);
}
},
/** @param {ChainAddress} intermediateRecipient */
setIntermediateRecipient(intermediateRecipient) {
/**
* @param {OrchestrationAccount<{chainId: 'noble-1'}>} intermediateRecipient
@param {ChainAddress} intermediateRecipientAddress */
setIntermediateRecipient(
intermediateRecipient,
intermediateRecipientAddress,
) {
this.state.intermediateRecipient = intermediateRecipient;
this.state.intermediateRecipientAddress =
intermediateRecipientAddress;
},
},
depositHandler: {
Expand All @@ -231,24 +269,50 @@ export const prepareAdvancerKit = (
* @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
*/
onFulfilled(result, ctx) {
const { poolAccount, intermediateRecipient, settlementAddress } =
this.state;
const {
poolAccount,
intermediateRecipientAddress,
settlementAddress,
} = this.state;
const { destination, advanceAmount, tmpSeat, ...detail } = ctx;
tmpSeat.exit();
const amount = harden({
denom: usdc.denom,
value: advanceAmount.value,
});

// TODO: use destination.chainId to determine if non-cosmos/ibc from ChainInfo
// either: ecosystem: 'evm', 'solana', 'cosmos' etc, or maybe assert connections.length?
// use absence of encoding: 'bech32' for now
const toIntermediate = destination.encoding !== 'bech32';

if (!intermediateRecipientAddress)
throw Fail`no 'intermediateRecipientAddress' found`;
const transferDest = toIntermediate
? intermediateRecipientAddress
: destination;

/**
* To agoric (`type: local`): use bank/Send
* To `type:cosmos`: use .transfer to EUD (PFM might be autogen'ed)
* To `type:evm|cosmos`: 1) use .transfer to NobleICA (intermediateRecipient, or a new ICA?)
* and 2) call depositForBurn with EUD as destination
*/
const transferOrSendV =
destination.chainId === settlementAddress.chainId
? E(poolAccount).send(destination, amount)
: E(poolAccount).transfer(destination, amount, {
forwardOpts: { intermediateRecipient },
: E(poolAccount).transfer(transferDest, amount, {
forwardOpts: {
intermediateRecipient: intermediateRecipientAddress,
},
});

return watch(transferOrSendV, this.facets.transferHandler, {
destination,
advanceAmount,
...detail,
// something to indicate we need to call depositForBurn
toIntermediate,
});
},
/**
Expand Down Expand Up @@ -283,8 +347,25 @@ export const prepareAdvancerKit = (
* @param {AdvancerVowCtx} ctx
*/
onFulfilled(result, ctx) {
const { notifier } = this.state;
const { advanceAmount, destination, ...detail } = ctx;
const { intermediateRecipient, notifier } = this.state;
const { advanceAmount, destination, toIntermediate, ...detail } = ctx;
if (toIntermediate) {
if (!intermediateRecipient)
throw Fail`no 'intermediateRecipient' found`;
const depositForBurnV = E(intermediateRecipient).depositForBurn(
destination,
harden({ denom: 'uusdc', value: advanceAmount.value }),
);
return watch(
depositForBurnV,
// TODO: worth a separate Settler handler, as $ won't be in the advancer account?
this.facets.transferHandler,
{
...ctx,
toIntermediate: false, // so we don't call depositForBurn twice
},
);
}
log('Advance succeeded', { advanceAmount, destination });
// During development, due to a bug, this call threw.
// The failure was silent (no diagnostics) due to:
Expand Down
2 changes: 1 addition & 1 deletion packages/fast-usdc/src/type-guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ harden(PendingTxShape);
/** @type {TypedPattern<AddressHook>} */
export const AddressHookShape = {
baseAddress: M.string(),
query: { EUD: M.string() },
query: M.splitRecord({ EUD: M.string() }, { CID: M.string() }, {}),
};
harden(AddressHookShape);

Expand Down
2 changes: 2 additions & 0 deletions packages/fast-usdc/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export type AddressHook = {
query: {
/** end user destination address */
EUD: string;
/** chain id for end user destination. necessary if EUD is not bech32 */
Copy link
Member

@turadg turadg Feb 14, 2025

Choose a reason for hiding this comment

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

in CAIP-10 parlance,
CID: chain_id (à la CAIP-2)
EUD: account_address

It also has an account_id definition (chain_id + ":" + account_address). I propose the "End User Destination" be that full "account_id". The enclosing object is a "query" and it's not sensical to change CID independent of EUD. IOW, EUD needs to specify the full chain-agnostic account ID.

For backwards compatibility we can make the chain_id implied.

CID?: string;
};
};

Expand Down
100 changes: 100 additions & 0 deletions packages/fast-usdc/test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const mockScenarios = [
'AGORIC_PLUS_AGORIC',
'AGORIC_NO_PARAMS',
'AGORIC_UNKNOWN_EUD',
'AGORIC_PLUS_SOLANA',
'AGORIC_PLUS_BASE',
'AGORIC_PLUS_BASE_NO_CHAIN_ID',
] as const;

type MockScenario = (typeof mockScenarios)[number];
Expand Down Expand Up @@ -43,6 +46,7 @@ export const MockCctpTxEvidences: Record<
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
CID: 'osmosis-1',
}),
},
chainId: 1,
Expand All @@ -65,6 +69,7 @@ export const MockCctpTxEvidences: Record<
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
CID: 'dydx-mainnet-1',
}),
},
chainId: 1,
Expand All @@ -87,6 +92,7 @@ export const MockCctpTxEvidences: Record<
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: 'agoric13rj0cc0hm5ac2nt0sdup2l7gvkx4v9tyvgq3h2',
CID: 'agoric-3',
}),
},
chainId: 1,
Expand Down Expand Up @@ -127,6 +133,72 @@ export const MockCctpTxEvidences: Record<
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: 'random1addr',
CID: 'random-1',
}),
},
chainId: 1,
}),
AGORIC_PLUS_SOLANA: (receiverAddress?: string) => ({
blockHash:
'0x70d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699',
blockNumber: 21037669n,
txHash:
'0xaa1bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799',
tx: {
amount: 210000000n,
forwardingAddress: 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelyyy',
sender: Senders.default,
},
aux: {
forwardingChannel: 'channel-21',
recipientAddress:
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: 'EUdL1XDvkcu7xAE5iack1h6zbR8k6wCebTfmtQGk8fFS',
CID: 'solana',
}),
},
chainId: 1,
}),
AGORIC_PLUS_BASE: (receiverAddress?: string) => ({
blockHash:
'0x70d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699',
blockNumber: 21037669n,
txHash:
'0xba1bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799',
tx: {
amount: 210000000n,
forwardingAddress: 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelyyy',
sender: Senders.default,
},
aux: {
forwardingChannel: 'channel-21',
recipientAddress:
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: '0xe0d43135EBd2593907F8f56c25ADC1Bf94FCf993',
CID: '8453', // integer, but only string permitted
}),
},
chainId: 1,
}),
AGORIC_PLUS_BASE_NO_CHAIN_ID: (receiverAddress?: string) => ({
blockHash:
'0x70d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699',
blockNumber: 21037669n,
txHash:
'0xba1bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799',
tx: {
amount: 210000000n,
forwardingAddress: 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelyyy',
sender: Senders.default,
},
aux: {
forwardingChannel: 'channel-21',
recipientAddress:
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: '0xe0d43135EBd2593907F8f56c25ADC1Bf94FCf993',
}),
},
chainId: 1,
Expand Down Expand Up @@ -192,6 +264,34 @@ export const MockVTransferEvents: Record<
recieverAddress ||
MockCctpTxEvidences.AGORIC_UNKNOWN_EUD().aux.recipientAddress,
}),
AGORIC_PLUS_SOLANA: (recieverAddress?: string) =>
buildVTransferEvent({
...nobleDefaultVTransferParams,
amount: MockCctpTxEvidences.AGORIC_PLUS_SOLANA().tx.amount,
sender: MockCctpTxEvidences.AGORIC_PLUS_SOLANA().tx.forwardingAddress,
receiver:
recieverAddress ||
MockCctpTxEvidences.AGORIC_PLUS_SOLANA().aux.recipientAddress,
}),
AGORIC_PLUS_BASE: (recieverAddress?: string) =>
buildVTransferEvent({
...nobleDefaultVTransferParams,
amount: MockCctpTxEvidences.AGORIC_PLUS_BASE().tx.amount,
sender: MockCctpTxEvidences.AGORIC_PLUS_BASE().tx.forwardingAddress,
receiver:
recieverAddress ||
MockCctpTxEvidences.AGORIC_PLUS_BASE().aux.recipientAddress,
}),
AGORIC_PLUS_BASE_NO_CHAIN_ID: (recieverAddress?: string) =>
buildVTransferEvent({
...nobleDefaultVTransferParams,
amount: MockCctpTxEvidences.AGORIC_PLUS_BASE_NO_CHAIN_ID().tx.amount,
sender:
MockCctpTxEvidences.AGORIC_PLUS_BASE_NO_CHAIN_ID().tx.forwardingAddress,
receiver:
recieverAddress ||
MockCctpTxEvidences.AGORIC_PLUS_BASE_NO_CHAIN_ID().aux.recipientAddress,
}),
};

export const intermediateRecipient: ChainAddress = harden({
Expand Down
11 changes: 11 additions & 0 deletions packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface CosmosAssetInfo extends Record<string, unknown> {
export type CosmosChainInfo = Readonly<{
/** can be used to lookup chainInfo (chainId) from an address value */
bech32Prefix?: string;
cctpDestinationDomain?: number;
chainId: string;

connections?: Record<string, IBCConnectionInfo>; // chainId or wellKnownName
Expand Down Expand Up @@ -300,6 +301,16 @@ export interface LiquidStakingMethods {
liquidStake: (amount: AmountArg) => Promise<void>;
}

export interface NobleMethods {
/** burn USDC on Noble and mint on a destination chain via CCTP */
depositForBurn: (
mintRecipient: ChainAddress,
amount: AmountArg, // is bigint better?
) => Promise<void>;
// consider including `registerForwardingAccount` (`MsgRegisterAccount`), so a contract can create its own forwarding address
// Requires `noble/forwarding` protos: https://github.com/noble-assets/forwarding/blob/main/proto/noble/forwarding/v1/tx.proto
}

// TODO support StakingAccountQueries
/** Methods supported only on Agoric chain accounts */
export interface LocalAccountMethods extends StakingAccountActions {
Expand Down
Loading
Loading