Skip to content

Commit

Permalink
fixup! feat: StatusManager scaffold
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Nov 6, 2024
1 parent 4e0f3d6 commit a9216ce
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 92 deletions.
8 changes: 5 additions & 3 deletions packages/fast-usdc/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
* Status values for the StatusManager. Listed in order:
*
* - 'OBSERVED': TX is observed via EventFeed
* - 'ADVANCED': advance funds are available and IBC transfer is initiated (but
* not necessarily settled)
* - 'SKIPPED': advance was skipped due to failed checks
* - 'ADVANCED': IBC transfer is initiated (but not necessarily settled)
* - 'SETTLED': settlement for matching advance received and funds dispersed
*
* @enum {(typeof TxStatus)[keyof typeof TxStatus]}
*/
export const TxStatus = /** @type {const} */ ({
/** when TX is observed via EventFeed */
Observed: 'OBSERVED',
/** advance funds are available and IBC transfer is initiated */
/** advance was skipped due to failed checks */
Skipped: 'SKIPPED',
/** IBC transfer is initiated */
Advanced: 'ADVANCED',
/** settlement for matching advance received and funds dispersed */
Settled: 'SETTLED',
Expand Down
67 changes: 22 additions & 45 deletions packages/fast-usdc/src/exos/README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,31 @@
## **StatusManager** sequence diagram, showing `TxStatus` state transitions
## **StatusManager** state diagram, showing `TxStatus` transitions

### Current Implementation

```mermaid
sequenceDiagram
participant I as Initial
participant O as OBSERVED
participant A as ADVANCED
participant S as SETTLED
Note over O,S: Currently Implemented State Transitions
I->>O: statusManager.observe()<br/>When TX observed via EventFeed
O->>A: statusManager.advance()<br/>When IBC transfer initiated
A->>S: statusManager.settle()<br/>When settlement received + dispersed
Note over O,A: Status updated when transfer starts
Note over A,S: Requires matching settlement transfer
stateDiagram-v2
[*] --> Observed: EventFeed .observe()
Observed --> Advanced: Advancer .advance()
Advanced --> Settled: Settler .settle() after fees
Observed --> Skipped: Advancer .skip()
Skipped --> Settled: Settler .settle() sans fees
Settled --> [*]
```

### Additional States to Consider (Not Implemented)
### Additional Scenarios to Consider

```mermaid
sequenceDiagram
participant I as Initial
participant O as OBSERVED
participant AG as ADVANCING
participant A as ADVANCED
participant S as SETTLED
participant AS as ADVANCE_SKIPPED
participant AF as ADVANCE_FAILED
participant OS as ORPHANED_SETTLE
Note over O,S: Normal Flow
I->>O: statusManager.observe()
O->>AG: initiate IBC transfer<br/>(if validation passes)
AG->>A: IBC transfer settled
A->>S: settlement received
Note over O,AS: Early Failures
O-->>AS: validation fails<br/>(missing chain config,<br/>invalid path,<br/>no pool funds)
Note over AG,AF: IBC Failures
AG-->>AF: timeout,<br/>unexpected error,<br/>insufficient funds
Note over O,OS: Edge Cases
I-->>OS: Settlement received without OBSERVED
O-->>S: OBSERVED->SETTLED without ADVANCING/ADVANCED<br/>is this a valid state transition?<br/>behavior is slowUSDC?
AG-->>S: Settlement received while ADVANCING<br/>wait for ADVANCED to SETTLE/disperse funds?
Note over AS,AF: Future: Queue for retry?
Note over OS: Future: Wait for OBSERVE?
stateDiagram-v2
[*] --> Observed: EventFeed .observe()
Observed --> Advanced: Advancer .advance()
Advanced --> Settled: Settler .settle() after fees
note right of Advanced
When IBC transfer starts;
needs to account for failure
end note
Observed --> Skipped: Advancer .skip()
Skipped --> Settled: Settler .settle() sans fees
Settled --> [*]
[*] --> [*]: Unobserved settlement
```
8 changes: 7 additions & 1 deletion packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,13 @@ export const prepareAdvancer = (
params: { EUD },
} = addressTools.getQueryParams(event.aux.recipientAddress);
if (!EUD) {
statusManager.skip(
event.tx.forwardingAddress,
event.tx.amount,
entryIndex,
);
throw makeError(
`'recipientAddress' does not contain EUD param: ${q(event.aux.recipientAddress)}`,
`recipientAddress does not contain EUD param: ${q(event.aux.recipientAddress)}`,
);
}

Expand All @@ -104,6 +109,7 @@ export const prepareAdvancer = (
// 2. ensure we can find chainID
// 3. ~~ensure valid PFM path~~ best observable via .transfer() vow rejection

// XXX this can throw, and should make a status update in the catch
const { chainId } = chainHub.getChainInfoByAddress(EUD);

/** @type {ChainAddress} */
Expand Down
4 changes: 2 additions & 2 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { M } from '@endo/patterns';
import { addressTools } from '../utils/address.js';

/**
* @import { FungibleTokenPacketData } from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.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 {NobleAddress} from '../types.js';
* @import {StatusManager} from './status-manager.js';
*/

Expand Down
61 changes: 46 additions & 15 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,23 @@ export const prepareStatusManager = zone => {
valueShape: M.arrayOf(StatusManagerValueShape),
});

/** @param {StatusManagerValue[]} entries */
const getPendingIdx = entries =>
entries.findIndex(
({ status }) =>
// TODO since we remove SETTLED, maybe this is not needed
// or could become `status !== TxStatus.Observed`
status === TxStatus.Advanced || status === TxStatus.Skipped,
);

return zone.exo(
'Fast USDC Status Manager',
M.interface('StatusManagerI', {
observe: M.call(CctpTxEvidenceShape).returns(M.number()),
advance: M.call(M.string(), M.bigint(), M.number()).returns(
M.undefined(),
),
skip: M.call(M.string(), M.bigint(), M.number()).returns(M.undefined()),
hasPendingSettlement: M.call(M.string(), M.bigint()).returns(M.boolean()),
settle: M.call(M.string(), M.bigint()).returns(M.undefined()),
view: M.call(M.string(), M.bigint())
Expand Down Expand Up @@ -82,6 +92,9 @@ export const prepareStatusManager = zone => {
advance(address, amount, index) {
const key = /** @type {StatusManagerKey} */ (`${address}-${amount}`);
const mutableEntries = [...txs.get(key)];
if (!mutableEntries[index]) {
throw makeError(`Cannot find ${q(key)} entry at index ${q(index)}`);
}
if (mutableEntries[index].status !== TxStatus.Observed) {
throw makeError(
`Cannot advance ${q(key)} entry ${q(index)} with status: ${q(mutableEntries[index].status)}`,
Expand All @@ -94,7 +107,32 @@ export const prepareStatusManager = zone => {
txs.set(key, harden(mutableEntries));
},
/**
* Find an `ADVANCED` tx waiting to be `SETTLED`
* Mark an `OBSERVED` transaction as `SKIPPED`.
*
* @param {string} address
* @param {bigint} amount
* @param {number} index
*/
skip(address, amount, index) {
const key = /** @type {StatusManagerKey} */ (`${address}-${amount}`);
const mutableEntries = [...txs.get(key)];
if (!mutableEntries[index]) {
throw makeError(`Cannot find ${q(key)} entry at index ${q(index)}`);
}
// TODO, consider ADVANCED -> SKIPPED for failed IBC Transfer
if (mutableEntries[index].status !== TxStatus.Observed) {
throw makeError(
`Cannot skip ${q(key)} entry ${q(index)} with status: ${q(mutableEntries[index].status)}`,
);
}
mutableEntries[index] = {
...mutableEntries[index],
status: TxStatus.Skipped,
};
txs.set(key, harden(mutableEntries));
},
/**
* Find an `ADVANCED` or `SKIPPED` tx waiting to be `SETTLED`
*
* @param {string} address
* @param {bigint} amount
Expand All @@ -103,11 +141,7 @@ export const prepareStatusManager = zone => {
hasPendingSettlement(address, amount) {
const key = /** @type {StatusManagerKey} */ (`${address}-${amount}`);
const entries = txs.get(key);
const unsettledIdx = entries.findIndex(
({ status }) => status === TxStatus.Advanced,
);
// TODO: if OBSERVED -> SETTLED is a valid transition, and
// (unsettledIdx > -1) === false, look for `OBSERVED` entry
const unsettledIdx = getPendingIdx(entries);
return unsettledIdx > -1;
},
/**
Expand All @@ -121,16 +155,13 @@ export const prepareStatusManager = zone => {
settle(address, amount) {
const key = /** @type {StatusManagerKey} */ (`${address}-${amount}`);
const mutableEntries = [...txs.get(key)];
const unsettledIdx = mutableEntries.findIndex(
({ status }) => status === TxStatus.Advanced,
);
const unsettledIdx = getPendingIdx(mutableEntries);
if (unsettledIdx === -1) {
throw makeError(`No pending advance found for ${q(key)}`);
throw makeError(`No unsettled entry for ${q(key)}`);
}
mutableEntries[unsettledIdx] = {
...mutableEntries[unsettledIdx],
status: TxStatus.Settled,
};
// TODO, vstorage update for `TxStatus.Settled`
// delete entry once SETTLED
mutableEntries.splice(unsettledIdx, 1);
txs.set(key, harden(mutableEntries));
},
/**
Expand All @@ -144,7 +175,7 @@ export const prepareStatusManager = zone => {
* @param {NobleAddress} address
* @param {bigint} amount
* @param {number} index
* @returns {StatusManagerValue}
* @returns {StatusManagerValue | undefined}
*/
/**
* View a StatusManagerValue or a list of them
Expand Down
30 changes: 30 additions & 0 deletions packages/fast-usdc/test/exos/advancer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,33 @@ test('advancer does not update status on failed transfer', async t => {
'"[Error: simulated error]"',
]);
});

test('advancer updated status to SKIPPED if pre-condition checks fail', async t => {
const { localDenom, makeAdvancer, statusManager, rootZone, vowTools } =
t.context;

const { poolAccount, poolAccountTransferVResolver } = prepareMockOrchAccounts(
rootZone.subZone('poolAcct2'),
{ vowTools, log: t.log },
);

const advancer = makeAdvancer({ poolAccount, localDenom });
t.truthy(advancer, 'advancer instantiates');

// simulate input from EventFeed
const mockCttpTxEvidence = MockCctpTxEvidences.AGORIC_NO_PARAMS();
t.throws(() => advancer.handleEvent(mockCttpTxEvidence), {
message:
'recipientAddress does not contain EUD param: "agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek"',
});

const entries = statusManager.view(
mockCttpTxEvidence.tx.forwardingAddress,
mockCttpTxEvidence.tx.amount,
);
t.deepEqual(
entries,
[{ ...mockCttpTxEvidence, status: TxStatus.Skipped }],
'tx status is still OBSERVED',
);
});
7 changes: 2 additions & 5 deletions packages/fast-usdc/test/exos/settler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,8 @@ test('StatusManger gets `SETTLED` update in happy path', async t => {
cctpTxEvidence.tx.amount,
index,
);
t.is(
entry?.status,
TxStatus.Settled,
'StatusManger entry updated with SETTLED status',
);
// TODO, confirm vstorage write for TxStatus.SETTLED
t.is(entry, undefined, 'SETTLED entry removed from StatusManger');
});

test.todo("StatusManager does not receive update when we can't settle");
Expand Down
Loading

0 comments on commit a9216ce

Please sign in to comment.