Skip to content

Commit

Permalink
feat: support non-cosmos EUDs
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Feb 7, 2025
1 parent f8f0a14 commit 6912ab5
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 27 deletions.
142 changes: 116 additions & 26 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 All @@ -71,21 +72,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 => {
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 @@ -121,20 +146,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 @@ -167,10 +189,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 @@ -210,9 +241,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 @@ -221,23 +259,49 @@ 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;
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 @@ -271,8 +335,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 All @@ -289,8 +370,17 @@ export const prepareAdvancerKit = (
onRejected(error, ctx) {
const { notifier } = this.state;
log('Advance failed', error);
const { advanceAmount: _, ...restCtx } = ctx;
const { advanceAmount: _, toIntermediate: __, ...restCtx } = ctx;
notifier.notifyAdvancingResult(restCtx, false);
// TODO FIX:
// 1. withdraw advanceAmount from poolAccount
// 2. returnToPool

// TODO (maybe in a separate handler)
// If DepositForBurn failed:
// 1. transfer back to poolAccount (no hook)
// 2. withdraw advance amount
// 3. returnToPool
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ export const prepareSettler = (
forward(txHash, fullValue, EUD) {
const { settlementAccount, intermediateRecipient } = this.state;
const dest = chainHub.makeChainAddress(EUD);
// TODO: should use bank send if forwarding to an agoric address
// XXX: can we factor/reuse code from Advancer here?
const txfrV = E(settlementAccount).transfer(
dest,
AmountMath.make(USDC, fullValue),
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 @@ -104,7 +104,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 @@ -130,6 +130,8 @@ export type AddressHook = {
query: {
/** end user destination address */
EUD: string;
/** chain id for end user destination. necessary if EUD is not bech32 */
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 @@ -40,6 +43,7 @@ export const MockCctpTxEvidences: Record<
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
CID: 'osmosis-1',
}),
},
chainId: 1,
Expand All @@ -61,6 +65,7 @@ export const MockCctpTxEvidences: Record<
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
CID: 'dydx-mainnet-1',
}),
},
chainId: 1,
Expand All @@ -82,6 +87,7 @@ export const MockCctpTxEvidences: Record<
receiverAddress ||
encodeAddressHook(settlementAddress.value, {
EUD: 'agoric13rj0cc0hm5ac2nt0sdup2l7gvkx4v9tyvgq3h2',
CID: 'agoric-3',
}),
},
chainId: 1,
Expand Down Expand Up @@ -120,6 +126,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 @@ -185,6 +257,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

0 comments on commit 6912ab5

Please sign in to comment.