Skip to content

Commit

Permalink
chore(fast-usdc): revise state handling in status manager
Browse files Browse the repository at this point in the history
  • Loading branch information
dckc committed Nov 22, 2024
1 parent 9c8d83f commit 3211e61
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 52 deletions.
2 changes: 1 addition & 1 deletion packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const prepareAdvancerKit = (
try {
// Mark as Advanced since `transferV` initiates the advance.
// Will throw if we've already .skipped or .advanced this evidence.
statusManager.advance(evidence);
statusManager.advancing(evidence);
} catch (e) {
// Only anticipated error is `assertNotSeen`, so intercept the
// catch so we don't call .skip which also performs this check
Expand Down
97 changes: 72 additions & 25 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { AmountMath } from '@agoric/ertp';
import { assertAllDefined, makeTracer } from '@agoric/internal';
import { atob } from '@endo/base64';
import { makeError, q } from '@endo/errors';
import { E } from '@endo/far';
import { M } from '@endo/patterns';

import { PendingTxStatus } from '../constants.js';
import { addressTools } from '../utils/address.js';
import { makeFeeTools } from '../utils/fees.js';
import { EvmHashShape } from '../type-guards.js';

/**
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
Expand All @@ -17,12 +17,21 @@ import { makeFeeTools } from '../utils/fees.js';
* @import {Zone} from '@agoric/zone';
* @import {HostOf, HostInterface} from '@agoric/async-flow';
* @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js';
* @import {NobleAddress, LiquidityPoolKit, FeeConfig} from '../types.js';
* @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash} from '../types.js';
* @import {StatusManager} from './status-manager.js';
*/

const trace = makeTracer('Settler');

/**
* NOTE: not meant to be parsable.
*
* @param {NobleAddress} addr
* @param {bigint} amount
*/
const makeMintedEarlyKey = (addr, amount) =>
`pendingTx:${JSON.stringify([addr, String(amount)])}`;

/**
* @param {Zone} zone
* @param {object} caps
Expand All @@ -42,9 +51,11 @@ export const prepareSettler = (
return zone.exoClass(
'Fast USDC Settler',
M.interface('SettlerI', {
monitorTransfers: M.callWhen().returns(M.any()),
monitorMintingDeposits: M.callWhen().returns(M.any()),
receiveUpcall: M.call(M.record()).returns(M.promise()),
settleSansFees: M.call(M.string(), M.string(), M.nat()).returns(
notifyAdvancingResult: M.call(M.string(), M.nat(), M.boolean()).returns(),
disburse: M.call(EvmHashShape, M.string(), M.nat()).returns(M.promise()),
forward: M.call(EvmHashShape, M.string(), M.nat(), M.string()).returns(
M.promise(),
),
}),
Expand All @@ -61,14 +72,17 @@ export const prepareSettler = (
...config,
/** @type {HostInterface<TargetRegistration>|undefined} */
registration: undefined,
/** @type {SetStore<ReturnType<typeof makeMintedEarlyKey>>} */
mintedEarly: zone.detached().setStore('mintedEarly'),
};
},
{
async monitorTransfers() {
async monitorMintingDeposits() {
const { settlementAccount } = this.state;
const registration = await vowTools.when(
settlementAccount.monitorTransfers(this.self),
);
assert.typeof(registration, 'object');
this.state.registration = registration;
},
/** @param {VTransferIBCEvent} event */
Expand Down Expand Up @@ -103,36 +117,67 @@ export const prepareSettler = (
return;
}

const amountInt = BigInt(tx.amount); // TODO: what if this throws?
const amount = BigInt(tx.amount); // TODO: what if this throws?

if (!statusManager.hasPendingSettlement(sender, amountInt)) {
// 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)}`,
);
}
const found = statusManager.dequeueStatus(sender, amount);
switch (found?.status) {
case undefined:
case PendingTxStatus.Observed:
return this.self.forward(found?.txHash, sender, amount, EUD);

const pending = statusManager.lookupPending(sender, amountInt);
if (pending.find(it => it.status === PendingTxStatus.Observed)) {
return this.self.settleSansFees(sender, EUD, amountInt);
}
case PendingTxStatus.Advancing:
this.state.mintedEarly.add(makeMintedEarlyKey(sender, amount));
return;

// Disperse funds
case PendingTxStatus.Advanced:
return this.self.disburse(found.txHash, sender, amount);

default:
throw Error('TODO: think harder');
}
},
/**
* @param {EvmHash} txHash
* @param {NobleAddress} sender
* @param {NatValue} amount
* @param {string} EUD
* @param {boolean} success
* @returns {void}
*/
notifyAdvancingResult(txHash, sender, amount, EUD, success) {
const { mintedEarly } = this.state;
const key = makeMintedEarlyKey(sender, amount);
if (mintedEarly.has(key)) {
mintedEarly.delete(key);
if (success) {
void this.self.disburse(txHash, sender, amount);
} else {
void this.self.forward(txHash, sender, amount, EUD);
}
} else {
statusManager.advanceOutcome(sender, amount, success);
}
},
/**
* @param {EvmHash} txHash
* @param {NobleAddress} sender
* @param {NatValue} amount
*/
async disburse(txHash, sender, amount) {
const { repayer, settlementAccount } = this.state;
const received = AmountMath.make(USDC, amountInt);
const received = AmountMath.make(USDC, amount);
const { zcfSeat: settlingSeat } = zcf.makeEmptySeatKit();
const { calculateSplit } = makeFeeTools(feeConfig);
const split = calculateSplit(received);
trace('dispersing', split);
trace('disbursing', split);

// TODO: what if this throws?
// arguably, it cannot. Even if deposits
// and notifications get out of order,
// we don't ever withdraw more than has been deposited.
await vowTools.when(
withdrawToSeat(
// @ts-expect-error Vow vs. Promise stuff. TODO: is this OK???
settlementAccount,
settlingSeat,
harden({ In: received }),
Expand All @@ -144,25 +189,26 @@ export const prepareSettler = (
repayer.repay(settlingSeat, split);

// update status manager, marking tx `SETTLED`
statusManager.settle(sender, amountInt);
statusManager.disbursed(txHash, sender, amount);
},
/**
* @param {EvmHash | undefined} txHash
* @param {NobleAddress} sender
* @param {NatValue} amount
* @param {string} EUD
* @param {bigint} amountInt
*/
async settleSansFees(sender, EUD, amountInt) {
async forward(txHash, sender, amount, EUD) {
const { settlementAccount } = this.state;

const dest = chainHub.makeChainAddress(EUD);

const txfrV = E(settlementAccount).transfer(
dest,
AmountMath.make(USDC, amountInt),
AmountMath.make(USDC, amount),
);
await vowTools.when(txfrV); // TODO: watch, handle failure

statusManager.settle(sender, amountInt);
statusManager.forwarded(txHash, sender, amount);
},
},
{
Expand All @@ -172,6 +218,7 @@ export const prepareSettler = (
registration: M.or(M.undefined(), M.remotable('Registration')),
sourceChannel: M.string(),
remoteDenom: M.string(),
mintedEarly: M.remotable('mintedEarly'),
}),
},
);
Expand Down
91 changes: 77 additions & 14 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { M } from '@endo/patterns';
import { makeError, q } from '@endo/errors';
import { Fail, makeError, q } from '@endo/errors';

import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
import { CctpTxEvidenceShape, PendingTxShape } from '../type-guards.js';
import {
CctpTxEvidenceShape,
EvmHashShape,
PendingTxShape,
} from '../type-guards.js';
import { PendingTxStatus } from '../constants.js';

/**
* @import {MapStore, SetStore} from '@agoric/store';
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx} from '../types.js';
* @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx, EvmHash} from '../types.js';
*/

/**
Expand Down Expand Up @@ -96,10 +100,20 @@ export const prepareStatusManager = zone => {
return zone.exo(
'Fast USDC Status Manager',
M.interface('StatusManagerI', {
advance: M.call(CctpTxEvidenceShape).returns(M.undefined()),
advancing: M.call(CctpTxEvidenceShape).returns(M.undefined()),
advanceOutcome: M.call(M.string(), M.nat(), M.boolean()).returns(),
observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
hasPendingSettlement: M.call(M.string(), M.bigint()).returns(M.boolean()),
settle: M.call(M.string(), M.bigint()).returns(M.undefined()),
dequeueStatus: M.call(M.string(), M.bigint()).returns({
txHash: EvmHashShape,
status: M.string(), // TODO: named shape?
}),
disbursed: M.call(EvmHashShape, M.string(), M.nat()).returns(
M.undefined(),
),
forwarded: M.call(M.opt(EvmHashShape), M.string(), M.nat()).returns(
M.undefined(),
),
lookupPending: M.call(M.string(), M.bigint()).returns(
M.arrayOf(PendingTxShape),
),
Expand All @@ -109,8 +123,32 @@ export const prepareStatusManager = zone => {
* Add a new transaction with ADVANCED status
* @param {CctpTxEvidence} evidence
*/
advance(evidence) {
recordPendingTx(evidence, PendingTxStatus.Advanced);
advancing(evidence) {
recordPendingTx(evidence, PendingTxStatus.Advancing);
},

/**
* @param {NobleAddress} sender
* @param {import('@agoric/ertp').NatValue} amount
* @param {boolean} success
*/
advanceOutcome(sender, amount, success) {
const key = makePendingTxKey(sender, amount);
const pending = pendingTxs.get(key);
const ix = pending.findIndex(
tx => tx.status === PendingTxStatus.Advancing,
);
ix >= 0 || Fail`no advancing tx with ${{ sender, amount }}`;
const [pre, tx, post] = [
pending.slice(0, ix),
pending[ix],
pending.slice(ix + 1),
];
const status = success
? PendingTxStatus.Advanced
: PendingTxStatus.AdvanceFailed;
const txpost = { ...tx, status };
pendingTxs.set(key, harden([...pre, txpost, ...post]));
},

/**
Expand All @@ -135,23 +173,48 @@ export const prepareStatusManager = zone => {
},

/**
* Mark an `ADVANCED` or `OBSERVED` transaction as `SETTLED` and remove it
* Remove and return an `ADVANCED` or `OBSERVED` tx waiting to be `SETTLED`.
*
* @param {NobleAddress} address
* @param {bigint} amount
*/
settle(address, amount) {
dequeueStatus(address, amount) {
const key = makePendingTxKey(address, amount);
const pending = pendingTxs.get(key);

if (!pending.length) {
throw makeError(`No unsettled entry for ${q(key)}`);
return undefined;
}

const pendingCopy = [...pending];
pendingCopy.shift();
// TODO, vstorage update for `TxStatus.Settled`
pendingTxs.set(key, harden(pendingCopy));
const [tx0, ...rest] = pending;
pendingTxs.set(key, harden(rest));
const { status, txHash } = tx0;
// TODO: store txHash -> evidence for txs pending settlement?
return harden({ status, txHash });
},

/**
* Mark a transaction as `DISBURSED`
*
* @param {EvmHash} txHash
* @param {NobleAddress} address
* @param {bigint} amount
*/
disbursed(txHash, address, amount) {
// TODO: store txHash -> evidence for txs pending settlement?
console.log('TODO: vstorage update', { txHash, address, amount });
},

/**
* Mark a transaction as `FORWARDED`
*
* @param {EvmHash | undefined} txHash - undefined in case mint before observed
* @param {NobleAddress} address
* @param {bigint} amount
*/
forwarded(txHash, address, amount) {
// TODO: store txHash -> evidence for txs pending settlement?
console.log('TODO: vstorage update', { txHash, address, amount });
},

/**
Expand Down
7 changes: 4 additions & 3 deletions packages/fast-usdc/test/exos/settler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ const makeTestContext = async t => {
};
t.log('Mock CCTP Evidence:', cctpTxEvidence);
t.log('Pretend we initiated advance, mark as `ADVANCED`');
statusManager.advance(cctpTxEvidence);
statusManager.advancing(cctpTxEvidence);
const { forwardingAddress, amount } = cctpTxEvidence.tx;
statusManager.advanceOutcome(forwardingAddress, BigInt(amount), true);

return cctpTxEvidence;
},
Expand Down Expand Up @@ -222,7 +224,7 @@ test('happy path: disburse to LPs; StatusManager removes tx', async t => {
// TODO, confirm vstorage write for TxStatus.SETTLED
});

test('slow path: disburse to LPs; StatusManager removes tx', async t => {
test('slow path: forward to EUD; remove pending tx', async t => {
const {
common,
makeSettler,
Expand All @@ -234,7 +236,6 @@ test('slow path: disburse to LPs; StatusManager removes tx', async t => {
peekCalls,
} = t.context;
const { usdc } = common.brands;
const { feeConfig } = common.commonPrivateArgs;

const settler = makeSettler({
repayer,
Expand Down
Loading

0 comments on commit 3211e61

Please sign in to comment.