-
Notifications
You must be signed in to change notification settings - Fork 221
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 advancer #10420
feat: fusdc advancer #10420
Changes from all commits
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 |
---|---|---|
@@ -1,124 +1,234 @@ | ||
import { AmountMath, AmountShape, PaymentShape } from '@agoric/ertp'; | ||
import { assertAllDefined } from '@agoric/internal'; | ||
import { ChainAddressShape } from '@agoric/orchestration'; | ||
import { pickFacet } from '@agoric/vat-data'; | ||
import { VowShape } from '@agoric/vow'; | ||
import { makeError, q } from '@endo/errors'; | ||
import { q } from '@endo/errors'; | ||
import { E } from '@endo/far'; | ||
import { M } from '@endo/patterns'; | ||
import { CctpTxEvidenceShape } from '../typeGuards.js'; | ||
import { CctpTxEvidenceShape, EudParamShape } from '../typeGuards.js'; | ||
import { addressTools } from '../utils/address.js'; | ||
|
||
const { isGTE } = AmountMath; | ||
|
||
/** | ||
* @import {HostInterface} from '@agoric/async-flow'; | ||
* @import {NatAmount} from '@agoric/ertp'; | ||
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration'; | ||
* @import {VowTools} from '@agoric/vow'; | ||
* @import {Zone} from '@agoric/zone'; | ||
* @import {CctpTxEvidence, LogFn} from '../types.js'; | ||
* @import {StatusManager} from './status-manager.js'; | ||
* @import {TransactionFeedKit} from './transaction-feed.js'; | ||
*/ | ||
|
||
/** | ||
* Expected interface from LiquidityPool | ||
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. I'll assume this @dckc agrees |
||
* | ||
* @typedef {{ | ||
* lookupBalance(): NatAmount; | ||
* borrow(amount: Amount<"nat">): Promise<Payment<"nat">>; | ||
* repay(payments: PaymentKeywordRecord): Promise<void> | ||
* }} AssetManagerFacet | ||
*/ | ||
|
||
/** | ||
* @typedef {{ | ||
* chainHub: ChainHub; | ||
* log: LogFn; | ||
* statusManager: StatusManager; | ||
* usdc: { brand: Brand<'nat'>; denom: Denom; }; | ||
* vowTools: VowTools; | ||
* }} AdvancerKitPowers | ||
*/ | ||
|
||
/** type guards internal to the AdvancerKit */ | ||
const AdvancerKitI = harden({ | ||
advancer: M.interface('AdvancerI', { | ||
handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(), | ||
}), | ||
depositHandler: M.interface('DepositHandlerI', { | ||
onFulfilled: M.call(AmountShape, { | ||
destination: ChainAddressShape, | ||
payment: PaymentShape, | ||
}).returns(VowShape), | ||
onRejected: M.call(M.error(), { | ||
destination: ChainAddressShape, | ||
payment: PaymentShape, | ||
}).returns(), | ||
}), | ||
transferHandler: M.interface('TransferHandlerI', { | ||
// TODO confirm undefined, and not bigint (sequence) | ||
onFulfilled: M.call(M.undefined(), { | ||
amount: AmountShape, | ||
destination: ChainAddressShape, | ||
}).returns(M.undefined()), | ||
onRejected: M.call(M.error(), { | ||
amount: AmountShape, | ||
destination: ChainAddressShape, | ||
}).returns(M.undefined()), | ||
}), | ||
}); | ||
|
||
/** | ||
* @param {Zone} zone | ||
* @param {object} caps | ||
* @param {ChainHub} caps.chainHub | ||
* @param {LogFn} caps.log | ||
* @param {StatusManager} caps.statusManager | ||
* @param {VowTools} caps.vowTools | ||
* @param {AdvancerKitPowers} caps | ||
*/ | ||
export const prepareAdvancer = ( | ||
export const prepareAdvancerKit = ( | ||
zone, | ||
{ chainHub, log, statusManager, vowTools: { watch } }, | ||
{ chainHub, log, statusManager, usdc, vowTools: { watch, when } }, | ||
) => { | ||
assertAllDefined({ statusManager, watch }); | ||
assertAllDefined({ | ||
chainHub, | ||
statusManager, | ||
watch, | ||
when, | ||
}); | ||
|
||
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()); | ||
}, | ||
}, | ||
); | ||
/** @param {bigint} value */ | ||
const toAmount = value => AmountMath.make(usdc.brand, value); | ||
|
||
return zone.exoClass( | ||
return zone.exoClassKit( | ||
'Fast USDC Advancer', | ||
M.interface('AdvancerI', { | ||
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape), | ||
}), | ||
AdvancerKitI, | ||
/** | ||
* @param {{ | ||
* localDenom: Denom; | ||
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>; | ||
* assetManagerFacet: AssetManagerFacet; | ||
* poolAccount: ERef<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), | ||
}); | ||
advancer: { | ||
/** | ||
* Must perform a status update for every observed transaction. | ||
* | ||
* We do not expect any callers to depend on the settlement of | ||
turadg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* `handleTransactionEvent` - errors caught are communicated to the | ||
* `StatusManager` - so we don't need to concern ourselves with | ||
* preserving the vow chain for callers. | ||
* | ||
* @param {CctpTxEvidence} evidence | ||
*/ | ||
async handleTransactionEvent(evidence) { | ||
await null; | ||
try { | ||
// TODO poolAccount might be a vow we need to unwrap | ||
const { assetManagerFacet, poolAccount } = this.state; | ||
const { recipientAddress } = evidence.aux; | ||
const { EUD } = addressTools.getQueryParams( | ||
recipientAddress, | ||
EudParamShape, | ||
); | ||
|
||
// TODO #10391 ensure there's enough funds in poolAccount | ||
// this will throw if the bech32 prefix is not found, but is handled by the catch | ||
const destination = chainHub.makeChainAddress(EUD); | ||
const requestedAmount = toAmount(evidence.tx.amount); | ||
|
||
const transferV = E(this.state.poolAccount).transfer( | ||
destination, | ||
requestedAmount, | ||
); | ||
// TODO: consider skipping and using `borrow()`s internal balance check | ||
const poolBalance = assetManagerFacet.lookupBalance(); | ||
if (!isGTE(poolBalance, requestedAmount)) { | ||
log( | ||
`Insufficient pool funds`, | ||
`Requested ${q(requestedAmount)} but only have ${q(poolBalance)}`, | ||
); | ||
statusManager.observe(evidence); | ||
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. I suppose we'll want to log this even to vstorage. But let's come back to it after getting more clarity with product on all the error handling and what the end user needs to see from it. We have that on the schedule. |
||
return; | ||
} | ||
|
||
// mark as Advanced since `transferV` initiates the advance | ||
statusManager.advance(evidence); | ||
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; | ||
} | ||
|
||
return watch(transferV, transferHandler, { | ||
destination, | ||
amount: requestedAmount.value, | ||
}); | ||
try { | ||
const payment = await assetManagerFacet.borrow(requestedAmount); | ||
const depositV = E(poolAccount).deposit(payment); | ||
void watch(depositV, this.facets.depositHandler, { | ||
destination, | ||
payment, | ||
}); | ||
} catch (e) { | ||
// `.borrow()` might fail if the balance changes since we | ||
// requested it. TODO - how to handle this? change ADVANCED -> OBSERVED? | ||
// Note: `depositHandler` handles the `.deposit()` failure | ||
log('🚨 advance borrow failed', q(e).toString()); | ||
} | ||
} catch (e) { | ||
log('Advancer error:', q(e).toString()); | ||
statusManager.observe(evidence); | ||
} | ||
}, | ||
}, | ||
depositHandler: { | ||
/** | ||
* @param {NatAmount} amount amount returned from deposit | ||
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx | ||
*/ | ||
onFulfilled(amount, { destination }) { | ||
const { poolAccount } = this.state; | ||
const transferV = E(poolAccount).transfer( | ||
destination, | ||
/** @type {DenomAmount} */ ({ | ||
denom: usdc.denom, | ||
value: amount.value, | ||
}), | ||
); | ||
return watch(transferV, this.facets.transferHandler, { | ||
destination, | ||
amount, | ||
}); | ||
}, | ||
/** | ||
* @param {Error} error | ||
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx | ||
*/ | ||
onRejected(error, { payment }) { | ||
// TODO return live payment from ctx to LP | ||
log('🚨 advance deposit failed', q(error).toString()); | ||
log('TODO live payment to return to LP', q(payment).toString()); | ||
}, | ||
}, | ||
transferHandler: { | ||
/** | ||
* @param {undefined} result TODO confirm this is not a bigint (sequence) | ||
* @param {{ destination: ChainAddress; amount: NatAmount; }} ctx | ||
*/ | ||
onFulfilled(result, { destination, amount }) { | ||
// TODO vstorage update? | ||
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()); | ||
}, | ||
}, | ||
}, | ||
{ | ||
stateShape: harden({ | ||
localDenom: M.string(), | ||
poolAccount: M.remotable(), | ||
assetManagerFacet: M.remotable(), | ||
poolAccount: M.or(VowShape, M.remotable()), | ||
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. TODO pick one 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. This seems like it'll depend on:
|
||
}), | ||
}, | ||
); | ||
}; | ||
harden(prepareAdvancerKit); | ||
|
||
/** | ||
* @param {Zone} zone | ||
* @param {AdvancerKitPowers} caps | ||
*/ | ||
export const prepareAdvancer = (zone, caps) => { | ||
const makeAdvancerKit = prepareAdvancerKit(zone, caps); | ||
return pickFacet(makeAdvancerKit, 'advancer'); | ||
}; | ||
harden(prepareAdvancer); |
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.
out of scope, but this helper should move eventually. It's about Exos, not vat data.
We could move it down to
@agoric/internal
but that is meant to not be consumed externally and this is valuable to other contract authors.I wonder if it belongs in Endo.