From 0128fd007e22a497425fef34a4edd3d09cd5c943 Mon Sep 17 00:00:00 2001 From: rabi-siddique Date: Thu, 19 Sep 2024 17:45:00 +0500 Subject: [PATCH 1/2] feat: show and use the latest invitations --- src/components/PsmPanel.tsx | 4 +++- src/components/VaultsPanel.tsx | 4 +++- src/components/VotePanel.tsx | 11 ++++++++--- src/lib/wallet.ts | 13 ++++++++----- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/components/PsmPanel.tsx b/src/components/PsmPanel.tsx index a2814e8..4bbf8ff 100644 --- a/src/components/PsmPanel.tsx +++ b/src/components/PsmPanel.tsx @@ -11,7 +11,7 @@ import ProposeParamChange from './ProposeParamChange'; import ProposePauseOffers from './ProposePauseOffers'; import CharterGuidance from './CharterGuidance'; import { useAtomValue } from 'jotai'; -import { walletUtilsAtom } from 'store/app'; +import { rpcUtilsAtom, walletUtilsAtom } from 'store/app'; // TODO fetch list from RPC const anchors = [ @@ -44,6 +44,7 @@ export default function PsmPanel() { const [anchorName, setAnchorName] = useState(anchors[0]); const [proposalType, setProposalType] = useState(ProposalTypes.paramChange); const walletUtils = useAtomValue(walletUtilsAtom); + const rpcUtils = useAtomValue(rpcUtilsAtom); const { data: walletCurrent, status } = usePublishedDatum( walletUtils ? `wallet.${walletUtils.getWalletAddress()}.current` @@ -54,6 +55,7 @@ export default function PsmPanel() { status, walletCurrent, charterInvitationSpec.description, + rpcUtils?.agoricNames.instance.econCommitteeCharter, ); const previousOfferId = invitationStatus.acceptedIn; diff --git a/src/components/VaultsPanel.tsx b/src/components/VaultsPanel.tsx index 3785092..52f19eb 100644 --- a/src/components/VaultsPanel.tsx +++ b/src/components/VaultsPanel.tsx @@ -14,7 +14,7 @@ import ChangeOracles, { ChangeOraclesMode } from './ChangeOracles'; import PauseLiquidations from './PauseLiquidations'; import AuctioneerParamChange from './AuctioneerParamChange'; import { useAtomValue } from 'jotai'; -import { walletUtilsAtom } from 'store/app'; +import { walletUtilsAtom, rpcUtilsAtom } from 'store/app'; const ProposalTypes = { addOracles: 'Add Oracle Operators', @@ -36,6 +36,7 @@ export default function VaultsPanel() { ProposalTypes.managerParamChange, ); const walletUtils = useAtomValue(walletUtilsAtom); + const rpcUtils = useAtomValue(rpcUtilsAtom); const filterProposals = networkProposalFilter(walletUtils); const { data: walletCurrent, status } = usePublishedDatum( @@ -48,6 +49,7 @@ export default function VaultsPanel() { status, walletCurrent, charterInvitationSpec.description, + rpcUtils?.agoricNames.instance.econCommitteeCharter, ); const charterOfferId = charterInvitationStatus.acceptedIn; diff --git a/src/components/VotePanel.tsx b/src/components/VotePanel.tsx index e2f8c53..ad7ec9e 100644 --- a/src/components/VotePanel.tsx +++ b/src/components/VotePanel.tsx @@ -8,7 +8,7 @@ import { timestampPassed } from 'utils/helpers'; import AcceptInvitation from './AcceptInvitation'; import { OfferId, VoteOnQuestion } from './questions'; import { useAtomValue } from 'jotai'; -import { walletUtilsAtom } from 'store/app'; +import { rpcUtilsAtom, walletUtilsAtom } from 'store/app'; interface Props {} @@ -90,6 +90,7 @@ function VoteOnQuestions(props: { export default function VotePanel(_props: Props) { const walletUtils = useAtomValue(walletUtilsAtom); + const rpcUtils = useAtomValue(rpcUtilsAtom); const { data, status } = usePublishedDatum( walletUtils ? `wallet.${walletUtils.getWalletAddress()}.current` @@ -98,8 +99,12 @@ export default function VotePanel(_props: Props) { const { status: instanceStatus, data: instance } = usePublishedDatum( 'agoricNames.instance', ); - - const invitationStatus = inferInvitationStatus(status, data, 'Voter'); + const invitationStatus = inferInvitationStatus( + status, + data, + 'Voter', + rpcUtils?.agoricNames.instance.economicCommittee, + ); const previousOfferId = invitationStatus.acceptedIn; return ( diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index 9d291a5..c247ba8 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -429,6 +429,7 @@ export const inferInvitationStatus = ( status: LoadStatus, current: CurrentWalletRecord | undefined, descriptionSubstr: string, + contractInstance, ) => { if (status === LoadStatus.Idle) { return { status: 'idle' }; @@ -440,7 +441,8 @@ export const inferInvitationStatus = ( // first check for accepted const usedInvitationEntry = coerceEntries(current.offerToUsedInvitation).find( ([_, invitationAmount]) => - invitationAmount.value[0].description.includes(descriptionSubstr), + invitationAmount.value[0].description.includes(descriptionSubstr) && + invitationAmount.value[0].instance === contractInstance, ); if (usedInvitationEntry) { return { @@ -448,16 +450,17 @@ export const inferInvitationStatus = ( acceptedIn: usedInvitationEntry[0], }; } - // if that's not available, see if there's an invitation that can be used + // if that's not available, see if there's an invitation that can be used const invitationPurse = current.purses.find(p => { // xxx take this as param return p.brand.toString().includes('Invitation'); }); - const invitation: Amount<'set'> | undefined = - invitationPurse.balance.value.find(a => - a.description.includes(descriptionSubstr), + invitationPurse.balance.value.find( + a => + a.description.includes(descriptionSubstr) && + a.instance === contractInstance, ); if (invitation) { return { From d3360c2e3c7d74d3ebfc8d72c244c0ca308d7502 Mon Sep 17 00:00:00 2001 From: Fraz Arshad Date: Tue, 1 Oct 2024 17:52:50 +0500 Subject: [PATCH 2/2] test: unit tests for inferInvitationStatus function --- src/lib/wallet.test.js | 288 +++++++++++++++++++++++++++++++++++++++++ vitest.config.ts | 3 + 2 files changed, 291 insertions(+) create mode 100644 src/lib/wallet.test.js diff --git a/src/lib/wallet.test.js b/src/lib/wallet.test.js new file mode 100644 index 0000000..3cced20 --- /dev/null +++ b/src/lib/wallet.test.js @@ -0,0 +1,288 @@ +import '../../src/installSesLockdown'; +import { test, expect, describe, vi } from 'vitest'; + +vi.stubGlobal('window', { + location: { search: '' }, + alert: vi.fn(), + keplr: { + experimentalSuggestChain: vi.fn(), + enable: vi.fn(), + getKey: () => ({ isNanoLedger: true }), + getOfflineSignerOnlyAmino: () => ({ + getAccounts: () => [{ address: 1 }], + }), + }, +}); + +import { inferInvitationStatus } from './wallet'; +import { LoadStatus } from './rpc'; + +const createMockWallet = ({ + usedCharterInviations = 0, + charterInvitations = 0, + usedCommitteeInvitations = 0, + committeeInvitations = 0, +}) => ({ + brands: [], + liveOffers: [], + offerToPublicSubscriberPaths: [], + offerToUsedInvitation: [ + ...[...Array(usedCharterInviations)].map((_, idx) => [ + `econgov-${1_000_000 + idx}`, + { + brand: 'Zoe Invitation Brand', + value: [ + { + description: 'charter member invitation', + handle: null, + installation: null, + instance: `instance${idx}`, + }, + ], + }, + ]), + ...[...Array(usedCommitteeInvitations)].map((_, idx) => [ + `econgov-${2_000_000 + idx}`, + { + brand: 'Zoe Invitation Brand', + value: [ + { + description: 'Voter0', + handle: null, + installation: null, + instance: `instance${idx}`, + }, + ], + }, + ]), + ], + purses: [ + { + balance: { + brand: 'Zoe Invitation Brand', + value: [ + ...[...Array(charterInvitations)].map((_, idx) => ({ + description: 'charter member invitation', + handle: null, + installation: null, + instance: `instance${idx}`, + })), + ...[...Array(committeeInvitations)].map((_, idx) => ({ + description: 'Voter0', + handle: null, + installation: null, + instance: `instance${idx}`, + })), + ], + }, + brand: 'Zoe Invitation Brand', + }, + ], +}); + +describe('inferInvitationStatus', () => { + test('should find one accepted charter and committee invitation', () => { + const mockWallet = createMockWallet({ + usedCharterInviations: 1, + usedCommitteeInvitations: 1, + }); + + const charterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'charter member invitation', + 'instance0', + ); + + const voterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'Voter', + 'instance0', + ); + + expect(charterInvitationStatus).toStrictEqual({ + acceptedIn: 'econgov-1000000', + status: 'accepted', + }); + + expect(voterInvitationStatus).toStrictEqual({ + acceptedIn: 'econgov-2000000', + status: 'accepted', + }); + }); + + test('should not find any accepted charter and committee invitation', () => { + const mockWallet = createMockWallet({}); + + const charterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'charter member invitation', + 'instance0', + ); + + const voterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'Voter', + 'instance0', + ); + + expect(charterInvitationStatus).toStrictEqual({ + status: 'missing', + }); + + expect(voterInvitationStatus).toStrictEqual({ + status: 'missing', + }); + }); + + test('should find unused charter and committee invitation', () => { + const mockWallet = createMockWallet({ + charterInvitations: 1, + committeeInvitations: 1, + }); + + const charterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'charter member invitation', + 'instance0', + ); + + const voterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'Voter', + 'instance0', + ); + + expect(charterInvitationStatus).toStrictEqual({ + invitation: { + description: 'charter member invitation', + handle: null, + installation: null, + instance: 'instance0', + }, + status: 'available', + }); + + expect(voterInvitationStatus).toStrictEqual({ + invitation: { + description: 'Voter0', + handle: null, + installation: null, + instance: 'instance0', + }, + status: 'available', + }); + }); + + test('should find new unused invitations before old used ones', () => { + const mockWallet = createMockWallet({ + charterInvitations: 2, + committeeInvitations: 2, + usedCharterInviations: 1, + usedCommitteeInvitations: 1, + }); + + const charterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'charter member invitation', + 'instance1', + ); + + const voterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'Voter', + 'instance1', + ); + + expect(charterInvitationStatus).toStrictEqual({ + invitation: { + description: 'charter member invitation', + handle: null, + installation: null, + instance: 'instance1', + }, + status: 'available', + }); + + expect(voterInvitationStatus).toStrictEqual({ + invitation: { + description: 'Voter0', + handle: null, + installation: null, + instance: 'instance1', + }, + status: 'available', + }); + }); + + test('should find newer used charter and committee invitation before old ones', () => { + const mockWallet = createMockWallet({ + usedCharterInviations: 2, + usedCommitteeInvitations: 2, + }); + + const charterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'charter member invitation', + 'instance1', + ); + + const voterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'Voter', + 'instance1', + ); + + // newer invitations will have the last digit as 1 + expect(charterInvitationStatus).toStrictEqual({ + acceptedIn: 'econgov-1000001', + status: 'accepted', + }); + + expect(voterInvitationStatus).toStrictEqual({ + acceptedIn: 'econgov-2000001', + status: 'accepted', + }); + }); + + test('should not find old charter and committee invitation (used or unused) when new instance is available', () => { + const mockWallet = createMockWallet({ + charterInvitations: 1, + committeeInvitations: 1, + usedCharterInviations: 1, + usedCommitteeInvitations: 1, + }); + + const charterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'charter member invitation', + 'instance1', + ); + + const voterInvitationStatus = inferInvitationStatus( + LoadStatus.Received, + mockWallet, + 'Voter', + 'instance1', + ); + + // newer invitations will have the last digit as 1 + expect(charterInvitationStatus).toStrictEqual({ + status: 'missing', + }); + + expect(voterInvitationStatus).toStrictEqual({ + status: 'missing', + }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 0ab6fae..e028807 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,6 +6,9 @@ export default mergeConfig( viteConfig, defineConfig({ test: { + deps: { + inline: ['@agoric/rpc'], + }, setupFiles: ['src/installSesLockdown.ts'], exclude: [...configDefaults.exclude, 'tests/e2e/**'], },