Skip to content
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(fast-usdc): write status updates to vstorage #10552

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/boot/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import type { TestFn } from 'ava';
import type { FastUSDCKit } from '@agoric/fast-usdc/src/fast-usdc.start.js';
import type { CctpTxEvidence } from '@agoric/fast-usdc/src/types.js';
import { MockCctpTxEvidences } from '@agoric/fast-usdc/test/fixtures.js';
import { documentStorageSchema } from '@agoric/governance/tools/storageDoc.js';
import { Fail } from '@endo/errors';
import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js';
Expand Down Expand Up @@ -115,6 +117,34 @@ test.serial('writes feed policy to vstorage', async t => {
await documentStorageSchema(t, storage, doc);
});

test.serial('writes status updates to vstorage', async t => {
const { walletFactoryDriver: wd, storage } = t.context;
const wallet = await wd.provideSmartWallet(
'agoric144rrhh4m09mh7aaffhm6xy223ym76gve2x7y78',
);
const submitMockEvidence = (mockEvidence: CctpTxEvidence) =>
wallet.sendOffer({
id: 'submit-mock-evidence',
invitationSpec: {
source: 'agoricContract',
instancePath: ['fastUsdc'],
callPipe: [['makeTestPushInvitation', [mockEvidence]]],
},
proposal: {},
});
const mockEvidence1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
const mockEvidence2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX();

await submitMockEvidence(mockEvidence1);
await submitMockEvidence(mockEvidence2);

const doc = {
node: `fastUsdc.status`,
owner: `the statuses of fast USDC transfers identified by their tx hashes`,
};
await documentStorageSchema(t, storage, doc);
});

test.serial('restart contract', async t => {
const { EV } = t.context.runUtils;
await null;
Expand Down
18 changes: 18 additions & 0 deletions packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,21 @@ Generated by [AVA](https://avajs.dev).
'{"blockHeight":"0","values":["{\\"chainPolicies\\":{\\"Arbitrum\\":{\\"cctpTokenMessengerAddress\\":\\"0x19330d10D9Cc8751218eaf51E8885D058642E08A\\",\\"chainId\\":42161,\\"confirmations\\":2,\\"nobleContractAddress\\":\\"0x19330d10D9Cc8751218eaf51E8885D058642E08A\\"}},\\"nobleAgoricChannelId\\":\\"channel-21\\",\\"nobleDomainId\\":4}"]}',
],
]

## writes status updates to vstorage

> Under "published", the "fastUsdc.status" node is delegated to the statuses of fast USDC transfers identified by their tx hashes.
> The example below illustrates the schema of the data published there.
>
> See also board marshalling conventions (_to appear_).

[
[
'published.fastUsdc.status.0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702',
'{"blockHeight":"0","values":["OBSERVED"]}',
],
[
'published.fastUsdc.status.0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799',
'{"blockHeight":"0","values":["OBSERVED"]}',
],
]
Binary file modified packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap
Binary file not shown.
6 changes: 3 additions & 3 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export const prepareSettler = (
repayer.repay(settlingSeat, split);

// update status manager, marking tx `SETTLED`
statusManager.disbursed(txHash, sender, amount);
statusManager.disbursed(txHash);
},
/**
* @param {EvmHash | undefined} txHash
Expand All @@ -248,7 +248,7 @@ export const prepareSettler = (
},
transferHandler: {
/**
* @param {unknown} result
* @param {unknown} _result
* @param {SettlerTransferCtx} ctx
*
* @typedef {{
Expand All @@ -257,7 +257,7 @@ export const prepareSettler = (
* amount: NatValue;
* }} SettlerTransferCtx
*/
onFulfilled(result, ctx) {
onFulfilled(_result, ctx) {
const { txHash, sender, amount } = ctx;
statusManager.forwarded(txHash, sender, amount);
},
Expand Down
57 changes: 43 additions & 14 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { M } from '@endo/patterns';
import { Fail, makeError, q } from '@endo/errors';

import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
import { E } from '@endo/eventual-send';
import { makeTracer } from '@agoric/internal';
import {
CctpTxEvidenceShape,
EvmHashShape,
PendingTxShape,
} from '../type-guards.js';
import { PendingTxStatus } from '../constants.js';
import { PendingTxStatus, TxStatus } from '../constants.js';

/**
* @import {MapStore, SetStore} from '@agoric/store';
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx, EvmHash} from '../types.js';
* @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx, EvmHash, LogFn} from '../types.js';
*/

/**
Expand Down Expand Up @@ -53,6 +54,12 @@ const seenTxKeyOf = evidence => {
return `seenTx:${JSON.stringify([txHash, chainId])}`;
};

/**
* @typedef {{
* log?: LogFn;
* }} StatusManagerPowers
*/

/**
* The `StatusManager` keeps track of Pending and Seen Transactions
* via {@link PendingTxStatus} states, aiding in coordination between the `Advancer`
Expand All @@ -61,8 +68,16 @@ const seenTxKeyOf = evidence => {
* XXX consider separate facets for `Advancing` and `Settling` capabilities.
*
* @param {Zone} zone
* @param {() => Promise<StorageNode>} makeStatusNode
* @param {StatusManagerPowers} caps
*/
export const prepareStatusManager = zone => {
export const prepareStatusManager = (
zone,
makeStatusNode,
{
log = makeTracer('Advancer', true),
} = /** @type {StatusManagerPowers} */ ({}),
) => {
/** @type {MapStore<PendingTxKey, PendingTx[]>} */
const pendingTxs = zone.mapStore('PendingTxs', {
keyShape: M.string(),
Expand All @@ -74,6 +89,17 @@ export const prepareStatusManager = zone => {
keyShape: M.string(),
});

/**
* @param {CctpTxEvidence['txHash']} hash
* @param {TxStatus} status
*/
const recordStatus = (hash, status) => {
const statusNodeP = makeStatusNode();
const txnNodeP = E(statusNodeP).makeChildNode(hash);
// Don't await, just writing to vstorage.
void E(txnNodeP).setValue(status);
};

/**
* Ensures that `txHash+chainId` has not been processed
* and adds entry to `seenTxs` set.
Expand All @@ -95,6 +121,7 @@ export const prepareStatusManager = zone => {
pendingTxKeyOf(evidence),
harden({ ...evidence, status }),
);
recordStatus(evidence.txHash, status);
};

return zone.exo(
Expand All @@ -118,9 +145,7 @@ export const prepareStatusManager = zone => {
M.undefined(),
),
),
disbursed: M.call(EvmHashShape, M.string(), M.nat()).returns(
M.undefined(),
),
disbursed: M.call(EvmHashShape).returns(M.undefined()),
forwarded: M.call(M.opt(EvmHashShape), M.string(), M.nat()).returns(
M.undefined(),
),
Expand Down Expand Up @@ -163,6 +188,7 @@ export const prepareStatusManager = zone => {
: PendingTxStatus.AdvanceFailed;
const txpost = { ...tx, status };
pendingTxs.set(key, harden([...prefix, txpost, ...suffix]));
recordStatus(tx.txHash, status);
},

/**
Expand Down Expand Up @@ -219,12 +245,9 @@ export const prepareStatusManager = zone => {
* Mark a transaction as `DISBURSED`
*
* @param {EvmHash} txHash
* @param {NobleAddress} address
* @param {bigint} amount
*/
disbursed(txHash, address, amount) {
// TODO: store txHash -> evidence for txs pending settlement?
console.log('TODO: vstorage update', { txHash, address, amount });
disbursed(txHash) {
recordStatus(txHash, TxStatus.Disbursed);
},

/**
Expand All @@ -235,8 +258,14 @@ export const prepareStatusManager = zone => {
* @param {bigint} amount
*/
forwarded(txHash, address, amount) {
// TODO: store txHash -> evidence for txs pending settlement?
console.log('TODO: vstorage update', { txHash, address, amount });
if (txHash) {
recordStatus(txHash, TxStatus.Forwarded);
} else {
// TODO store (early) `Minted` transactions to check against incoming evidence
log(
`⚠️ Forwarded minted amount ${amount} from account ${address} before it was observed.`,
);
}
},

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import * as flows from './fast-usdc.flows.js';

const trace = makeTracer('FastUsdc');

const STATUS_NODE = 'status';

/**
* @import {HostInterface} from '@agoric/async-flow';
* @import {CosmosChainInfo, Denom, DenomDetail, OrchestrationAccount} from '@agoric/orchestration';
Expand Down Expand Up @@ -84,7 +86,9 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
marshaller,
);

const statusManager = prepareStatusManager(zone);
const makeStatusNode = () =>
E(privateArgs.storageNode).makeChildNode(STATUS_NODE);
const statusManager = prepareStatusManager(zone, makeStatusNode);

const { USDC } = terms.brands;
const { withdrawToSeat } = tools.zoeTools;
Expand Down
4 changes: 2 additions & 2 deletions packages/fast-usdc/test/exos/advancer.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { TestFn } from 'ava';
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';
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 { Far } from '@endo/pass-style';
import { makePromiseKit } from '@endo/promise-kit';
import type { NatAmount } from '@agoric/ertp';
import { type ZoeTools } from '@agoric/orchestration/src/utils/zoe-tools.js';
import { q } from '@endo/errors';
Expand Down Expand Up @@ -36,6 +34,7 @@ const createTestExtensions = (t, common: CommonSetup) => {
bootstrap: { rootZone, vowTools },
facadeServices: { chainHub },
brands: { usdc },
commonPrivateArgs: { storageNode },
} = common;

const { log, inspectLogs } = makeTestLogger(t.log);
Expand All @@ -45,6 +44,7 @@ const createTestExtensions = (t, common: CommonSetup) => {

const statusManager = prepareStatusManager(
rootZone.subZone('status-manager'),
async () => storageNode.makeChildNode('status'),
);

const mockAccounts = prepareMockOrchAccounts(rootZone.subZone('accounts'), {
Expand Down
37 changes: 26 additions & 11 deletions packages/fast-usdc/test/exos/settler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
import { PendingTxStatus } from '../../src/constants.js';
import { prepareSettler } from '../../src/exos/settler.js';
import { prepareStatusManager } from '../../src/exos/status-manager.js';
import type { CctpTxEvidence } from '../../src/types.js';
import { commonSetup } from '../supports.js';
import { MockCctpTxEvidences, MockVTransferEvents } from '../fixtures.js';
import { prepareMockOrchAccounts } from '../mocks.js';
import { commonSetup, provideDurableZone } from '../supports.js';
import type { CctpTxEvidence } from '../../src/types.js';
import { makeTestLogger, prepareMockOrchAccounts } from '../mocks.js';
import { makeFeeTools } from '../../src/utils/fees.js';

const mockZcf = (zone: Zone) => {
Expand Down Expand Up @@ -40,8 +40,12 @@ const mockZcf = (zone: Zone) => {
const makeTestContext = async t => {
const common = await commonSetup(t);
const { rootZone: zone } = common.bootstrap;
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

const { log, inspectLogs } = makeTestLogger(t.log);
const statusManager = prepareStatusManager(
zone.subZone('status-manager'),
async () => common.commonPrivateArgs.storageNode.makeChildNode('status'),
{ log },
);
const { zcf, callLog } = mockZcf(zone.subZone('Mock ZCF'));

const { rootZone, vowTools } = common.bootstrap;
Expand Down Expand Up @@ -126,7 +130,9 @@ const makeTestContext = async t => {
simulate,
repayer,
peekCalls: () => harden([...callLog]),
inspectLogs,
accounts: mockAccounts,
storage: common.bootstrap.storage,
};
};

Expand Down Expand Up @@ -221,7 +227,12 @@ test('happy path: disburse to LPs; StatusManager removes tx', async t => {
[],
'SETTLED entry removed from StatusManger',
);
// TODO, confirm vstorage write for TxStatus.SETTLED
await eventLoopIteration();
const vstorage = t.context.storage.data;
t.is(
vstorage.get(`mockChainStorageRoot.status.${cctpTxEvidence.txHash}`),
'DISBURSED',
);
});

test('slow path: forward to EUD; remove pending tx', async t => {
Expand Down Expand Up @@ -279,19 +290,22 @@ test('slow path: forward to EUD; remove pending tx', async t => {
[],
'SETTLED entry removed from StatusManger',
);
// TODO, confirm vstorage write for TxStatus.SETTLED
const vstorage = t.context.storage.data;
t.is(
vstorage.get(`mockChainStorageRoot.status.${cctpTxEvidence.txHash}`),
'FORWARDED',
);
});

test('Settlement for unknown transaction', async t => {
const {
common,
makeSettler,
statusManager,
defaultSettlerParams,
repayer,
simulate,
accounts,
peekCalls,
inspectLogs,
} = t.context;
const { usdc } = common.brands;

Expand All @@ -318,8 +332,9 @@ test('Settlement for unknown transaction', async t => {
usdc.units(150),
],
]);

// TODO, confirm vstorage write for TxStatus.FORWARDED
t.deepEqual(inspectLogs(0), [
'⚠️ Forwarded minted amount 150000000 from account noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd before it was observed.',
]);
});

test.todo("StatusManager does not receive update when we can't settle");
Loading
Loading