-
Notifications
You must be signed in to change notification settings - Fork 230
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- shows the various `StatusManager` states updates (`OBSERVED`, `ADVANCED`, `SETTLED`) via `Advancer` and `Settler` stubs
- Loading branch information
1 parent
b89d0af
commit a4ae691
Showing
9 changed files
with
666 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
## **StatusManager** sequence diagram, showing `TxStatus` state transitions | ||
|
||
### Current Implementation | ||
|
||
```mermaid | ||
sequenceDiagram | ||
participant I as Initial | ||
participant O as OBSERVED | ||
participant A as ADVANCED | ||
participant S as SETTLED | ||
Note over O,S: Currently Implemented State Transitions | ||
I->>O: statusManager.observe()<br/>When TX observed via EventFeed | ||
O->>A: statusManager.advance()<br/>When IBC transfer initiated | ||
A->>S: statusManager.settle()<br/>When settlement received + dispersed | ||
Note over O,A: Status updated when transfer starts | ||
Note over A,S: Requires matching settlement transfer | ||
``` | ||
|
||
### Additional States to Consider (Not Implemented) | ||
|
||
```mermaid | ||
sequenceDiagram | ||
participant I as Initial | ||
participant O as OBSERVED | ||
participant AG as ADVANCING | ||
participant A as ADVANCED | ||
participant S as SETTLED | ||
participant AS as ADVANCE_SKIPPED | ||
participant AF as ADVANCE_FAILED | ||
participant OS as ORPHANED_SETTLE | ||
Note over O,S: Normal Flow | ||
I->>O: statusManager.observe() | ||
O->>AG: initiate IBC transfer<br/>(if validation passes) | ||
AG->>A: IBC transfer settled | ||
A->>S: settlement received | ||
Note over O,AS: Early Failures | ||
O-->>AS: validation fails<br/>(missing chain config,<br/>invalid path,<br/>no pool funds) | ||
Note over AG,AF: IBC Failures | ||
AG-->>AF: timeout,<br/>unexpected error,<br/>insufficient funds | ||
Note over O,OS: Edge Cases | ||
I-->>OS: Settlement received without OBSERVED | ||
O-->>S: OBSERVED->SETTLED without ADVANCING/ADVANCED<br/>is this a valid state transition?<br/>behavior is slowUSDC? | ||
AG-->>S: Settlement received while ADVANCING<br/>wait for ADVANCED to SETTLE/disperse funds? | ||
Note over AS,AF: Future: Queue for retry? | ||
Note over OS: Future: Wait for OBSERVE? | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.