Skip to content

Commit

Permalink
feat: StatusManager scaffold
Browse files Browse the repository at this point in the history
- shows the various `StatusManager` states updates (`OBSERVED`, `ADVANCED`, `SETTLED`) via `Advancer` and `Settler` stubs
  • Loading branch information
0xpatrickdev committed Nov 5, 2024
1 parent 06c998f commit af07f34
Show file tree
Hide file tree
Showing 8 changed files with 612 additions and 13 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
135 changes: 129 additions & 6 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,142 @@
import { assertAllDefined } from '@agoric/internal';
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, NobleAddress} 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 {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, 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(), {
address: M.string(),
amount: M.bigint(),
index: M.number(),
}).returns(M.undefined()),
onRejected: M.call(M.error(), {
address: M.string(),
amount: M.bigint(),
index: M.number(),
}).returns(M.undefined()),
}),
{
/**
* @param {undefined} result
* @param {{ address: NobleAddress; amount: bigint; index: number; }} ctx
*/
onFulfilled(result, { address, amount, index }) {
// TODO, endow with logger
console.log('@@@fulfilled', { address, amount, index, result });
statusManager.advance(address, amount, index);
},
onRejected(error) {
// XXX retry logic?
// What do we do if we fail, should we keep a Status?
// TODO, endow with logger
console.log('@@@rejected', { error });
},
},
);

return zone.exoClass(
'Fast USDC Advancer',
M.interface('AdvancerI', {
handleEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
}),
/**
* @param {{
* localDenom: Denom;
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
* }} config
*/
config => harden(config),
{
/**
* TODO riff on name. for now, assume this is invoked when a new entry is
* observed via the EventFeed.
*
* @param {CctpTxEvidence} event
*/
handleEvent(event) {
// observe regardless of validation checks
// TODO - should we only observe after validation checks?
const entryIndex = statusManager.observe(event);

const {
params: { EUD },
} = addressTools.getQueryParams(event.aux.recipientAddress);
if (!EUD) {
throw makeError(
`'recipientAddress' does not contain EUD param: ${q(event.aux.recipientAddress)}`,
);
}

// TODO validation checks:
// 1. ensure there's enough $
// 2. ensure we can find chainID
// 3. ~~ensure valid PFM path~~ best observable via .transfer() vow rejection

const { chainId } = chainHub.getChainInfoByAddress(EUD);

/** @type {ChainAddress} */
const destination = harden({
chainId,
value: EUD,
encoding: /** @type {const} */ ('bech32'),
});

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

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

// onFulfilled, update StatusManger with `SETTLED`
// onRejected, TBD
return watch(transferV, transferHandler, {
index: entryIndex,
address: event.tx.forwardingAddress,
amount: amount.value,
});
},
},
{
stateShape: harden({
localDenom: M.string(),
poolAccount: M.remotable(),
}),
},
);
};
harden(prepareAdvancer);
84 changes: 81 additions & 3 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,95 @@
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) {
// 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;
}

const hasPendingSettlement = statusManager.hasPendingSettlement(
tx.sender,
BigInt(tx.amount),
);
if (!hasPendingSettlement) {
// TODO FAILURE PATH -> put money in recovery account or .transfer to receiver
// TODO should we have a 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 af07f34

Please sign in to comment.