-
Notifications
You must be signed in to change notification settings - Fork 212
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: fusdc status manager states #10406
Changes from all commits
1376020
646df78
5e4139c
5a7b3d2
1063efd
6f97e13
3f71342
980463f
f3d1e36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* Status values for FastUSDC. | ||
* | ||
* @enum {(typeof TxStatus)[keyof typeof TxStatus]} | ||
*/ | ||
export const TxStatus = /** @type {const} */ ({ | ||
/** tx was observed but not advanced */ | ||
Observed: 'OBSERVED', | ||
/** IBC transfer is initiated */ | ||
Advanced: 'ADVANCED', | ||
/** settlement for matching advance received and funds dispersed */ | ||
Settled: 'SETTLED', | ||
}); | ||
harden(TxStatus); | ||
|
||
/** | ||
* Status values for the StatusManager. | ||
* | ||
* @enum {(typeof PendingTxStatus)[keyof typeof PendingTxStatus]} | ||
*/ | ||
export const PendingTxStatus = /** @type {const} */ ({ | ||
/** tx was observed but not advanced */ | ||
Observed: 'OBSERVED', | ||
/** IBC transfer is initiated */ | ||
Advanced: 'ADVANCED', | ||
}); | ||
harden(PendingTxStatus); |
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 | ||
``` |
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); |
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why would Settler get here? seems worth logging. Same goes for all these checks with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok planning to tackle these as part of #10391, will add a TODO. These are also the main callouts from
An actor could decide to send an IBC transfer to this address |
||||||
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)}`, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this recoverable? It looks like it.
Suggested change
per Logging docs:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 was intentional - I think we should leave it until we recover from here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might want to capture this in the StatusManager with an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's come back to this when we're going all the error cases |
||||||
); | ||||||
} | ||||||
|
||||||
// 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WFM