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: fusdc status manager states #10406

Merged
merged 9 commits into from
Nov 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ Generated by [AVA](https://avajs.dev).
},
},
},
denomHash: Function denomHash {},
prepareChainHubAdmin: Function prepareChainHubAdmin {},
prepareCosmosInterchainService: Function prepareCosmosInterchainService {},
withOrchestration: Function withOrchestration {},
Expand Down
Binary file not shown.
1 change: 1 addition & 0 deletions packages/fast-usdc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@agoric/orchestration": "^0.1.0",
"@agoric/store": "^0.9.2",
"@agoric/vow": "^0.1.0",
"@endo/base64": "^1.0.8",
"@endo/common": "^1.2.7",
"@endo/errors": "^1.2.7",
"@endo/eventual-send": "^1.2.7",
Expand Down
27 changes: 27 additions & 0 deletions packages/fast-usdc/src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Status values for FastUSDC.
*
* @enum {(typeof TxStatus)[keyof typeof TxStatus]}
*/
export const TxStatus = /** @type {const} */ ({
/** tx was observed but not advanced */
Observed: 'OBSERVED',
/** IBC transfer is initiated */
Advanced: 'ADVANCED',
/** settlement for matching advance received and funds dispersed */
Settled: 'SETTLED',
});
harden(TxStatus);

/**
* Status values for the StatusManager.
*
* @enum {(typeof PendingTxStatus)[keyof typeof PendingTxStatus]}
*/
export const PendingTxStatus = /** @type {const} */ ({
/** tx was observed but not advanced */
Observed: 'OBSERVED',
/** IBC transfer is initiated */
Advanced: 'ADVANCED',
});
harden(PendingTxStatus);
26 changes: 26 additions & 0 deletions packages/fast-usdc/src/exos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## **StatusManager** state diagram, showing different transitions


### Contract state diagram

*Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.*

```mermaid
stateDiagram-v2
[*] --> Advanced: Advancer .advance()
Advanced --> Settled: Settler .settle() after fees
[*] --> Observed: Advancer .observed()
Observed --> Settled: Settler .settle() sans fees
Settled --> [*]
```

### Complete state diagram (starting from OCW)
Copy link
Member

Choose a reason for hiding this comment

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

WFM


```mermaid
stateDiagram-v2
Observed --> Qualified
Observed --> Unqualified
Qualified --> Advanced
Advanced --> Settled
Qualified --> Settled
```
118 changes: 112 additions & 6 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,125 @@
import { assertAllDefined } from '@agoric/internal';
import { ChainAddressShape } from '@agoric/orchestration';
import { VowShape } from '@agoric/vow';
import { makeError, q } from '@endo/errors';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../typeGuards.js';
import { addressTools } from '../utils/address.js';

/**
* @import {HostInterface} from '@agoric/async-flow';
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
* @import {VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {TransactionFeed} from './transaction-feed.js';
* @import {CctpTxEvidence, LogFn} from '../types.js';
* @import {StatusManager} from './status-manager.js';
* @import {TransactionFeed} from './transaction-feed.js';
*/

import { assertAllDefined } from '@agoric/internal';

/**
* @param {Zone} zone
* @param {object} caps
* @param {ChainHub} caps.chainHub
* @param {TransactionFeed} caps.feed
* @param {LogFn} caps.log
* @param {StatusManager} caps.statusManager
* @param {VowTools} caps.vowTools
*/
export const prepareAdvancer = (zone, { feed, statusManager }) => {
assertAllDefined({ feed, statusManager });
return zone.exo('Fast USDC Advancer', undefined, {});
export const prepareAdvancer = (
zone,
{ chainHub, feed, log, statusManager, vowTools: { watch } },
) => {
assertAllDefined({ feed, statusManager, watch });

const transferHandler = zone.exo(
'Fast USDC Advance Transfer Handler',
M.interface('TransferHandlerI', {
// TODO confirm undefined, and not bigint (sequence)
onFulfilled: M.call(M.undefined(), {
amount: M.bigint(),
destination: ChainAddressShape,
}).returns(M.undefined()),
onRejected: M.call(M.error(), {
amount: M.bigint(),
destination: ChainAddressShape,
}).returns(M.undefined()),
}),
{
/**
* @param {undefined} result TODO confirm this is not a bigint (sequence)
* @param {{ destination: ChainAddress; amount: bigint; }} ctx
*/
onFulfilled(result, { destination, amount }) {
log(
'Advance transfer fulfilled',
q({ amount, destination, result }).toString(),
);
},
onRejected(error) {
// XXX retry logic?
// What do we do if we fail, should we keep a Status?
log('Advance transfer rejected', q(error).toString());
},
},
);

return zone.exoClass(
'Fast USDC Advancer',
M.interface('AdvancerI', {
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
}),
/**
* @param {{
* localDenom: Denom;
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
* }} config
*/
config => harden(config),
{
/** @param {CctpTxEvidence} evidence */
handleTransactionEvent(evidence) {
// TODO EventFeed will perform input validation checks.
const { recipientAddress } = evidence.aux;
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
if (!EUD) {
statusManager.observe(evidence);
throw makeError(
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
);
}

// TODO #10391 this can throw, and should make a status update in the catch
const destination = chainHub.makeChainAddress(EUD);

/** @type {DenomAmount} */
const requestedAmount = harden({
denom: this.state.localDenom,
value: BigInt(evidence.tx.amount),
});

// TODO #10391 ensure there's enough funds in poolAccount

const transferV = E(this.state.poolAccount).transfer(
destination,
requestedAmount,
);

// mark as Advanced since `transferV` initiates the advance
statusManager.advance(evidence);

return watch(transferV, transferHandler, {
destination,
amount: requestedAmount.value,
});
},
},
{
stateShape: harden({
localDenom: M.string(),
poolAccount: M.remotable(),
}),
},
);
};
harden(prepareAdvancer);
87 changes: 84 additions & 3 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,98 @@
import { assertAllDefined } from '@agoric/internal';
import { atob } from '@endo/base64';
import { makeError, q } from '@endo/errors';
import { M } from '@endo/patterns';

import { addressTools } from '../utils/address.js';

/**
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
* @import {Denom} from '@agoric/orchestration';
* @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats';
* @import {Zone} from '@agoric/zone';
* @import {NobleAddress} from '../types.js';
* @import {StatusManager} from './status-manager.js';
*/

import { assertAllDefined } from '@agoric/internal';

/**
* @param {Zone} zone
* @param {object} caps
* @param {StatusManager} caps.statusManager
*/
export const prepareSettler = (zone, { statusManager }) => {
assertAllDefined({ statusManager });
return zone.exo('Fast USDC Settler', undefined, {});
return zone.exoClass(
'Fast USDC Settler',
M.interface('SettlerI', {
receiveUpcall: M.call(M.record()).returns(M.promise()),
}),
/**
*
* @param {{
* sourceChannel: IBCChannelID;
* remoteDenom: Denom
* }} config
*/
config => harden(config),
{
/** @param {VTransferIBCEvent} event */
async receiveUpcall(event) {
if (event.packet.source_channel !== this.state.sourceChannel) {
// TODO #10390 log all early returns
// only interested in packets from the issuing chain
Copy link
Member

Choose a reason for hiding this comment

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

why would Settler get here? seems worth logging. Same goes for all these checks with return

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok planning to tackle these as part of #10391, will add a TODO. These are also the main callouts from yarn test:c8

why would Settler get here?

An actor could decide to send an IBC transfer to this address

return;
}
const tx = /** @type {FungibleTokenPacketData} */ (
JSON.parse(atob(event.packet.data))
);
if (tx.denom !== this.state.remoteDenom) {
// only interested in uusdc
return;
}

if (!addressTools.hasQueryParams(tx.receiver)) {
// only interested in receivers with query params
return;
}

const { params } = addressTools.getQueryParams(tx.receiver);
// TODO - what's the schema address parameter schema for FUSDC?
if (!params?.EUD) {
// only interested in receivers with EUD parameter
return;
}

// TODO discern between SETTLED and OBSERVED; each has different fees/destinations
const hasPendingSettlement = statusManager.hasPendingSettlement(
// given the sourceChannel check, we can be certain of this cast
/** @type {NobleAddress} */ (tx.sender),
BigInt(tx.amount),
);
if (!hasPendingSettlement) {
// TODO FAILURE PATH -> put money in recovery account or .transfer to receiver
// TODO should we have an ORPHANED TxStatus for this?
throw makeError(
`🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,
Copy link
Member

Choose a reason for hiding this comment

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

Is this recoverable? It looks like it.

Suggested change
`🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,
`⚠️ No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,

per Logging docs:

🚨 indicates something that should never happen.

⚠️ indicates something that is not expected but was recovered from.

Copy link
Member Author

Choose a reason for hiding this comment

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

🚨 was intentional - I think we should leave it until we recover from here

Copy link
Member Author

Choose a reason for hiding this comment

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

We might want to capture this in the StatusManager with an ORPHANED state, and add some logic in the observe() method to initiate a SETTLE / settlement

Copy link
Member

Choose a reason for hiding this comment

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

Let's come back to this when we're going all the error cases

);
}

// TODO disperse funds
// ~1. fee to contractFeeAccount
// ~2. remainder in poolAccount

// update status manager, marking tx `SETTLED`
statusManager.settle(
/** @type {NobleAddress} */ (tx.sender),
BigInt(tx.amount),
);
},
},
{
stateShape: harden({
sourceChannel: M.string(),
remoteDenom: M.string(),
}),
},
);
};
harden(prepareSettler);
Loading
Loading