Skip to content

Commit

Permalink
feat: StatusManager scaffold
Browse files Browse the repository at this point in the history
- shows the various `StatusManager` MapStore states updates (`OBSERVED`, `ADVANCED`) via `Advancer` and `Settler` stubs
  • Loading branch information
0xpatrickdev committed Nov 11, 2024
1 parent 631deab commit 1d2679d
Show file tree
Hide file tree
Showing 12 changed files with 894 additions and 15 deletions.
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
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)

```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
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)}`,
);
}

// 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

0 comments on commit 1d2679d

Please sign in to comment.