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

fusdc: status manager fixups #10544

Closed
wants to merge 6 commits into from
Closed
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
26 changes: 26 additions & 0 deletions packages/fast-usdc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,29 @@ sequenceDiagram

A->>TF: notify(evidence)
```

# Status Manager

### Contract state diagram

*Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.*

```mermaid
stateDiagram-v2
[*] --> Advanced: Advancer .advance()
Advanced --> Settled: Settler .settle() after fees
[*] --> Observed: Advancer .observed()
Observed --> Settled: Settler .settle() sans fees
Settled --> [*]
```

### Complete state diagram (starting from OCW)

```mermaid
stateDiagram-v2
Observed --> Qualified
Observed --> Unqualified
Qualified --> Advanced
Advanced --> Settled
Qualified --> Settled
```
26 changes: 0 additions & 26 deletions packages/fast-usdc/src/exos/README.md

This file was deleted.

41 changes: 26 additions & 15 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { PendingTxStatus } from '../constants.js';
/**
* Create the key for the pendingTxs MapStore.
*
* The key is a composite of `txHash` and `chainId` and not meant to be
* parsable.
* The key is a composite of Noble address `addr` and transaction `amount` and
* not meant to be parsable.
*
* @param {NobleAddress} addr
* @param {bigint} amount
Expand All @@ -38,15 +38,14 @@ const pendingTxKeyOf = evidence => {
/**
* Get the key for the seenTxs SetStore.
*
* The key is a composite of `NobleAddress` and transaction `amount` and not
* meant to be parsable.
* The key is based on `txHash` and not meant to be parsable.
*
* @param {CctpTxEvidence} evidence
* @returns {SeenTxKey}
*/
const seenTxKeyOf = evidence => {
const { txHash, chainId } = evidence;
return `seenTx:${JSON.stringify([txHash, chainId])}`;
const { txHash } = evidence;
return `seenTx:${txHash}`;
};

/**
Expand Down Expand Up @@ -108,6 +107,7 @@ export const prepareStatusManager = zone => {
/**
* Add a new transaction with ADVANCED status
* @param {CctpTxEvidence} evidence
* @throws {Error} if transaction was already seen
*/
advance(evidence) {
recordPendingTx(evidence, PendingTxStatus.Advanced);
Expand All @@ -116,6 +116,7 @@ export const prepareStatusManager = zone => {
/**
* Add a new transaction with OBSERVED status
* @param {CctpTxEvidence} evidence
* @throws {Error} if transaction was already seen
*/
observe(evidence) {
recordPendingTx(evidence, PendingTxStatus.Observed);
Expand All @@ -130,41 +131,51 @@ export const prepareStatusManager = zone => {
*/
hasPendingSettlement(address, amount) {
const key = makePendingTxKey(address, amount);
if (!pendingTxs.has(key)) return false;
const pending = pendingTxs.get(key);
return !!pending.length;
},

/**
* Mark an `ADVANCED` or `OBSERVED` transaction as `SETTLED` and remove it
* Mark an `ADVANCED` or `OBSERVED` transaction as `SETTLED` and remove
* it.
*
* If there are multiple EDU+amt matches, we are unable to know which tx
* the settlement is for, but we’ll act as if it’s the earliest one.
*
* @param {NobleAddress} address
* @param {bigint} amount
* @throws {Error} if a pending settlement was not found for the address + amount
*/
settle(address, amount) {
const key = makePendingTxKey(address, amount);
const pending = pendingTxs.get(key);

// @ts-expect-error this.self not recognized
const pending = this.self.lookupPending(address, amount);
if (!pending.length) {
throw makeError(`No unsettled entry for ${q(key)}`);
throw makeError(`No unsettled entry for ${q([address, amount])}`);
}

const pendingCopy = [...pending];
pendingCopy.shift();
// TODO, vstorage update for `TxStatus.Settled`
pendingTxs.set(key, harden(pendingCopy));

const key = makePendingTxKey(address, amount);
if (pendingCopy.length) {
return pendingTxs.set(key, harden(pendingCopy));
}
return pendingTxs.delete(key);
},

/**
* Lookup all pending entries for a given address and amount
* Lookup all pending entries for a given address and amount.
*
* @param {NobleAddress} address
* @param {bigint} amount
* @returns {PendingTx[]}
* @returns {PendingTx[]} pendingTxs or empty array if nothing is found
*/
lookupPending(address, amount) {
const key = makePendingTxKey(address, amount);
if (!pendingTxs.has(key)) {
throw makeError(`Key ${q(key)} not yet observed`);
return harden([]);
}
return pendingTxs.get(key);
},
Expand Down
2 changes: 0 additions & 2 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
const creatorFacet = zone.exo('Fast USDC Creator', undefined, {
/** @type {(operatorId: string) => Promise<Invitation<OperatorKit>>} */
async makeOperatorInvitation(operatorId) {
// eslint-disable-next-line no-use-before-define
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this based on master? those lines shouldn't be there anymore

return feedKit.creator.makeOperatorInvitation(operatorId);
},
/**
Expand Down Expand Up @@ -157,7 +156,6 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
* @param {CctpTxEvidence} evidence
*/
makeTestPushInvitation(evidence) {
// eslint-disable-next-line no-use-before-define
void advancer.handleTransactionEvent(evidence);
return makeTestInvitation();
},
Expand Down
2 changes: 1 addition & 1 deletion packages/fast-usdc/test/exos/advancer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ test('will not advance same txHash:chainId evidence twice', async t => {

t.deepEqual(inspectLogs(1), [
'Advancer error:',
'"[Error: Transaction already seen: \\"seenTx:[\\\\\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\\\\\",1]\\"]"',
'"[Error: Transaction already seen: \\"seenTx:0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\"]"',
]);
});

Expand Down
37 changes: 27 additions & 10 deletions packages/fast-usdc/test/exos/status-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,16 @@ test('cannot process same tx twice', t => {

t.throws(() => statusManager.advance(evidence), {
message:
'Transaction already seen: "seenTx:[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]"',
'Transaction already seen: "seenTx:0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702"',
});

t.throws(() => statusManager.observe(evidence), {
message:
'Transaction already seen: "seenTx:[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]"',
'Transaction already seen: "seenTx:0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702"',
});

// new txHash should not throw
t.notThrows(() => statusManager.advance({ ...evidence, txHash: '0xtest2' }));
// new chainId with existing txHash should not throw
t.notThrows(() => statusManager.advance({ ...evidence, chainId: 9999 }));
});

test('settle removes entries from PendingTxs', t => {
Expand Down Expand Up @@ -89,7 +87,7 @@ test('cannot SETTLE without an ADVANCED or OBSERVED entry', t => {
statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount),
{
message:
'key "pendingTx:[\\"noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd\\",\\"150000000\\"]" not found in collection "PendingTxs"',
'No unsettled entry for ["noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd","[150000000n]"]',
},
);
});
Expand Down Expand Up @@ -141,19 +139,17 @@ test('settle SETTLES first matched entry', t => {
statusManager.settle(evidence.tx.forwardingAddress, evidence.tx.amount),
{
message:
'No unsettled entry for "pendingTx:[\\"noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd\\",\\"150000000\\"]"',
'No unsettled entry for ["noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd","[150000000n]"]',
},
'No more matches to settle',
);
});

test('lookup throws when presented a key it has not seen', t => {
test('lookingPending returns an 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), []);
});

test('StatusManagerKey logic handles addresses with hyphens', async t => {
Expand All @@ -180,3 +176,24 @@ test('StatusManagerKey logic handles addresses with hyphens', async t => {
);
t.is(remainingEntries.length, 0, 'Entry should be settled');
});

test('hasPendingSettlement code paths', t => {
const zone = provideDurableZone('status-test');
const statusManager = prepareStatusManager(zone.subZone('status-manager'));
const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
const { forwardingAddress, amount } = evidence.tx;

// Nothing in store
t.false(statusManager.hasPendingSettlement(forwardingAddress, amount));

// Observed in store
statusManager.observe(evidence);
t.true(statusManager.hasPendingSettlement(forwardingAddress, amount));
statusManager.settle(forwardingAddress, amount);

// Advanced in store
statusManager.advance({ ...evidence, txHash: '0xtest2' });
t.true(statusManager.hasPendingSettlement(forwardingAddress, amount));
statusManager.settle(forwardingAddress, amount);
t.false(statusManager.hasPendingSettlement(forwardingAddress, amount));
});
Loading