Skip to content

Commit

Permalink
chore: tx statuses in vstorage
Browse files Browse the repository at this point in the history
- ensure "minted before observed" `Observe` statuses are recorded in vstorage
- ensure "minted while Advancing" `Advanced` and `AdvanceFailed` statuses are recorded in vstorage
  • Loading branch information
0xpatrickdev committed Dec 20, 2024
1 parent 95d9206 commit 08bf844
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 70 deletions.
40 changes: 10 additions & 30 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,25 +158,15 @@ export const prepareAdvancerKit = (
const destination = chainHub.makeChainAddress(EUD);

const fullAmount = toAmount(evidence.tx.amount);
const {
tx: { forwardingAddress },
txHash,
} = evidence;

const { borrowerFacet, notifyFacet, poolAccount } = this.state;
if (
notifyFacet.forwardIfMinted(
destination,
forwardingAddress,
fullAmount,
txHash,
)
) {
// settlement already received; tx will Forward.
// do not add to `pendingSettleTxs` by calling `.observe()`
log('⚠️ minted before Observed');
return;
}

// do not advance if we've already received a mint/settlement
const mintedEarly = notifyFacet.checkMintedEarly(
evidence,
destination,
);
if (mintedEarly) return;

// throws if requested does not exceed fees
const advanceAmount = feeTools.calculateAdvance(fullAmount);
Expand All @@ -200,7 +190,7 @@ export const prepareAdvancerKit = (
forwardingAddress: evidence.tx.forwardingAddress,
fullAmount,
tmpSeat,
txHash,
txHash: evidence.txHash,
});
} catch (error) {
log('Advancer error:', error);
Expand All @@ -219,13 +209,7 @@ export const prepareAdvancerKit = (
*/
onFulfilled(result, ctx) {
const { poolAccount, intermediateRecipient } = this.state;
const {
destination,
advanceAmount,
// eslint-disable-next-line no-unused-vars
tmpSeat,
...detail
} = ctx;
const { destination, advanceAmount, tmpSeat: _, ...detail } = ctx;
const transferV = E(poolAccount).transfer(
destination,
{ denom: usdc.denom, value: advanceAmount.value },
Expand Down Expand Up @@ -290,11 +274,7 @@ export const prepareAdvancerKit = (
onRejected(error, ctx) {
const { notifyFacet } = this.state;
log('Advance transfer rejected', error);
const {
// eslint-disable-next-line no-unused-vars
advanceAmount,
...restCtx
} = ctx;
const { advanceAmount: _, ...restCtx } = ctx;
notifyFacet.notifyAdvancingResult(restCtx, false);
},
},
Expand Down
36 changes: 20 additions & 16 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { M } from '@endo/patterns';
import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
import { PendingTxStatus } from '../constants.js';
import { makeFeeTools } from '../utils/fees.js';
import { EvmHashShape, makeNatAmountShape } from '../type-guards.js';
import {
CctpTxEvidenceShape,
EvmHashShape,
makeNatAmountShape,
} from '../type-guards.js';

/**
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
Expand All @@ -18,7 +22,7 @@ import { EvmHashShape, makeNatAmountShape } from '../type-guards.js';
* @import {Zone} from '@agoric/zone';
* @import {HostOf, HostInterface} from '@agoric/async-flow';
* @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js';
* @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn} from '../types.js';
* @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn, CctpTxEvidence} from '../types.js';
* @import {StatusManager} from './status-manager.js';
*/

Expand Down Expand Up @@ -81,8 +85,9 @@ export const prepareSettler = (
makeAdvanceDetailsShape(USDC),
M.boolean(),
).returns(),
forwardIfMinted: M.call(
...Object.values(makeAdvanceDetailsShape(USDC)),
checkMintedEarly: M.call(
CctpTxEvidenceShape,
ChainAddressShape,
).returns(M.boolean()),
}),
self: M.interface('SettlerSelfI', {
Expand Down Expand Up @@ -215,16 +220,14 @@ export const prepareSettler = (
) {
const { mintedEarly } = this.state;
const { value: fullValue } = fullAmount;
// XXX i think this only contains Advancing txs - should this be a separate store?
const key = makeMintedEarlyKey(forwardingAddress, fullValue);
if (mintedEarly.has(key)) {
mintedEarly.delete(key);
statusManager.notifyAdvanceOutcome(txHash, success);
if (success) {
// TODO: does not write `ADVANCED` to vstorage
// this is the "slow" experience, but we've already earmarked fees so disburse
void this.facets.self.disburse(txHash, fullValue);
} else {
// TODO: does not write `ADVANCE_FAILED` to vstorage
// if advance fails, attempt to forward (no fees)
void this.facets.self.forward(
txHash,
Expand All @@ -237,26 +240,27 @@ export const prepareSettler = (
}
},
/**
* @param {CctpTxEvidence} evidence
* @param {ChainAddress} destination
* @param {NobleAddress} forwardingAddress
* @param {Amount<'nat'>} fullAmount
* @param {EvmHash} txHash
* @returns {boolean}
* @throws {Error} if minted early, so advancer doesn't advance
*/
forwardIfMinted(destination, forwardingAddress, fullAmount, txHash) {
const { value: fullValue } = fullAmount;
const key = makeMintedEarlyKey(forwardingAddress, fullValue);
checkMintedEarly(evidence, destination) {
const {
tx: { forwardingAddress, amount },
txHash,
} = evidence;
const key = makeMintedEarlyKey(forwardingAddress, amount);
const { mintedEarly } = this.state;
if (mintedEarly.has(key)) {
log(
'matched minted early key, initiating forward',
forwardingAddress,
fullValue,
amount,
);
mintedEarly.delete(key);
// TODO: does not write `OBSERVED` to vstorage
void this.facets.self.forward(txHash, fullValue, destination.value);
statusManager.notifyObserved(evidence);
void this.facets.self.forward(txHash, amount, destination.value);
return true;
}
return false;
Expand Down
45 changes: 42 additions & 3 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,11 @@ export const prepareStatusManager = (
'Fast USDC Status Manager',
M.interface('StatusManagerI', {
// TODO: naming scheme for transition events
advance: M.call(CctpTxEvidenceShape).returns(M.undefined()),
advance: M.call(CctpTxEvidenceShape).returns(),
advanceOutcome: M.call(M.string(), M.nat(), M.boolean()).returns(),
observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
notifyAdvanceOutcome: M.call(EvmHashShape, M.boolean()).returns(),
notifyObserved: M.call(CctpTxEvidenceShape).returns(),
observe: M.call(CctpTxEvidenceShape).returns(),
hasBeenObserved: M.call(CctpTxEvidenceShape).returns(M.boolean()),
deleteCompletedTxs: M.call().returns(M.undefined()),
dequeueStatus: M.call(M.string(), M.bigint()).returns(
Expand All @@ -210,7 +212,7 @@ export const prepareStatusManager = (
disbursed: M.call(EvmHashShape, AmountKeywordRecordShape).returns(
M.undefined(),
),
forwarded: M.call(EvmHashShape, M.boolean()).returns(M.undefined()),
forwarded: M.call(EvmHashShape, M.boolean()).returns(),
lookupPending: M.call(M.string(), M.bigint()).returns(
M.arrayOf(PendingTxShape),
),
Expand Down Expand Up @@ -242,6 +244,43 @@ export const prepareStatusManager = (
);
},

/**
* If minted while advancing, publish a status update for the advance
* to vstorage.
*
* Does not add or amend `pendingSettleTxs` as this has
* already settled.
*
* @param {EvmHash} txHash
* @param {boolean} success whether the Transfer succeeded
*/
notifyAdvanceOutcome(txHash, success) {
void publishTxnRecord(
txHash,
harden({
status: success
? PendingTxStatus.Advanced
: PendingTxStatus.AdvanceFailed,
}),
);
},

/**
* If minted before observed and the evidence is eventually
* reported, publish the evidence without adding to `pendingSettleTxs`
*
* @param {CctpTxEvidence} evidence
*/
notifyObserved(evidence) {
const { txHash } = evidence;
// unexpected path, since `hasBeenObserved` will be called before this
if (seenTxs.has(txHash)) {
throw makeError(`Transaction already seen: ${q(txHash)}`);
}
seenTxs.add(txHash);
publishEvidence(txHash, evidence);
},

/**
* Add a new transaction with OBSERVED status
* @param {CctpTxEvidence} evidence
Expand Down
19 changes: 9 additions & 10 deletions packages/fast-usdc/test/exos/advancer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {
} from '@agoric/cosmic-proto/address-hooks.js';
import type { NatAmount } from '@agoric/ertp';
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
import { denomHash } from '@agoric/orchestration';
import { ChainAddressShape, denomHash } from '@agoric/orchestration';
import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js';
import { type ZoeTools } from '@agoric/orchestration/src/utils/zoe-tools.js';
import { Fail, q } from '@endo/errors';
import { q } from '@endo/errors';
import { Far } from '@endo/pass-style';
import type { TestFn } from 'ava';
import { makeTracer } from '@agoric/internal';
Expand All @@ -34,6 +34,7 @@ import {
prepareMockOrchAccounts,
} from '../mocks.js';
import { commonSetup } from '../supports.js';
import { CctpTxEvidenceShape } from '../../src/type-guards.js';

const trace = makeTracer('AdvancerTest', false);

Expand Down Expand Up @@ -118,11 +119,9 @@ const createTestExtensions = (t, common: CommonSetup) => {
notifyAdvancingResultCalls.push(args);
},
// assume this never returns true for most tests
forwardIfMinted: (...args) => {
mustMatch(
harden(args),
harden([...Object.values(makeAdvanceDetailsShape(usdc.brand))]),
);
checkMintedEarly: (evidence, destination) => {
mustMatch(harden(evidence), CctpTxEvidenceShape);
mustMatch(destination, ChainAddressShape);
return false;
},
});
Expand Down Expand Up @@ -640,7 +639,7 @@ test('rejects advances to unknown settlementAccount', async t => {
]);
});

test('no status update if `forwardIfMinted` returns true', async t => {
test('no status update if `checkMintedEarly` returns true', async t => {
const {
brands: { usdc },
bootstrap: { storage },
Expand All @@ -653,7 +652,7 @@ test('no status update if `forwardIfMinted` returns true', async t => {

const mockNotifyF = Far('Settler Notify Facet', {
notifyAdvancingResult: () => {},
forwardIfMinted: (destination, forwardingAddress, fullAmount, txHash) => {
checkMintedEarly: (evidence, destination) => {
return true;
},
});
Expand All @@ -678,6 +677,6 @@ test('no status update if `forwardIfMinted` returns true', async t => {

t.deepEqual(inspectLogs(), [
['decoded EUD: dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men'],
['⚠️ minted before Observed'],
// no add'l logs as we return early
]);
});
13 changes: 4 additions & 9 deletions packages/fast-usdc/test/exos/settler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,7 @@ const makeTestContext = async t => {
const cctpTxEvidence = makeEvidence(evidence);
const { destination, forwardingAddress, fullAmount, txHash } =
makeNotifyInfo(cctpTxEvidence);
notifyFacet.forwardIfMinted(
destination,
forwardingAddress,
fullAmount,
txHash,
);
notifyFacet.checkMintedEarly(cctpTxEvidence, destination);
return cctpTxEvidence;
},
});
Expand Down Expand Up @@ -454,7 +449,7 @@ test('Settlement for unknown transaction (minted early)', async t => {
accounts.settlement.transferVResolver.resolve(undefined);
await eventLoopIteration();
t.deepEqual(storage.getDeserialized(`fun.txns.${evidence.txHash}`), [
/// TODO with no observed / evidence, does this break reporting reqs?
{ evidence, status: 'OBSERVED' },
{ status: 'FORWARDED' },
]);
});
Expand Down Expand Up @@ -510,7 +505,7 @@ test('Settlement for Advancing transaction (advance succeeds)', async t => {
t.deepEqual(storage.getDeserialized(`fun.txns.${cctpTxEvidence.txHash}`), [
{ evidence: cctpTxEvidence, status: 'OBSERVED' },
{ status: 'ADVANCING' },
// { status: 'ADVANCED' }, TODO: not reported by notifyAdvancingResult
{ status: 'ADVANCED' },
{ split: expectedSplit, status: 'DISBURSED' },
]);
});
Expand Down Expand Up @@ -576,7 +571,7 @@ test('Settlement for Advancing transaction (advance fails)', async t => {
t.deepEqual(storage.getDeserialized(`fun.txns.${cctpTxEvidence.txHash}`), [
{ evidence: cctpTxEvidence, status: 'OBSERVED' },
{ status: 'ADVANCING' },
// { status: 'ADVANCE_FAILED' }, TODO: not reported by notifyAdvancingResult
{ status: 'ADVANCE_FAILED' },
{ status: 'FORWARDED' },
]);
});
Expand Down
4 changes: 2 additions & 2 deletions packages/fast-usdc/test/fast-usdc.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@ test.serial('Settlement for unknown transaction (operator down)', async t => {
await eventLoopIteration();

t.deepEqual(storage.getDeserialized(`fun.txns.${sent.txHash}`), [
// { evidence: sent, status: 'OBSERVED' }, // no OBSERVED state recorded
{ evidence: sent, status: 'OBSERVED' },
{ status: 'FORWARDED' },
]);
});
Expand Down Expand Up @@ -924,7 +924,7 @@ test.serial('mint received why ADVANCING', async t => {
t.deepEqual(storage.getDeserialized(`fun.txns.${sent.txHash}`), [
{ evidence: sent, status: 'OBSERVED' },
{ status: 'ADVANCING' },
// { status: 'ADVANCED' }, TODO: not reported by notifyAdvancingResult
{ status: 'ADVANCED' },
{ split, status: 'DISBURSED' },
]);
});
Expand Down

0 comments on commit 08bf844

Please sign in to comment.