Skip to content

Commit

Permalink
feat(fast-usdc): write status updates to vstorage
Browse files Browse the repository at this point in the history
  • Loading branch information
samsiegart committed Nov 27, 2024
1 parent 23120a9 commit 8403789
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 62 deletions.
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

0 comments on commit 8403789

Please sign in to comment.