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(fast-usdc): settler disburses or forwards funds #10530

Merged
merged 7 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/fast-usdc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,44 @@ sequenceDiagram

A->>TF: notify(evidence)
```

# Status Manager

### Pending Advance State Diagram

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

```mermaid
stateDiagram-v2
[*] --> Observed: observe()
[*] --> Advancing: advancing()

Advancing --> Advanced: advanceOutcome(...true)
Advancing --> AdvanceFailed: advanceOutcome(...false)

Observed --> [*]: dequeueStatus()
Advanced --> [*]: dequeueStatus()
AdvanceFailed --> [*]: dequeueStatus()

note right of [*]
After dequeueStatus():
Transaction is removed
from pendingTxs store.
Settler will .disburse()
or .forward()
end note
```

### Complete state diagram (starting from Transaction Feed into Advancer)

```mermaid
stateDiagram-v2
Observed --> Advancing
Observed --> Forwarding:Minted
Forwarding --> Forwarded
Advancing --> Advanced
Advanced --> Disbursed
AdvanceFailed --> Forwarding
Advancing --> AdvanceFailed
Forwarding --> ForwardFailed
```
18 changes: 16 additions & 2 deletions packages/fast-usdc/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,22 @@ export const TxStatus = /** @type {const} */ ({
/** tx was observed but not advanced */
Observed: 'OBSERVED',
/** IBC transfer is initiated */
Advancing: 'ADVANCING',
/** IBC transfer is complete */
Advanced: 'ADVANCED',
/** settlement for matching advance received and funds dispersed */
Settled: 'SETTLED',
/** IBC transfer failed (timed out) */
AdvanceFailed: 'ADVANCE_FAILED',
/** settlement for matching advance received and funds disbursed */
Disbursed: 'DISBURSED',
/** fallback: do not collect fees */
Forwarded: 'FORWARDED',
/** failed to forward to EUD */
ForwardFailed: 'FORWARD_FAILED',
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
ForwardFailed: 'FORWARD_FAILED',
ForwardFailed: 'FORWARD_FAILED',
/** started IBC transfer for fallback path */
Forwarding: 'FORWARDING',

});
harden(TxStatus);

// TODO: define valid state transitions

/**
* Status values for the StatusManager.
*
Expand All @@ -22,6 +32,10 @@ export const PendingTxStatus = /** @type {const} */ ({
/** tx was observed but not advanced */
Observed: 'OBSERVED',
/** IBC transfer is initiated */
Advancing: 'ADVANCING',
/** IBC transfer failed (timed out) */
AdvanceFailed: 'ADVANCE_FAILED',
/** IBC transfer is complete */
Advanced: 'ADVANCED',
});
harden(PendingTxStatus);
26 changes: 0 additions & 26 deletions packages/fast-usdc/src/exos/README.md

This file was deleted.

119 changes: 72 additions & 47 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@ import { VowShape } from '@agoric/vow';
import { q } from '@endo/errors';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape, EudParamShape } from '../type-guards.js';
import {
CctpTxEvidenceShape,
EudParamShape,
EvmHashShape,
} from '../type-guards.js';
import { addressTools } from '../utils/address.js';
import { makeFeeTools } from '../utils/fees.js';

const { isGTE } = AmountMath;

/**
* @import {HostInterface} from '@agoric/async-flow';
* @import {NatAmount} from '@agoric/ertp';
* @import {ChainAddress, ChainHub, Denom, OrchestrationAccount} from '@agoric/orchestration';
* @import {ZoeTools} from '@agoric/orchestration/src/utils/zoe-tools.js';
* @import {VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence, FeeConfig, LogFn} from '../types.js';
* @import {CctpTxEvidence, EvmHash, FeeConfig, LogFn, NobleAddress} from '../types.js';
* @import {StatusManager} from './status-manager.js';
* @import {LiquidityPoolKit} from './liquidity-pool.js';
*/
Expand All @@ -46,27 +48,44 @@ const AdvancerKitI = harden({
onFulfilled: M.call(M.undefined(), {
amount: AmountShape,
destination: ChainAddressShape,
forwardingAddress: M.string(),
tmpSeat: M.remotable(),
txHash: EvmHashShape,
}).returns(VowShape),
onRejected: M.call(M.error(), {
amount: AmountShape,
destination: ChainAddressShape,
forwardingAddress: M.string(),
tmpSeat: M.remotable(),
txHash: EvmHashShape,
}).returns(),
}),
transferHandler: M.interface('TransferHandlerI', {
// TODO confirm undefined, and not bigint (sequence)
onFulfilled: M.call(M.undefined(), {
amount: AmountShape,
destination: ChainAddressShape,
forwardingAddress: M.string(),
txHash: EvmHashShape,
}).returns(M.undefined()),
onRejected: M.call(M.error(), {
amount: AmountShape,
destination: ChainAddressShape,
forwardingAddress: M.string(),
txHash: EvmHashShape,
}).returns(M.undefined()),
}),
});

/**
* @typedef {{
* amount: NatAmount;
* destination: ChainAddress;
* forwardingAddress: NobleAddress;
* txHash: EvmHash;
* }} AdvancerVowCtx
*/

/**
* @param {Zone} zone
* @param {AdvancerKitPowers} caps
Expand Down Expand Up @@ -100,6 +119,7 @@ export const prepareAdvancerKit = (
AdvancerKitI,
/**
* @param {{
* notifyFacet: import('./settler.js').SettlerKit['notify'];
* borrowerFacet: LiquidityPoolKit['borrower'];
* poolAccount: HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>;
* }} config
Expand All @@ -120,51 +140,32 @@ export const prepareAdvancerKit = (
async handleTransactionEvent(evidence) {
await null;
try {
if (statusManager.hasBeenObserved(evidence)) {
log('txHash already seen:', evidence.txHash);
return;
}

const { borrowerFacet, poolAccount } = this.state;
const { recipientAddress } = evidence.aux;
// throws if EUD is not found
const { EUD } = addressTools.getQueryParams(
recipientAddress,
EudParamShape,
);

// this will throw if the bech32 prefix is not found, but is handled by the catch
// throws if the bech32 prefix is not found
const destination = chainHub.makeChainAddress(EUD);

const requestedAmount = toAmount(evidence.tx.amount);
// throws if requested does not exceed fees
const advanceAmount = feeTools.calculateAdvance(requestedAmount);

// TODO: consider skipping and using `borrow()`s internal balance check
const poolBalance = borrowerFacet.getBalance();
if (!isGTE(poolBalance, requestedAmount)) {
log(
`Insufficient pool funds`,
`Requested ${q(advanceAmount)} but only have ${q(poolBalance)}`,
);
statusManager.observe(evidence);
return;
}

try {
// Mark as Advanced since `transferV` initiates the advance.
// Will throw if we've already .skipped or .advanced this evidence.
statusManager.advance(evidence);
} catch (e) {
// Only anticipated error is `assertNotSeen`, so intercept the
// catch so we don't call .skip which also performs this check
log('Advancer error:', q(e).toString());
return;
}

const { zcfSeat: tmpSeat } = zcf.makeEmptySeatKit();
const amountKWR = harden({ USDC: advanceAmount });
try {
borrowerFacet.borrow(tmpSeat, amountKWR);
} catch (e) {
// We do not expect this to fail since there are no turn boundaries
// between .getBalance() and .borrow().
// We catch to report outside of the normal error flow since this is
// not expected.
log('🚨 advance borrow failed', q(e).toString());
}
// throws if the pool has insufficient funds
borrowerFacet.borrow(tmpSeat, amountKWR);

// this cannot throw since `.isSeen()` is called in the same turn
statusManager.advance(evidence);

const depositV = localTransfer(
tmpSeat,
Expand All @@ -175,7 +176,9 @@ export const prepareAdvancerKit = (
void watch(depositV, this.facets.depositHandler, {
amount: advanceAmount,
destination,
forwardingAddress: evidence.tx.forwardingAddress,
tmpSeat,
txHash: evidence.txHash,
});
} catch (e) {
log('Advancer error:', q(e).toString());
Expand All @@ -186,22 +189,25 @@ export const prepareAdvancerKit = (
depositHandler: {
/**
* @param {undefined} result
* @param {{ amount: Amount<'nat'>; destination: ChainAddress; tmpSeat: ZCFSeat }} ctx
* @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
*/
onFulfilled(result, { amount, destination }) {
onFulfilled(result, ctx) {
const { poolAccount } = this.state;
const { amount, destination, forwardingAddress, txHash } = ctx;
const transferV = E(poolAccount).transfer(destination, {
denom: usdc.denom,
value: amount.value,
});
return watch(transferV, this.facets.transferHandler, {
destination,
amount,
forwardingAddress,
txHash,
});
},
/**
* @param {Error} error
* @param {{ amount: Amount<'nat'>; destination: ChainAddress; tmpSeat: ZCFSeat }} ctx
* @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
*/
onRejected(error, { tmpSeat }) {
// TODO return seat allocation from ctx to LP?
Expand All @@ -217,25 +223,44 @@ export const prepareAdvancerKit = (
transferHandler: {
/**
* @param {undefined} result TODO confirm this is not a bigint (sequence)
* @param {{ destination: ChainAddress; amount: NatAmount; }} ctx
* @param {AdvancerVowCtx} ctx
*/
onFulfilled(result, { destination, amount }) {
// TODO vstorage update? We don't currently have a status for
// Advanced + transferV settled
onFulfilled(result, ctx) {
const { notifyFacet } = this.state;
const { amount, destination, forwardingAddress, txHash } = ctx;
log(
'Advance transfer fulfilled',
q({ amount, destination, result }).toString(),
);
notifyFacet.notifyAdvancingResult(
txHash,
forwardingAddress,
amount.value,
destination.value,
true,
);
},
onRejected(error) {
// TODO #10510 (comprehensive error testing) determine
// course of action here. This might fail due to timeout.
/**
* @param {Error} error
* @param {AdvancerVowCtx} ctx
*/
onRejected(error, ctx) {
const { notifyFacet } = this.state;
const { amount, destination, forwardingAddress, txHash } = ctx;
log('Advance transfer rejected', q(error).toString());
notifyFacet.notifyAdvancingResult(
txHash,
forwardingAddress,
amount.value,
destination.value,
false,
);
},
},
},
{
stateShape: harden({
notifyFacet: M.remotable(),
borrowerFacet: M.remotable(),
poolAccount: M.remotable(),
}),
Expand Down
7 changes: 1 addition & 6 deletions packages/fast-usdc/src/exos/liquidity-pool.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AmountMath, AmountShape } from '@agoric/ertp';
import { AmountMath } from '@agoric/ertp';
import {
makeRecorderTopic,
TopicsRecordShape,
Expand Down Expand Up @@ -84,7 +84,6 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
'Liquidity Pool',
{
borrower: M.interface('borrower', {
getBalance: M.call().returns(AmountShape),
borrow: M.call(
SeatShape,
harden({ USDC: makeNatAmountShape(USDC, 1n) }),
Expand Down Expand Up @@ -152,10 +151,6 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
},
{
borrower: {
getBalance() {
const { poolSeat } = this.state;
return poolSeat.getAmountAllocated('USDC', USDC);
},
/**
* @param {ZCFSeat} toSeat
* @param {{ USDC: Amount<'nat'>}} amountKWR
Expand Down
Loading
Loading