Skip to content

Commit

Permalink
feat: report after mint results in forward
Browse files Browse the repository at this point in the history
- Advancer calls `forwardIfMinted` to check for an early matching mint
- if found, no Advance occurs and the minted funds are forwarded
  • Loading branch information
0xpatrickdev committed Dec 19, 2024
1 parent 7fbcaa1 commit 65ca559
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 24 deletions.
42 changes: 35 additions & 7 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ const AdvancerKitI = harden({
onRejected: M.call(M.error(), AdvancerVowCtxShape).returns(),
}),
transferHandler: M.interface('TransferHandlerI', {
// TODO confirm undefined, and not bigint (sequence)
onFulfilled: M.call(M.undefined(), AdvancerVowCtxShape).returns(
M.undefined(),
),
Expand Down Expand Up @@ -146,9 +145,7 @@ export const prepareAdvancerKit = (
log('txHash already seen:', evidence.txHash);
return;
}

const { borrowerFacet, poolAccount, settlementAddress } =
this.state;
const { settlementAddress } = this.state;
const { recipientAddress } = evidence.aux;
const decoded = decodeAddressHook(recipientAddress);
mustMatch(decoded, AddressHookShape);
Expand All @@ -161,6 +158,26 @@ 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;
}

// throws if requested does not exceed fees
const advanceAmount = feeTools.calculateAdvance(fullAmount);

Expand All @@ -183,7 +200,7 @@ export const prepareAdvancerKit = (
forwardingAddress: evidence.tx.forwardingAddress,
fullAmount,
tmpSeat,
txHash: evidence.txHash,
txHash,
});
} catch (error) {
log('Advancer error:', error);
Expand All @@ -202,7 +219,13 @@ export const prepareAdvancerKit = (
*/
onFulfilled(result, ctx) {
const { poolAccount, intermediateRecipient } = this.state;
const { destination, advanceAmount, ...detail } = ctx;
const {
destination,
advanceAmount,
// eslint-disable-next-line no-unused-vars
tmpSeat,
...detail
} = ctx;
const transferV = E(poolAccount).transfer(
destination,
{ denom: usdc.denom, value: advanceAmount.value },
Expand Down Expand Up @@ -267,7 +290,12 @@ export const prepareAdvancerKit = (
onRejected(error, ctx) {
const { notifyFacet } = this.state;
log('Advance transfer rejected', error);
notifyFacet.notifyAdvancingResult(ctx, false);
const {
// eslint-disable-next-line no-unused-vars
advanceAmount,
...restCtx
} = ctx;
notifyFacet.notifyAdvancingResult(restCtx, false);
},
},
},
Expand Down
33 changes: 32 additions & 1 deletion packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export const prepareSettler = (
makeAdvanceDetailsShape(USDC),
M.boolean(),
).returns(),
forwardIfMinted: M.call(
...Object.values(makeAdvanceDetailsShape(USDC)),
).returns(M.boolean()),
}),
self: M.interface('SettlerSelfI', {
disburse: M.call(EvmHashShape, M.nat()).returns(M.promise()),
Expand Down Expand Up @@ -189,7 +192,10 @@ export const prepareSettler = (

case undefined:
default:
log('⚠️ tap: no status for ', nfa, amount);
log('⚠️ tap: minted before observed', nfa, amount);
// XXX consider capturing in vstorage
// we would need a new key, as this does not have a txHash
this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount));
}
},
},
Expand Down Expand Up @@ -230,6 +236,31 @@ export const prepareSettler = (
statusManager.advanceOutcome(forwardingAddress, fullValue, success);
}
},
/**
* @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);
const { mintedEarly } = this.state;
if (mintedEarly.has(key)) {
log(
'matched minted early key, initiating forward',
forwardingAddress,
fullValue,
);
mintedEarly.delete(key);
// TODO: does not write `OBSERVED` to vstorage
void this.facets.self.forward(txHash, fullValue, destination.value);
return true;
}
return false;
},
},
self: {
/**
Expand Down
62 changes: 59 additions & 3 deletions packages/fast-usdc/test/exos/advancer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
import { 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 { q } from '@endo/errors';
import { Fail, q } from '@endo/errors';
import { Far } from '@endo/pass-style';
import type { TestFn } from 'ava';
import { makeTracer } from '@agoric/internal';
import { M, mustMatch } from '@endo/patterns';
import { PendingTxStatus } from '../../src/constants.js';
import { prepareAdvancer } from '../../src/exos/advancer.js';
import type { SettlerKit } from '../../src/exos/settler.js';
import {
makeAdvanceDetailsShape,
type SettlerKit,
} from '../../src/exos/settler.js';
import { prepareStatusManager } from '../../src/exos/status-manager.js';
import type { LiquidityPoolKit } from '../../src/types.js';
import { makeFeeTools } from '../../src/utils/fees.js';
Expand Down Expand Up @@ -108,8 +112,19 @@ const createTestExtensions = (t, common: CommonSetup) => {
const mockNotifyF = Far('Settler Notify Facet', {
notifyAdvancingResult: (...args: NotifyArgs) => {
trace('Settler.notifyAdvancingResult called with', args);
const [advanceDetails, success] = args;
mustMatch(harden(advanceDetails), makeAdvanceDetailsShape(usdc.brand));
mustMatch(success, M.boolean());
notifyAdvancingResultCalls.push(args);
},
// assume this never returns true for most tests
forwardIfMinted: (...args) => {
mustMatch(
harden(args),
harden([...Object.values(makeAdvanceDetailsShape(usdc.brand))]),
);
return false;
},
});

const mockBorrowerFacetCalls: {
Expand Down Expand Up @@ -361,7 +376,6 @@ test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t
txHash: evidence.txHash,
forwardingAddress: evidence.tx.forwardingAddress,
fullAmount: usdc.make(evidence.tx.amount),
advanceAmount: feeTools.calculateAdvance(usdc.make(evidence.tx.amount)),
destination: {
value: decodeAddressHook(evidence.aux.recipientAddress).query.EUD,
},
Expand Down Expand Up @@ -625,3 +639,45 @@ test('rejects advances to unknown settlementAccount', async t => {
],
]);
});

test('no status update if `forwardIfMinted` returns true', async t => {
const {
brands: { usdc },
bootstrap: { storage },
extensions: {
services: { makeAdvancer },
helpers: { inspectLogs },
mocks: { mockPoolAccount, mockBorrowerF },
},
} = t.context;

const mockNotifyF = Far('Settler Notify Facet', {
notifyAdvancingResult: () => {},
forwardIfMinted: (destination, forwardingAddress, fullAmount, txHash) => {
return true;
},
});

const advancer = makeAdvancer({
borrowerFacet: mockBorrowerF,
notifyFacet: mockNotifyF,
poolAccount: mockPoolAccount.account,
intermediateRecipient,
settlementAddress,
});

const evidence = MockCctpTxEvidences.AGORIC_PLUS_DYDX();
void advancer.handleTransactionEvent(evidence);
await eventLoopIteration();

// advancer does not post a tx status; settler will Forward and
// communicate Forwarded/ForwardFailed status'
t.throws(() => storage.getDeserialized(`fun.txns.${evidence.txHash}`), {
message: /no data at path fun.txns.0x/,
});

t.deepEqual(inspectLogs(), [
['decoded EUD: dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men'],
['⚠️ minted before Observed'],
]);
});
87 changes: 87 additions & 0 deletions packages/fast-usdc/test/exos/settler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@ const makeTestContext = async t => {

return cctpTxEvidence;
},
/**
* mint early path. caller must simulate tap before calling
* @param evidence
*/
observeLate: (evidence?: CctpTxEvidence) => {
const cctpTxEvidence = makeEvidence(evidence);
const { destination, forwardingAddress, fullAmount, txHash } =
makeNotifyInfo(cctpTxEvidence);
notifyFacet.forwardIfMinted(
destination,
forwardingAddress,
fullAmount,
txHash,
);
return cctpTxEvidence;
},
});
return simulate;
};
Expand Down Expand Up @@ -372,6 +388,77 @@ test('slow path: forward to EUD; remove pending tx', async t => {
t.is(storage.data.get(`fun.txns.${cctpTxEvidence.txHash}`), undefined);
});

test('Settlement for unknown transaction (minted early)', async t => {
const {
common: {
brands: { usdc },
},
makeSettler,
defaultSettlerParams,
repayer,
accounts,
peekCalls,
inspectLogs,
makeSimulate,
storage,
} = t.context;

const settler = makeSettler({
repayer,
settlementAccount: accounts.settlement.account,
...defaultSettlerParams,
});
const simulate = makeSimulate(settler.notify);

t.log('Simulate incoming IBC settlement');
void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO());
await eventLoopIteration();

t.log('Nothing was transferred');
t.deepEqual(peekCalls(), []);
t.deepEqual(accounts.settlement.callLog, []);
const tapLogs = inspectLogs();
t.like(tapLogs, [
['config', { sourceChannel: 'channel-21' }],
['upcall event'],
['dequeued', undefined],
['⚠️ tap: minted before observed'],
]);

t.log('Oracle operators eventually report...');
const evidence = simulate.observeLate();
t.deepEqual(inspectLogs().slice(tapLogs.length - 1), [
[
'matched minted early key, initiating forward',
'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd',
150000000n,
],
]);
await eventLoopIteration();
t.like(accounts.settlement.callLog, [
[
'transfer',
{
value: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
},
usdc.units(150),
{
forwardOpts: {
intermediateRecipient: {
value: 'noble1test',
},
},
},
],
]);
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?
{ status: 'FORWARDED' },
]);
});

test('Settlement for Advancing transaction (advance succeeds)', async t => {
const {
accounts,
Expand Down
Loading

0 comments on commit 65ca559

Please sign in to comment.