Skip to content

Commit

Permalink
feat: status manager tests
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Nov 22, 2024
1 parent 09cd317 commit 1a8aafd
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 53 deletions.
41 changes: 31 additions & 10 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,19 @@ export const prepareStatusManager = zone => {
advanceOutcome: M.call(M.string(), M.nat(), M.boolean()).returns(),
observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
isSeen: M.call(CctpTxEvidenceShape).returns(M.boolean()),
dequeueStatus: M.call(M.string(), M.bigint()).returns({
txHash: EvmHashShape,
status: M.string(), // TODO: named shape?
}),
dequeueStatus: M.call(M.string(), M.bigint()).returns(
M.or(
{
txHash: EvmHashShape,
status: M.or(
PendingTxStatus.Advanced,
PendingTxStatus.AdvanceFailed,
PendingTxStatus.Observed,
),
},
M.undefined(),
),
),
disbursed: M.call(EvmHashShape, M.string(), M.nat()).returns(
M.undefined(),
),
Expand Down Expand Up @@ -135,6 +144,7 @@ export const prepareStatusManager = zone => {
*/
advanceOutcome(sender, amount, success) {
const key = makePendingTxKey(sender, amount);
pendingTxs.has(key) || Fail`no advancing tx with ${{ sender, amount }}`;
const pending = pendingTxs.get(key);
const ix = pending.findIndex(
tx => tx.status === PendingTxStatus.Advancing,
Expand Down Expand Up @@ -177,15 +187,24 @@ export const prepareStatusManager = zone => {
*/
dequeueStatus(address, amount) {
const key = makePendingTxKey(address, amount);
if (!pendingTxs.has(key)) return undefined;
const pending = pendingTxs.get(key);
if (!pending.length) return undefined;

const dequeueIdx = pending.findIndex(
x => x.status !== PendingTxStatus.Advancing,
);
if (dequeueIdx < 0) return undefined;

if (!pending.length) {
return undefined;
if (pending.length > 1) {
const pendingCopy = [...pending];
pendingCopy.splice(dequeueIdx, 1);
pendingTxs.set(key, harden(pendingCopy));
} else {
pendingTxs.delete(key);
}

const [tx0, ...rest] = pending;
pendingTxs.set(key, harden(rest));
const { status, txHash } = tx0;
const { status, txHash } = pending[dequeueIdx];
// TODO: store txHash -> evidence for txs pending settlement?
return harden({ status, txHash });
},
Expand Down Expand Up @@ -217,14 +236,16 @@ export const prepareStatusManager = zone => {
/**
* Lookup all pending entries for a given address and amount
*
* XXX only used in tests. should we remove?
*
* @param {NobleAddress} address
* @param {bigint} amount
* @returns {PendingTx[]}
*/
lookupPending(address, amount) {
const key = makePendingTxKey(address, amount);
if (!pendingTxs.has(key)) {
throw makeError(`Key ${q(key)} not yet observed`);
return [];
}
return pendingTxs.get(key);
},
Expand Down
233 changes: 190 additions & 43 deletions packages/fast-usdc/test/exos/status-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { provideDurableZone } from '../supports.js';
import { MockCctpTxEvidences } from '../fixtures.js';
import type { CctpTxEvidence } from '../../src/types.js';

test('advance creates new entry with ADVANCED status', t => {
test('advancing creates new entry with ADVANCING status', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

Expand All @@ -17,9 +17,9 @@ test('advance creates new entry with ADVANCED status', t => {
evidence.tx.amount,
);

t.is(entries[0]?.status, PendingTxStatus.Advanced);
t.is(entries[0]?.status, PendingTxStatus.Advancing);
});
test.todo('ADVANCED transactions are published to vstorage');
test.todo('ADVANCING transactions are published to vstorage');

test('observe creates new entry with OBSERVED status', t => {
const zone = provideDurableZone('status-test');
Expand Down Expand Up @@ -62,54 +62,176 @@ test('cannot process same tx twice', t => {
t.notThrows(() => statusManager.advancing({ ...evidence, chainId: 9999 }));
});

test('settle removes entries from PendingTxs', t => {
test('isSeen checks if a tx has been processed', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
statusManager.advancing(evidence);
statusManager.observe({ ...evidence, txHash: '0xtest1' });
const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
t.false(statusManager.isSeen(e1));
statusManager.advancing(e1);
t.true(statusManager.isSeen(e1));

statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount);
statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount);
const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX();
t.false(statusManager.isSeen(e2));
statusManager.observe(e2);
t.true(statusManager.isSeen(e2));
});

const entries = statusManager.lookupPending(
evidence.tx.forwardingAddress,
evidence.tx.amount,
test('dequeueStatus removes entries from PendingTxs', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX();

statusManager.advancing(e1);
statusManager.advanceOutcome(e1.tx.forwardingAddress, e1.tx.amount, true);
statusManager.advancing(e2);
statusManager.advanceOutcome(e2.tx.forwardingAddress, e2.tx.amount, false);
statusManager.observe({ ...e1, txHash: '0xtest1' });

t.deepEqual(
statusManager.dequeueStatus(e1.tx.forwardingAddress, e1.tx.amount),
{
txHash: e1.txHash,
status: PendingTxStatus.Advanced,
},
);

t.deepEqual(
statusManager.dequeueStatus(e2.tx.forwardingAddress, e2.tx.amount),
{
txHash: e2.txHash,
status: PendingTxStatus.AdvanceFailed,
},
);

t.deepEqual(
statusManager.dequeueStatus(e1.tx.forwardingAddress, e1.tx.amount),
{
txHash: '0xtest1',
status: PendingTxStatus.Observed,
},
);

t.is(
statusManager.lookupPending(e1.tx.forwardingAddress, e1.tx.amount).length,
0,
'Settled entries should be deleted',
);

t.is(
statusManager.lookupPending(e2.tx.forwardingAddress, e2.tx.amount).length,
0,
'Settled entry should be deleted',
);
t.is(entries.length, 0, 'Settled entry should be deleted');
});

test('cannot SETTLE without an ADVANCED or OBSERVED entry', t => {
test('cannot advanceOutcome without ADVANCING entry', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
const advanceOutcomeFn = () =>
statusManager.advanceOutcome(e1.tx.forwardingAddress, e1.tx.amount, true);
const expectedErrMsg =
'no advancing tx with {"amount":"[150000000n]","sender":"noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd"}';

t.throws(
() =>
statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount),
t.throws(advanceOutcomeFn, {
message: expectedErrMsg,
});

statusManager.observe(e1);
t.throws(advanceOutcomeFn, {
message: expectedErrMsg,
});

const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX();
statusManager.advancing(e2);
t.notThrows(() =>
statusManager.advanceOutcome(e2.tx.forwardingAddress, e2.tx.amount, true),
);
});

test('advanceOutcome transitions to ADVANCED and ADVANCE_FAILED', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));
const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX();

statusManager.advancing(e1);
statusManager.advanceOutcome(e1.tx.forwardingAddress, e1.tx.amount, true);
t.like(statusManager.lookupPending(e1.tx.forwardingAddress, e1.tx.amount), [
{
message:
'key "pendingTx:[\\"noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd\\",\\"150000000\\"]" not found in collection "PendingTxs"',
status: PendingTxStatus.Advanced,
},
]);

statusManager.advancing(e2);
statusManager.advanceOutcome(e2.tx.forwardingAddress, e2.tx.amount, false);
t.like(statusManager.lookupPending(e2.tx.forwardingAddress, e2.tx.amount), [
{
status: PendingTxStatus.AdvanceFailed,
},
]);
});
test.todo('ADVANCED transactions are published to vstorage');

test('dequeueStatus returns undefined when nothing is settleable', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO();

t.is(
statusManager.dequeueStatus(e1.tx.forwardingAddress, e1.tx.amount),
undefined,
);
});

test('settle SETTLES first matched entry', t => {
test('dequeueStatus returns first (earliest) matched entry', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO();

// advance two
// advance two txs
statusManager.advancing(evidence);
statusManager.advancing({ ...evidence, txHash: '0xtest2' });
// also settles OBSERVED statuses

// cannot dequeue ADVANCING pendingTx
t.is(
statusManager.dequeueStatus(
evidence.tx.forwardingAddress,
evidence.tx.amount,
),
undefined,
);

statusManager.advanceOutcome(
evidence.tx.forwardingAddress,
evidence.tx.amount,
true,
);
statusManager.advanceOutcome(
evidence.tx.forwardingAddress,
evidence.tx.amount,
true,
);

// also can dequeue OBSERVED statuses
statusManager.observe({ ...evidence, txHash: '0xtest3' });

// settle will settle the first match
statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount);
// dequeue will return the first match
t.like(
statusManager.dequeueStatus(
evidence.tx.forwardingAddress,
evidence.tx.amount,
),
{
status: PendingTxStatus.Advanced,
},
);
const entries0 = statusManager.lookupPending(
evidence.tx.forwardingAddress,
evidence.tx.amount,
Expand All @@ -127,37 +249,51 @@ test('settle SETTLES first matched entry', t => {
'order of remaining entries preserved',
);

// settle again wih same args settles 2nd advance
statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount);
// settle again wih same args settles remaining observe
statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount);
// dequeue again wih same args to settle 2nd advance
t.like(
statusManager.dequeueStatus(
evidence.tx.forwardingAddress,
evidence.tx.amount,
),
{
status: 'ADVANCED',
},
);
// dequeue again wih same ags to settle remaining observe
t.like(
statusManager.dequeueStatus(
evidence.tx.forwardingAddress,
evidence.tx.amount,
),
{
status: 'OBSERVED',
},
);
const entries1 = statusManager.lookupPending(
evidence.tx.forwardingAddress,
evidence.tx.amount,
);
// TODO, check vstorage for TxStatus.Settled
t.is(entries1?.length, 0, 'settled entries are deleted');

t.throws(
() =>
statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount),
{
message:
'No unsettled entry for "pendingTx:[\\"noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd\\",\\"150000000\\"]"',
},
t.is(
statusManager.dequeueStatus(
evidence.tx.forwardingAddress,
evidence.tx.amount,
),
undefined,
'No more matches to settle',
);
});

test('lookup throws when presented a key it has not seen', t => {
test('lookupPending returns empty array when presented a key it has not seen', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

t.throws(() => statusManager.lookupPending('noble123', 1n), {
message: 'Key "pendingTx:[\\"noble123\\",\\"1\\"]" not yet observed',
});
t.deepEqual(statusManager.lookupPending('noble123', 1n), []);
});

// TODO: remove? this doesn't seem to hold it's weight
test('StatusManagerKey logic handles addresses with hyphens', async t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));
Expand All @@ -173,12 +309,23 @@ test('StatusManagerKey logic handles addresses with hyphens', async t => {
);

t.is(entries.length, 1);
t.is(entries[0]?.status, PendingTxStatus.Advanced);
t.is(entries[0]?.status, PendingTxStatus.Advancing);

statusManager.advanceOutcome(
evidence.tx.forwardingAddress,
evidence.tx.amount,
true,
);

statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount);
statusManager.dequeueStatus(
evidence.tx.forwardingAddress,
evidence.tx.amount,
);
const remainingEntries = statusManager.lookupPending(
evidence.tx.forwardingAddress,
evidence.tx.amount,
);
t.is(remainingEntries.length, 0, 'Entry should be settled');
t.is(remainingEntries.length, 0, 'Entry should be dequeued from pending');
});

test.todo('ADVANCE_FAILED -> FORWARDED transition');

0 comments on commit 1a8aafd

Please sign in to comment.