-
Notifications
You must be signed in to change notification settings - Fork 207
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
9 changed files
with
541 additions
and
200 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 |
---|---|---|
@@ -1,124 +1,237 @@ | ||
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 { E } from '@endo/far'; | ||
import { M } from '@endo/patterns'; | ||
import { CctpTxEvidenceShape } 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 | ||
* | ||
* @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 | ||
* `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).params; | ||
if (!EUD) { | ||
throw makeError( | ||
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`, | ||
); | ||
} | ||
|
||
// 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); | ||
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()), | ||
}), | ||
}, | ||
); | ||
}; | ||
harden(prepareAdvancerKit); | ||
|
||
/** | ||
* @param {Zone} zone | ||
* @param {AdvancerKitPowers} caps | ||
*/ | ||
export const prepareAdvancer = (zone, caps) => { | ||
const makeAdvancerKit = prepareAdvancerKit(zone, caps); | ||
return pickFacet(makeAdvancerKit, 'advancer'); | ||
}; | ||
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
Oops, something went wrong.