From 1a9a2979f0399cd9d5a20da31b2ae399dd5d1a1d Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Mon, 3 Jun 2024 14:29:20 +0200 Subject: [PATCH 01/11] change order of messages --- .../components/AddLinkedOrganizationModal.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx b/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx index 79f0934722..60ddeae4b5 100644 --- a/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx +++ b/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx @@ -71,6 +71,8 @@ const AddLinkedOrganizationModal = () => { // this is needed as otherwise the camera may stay turned on const [showScanner, setShowScanner] = useState(false); + const [linkedOrganization, setLinkedOrganization] = useState(); + const onRequestChallenge = useCallback(() => { requestChallenge(laoId) .then(() => {}) @@ -157,7 +159,7 @@ const AddLinkedOrganizationModal = () => { if (isInitiatingOrganizer) { requestChallengeAndDisplayQRCode(); setShowQRCodeModal(true); - onFederationInit(scannedLinkedOrganization); + setLinkedOrganization(scannedLinkedOrganization); } else { onFederationExpect(scannedLinkedOrganization); navigation.navigate(STRINGS.navigation_linked_organizations); @@ -267,6 +269,7 @@ const AddLinkedOrganizationModal = () => { setShowQRScannerModal(true); setShowScanner(true); } else { + onFederationInit(linkedOrganization!); navigation.navigate(STRINGS.navigation_linked_organizations); } setShowQRCodeModal(false); From 176515a281c80ae0cc2375cfa1448fb602d48aef Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Mon, 3 Jun 2024 15:57:45 +0200 Subject: [PATCH 02/11] fix small changes for consistency with fe2 --- .../components/QRCodeScannerModal.tsx | 10 +++++++++- .../LinkedOrganizationsScreen.test.tsx.snap | 10 +++++----- fe1-web/src/resources/strings.ts | 6 +++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/fe1-web/src/features/linked-organizations/components/QRCodeScannerModal.tsx b/fe1-web/src/features/linked-organizations/components/QRCodeScannerModal.tsx index 93e28188e8..0c36162eaa 100644 --- a/fe1-web/src/features/linked-organizations/components/QRCodeScannerModal.tsx +++ b/fe1-web/src/features/linked-organizations/components/QRCodeScannerModal.tsx @@ -29,6 +29,14 @@ const styles = StyleSheet.create({ top: '25%', bottom: '50%', } as ViewStyle, + qrCodeMobile: { + scale: 0.65, + alignItems: 'center', + justifyContent: 'center', + opacity: 0.5, + top: '20%', + bottom: '50%', + } as ViewStyle, scannerTextItems: { top: '35%', } as ViewStyle, @@ -68,7 +76,7 @@ const QRCodeScannerModal: React.FC = ({ - + diff --git a/fe1-web/src/features/linked-organizations/screens/__tests__/__snapshots__/LinkedOrganizationsScreen.test.tsx.snap b/fe1-web/src/features/linked-organizations/screens/__tests__/__snapshots__/LinkedOrganizationsScreen.test.tsx.snap index f7c541482a..4891f5b0be 100644 --- a/fe1-web/src/features/linked-organizations/screens/__tests__/__snapshots__/LinkedOrganizationsScreen.test.tsx.snap +++ b/fe1-web/src/features/linked-organizations/screens/__tests__/__snapshots__/LinkedOrganizationsScreen.test.tsx.snap @@ -804,7 +804,7 @@ exports[`LinkedOrganizationsScreen manual input of correct organization details } } > - Join Organization + Invite an Organization - Link Organization + Join an Invitation - Join Organization + Invite an Organization - Link Organization + Join an Invitation - Link Organization + Join an Invitation Date: Thu, 6 Jun 2024 10:40:06 +0200 Subject: [PATCH 03/11] add tests and fix linting --- .../network/jsonrpc/messages/MessageData.ts | 1 + .../jsonrpc/messages/MessageRegistry.ts | 2 + .../src/core/network/validation/Validator.ts | 1 + .../components/AddLinkedOrganizationModal.tsx | 25 ++-- .../network/LinkedOrgHandler.ts | 62 +++++++++- .../__tests__/LinkedOrgHandler.test.ts | 68 ++++++++++- .../linked-organizations/network/index.ts | 15 ++- .../network/messages/ChallengeMessage.ts | 4 +- .../network/messages/FederationResult.ts | 58 +++++++++ .../network/messages/index.ts | 1 + .../reducer/ChallengeReducer.ts | 83 ++++++++++++- .../reducer/LinkedOrganizationsReducer.ts | 89 +++++++++++++- .../__tests__/ChallengeReducer.test.ts | 103 +++++++++++++++- .../LinkedOrganizationsReducer.test.ts | 110 +++++++++++++++++- .../screens/LinkedOrganizationsScreen.tsx | 61 +++++++++- .../network/messages/WitnessRegistry.ts | 2 + 16 files changed, 656 insertions(+), 29 deletions(-) create mode 100644 fe1-web/src/features/linked-organizations/network/messages/FederationResult.ts diff --git a/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts b/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts index da26c6bf41..1217a4301e 100644 --- a/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts +++ b/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts @@ -43,6 +43,7 @@ export enum ActionType { CHALLENGE = 'challenge', FEDERATION_INIT = 'init', FEDERATION_EXPECT = 'expect', + FEDERATION_RESULT = 'result', } /** Enumeration of all possible signatures of a message */ diff --git a/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts b/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts index e219bc1abc..0b63f3d193 100644 --- a/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts +++ b/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts @@ -32,6 +32,7 @@ const { CHALLENGE, FEDERATION_INIT, FEDERATION_EXPECT, + FEDERATION_RESULT, } = ActionType; const { KEYPAIR, POP_TOKEN } = SignatureType; @@ -107,6 +108,7 @@ export class MessageRegistry { [k(FEDERATION, CHALLENGE), { signature: KEYPAIR }], [k(FEDERATION, FEDERATION_INIT), { signature: KEYPAIR }], [k(FEDERATION, FEDERATION_EXPECT), { signature: KEYPAIR }], + [k(FEDERATION, FEDERATION_RESULT), { signature: KEYPAIR }], ]); /** diff --git a/fe1-web/src/core/network/validation/Validator.ts b/fe1-web/src/core/network/validation/Validator.ts index f8e710f265..7a35062978 100644 --- a/fe1-web/src/core/network/validation/Validator.ts +++ b/fe1-web/src/core/network/validation/Validator.ts @@ -79,6 +79,7 @@ const schemaIds: Record> = { [ActionType.CHALLENGE]: 'dataFederationChallenge', [ActionType.FEDERATION_INIT]: 'dataFederationInit', [ActionType.FEDERATION_EXPECT]: 'dataFederationExpect', + [ActionType.FEDERATION_RESULT]: 'dataFederationResult', }, }; diff --git a/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx b/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx index 60ddeae4b5..ef2d4aa9e4 100644 --- a/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx +++ b/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx @@ -21,7 +21,7 @@ import { expectFederation, initFederation, requestChallenge } from '../network'; import { Challenge } from '../objects/Challenge'; import { LinkedOrganization } from '../objects/LinkedOrganization'; import { makeChallengeSelector } from '../reducer'; -import { addLinkedOrganization } from '../reducer/LinkedOrganizationsReducer'; +import { addScannedLinkedOrganization } from '../reducer/LinkedOrganizationsReducer'; import ManualInputModal from './ManualInputModal'; import QRCodeModal from './QRCodeModal'; import QRCodeScannerModal from './QRCodeScannerModal'; @@ -102,6 +102,14 @@ const AddLinkedOrganizationModal = () => { const onFederationExpect = useCallback( (org: LinkedOrganization) => { if (challengeState) { + const linkedorg = new LinkedOrganization({ + lao_id: org.lao_id, + server_address: org.server_address, + public_key: org.public_key, + challenge: Challenge.fromState(challengeState), + }); + setLinkedOrganization(linkedorg); + dispatch(addScannedLinkedOrganization(laoId, linkedorg.toState())); expectFederation( laoId, org.lao_id, @@ -110,12 +118,7 @@ const AddLinkedOrganizationModal = () => { Challenge.fromState(challengeState), ) .then(() => { - toast.show(`Success: Expect Federation`, { - type: 'success', - placement: 'bottom', - duration: FOUR_SECONDS, - }); - dispatch(addLinkedOrganization(laoId, org.toState())); + console.log('Expect Federation successfull'); }) .catch((err) => { toast.show(`Could not expect Federation, error: ${err}`, { @@ -133,12 +136,7 @@ const AddLinkedOrganizationModal = () => { (org: LinkedOrganization) => { initFederation(laoId, org.lao_id, org.server_address, org.public_key, org.challenge!) .then(() => { - toast.show(`Success: Init Federation`, { - type: 'success', - placement: 'bottom', - duration: FOUR_SECONDS, - }); - dispatch(addLinkedOrganization(laoId, org.toState())); + console.log('Init Federation successfull'); }) .catch((err) => { toast.show(`Could not init Federation, error: ${err}`, { @@ -159,6 +157,7 @@ const AddLinkedOrganizationModal = () => { if (isInitiatingOrganizer) { requestChallengeAndDisplayQRCode(); setShowQRCodeModal(true); + dispatch(addScannedLinkedOrganization(laoId, scannedLinkedOrganization.toState())); setLinkedOrganization(scannedLinkedOrganization); } else { onFederationExpect(scannedLinkedOrganization); diff --git a/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts b/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts index 855c14e08a..33050923a6 100644 --- a/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts +++ b/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts @@ -1,10 +1,17 @@ import { ActionType, ObjectType, ProcessableMessage } from 'core/network/jsonrpc/messages'; +import { Base64UrlData } from 'core/objects'; import { dispatch } from 'core/redux'; import { LinkedOrganizationsConfiguration } from '../interface'; import { Challenge } from '../objects/Challenge'; -import { setChallenge } from '../reducer'; -import { ChallengeRequest, ChallengeMessage, FederationExpect, FederationInit } from './messages'; +import { addReceivedChallenge, setChallenge } from '../reducer'; +import { + ChallengeRequest, + ChallengeMessage, + FederationExpect, + FederationInit, + FederationResult, +} from './messages'; /** * Handler for linked organization messages @@ -145,3 +152,54 @@ export const handleFederationExpectMessage = } return false; }; + +/** + * Handles an federationResult message. + */ +export const handleFederationResultMessage = + (getCurrentLaoId: LinkedOrganizationsConfiguration['getCurrentLaoId']) => + (msg: ProcessableMessage) => { + console.log('HANLDE FEDRES'); + console.log(msg); + if ( + msg.messageData.object !== ObjectType.FEDERATION || + msg.messageData.action !== ActionType.FEDERATION_RESULT + ) { + console.warn('handleFederationResultMessage was called to process an unsupported message'); + return false; + } + + const makeErr = (err: string) => `federation/result was not processed: ${err}`; + + const laoId = getCurrentLaoId(); + if (!laoId) { + console.warn(makeErr('no Lao is currently active')); + return false; + } + + if (msg.messageData instanceof FederationResult) { + const federationResult = msg.messageData as FederationResult; + try { + if ( + federationResult.status && + federationResult.challenge && + (federationResult.reason || federationResult.public_key) + ) { + const b64urldata = new Base64UrlData(federationResult.challenge.data.toString()); + const js = JSON.parse(b64urldata.decode()); + const challengeMessage = ChallengeMessage.fromJson(js); + const challenge = new Challenge({ + value: challengeMessage.value, + valid_until: challengeMessage.valid_until, + }); + dispatch(addReceivedChallenge(laoId, challenge, federationResult.public_key)); + } + } catch (e) { + console.log(e); + return false; + } + + return true; + } + return false; + }; diff --git a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts index ab083970c3..12ff2a9e15 100644 --- a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts +++ b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts @@ -21,13 +21,19 @@ import { handleFederationExpectMessage, handleFederationInitMessage, } from '../LinkedOrgHandler'; -import { ChallengeRequest, ChallengeMessage, FederationExpect, FederationInit } from '../messages'; +import { + ChallengeRequest, + ChallengeMessage, + FederationExpect, + FederationInit, + FederationResult, +} from '../messages'; jest.mock('core/network/jsonrpc/messages/Message', () => { return { - Message: jest.fn().mockImplementation(() => { + Message: jest.fn().mockImplementation((x) => { return { - buildMessageData: jest.fn((input) => JSON.stringify(input)), + buildMessageData: jest.fn(() => JSON.parse(JSON.stringify(x))), }; }), }; @@ -318,3 +324,59 @@ describe('handleFederationExpectMessage', () => { ).toBeTrue(); }); }); + +describe('handleFederationResultMessage', () => { + it('should return false if the object type is wrong', () => { + expect( + handleChallengeMessage()({ + ...mockMessageData, + messageData: { + object: ObjectType.MEETING, + action: ActionType.FEDERATION_RESULT, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if the action type is wrong', () => { + expect( + handleChallengeMessage()({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.ADD, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if there is an issue with the message data', () => { + expect( + handleChallengeMessage()({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.FEDERATION_RESULT, + challenge: undefined as unknown as Message, + status: undefined as unknown as string, + public_key: undefined as unknown as PublicKey, + } as FederationResult, + }), + ).toBeFalse(); + }); + it('should return false if there is both a public key and a reason in the message data', () => { + expect( + handleChallengeMessage()({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.FEDERATION_RESULT, + challenge: mockChallengMessageData, + status: 'success', + reason: 'error', + public_key: mockSender, + } as FederationResult, + }), + ).toBeFalse(); + }); +}); diff --git a/fe1-web/src/features/linked-organizations/network/index.ts b/fe1-web/src/features/linked-organizations/network/index.ts index 27bcee8af2..bf86f29593 100644 --- a/fe1-web/src/features/linked-organizations/network/index.ts +++ b/fe1-web/src/features/linked-organizations/network/index.ts @@ -6,8 +6,15 @@ import { handleChallengeRequestMessage, handleFederationExpectMessage, handleFederationInitMessage, + handleFederationResultMessage, } from './LinkedOrgHandler'; -import { ChallengeRequest, ChallengeMessage, FederationExpect, FederationInit } from './messages'; +import { + ChallengeRequest, + ChallengeMessage, + FederationExpect, + FederationInit, + FederationResult, +} from './messages'; export * from './LinkedOrgMessageApi'; @@ -41,4 +48,10 @@ export function configureNetwork(configuration: LinkedOrganizationsConfiguration handleFederationExpectMessage(configuration.getCurrentLaoId), FederationExpect.fromJson, ); + configuration.messageRegistry.add( + ObjectType.FEDERATION, + ActionType.FEDERATION_RESULT, + handleFederationResultMessage(configuration.getCurrentLaoId), + FederationResult.fromJson, + ); } diff --git a/fe1-web/src/features/linked-organizations/network/messages/ChallengeMessage.ts b/fe1-web/src/features/linked-organizations/network/messages/ChallengeMessage.ts index 37b17865c5..a720fe4c00 100644 --- a/fe1-web/src/features/linked-organizations/network/messages/ChallengeMessage.ts +++ b/fe1-web/src/features/linked-organizations/network/messages/ChallengeMessage.ts @@ -34,8 +34,8 @@ export class ChallengeMessage implements MessageData { throw new ProtocolError(`Invalid challenge\n\n${errors}`); } return new ChallengeMessage({ - value: obj.value, - valid_until: obj.valid_until, + value: new Hash(obj.value), + valid_until: new Timestamp(obj.valid_until), }); } } diff --git a/fe1-web/src/features/linked-organizations/network/messages/FederationResult.ts b/fe1-web/src/features/linked-organizations/network/messages/FederationResult.ts new file mode 100644 index 0000000000..685b67a190 --- /dev/null +++ b/fe1-web/src/features/linked-organizations/network/messages/FederationResult.ts @@ -0,0 +1,58 @@ +import { ActionType, Message, MessageData, ObjectType } from 'core/network/jsonrpc/messages'; +import { validateDataObject } from 'core/network/validation'; +import { ProtocolError, PublicKey } from 'core/objects'; + +/** Result received for the Federation Authentication */ +export class FederationResult implements MessageData { + public readonly object: ObjectType = ObjectType.FEDERATION; + + public readonly action: ActionType = ActionType.FEDERATION_RESULT; + + public readonly reason?: String; + + public readonly public_key?: PublicKey; + + public readonly status: String; + + public readonly challenge: Message; + + constructor(msg: Partial) { + if (!msg.status) { + throw new ProtocolError("Undefined 'status' parameter encountered during 'FederationResult'"); + } + if (!msg.challenge) { + throw new ProtocolError( + "Undefined 'challenge' parameter encountered during 'FederationResult'", + ); + } + + if (!msg.reason && !msg.public_key) { + throw new ProtocolError( + "Undefined 'reason' or 'public_key' parameter encountered during 'FederationResult'", + ); + } + + this.status = msg.status; + this.reason = msg.reason; + this.public_key = msg.public_key; + this.challenge = msg.challenge; + } + + /** + * Creates an FederationResult object from a given object + * @param obj + */ + public static fromJson(obj: any): FederationResult { + const { errors } = validateDataObject(ObjectType.FEDERATION, ActionType.FEDERATION_RESULT, obj); + if (errors !== null) { + throw new ProtocolError(`Invalid federation result\n\n${errors}`); + } + + return new FederationResult({ + reason: obj.reason, + challenge: obj.challenge, + status: obj.status, + public_key: obj.public_key, + }); + } +} diff --git a/fe1-web/src/features/linked-organizations/network/messages/index.ts b/fe1-web/src/features/linked-organizations/network/messages/index.ts index 621a91753d..19226bc8a5 100644 --- a/fe1-web/src/features/linked-organizations/network/messages/index.ts +++ b/fe1-web/src/features/linked-organizations/network/messages/index.ts @@ -2,3 +2,4 @@ export * from './ChallengeRequest'; export * from './ChallengeMessage'; export * from './FederationExpect'; export * from './FederationInit'; +export * from './FederationResult'; diff --git a/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts b/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts index b45ac0728f..ae655d682d 100644 --- a/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts +++ b/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts @@ -5,18 +5,20 @@ /* eslint-disable no-param-reassign */ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Hash } from 'core/objects'; +import { Hash, PublicKey } from 'core/objects'; -import { ChallengeState } from '../objects/Challenge'; +import { Challenge, ChallengeState } from '../objects/Challenge'; export const CHALLENGE_REDUCER_PATH = 'challenge'; export interface ChallengeReducerState { byLaoId: Record; + recvChallenges: Record; } const initialState: ChallengeReducerState = { byLaoId: {}, + recvChallenges: {}, }; const challengeSlice = createSlice({ @@ -37,10 +39,65 @@ const challengeSlice = createSlice({ state.byLaoId[laoId] = challenge; }, }, + addReceivedChallenge: { + prepare(laoId: Hash, challenge: Challenge, publicKey?: PublicKey) { + return { + payload: { + laoId: laoId.valueOf(), + challenge: challenge, + publicKey: publicKey, + }, + }; + }, + reducer( + state, + action: PayloadAction<{ laoId: string; challenge: Challenge; publicKey?: PublicKey }>, + ) { + const { laoId, challenge, publicKey } = action.payload; + if (state.recvChallenges[laoId] === undefined) { + state.recvChallenges[laoId] = []; + } + if ( + state.recvChallenges[laoId].find( + ([challenge1]) => + challenge1.value === challenge.value && + challenge1.valid_until === challenge.valid_until, + ) + ) { + return; + } + state.recvChallenges[laoId].push([challenge, publicKey]); + }, + }, + removeReceivedChallenge: { + prepare(laoId: Hash, challenge: Challenge, publicKey?: PublicKey) { + return { + payload: { + laoId: laoId.valueOf(), + challenge: challenge, + publicKey: publicKey, + }, + }; + }, + reducer( + state, + action: PayloadAction<{ laoId: string; challenge: Challenge; publicKey?: PublicKey }>, + ) { + const { laoId, challenge } = action.payload; + if (state.recvChallenges[laoId] === undefined) { + return; + } + + state.recvChallenges[laoId] = state.recvChallenges[laoId].filter( + ([challenge1]) => challenge1 !== challenge, + ); + }, + }, }, }); -export const { setChallenge } = challengeSlice.actions; +export const { setChallenge, addReceivedChallenge, removeReceivedChallenge } = + challengeSlice.actions; export const getChallengeState = (state: any): ChallengeReducerState => state[CHALLENGE_REDUCER_PATH]; @@ -65,6 +122,26 @@ export const makeChallengeSelector = (laoId: Hash) => { ); }; +/** + * Retrives all received challenges from a lao + * @param laoId The id of the lao + * @returns Array of challenges and publickeys + */ +export const makeChallengeReceveidSelector = (laoId: Hash) => { + return createSelector( + // First input: a map containing all challenges + (state: any) => getChallengeState(state), + // Selector: returns the challenge for a specific lao + (challengeState: ChallengeReducerState): [Challenge, PublicKey?][] | undefined => { + const serializedLaoId = laoId.valueOf(); + if (!challengeState) { + return undefined; + } + return challengeState.recvChallenges[serializedLaoId]; + }, + ); +}; + export const challengeReduce = challengeSlice.reducer; export default { diff --git a/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts b/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts index 862affafc1..cf0a6d20ad 100644 --- a/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts +++ b/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts @@ -17,6 +17,7 @@ export interface LinkedOrganizationReducerState { byLinkedLaoId: Record; allLaoIds: string[]; allLaos: LinkedOrganizationState[]; + allScannedLaos: LinkedOrganizationState[]; }; }; } @@ -50,6 +51,7 @@ const linkedOrganizationSlice = createSlice({ allLaoIds: [], byLinkedLaoId: {}, allLaos: [], + allScannedLaos: [], }; } @@ -64,10 +66,72 @@ const linkedOrganizationSlice = createSlice({ state.byLaoId[laoId].byLinkedLaoId[linkedOrganization.lao_id] = linkedOrganization; }, }, + addScannedLinkedOrganization: { + prepare(laoId: Hash, linkedOrganization: LinkedOrganizationState) { + return { + payload: { + laoId: laoId.valueOf(), + linkedOrganization: linkedOrganization, + }, + }; + }, + reducer( + state, + action: PayloadAction<{ laoId: string; linkedOrganization: LinkedOrganizationState }>, + ) { + const { laoId, linkedOrganization } = action.payload; + + if (state.byLaoId[laoId] === undefined) { + state.byLaoId[laoId] = { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [], + }; + } + + if (state.byLaoId[laoId].allLaoIds.includes(linkedOrganization.lao_id.valueOf())) { + throw new Error( + `Tried to store organization with lao id ${linkedOrganization.lao_id} but there already exists one with the same lao id`, + ); + } + + state.byLaoId[laoId].allScannedLaos.push(linkedOrganization); + }, + }, + removeScannedLinkedOrganization: { + prepare(laoId: Hash, linkedLaoId: string) { + return { + payload: { + laoId: laoId.valueOf(), + linkedLaoId: linkedLaoId, + }, + }; + }, + reducer(state, action: PayloadAction<{ laoId: string; linkedLaoId: string }>) { + const { laoId, linkedLaoId } = action.payload; + + if (state.byLaoId[laoId] === undefined) { + state.byLaoId[laoId] = { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [], + }; + } + state.byLaoId[laoId].allScannedLaos = state.byLaoId[laoId].allScannedLaos.filter( + (org) => org.lao_id !== linkedLaoId, + ); + }, + }, }, }); -export const { addLinkedOrganization } = linkedOrganizationSlice.actions; +export const { + addLinkedOrganization, + addScannedLinkedOrganization, + removeScannedLinkedOrganization, +} = linkedOrganizationSlice.actions; export const getLinkedOrganizationState = (state: any): LinkedOrganizationReducerState => state[LINKEDORGANIZATIONS_REDUCER_PATH]; @@ -115,6 +179,29 @@ export const makeLinkedOrganizationSelector = (laoId: Hash) => { ); }; +/** + * Retrives all scanned linked organization state by lao id + * @param laoId The id of the lao + * @returns A list of linked organization state + */ +export const makeScannedLinkedOrganizationSelector = (laoId: Hash) => { + return createSelector( + // First input: a map containing all linked organizations + (state: any) => getLinkedOrganizationState(state), + // Selector: returns the linked organization for a specific lao and linked_lao_id + (linkedOrganizationState: LinkedOrganizationReducerState): LinkedOrganizationState[] | [] => { + const serializedLaoId = laoId.valueOf(); + if (!linkedOrganizationState) { + return []; + } + if (!linkedOrganizationState.byLaoId[serializedLaoId]) { + return []; + } + return linkedOrganizationState.byLaoId[serializedLaoId].allScannedLaos; + }, + ); +}; + export const linkedOrganizationsReduce = linkedOrganizationSlice.reducer; export default { diff --git a/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts b/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts index 7dbd20b9d8..8adf87602c 100644 --- a/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts +++ b/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts @@ -1,7 +1,7 @@ import { describe } from '@jest/globals'; import { AnyAction } from 'redux'; -import { mockLaoId, mockLaoId2, serializedMockLaoId } from '__tests__/utils'; +import { mockLao, mockLaoId, mockLaoId2, serializedMockLaoId } from '__tests__/utils'; import { Hash, Timestamp } from 'core/objects'; import { Challenge, ChallengeState } from 'features/linked-organizations/objects/Challenge'; import { DAY_IN_SECONDS } from 'resources/const'; @@ -12,6 +12,9 @@ import { challengeReduce, ChallengeReducerState, makeChallengeSelector, + addReceivedChallenge, + removeReceivedChallenge, + makeChallengeReceveidSelector, } from '../ChallengeReducer'; const mockChallenge: Challenge = new Challenge({ @@ -26,6 +29,7 @@ describe('ChallengeReducer', () => { it('returns a valid initial state', () => { expect(challengeReduce(undefined, {} as AnyAction)).toEqual({ byLaoId: {}, + recvChallenges: {}, } as ChallengeReducerState); }); }); @@ -35,12 +39,55 @@ describe('ChallengeReducer', () => { const newState = challengeReduce( { byLaoId: {}, + recvChallenges: {}, } as ChallengeReducerState, setChallenge(mockLaoId, mockChallengeState), ); expect(newState.byLaoId[serializedMockLaoId]).toEqual(mockChallengeState); }); }); + + describe('addReceivedChallenge', () => { + it('adds new received challenge (from fed-result) to the state', () => { + const newState = challengeReduce( + { + byLaoId: {}, + recvChallenges: {}, + } as ChallengeReducerState, + addReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + ); + expect( + newState.recvChallenges[mockLaoId.valueOf()]?.find( + ([challenge]) => + challenge.value === mockChallenge.value && + challenge.valid_until === mockChallenge.valid_until, + ), + ).toBeTruthy(); + }); + }); + + describe('removeReceivedChallenge', () => { + it('removes new received challenge (from fed-result) from the state', () => { + let newState = challengeReduce( + { + byLaoId: {}, + recvChallenges: {}, + } as ChallengeReducerState, + addReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + ); + newState = challengeReduce( + newState, + removeReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + ); + expect( + newState.recvChallenges[mockLaoId.valueOf()]?.find( + ([challenge]) => + challenge.value === mockChallenge.value && + challenge.valid_until === mockChallenge.valid_until, + ), + ).toBeUndefined(); + }); + }); }); describe('makeChallengeSelector', () => { @@ -48,6 +95,7 @@ describe('makeChallengeSelector', () => { const newState = challengeReduce( { byLaoId: {}, + recvChallenges: {}, } as ChallengeReducerState, setChallenge(mockLaoId, mockChallengeState), ); @@ -65,6 +113,7 @@ describe('makeChallengeSelector', () => { const newState = challengeReduce( { byLaoId: {}, + recvChallenges: {}, } as ChallengeReducerState, setChallenge(mockLaoId, mockChallengeState), ); @@ -78,3 +127,55 @@ describe('makeChallengeSelector', () => { ).toBeUndefined(); }); }); + +describe('makeChallengeReceveidSelector', () => { + it('returns the correct challenge(s)', () => { + const newState = challengeReduce( + { + byLaoId: {}, + recvChallenges: {}, + } as ChallengeReducerState, + addReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + ); + expect( + newState.recvChallenges[mockLaoId.valueOf()]?.find( + ([challenge]) => + challenge.value === mockChallenge.value && + challenge.valid_until === mockChallenge.valid_until, + ), + ).toBeTruthy(); + expect( + makeChallengeReceveidSelector(mockLaoId)({ + [CHALLENGE_REDUCER_PATH]: { + byLaoId: {}, + recvChallenges: { [serializedMockLaoId]: [[mockChallenge, mockLao.organizer]] }, + } as ChallengeReducerState, + }), + ).toEqual([[mockChallenge, mockLao.organizer]]); + }); + + it('returns undefined if the laoId is not in the store', () => { + const newState = challengeReduce( + { + byLaoId: {}, + recvChallenges: {}, + } as ChallengeReducerState, + addReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + ); + expect( + newState.recvChallenges[mockLaoId.valueOf()]?.find( + ([challenge]) => + challenge.value === mockChallenge.value && + challenge.valid_until === mockChallenge.valid_until, + ), + ).toBeTruthy(); + expect( + makeChallengeReceveidSelector(mockLaoId2)({ + [CHALLENGE_REDUCER_PATH]: { + byLaoId: {}, + recvChallenges: { [serializedMockLaoId]: [[mockChallenge, mockLao.organizer]] }, + } as ChallengeReducerState, + }), + ).toBeUndefined(); + }); +}); diff --git a/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts b/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts index 4e0467c93f..71a9a921c2 100644 --- a/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts +++ b/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts @@ -8,11 +8,14 @@ import { LinkedOrganizationState } from 'features/linked-organizations/objects/L import { addLinkedOrganization, + addScannedLinkedOrganization, LinkedOrganizationReducerState, LINKEDORGANIZATIONS_REDUCER_PATH, linkedOrganizationsReduce, makeLinkedOrganizationSelector, + makeScannedLinkedOrganizationSelector, makeSingleLinkedOrganizationSelector, + removeScannedLinkedOrganization, } from '../LinkedOrganizationsReducer'; const mockChallenge: Challenge = new Challenge({ @@ -73,6 +76,55 @@ describe('LinkedOrganizationReducer', () => { ).toThrow(); }); }); + + describe('addScannedLinkedOrganization', () => { + it('adds new scanned linked organization to the state', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + const newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + expect(newState.byLaoId[serializedMockLaoId2].allScannedLaos).toEqual([ + mockOrganizationState, + ]); + }); + + it('throws an error if the store already contains an linked organization with the same id', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + const newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + expect(newState.byLaoId[serializedMockLaoId2].allLaoIds).toEqual([serializedMockLaoId]); + expect(() => + linkedOrganizationsReduce( + newState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ), + ).toThrow(); + }); + }); + + describe('removeScannedLinkedOrganization', () => { + it('removes new scanned linked organization from the state', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + let newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + newState = linkedOrganizationsReduce( + newState, + removeScannedLinkedOrganization(mockLaoId2, mockOrganizationState.lao_id), + ); + expect(newState.byLaoId[serializedMockLaoId2].allScannedLaos).toEqual([]); + }); + }); }); describe('makeSingleLinkedOrganizationsSelector', () => { @@ -166,7 +218,7 @@ describe('makeLinkedOrganizationsSelector', () => { ).toEqual([mockOrganizationState]); }); - it('returns undefined if the linked organization is not in the store', () => { + it('returns empty array if the linked organization is not in the store', () => { const serializedMockLaoId2 = mockLaoId2.valueOf(); const newState = linkedOrganizationsReduce( { @@ -192,4 +244,60 @@ describe('makeLinkedOrganizationsSelector', () => { }), ).toEqual([]); }); + + describe('makeScannedLinkedOrganizationSelector', () => { + it('returns the correct scanned linked organization', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + const newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + expect(newState.byLaoId[serializedMockLaoId2].allScannedLaos).toEqual([ + mockOrganizationState, + ]); + expect( + makeScannedLinkedOrganizationSelector(mockLaoId2)({ + [LINKEDORGANIZATIONS_REDUCER_PATH]: { + byLaoId: { + [serializedMockLaoId2]: { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [mockOrganizationState], + }, + }, + } as LinkedOrganizationReducerState, + }), + ).toEqual([mockOrganizationState]); + }); + + it('returns empty array if the scanned linked organization is not in the store', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + const newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + expect(newState.byLaoId[serializedMockLaoId2].allScannedLaos).toEqual([ + mockOrganizationState, + ]); + expect( + makeScannedLinkedOrganizationSelector(mockLaoId)({ + [LINKEDORGANIZATIONS_REDUCER_PATH]: { + byLaoId: { + [serializedMockLaoId2]: { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [mockOrganizationState], + }, + }, + } as LinkedOrganizationReducerState, + }), + ).toEqual([]); + }); + }); }); diff --git a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx index c1630c646e..57cf4db1bd 100644 --- a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx +++ b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx @@ -1,15 +1,24 @@ import { ListItem } from '@rneui/themed'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Text, View, StyleSheet, ViewStyle } from 'react-native'; +import { useToast } from 'react-native-toast-notifications'; import { useSelector } from 'react-redux'; import { PoPIcon } from 'core/components'; import ScreenWrapper from 'core/components/ScreenWrapper'; +import { dispatch } from 'core/redux'; import { List, Typography } from 'core/styles'; +import { FOUR_SECONDS } from 'resources/const'; import STRINGS from 'resources/strings'; import { LinkedOrganizationsHooks } from '../hooks'; -import { makeLinkedOrganizationSelector } from '../reducer/LinkedOrganizationsReducer'; +import { makeChallengeReceveidSelector, removeReceivedChallenge } from '../reducer'; +import { + addLinkedOrganization, + makeLinkedOrganizationSelector, + makeScannedLinkedOrganizationSelector, + removeScannedLinkedOrganization, +} from '../reducer/LinkedOrganizationsReducer'; const styles = StyleSheet.create({ flexibleView: { @@ -19,10 +28,58 @@ const styles = StyleSheet.create({ const LinkedOrganizationsScreen = () => { const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); + const toast = useToast(); const isOrganizer = LinkedOrganizationsHooks.useIsLaoOrganizer(laoId); const linkedOrganizationSelector = useMemo(() => makeLinkedOrganizationSelector(laoId), [laoId]); const linkedOrganizationStates = useSelector(linkedOrganizationSelector); + const recvChallengeSelector = useMemo(() => makeChallengeReceveidSelector(laoId), [laoId]); + const recvChallengeState = useSelector(recvChallengeSelector); + + const scannedLinkedOrgSelector = useMemo( + () => makeScannedLinkedOrganizationSelector(laoId), + [laoId], + ); + const scannedLinkedOrgStates = useSelector(scannedLinkedOrgSelector); + + useEffect(() => { + if ( + recvChallengeState && + scannedLinkedOrgStates && + recvChallengeState.length !== 0 && + scannedLinkedOrgStates.length !== 0 + ) { + try { + for (const [challenge, publicKey] of recvChallengeState) { + const chall2 = challenge.toState(); + const matchingOrg = scannedLinkedOrgStates.find( + (org) => + org.challenge!.value === chall2.value && + org.challenge!.valid_until === chall2.valid_until, + ); + if (matchingOrg && publicKey) { + dispatch(addLinkedOrganization(laoId, matchingOrg!)); + toast.show(`LAO linked successfully`, { + type: 'success', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + dispatch(removeScannedLinkedOrganization(laoId, matchingOrg.lao_id)); + dispatch(removeReceivedChallenge(laoId, challenge, publicKey)); + } else { + toast.show(`Could not link organizations`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + } + } + } catch (e) { + console.log(e); + } + } + }, [recvChallengeState, laoId, toast, scannedLinkedOrgStates]); + return ( diff --git a/fe1-web/src/features/witness/network/messages/WitnessRegistry.ts b/fe1-web/src/features/witness/network/messages/WitnessRegistry.ts index 043e93d72d..c26abb7ee0 100644 --- a/fe1-web/src/features/witness/network/messages/WitnessRegistry.ts +++ b/fe1-web/src/features/witness/network/messages/WitnessRegistry.ts @@ -21,6 +21,7 @@ const { CHALLENGE_REQUEST, FEDERATION_INIT, FEDERATION_EXPECT, + FEDERATION_RESULT, } = ActionType; export enum WitnessingType { @@ -76,6 +77,7 @@ const WITNESSING_TYPE_MAP = new Map([ [k(FEDERATION, CHALLENGE_REQUEST), { type: WitnessingType.NO_WITNESSING }], [k(FEDERATION, FEDERATION_INIT), { type: WitnessingType.NO_WITNESSING }], [k(FEDERATION, FEDERATION_EXPECT), { type: WitnessingType.NO_WITNESSING }], + [k(FEDERATION, FEDERATION_RESULT), { type: WitnessingType.NO_WITNESSING }], ]); const getWitnessRegistryEntry = (data: MessageData): WitnessEntry | undefined => { From db5b9ef413ebf7140f2f767e7cbd6ff5fd678767 Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Thu, 6 Jun 2024 11:48:20 +0200 Subject: [PATCH 04/11] change Challenge to ChallengeState --- .../network/LinkedOrgHandler.ts | 2 +- .../reducer/ChallengeReducer.ts | 24 ++++++++------ .../__tests__/ChallengeReducer.test.ts | 32 +++++++++---------- .../screens/LinkedOrganizationsScreen.tsx | 5 ++- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts b/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts index 33050923a6..cb1028e50b 100644 --- a/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts +++ b/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts @@ -192,7 +192,7 @@ export const handleFederationResultMessage = value: challengeMessage.value, valid_until: challengeMessage.valid_until, }); - dispatch(addReceivedChallenge(laoId, challenge, federationResult.public_key)); + dispatch(addReceivedChallenge(laoId, challenge.toState(), federationResult.public_key)); } } catch (e) { console.log(e); diff --git a/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts b/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts index ae655d682d..d0f535eed5 100644 --- a/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts +++ b/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts @@ -7,13 +7,13 @@ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Hash, PublicKey } from 'core/objects'; -import { Challenge, ChallengeState } from '../objects/Challenge'; +import { ChallengeState } from '../objects/Challenge'; export const CHALLENGE_REDUCER_PATH = 'challenge'; export interface ChallengeReducerState { byLaoId: Record; - recvChallenges: Record; + recvChallenges: Record; } const initialState: ChallengeReducerState = { @@ -40,7 +40,7 @@ const challengeSlice = createSlice({ }, }, addReceivedChallenge: { - prepare(laoId: Hash, challenge: Challenge, publicKey?: PublicKey) { + prepare(laoId: Hash, challenge: ChallengeState, publicKey?: PublicKey) { return { payload: { laoId: laoId.valueOf(), @@ -51,7 +51,7 @@ const challengeSlice = createSlice({ }, reducer( state, - action: PayloadAction<{ laoId: string; challenge: Challenge; publicKey?: PublicKey }>, + action: PayloadAction<{ laoId: string; challenge: ChallengeState; publicKey?: PublicKey }>, ) { const { laoId, challenge, publicKey } = action.payload; if (state.recvChallenges[laoId] === undefined) { @@ -60,8 +60,8 @@ const challengeSlice = createSlice({ if ( state.recvChallenges[laoId].find( ([challenge1]) => - challenge1.value === challenge.value && - challenge1.valid_until === challenge.valid_until, + challenge1.value.valueOf() === challenge.value.valueOf() && + challenge1.valid_until.valueOf() === challenge.valid_until.valueOf(), ) ) { return; @@ -70,7 +70,7 @@ const challengeSlice = createSlice({ }, }, removeReceivedChallenge: { - prepare(laoId: Hash, challenge: Challenge, publicKey?: PublicKey) { + prepare(laoId: Hash, challenge: ChallengeState, publicKey?: PublicKey) { return { payload: { laoId: laoId.valueOf(), @@ -81,7 +81,7 @@ const challengeSlice = createSlice({ }, reducer( state, - action: PayloadAction<{ laoId: string; challenge: Challenge; publicKey?: PublicKey }>, + action: PayloadAction<{ laoId: string; challenge: ChallengeState; publicKey?: PublicKey }>, ) { const { laoId, challenge } = action.payload; if (state.recvChallenges[laoId] === undefined) { @@ -89,7 +89,11 @@ const challengeSlice = createSlice({ } state.recvChallenges[laoId] = state.recvChallenges[laoId].filter( - ([challenge1]) => challenge1 !== challenge, + ([challenge1]) => + !( + challenge1.valid_until.valueOf() === challenge.valid_until.valueOf() && + challenge1.value.valueOf() === challenge.value.valueOf() + ), ); }, }, @@ -132,7 +136,7 @@ export const makeChallengeReceveidSelector = (laoId: Hash) => { // First input: a map containing all challenges (state: any) => getChallengeState(state), // Selector: returns the challenge for a specific lao - (challengeState: ChallengeReducerState): [Challenge, PublicKey?][] | undefined => { + (challengeState: ChallengeReducerState): [ChallengeState, PublicKey?][] | undefined => { const serializedLaoId = laoId.valueOf(); if (!challengeState) { return undefined; diff --git a/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts b/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts index 8adf87602c..4cf7f8c918 100644 --- a/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts +++ b/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts @@ -54,13 +54,13 @@ describe('ChallengeReducer', () => { byLaoId: {}, recvChallenges: {}, } as ChallengeReducerState, - addReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + addReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), ); expect( newState.recvChallenges[mockLaoId.valueOf()]?.find( ([challenge]) => - challenge.value === mockChallenge.value && - challenge.valid_until === mockChallenge.valid_until, + challenge.value === mockChallengeState.value && + challenge.valid_until === mockChallengeState.valid_until, ), ).toBeTruthy(); }); @@ -73,17 +73,17 @@ describe('ChallengeReducer', () => { byLaoId: {}, recvChallenges: {}, } as ChallengeReducerState, - addReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + addReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), ); newState = challengeReduce( newState, - removeReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + removeReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), ); expect( newState.recvChallenges[mockLaoId.valueOf()]?.find( ([challenge]) => - challenge.value === mockChallenge.value && - challenge.valid_until === mockChallenge.valid_until, + challenge.value === mockChallengeState.value && + challenge.valid_until === mockChallengeState.valid_until, ), ).toBeUndefined(); }); @@ -135,23 +135,23 @@ describe('makeChallengeReceveidSelector', () => { byLaoId: {}, recvChallenges: {}, } as ChallengeReducerState, - addReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + addReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), ); expect( newState.recvChallenges[mockLaoId.valueOf()]?.find( ([challenge]) => - challenge.value === mockChallenge.value && - challenge.valid_until === mockChallenge.valid_until, + challenge.value.valueOf() === mockChallengeState.value.valueOf() && + challenge.valid_until.valueOf() === mockChallengeState.valid_until.valueOf(), ), ).toBeTruthy(); expect( makeChallengeReceveidSelector(mockLaoId)({ [CHALLENGE_REDUCER_PATH]: { byLaoId: {}, - recvChallenges: { [serializedMockLaoId]: [[mockChallenge, mockLao.organizer]] }, + recvChallenges: { [serializedMockLaoId]: [[mockChallengeState, mockLao.organizer]] }, } as ChallengeReducerState, }), - ).toEqual([[mockChallenge, mockLao.organizer]]); + ).toEqual([[mockChallengeState, mockLao.organizer]]); }); it('returns undefined if the laoId is not in the store', () => { @@ -160,20 +160,20 @@ describe('makeChallengeReceveidSelector', () => { byLaoId: {}, recvChallenges: {}, } as ChallengeReducerState, - addReceivedChallenge(mockLaoId, mockChallenge, mockLao.organizer), + addReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), ); expect( newState.recvChallenges[mockLaoId.valueOf()]?.find( ([challenge]) => - challenge.value === mockChallenge.value && - challenge.valid_until === mockChallenge.valid_until, + challenge.value.valueOf() === mockChallengeState.value.valueOf() && + challenge.valid_until.valueOf() === mockChallengeState.valid_until.valueOf(), ), ).toBeTruthy(); expect( makeChallengeReceveidSelector(mockLaoId2)({ [CHALLENGE_REDUCER_PATH]: { byLaoId: {}, - recvChallenges: { [serializedMockLaoId]: [[mockChallenge, mockLao.organizer]] }, + recvChallenges: { [serializedMockLaoId]: [[mockChallengeState, mockLao.organizer]] }, } as ChallengeReducerState, }), ).toBeUndefined(); diff --git a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx index 57cf4db1bd..43a9649469 100644 --- a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx +++ b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx @@ -51,11 +51,10 @@ const LinkedOrganizationsScreen = () => { ) { try { for (const [challenge, publicKey] of recvChallengeState) { - const chall2 = challenge.toState(); const matchingOrg = scannedLinkedOrgStates.find( (org) => - org.challenge!.value === chall2.value && - org.challenge!.valid_until === chall2.valid_until, + org.challenge!.value.valueOf() === challenge.value.valueOf() && + org.challenge!.valid_until.valueOf() === challenge.valid_until.valueOf(), ); if (matchingOrg && publicKey) { dispatch(addLinkedOrganization(laoId, matchingOrg!)); From dc988789250902cfcf10a94a268abc26c9f3cd77 Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Tue, 25 Jun 2024 15:26:33 +0200 Subject: [PATCH 05/11] add handling extended json rpc request --- fe1-web/src/core/network/ingestion/Handler.ts | 7 ++++++- .../src/features/linked-organizations/objects/Challenge.ts | 6 +++--- .../objects/__tests__/Challenge.test.ts | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/fe1-web/src/core/network/ingestion/Handler.ts b/fe1-web/src/core/network/ingestion/Handler.ts index 1d58837f41..222f768a8d 100644 --- a/fe1-web/src/core/network/ingestion/Handler.ts +++ b/fe1-web/src/core/network/ingestion/Handler.ts @@ -1,6 +1,6 @@ import { dispatch } from 'core/redux'; -import { Broadcast, JsonRpcMethod, ExtendedJsonRpcRequest } from '../jsonrpc'; +import { Broadcast, JsonRpcMethod, ExtendedJsonRpcRequest, Publish } from '../jsonrpc'; import { ActionType, MessageRegistry, ObjectType } from '../jsonrpc/messages'; import { ExtendedMessage } from './ExtendedMessage'; import { addMessages } from './MessageReducer'; @@ -61,6 +61,11 @@ export function handleExtendedRpcRequests(req: ExtendedJsonRpcRequest) { broadcastParams.channel, ), ); + } else if (req.request.method === JsonRpcMethod.PUBLISH) { + const publishParams = req.request.params as Publish; + storeMessage( + ExtendedMessage.fromMessage(publishParams.message, req.receivedFrom, publishParams.channel), + ); } else { console.warn('A request was received but it is currently unsupported:', req); } diff --git a/fe1-web/src/features/linked-organizations/objects/Challenge.ts b/fe1-web/src/features/linked-organizations/objects/Challenge.ts index 7996ff827a..b562247625 100644 --- a/fe1-web/src/features/linked-organizations/objects/Challenge.ts +++ b/fe1-web/src/features/linked-organizations/objects/Challenge.ts @@ -3,7 +3,7 @@ import { OmitMethods } from 'core/types'; export interface ChallengeState { value: HashState; - valid_until: Timestamp; + valid_until: number; } export class Challenge { @@ -32,14 +32,14 @@ export class Challenge { public toState(): ChallengeState { return { value: this.value.toState(), - valid_until: this.valid_until, + valid_until: this.valid_until.valueOf(), }; } public static fromState(challengeState: ChallengeState): Challenge { return new Challenge({ value: Hash.fromState(challengeState.value), - valid_until: challengeState.valid_until, + valid_until: new Timestamp(challengeState.valid_until), }); } diff --git a/fe1-web/src/features/linked-organizations/objects/__tests__/Challenge.test.ts b/fe1-web/src/features/linked-organizations/objects/__tests__/Challenge.test.ts index 557291f369..2121a0200a 100644 --- a/fe1-web/src/features/linked-organizations/objects/__tests__/Challenge.test.ts +++ b/fe1-web/src/features/linked-organizations/objects/__tests__/Challenge.test.ts @@ -13,7 +13,7 @@ describe('Challenge object', () => { it('does a state round trip correctly', () => { const challengeState: ChallengeState = { value: VALID_HASH_VALUE.toState(), - valid_until: VALID_TIMESTAMP, + valid_until: VALID_TIMESTAMP.valueOf(), }; const challenge = Challenge.fromState(challengeState); expect(challenge.toState()).toStrictEqual(challengeState); From e6c75cc0dd509d104c5a4f84ec5f6cd32fde3b8d Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Tue, 25 Jun 2024 15:35:54 +0200 Subject: [PATCH 06/11] fix docs --- .../network/__tests__/LinkedOrgMessageApi.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts index 49d85c766e..9385dfd217 100644 --- a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts +++ b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts @@ -87,7 +87,7 @@ describe('LinkedOrgMessageApi', () => { it('should create the correct request for initFederation', async () => { const challengeState: ChallengeState = { value: VALID_HASH_VALUE.toState(), - valid_until: VALID_TIMESTAMP, + valid_until: VALID_TIMESTAMP.valueOf(), }; const challenge = Challenge.fromState(challengeState); await msApi.initFederation( @@ -107,7 +107,7 @@ describe('LinkedOrgMessageApi', () => { it('should create the correct request for expectFederation', async () => { const challengeState: ChallengeState = { value: VALID_HASH_VALUE.toState(), - valid_until: VALID_TIMESTAMP, + valid_until: VALID_TIMESTAMP.valueOf(), }; const challenge = Challenge.fromState(challengeState); await msApi.expectFederation( From d761e57e3bd829bb5c579a5c750f8c52287ad11f Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Thu, 27 Jun 2024 13:21:40 +0200 Subject: [PATCH 07/11] apply data exchange branch changes --- fe1-web/src/core/network/ingestion/Handler.ts | 3 ++ fe1-web/src/features/index.ts | 3 ++ fe1-web/src/features/lao/objects/Lao.ts | 4 +- .../components/BroadcastLinkedOrgInfo.tsx | 44 +++++++++++++++++++ .../hooks/LinkedOrganizationsHooks.ts | 10 +++++ .../features/linked-organizations/index.ts | 4 +- .../interface/Configuration.ts | 15 ++++++- .../linked-organizations/interface/Feature.ts | 13 +++++- .../screens/LinkedOrganizationsScreen.tsx | 18 +++++++- 9 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx diff --git a/fe1-web/src/core/network/ingestion/Handler.ts b/fe1-web/src/core/network/ingestion/Handler.ts index 222f768a8d..7fd020789d 100644 --- a/fe1-web/src/core/network/ingestion/Handler.ts +++ b/fe1-web/src/core/network/ingestion/Handler.ts @@ -51,6 +51,7 @@ export function storeMessage(msg: ExtendedMessage) { } export function handleExtendedRpcRequests(req: ExtendedJsonRpcRequest) { + console.log(req); if (req.request.method === JsonRpcMethod.BROADCAST) { const broadcastParams = req.request.params as Broadcast; @@ -62,7 +63,9 @@ export function handleExtendedRpcRequests(req: ExtendedJsonRpcRequest) { ), ); } else if (req.request.method === JsonRpcMethod.PUBLISH) { + console.log("Here!"); const publishParams = req.request.params as Publish; + console.log(publishParams.message) storeMessage( ExtendedMessage.fromMessage(publishParams.message, req.receivedFrom, publishParams.channel), ); diff --git a/fe1-web/src/features/index.ts b/fe1-web/src/features/index.ts index 24924eee97..0af6921626 100644 --- a/fe1-web/src/features/index.ts +++ b/fe1-web/src/features/index.ts @@ -16,6 +16,7 @@ import * as rollCall from './rollCall'; import * as social from './social'; import * as wallet from './wallet'; import * as witness from './witness'; +import { getRollCallById } from './rollCall/functions/RollCall'; export function configureFeatures() { const messageRegistry = new MessageRegistry(); @@ -140,6 +141,8 @@ export function configureFeatures() { useCurrentLao: laoConfiguration.hooks.useCurrentLao, getCurrentLaoId: laoConfiguration.functions.getCurrentLaoId, getLaoOrganizerBackendPublicKey: laoConfiguration.functions.getLaoOrganizerBackendPublicKey, + getLaoById: laoConfiguration.functions.getLaoById, + getRollCallById: rollCallConfiguration.functions.getRollCallById, }); // compose features diff --git a/fe1-web/src/features/lao/objects/Lao.ts b/fe1-web/src/features/lao/objects/Lao.ts index aa54fea70a..864716dcc8 100644 --- a/fe1-web/src/features/lao/objects/Lao.ts +++ b/fe1-web/src/features/lao/objects/Lao.ts @@ -73,11 +73,11 @@ export class Lao { this.name = obj.name; this.id = obj.id; + this.last_roll_call_id = obj.last_roll_call_id; this.creation = obj.creation; this.last_modified = obj.last_modified; this.organizer = obj.organizer; this.witnesses = [...obj.witnesses]; - this.last_roll_call_id = obj.last_roll_call_id; this.last_tokenized_roll_call_id = obj.last_tokenized_roll_call_id; this.server_addresses = obj.server_addresses || []; @@ -93,11 +93,11 @@ export class Lao { return new Lao({ name: lao.name, id: Hash.fromState(lao.id), + last_roll_call_id: lao.last_roll_call_id ? Hash.fromState(lao.last_roll_call_id) : undefined, creation: Timestamp.fromState(lao.creation), last_modified: Timestamp.fromState(lao.last_modified), organizer: PublicKey.fromState(lao.organizer), witnesses: lao.witnesses.map((w) => PublicKey.fromState(w)), - last_roll_call_id: lao.last_roll_call_id ? Hash.fromState(lao.last_roll_call_id) : undefined, last_tokenized_roll_call_id: lao.last_tokenized_roll_call_id ? Hash.fromState(lao.last_tokenized_roll_call_id) : undefined, diff --git a/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx b/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx new file mode 100644 index 0000000000..ff1c3f408c --- /dev/null +++ b/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx @@ -0,0 +1,44 @@ +import React, { useEffect } from 'react'; +import { Hash } from 'core/objects'; +import { LinkedOrganizationsHooks } from '../hooks'; +import { catchup, subscribeToChannel } from 'core/network'; +import { dispatch } from 'core/redux'; +import { useToast } from 'react-native-toast-notifications'; +import { FOUR_SECONDS } from 'resources/const'; + +interface BroadcastLinkedOrgInfoProps { + linkedLaoId: Hash, +} + + + +const BroadcastLinkedOrgInfo: React.FC = ({ + linkedLaoId +}) => { + const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); + console.log(LinkedOrganizationsHooks.useGetLaoById(laoId)) + const isOrganizer = LinkedOrganizationsHooks.useIsLaoOrganizer(laoId); + const toast = useToast(); + const fetchedLao = LinkedOrganizationsHooks.useGetLaoById(linkedLaoId); + if (fetchedLao?.last_roll_call_id === undefined) { + toast.show(`Data Exchange failed: Linked Organization has no last roll call id`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + return (null); + } + const fetchedRollCall = LinkedOrganizationsHooks.useGetRollCallById(fetchedLao.last_roll_call_id); + if (fetchedRollCall === undefined) { + toast.show(`Data Exchange failed: RollCall ID is undefined`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + return (null); + } + console.log(fetchedRollCall.attendees) + return (null); +}; + +export default BroadcastLinkedOrgInfo; diff --git a/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts b/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts index 2d05ac4fd0..d6d0606bae 100644 --- a/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts +++ b/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts @@ -30,6 +30,16 @@ export namespace LinkedOrganizationsHooks { */ export const useCurrentLao = () => useLinkedOrganizationsContext().useCurrentLao(); + /** + * Gets the function to retrieve a lao by its id + */ + export const useGetLaoById = (laoId: Hash) => useLinkedOrganizationsContext().getLaoById(laoId); + + /** + * Gets the function to retrieve a rollcall by its id + */ + export const useGetRollCallById = (id: Hash) => useLinkedOrganizationsContext().getRollCallById(id); + /** * Gets whether the current user is organizer of the given lao */ diff --git a/fe1-web/src/features/linked-organizations/index.ts b/fe1-web/src/features/linked-organizations/index.ts index 42fc98126c..5f8b01f173 100644 --- a/fe1-web/src/features/linked-organizations/index.ts +++ b/fe1-web/src/features/linked-organizations/index.ts @@ -13,7 +13,7 @@ import { challengeReducer, linkedOrganizationsReducer } from './reducer'; export function configure( configuration: LinkedOrganizationsConfiguration, ): LinkedOrganizationsInterface { - const { useCurrentLao, useCurrentLaoId, useIsLaoOrganizer } = configuration; + const { useCurrentLao, useCurrentLaoId, useIsLaoOrganizer, getLaoById, getRollCallById } = configuration; configureNetwork(configuration); return { identifier: LINKED_ORGANIZATIONS_FEATURE_IDENTIFIER, @@ -26,6 +26,8 @@ export function configure( useCurrentLaoId: useCurrentLaoId, useIsLaoOrganizer: useIsLaoOrganizer, useCurrentLao: useCurrentLao, + getLaoById: getLaoById, + getRollCallById: getRollCallById, }, }; } diff --git a/fe1-web/src/features/linked-organizations/interface/Configuration.ts b/fe1-web/src/features/linked-organizations/interface/Configuration.ts index 2759777cc5..286ad780f3 100644 --- a/fe1-web/src/features/linked-organizations/interface/Configuration.ts +++ b/fe1-web/src/features/linked-organizations/interface/Configuration.ts @@ -52,6 +52,19 @@ export interface LinkedOrganizationsConfiguration { * @returns The current lao */ useCurrentLao: () => LinkedOrganizationsFeature.Lao; + + /** + * Gets a lao from the store by its id + * @param laoId The id of the lao + * @returns A lao or undefined if none was found + */ + getLaoById(laoId: Hash): LinkedOrganizationsFeature.Lao | undefined; + + /** + * Returns a map from laoIds to names + */ + + getRollCallById: (id: Hash) => LinkedOrganizationsFeature.RollCall | undefined; } /** @@ -59,7 +72,7 @@ export interface LinkedOrganizationsConfiguration { */ export type LinkedOrganizationsReactContext = Pick< LinkedOrganizationsConfiguration, - 'useCurrentLaoId' | 'useIsLaoOrganizer' | 'useCurrentLao' + 'useCurrentLaoId' | 'useIsLaoOrganizer' | 'useCurrentLao' | 'getLaoById' | 'getRollCallById' >; /** diff --git a/fe1-web/src/features/linked-organizations/interface/Feature.ts b/fe1-web/src/features/linked-organizations/interface/Feature.ts index a4665625f1..2c1976c9c5 100644 --- a/fe1-web/src/features/linked-organizations/interface/Feature.ts +++ b/fe1-web/src/features/linked-organizations/interface/Feature.ts @@ -1,6 +1,6 @@ import { LaoParamList } from 'core/navigation/typing/LaoParamList'; import { NavigationDrawerScreen } from 'core/navigation/typing/Screen'; -import { Hash, PublicKey } from 'core/objects'; +import { Hash, PopToken, PublicKey } from 'core/objects'; export namespace LinkedOrganizationsFeature { export interface LaoScreen extends NavigationDrawerScreen { @@ -10,5 +10,16 @@ export namespace LinkedOrganizationsFeature { id: Hash; server_addresses: string[]; organizer: PublicKey; + last_tokenized_roll_call_id?: Hash | undefined; + last_roll_call_id?: Hash | undefined; + } + + export interface RollCall { + id: Hash; + name: string; + status: number; + attendees?: PublicKey[]; + + containsToken(token: PopToken | undefined): boolean; } } diff --git a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx index 43a9649469..ea4e268899 100644 --- a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx +++ b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx @@ -1,5 +1,5 @@ import { ListItem } from '@rneui/themed'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Text, View, StyleSheet, ViewStyle } from 'react-native'; import { useToast } from 'react-native-toast-notifications'; import { useSelector } from 'react-redux'; @@ -19,6 +19,10 @@ import { makeScannedLinkedOrganizationSelector, removeScannedLinkedOrganization, } from '../reducer/LinkedOrganizationsReducer'; +import { LinkedOrganization } from '../objects/LinkedOrganization'; +import { Hash } from 'core/objects'; +import BroadcastLinkedOrgInfo from '../components/BroadcastLinkedOrgInfo'; +import { catchup, subscribeToChannel } from 'core/network'; const styles = StyleSheet.create({ flexibleView: { @@ -26,6 +30,7 @@ const styles = StyleSheet.create({ } as ViewStyle, }); + const LinkedOrganizationsScreen = () => { const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); const toast = useToast(); @@ -41,8 +46,16 @@ const LinkedOrganizationsScreen = () => { [laoId], ); const scannedLinkedOrgStates = useSelector(scannedLinkedOrgSelector); + const [linkedLaoId, setLinkedLaoId] = useState(null); + useEffect(() => { + const fetchData = async (linkedLaoId: Hash) => { + await subscribeToChannel(linkedLaoId, dispatch, '/root/' + linkedLaoId.valueOf()); + await catchup('/root/' + linkedLaoId.valueOf()); + await new Promise(f => setTimeout(f, 1000)); + setLinkedLaoId(linkedLaoId); + }; if ( recvChallengeState && scannedLinkedOrgStates && @@ -65,6 +78,8 @@ const LinkedOrganizationsScreen = () => { }); dispatch(removeScannedLinkedOrganization(laoId, matchingOrg.lao_id)); dispatch(removeReceivedChallenge(laoId, challenge, publicKey)); + const linkedOrg = LinkedOrganization.fromState(matchingOrg); + fetchData(linkedOrg.lao_id); } else { toast.show(`Could not link organizations`, { type: 'danger', @@ -99,6 +114,7 @@ const LinkedOrganizationsScreen = () => { ))} + {linkedLaoId && } ); From e26cafc614e6adf15df096b728f9359ac06f5ed5 Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Fri, 28 Jun 2024 14:23:42 +0200 Subject: [PATCH 08/11] add data exchange --- fe1-web/src/core/network/ingestion/Handler.ts | 3 - .../network/jsonrpc/messages/MessageData.ts | 1 + .../jsonrpc/messages/MessageRegistry.ts | 2 + .../src/core/network/validation/Validator.ts | 1 + .../network/validation/schemas/dataSchemas.ts | 2 + .../features/home/screens/ConnectConfirm.tsx | 3 +- .../src/features/home/screens/ConnectScan.tsx | 3 +- fe1-web/src/features/index.ts | 1 - .../components/AddLinkedOrganizationModal.tsx | 7 +- .../components/BroadcastLinkedOrgInfo.tsx | 53 +++++++----- .../hooks/LinkedOrganizationsHooks.ts | 5 +- .../features/linked-organizations/index.ts | 3 +- .../network/LinkedOrgHandler.ts | 81 +++++++++++++++++-- .../network/LinkedOrgMessageApi.ts | 21 +++++ .../linked-organizations/network/index.ts | 8 ++ .../network/messages/TokensExchange.ts | 61 ++++++++++++++ .../linked-organizations/objects/Challenge.ts | 2 +- .../reducer/LinkedOrganizationsReducer.ts | 39 +++++++-- .../LinkedOrganizationsReducer.test.ts | 2 +- .../screens/LinkedOrganizationsScreen.tsx | 37 ++++----- .../network/messages/WitnessRegistry.ts | 2 + protocol/examples/answer/rumor_state_ans.json | 12 +++ .../federation_tokens_exchange.json | 8 ++ protocol/examples/query/rumor/rumor.json | 6 ++ .../rumor/wrong_rumor_additional_params.json | 6 ++ .../rumor/wrong_rumor_missing_channel.json | 6 ++ .../rumor/wrong_rumor_missing_messages.json | 8 +- .../rumor/wrong_rumor_missing_rumor_id.json | 6 ++ .../rumor/wrong_rumor_missing_sender_id.json | 6 ++ .../rumor/wrong_rumor_missing_timestamp.json | 35 ++++++++ protocol/query/method/message/data/data.json | 3 + .../data/dataFederationTokensExchange.json | 48 +++++++++++ protocol/query/method/object/rumor.json | 7 +- protocol/test/main.js | 2 + protocol/test/main.test.js | 3 + 35 files changed, 424 insertions(+), 69 deletions(-) create mode 100644 fe1-web/src/features/linked-organizations/network/messages/TokensExchange.ts create mode 100644 protocol/examples/messageData/federation_tokens_exchange/federation_tokens_exchange.json create mode 100644 protocol/examples/query/rumor/wrong_rumor_missing_timestamp.json create mode 100644 protocol/query/method/message/data/dataFederationTokensExchange.json diff --git a/fe1-web/src/core/network/ingestion/Handler.ts b/fe1-web/src/core/network/ingestion/Handler.ts index 7fd020789d..222f768a8d 100644 --- a/fe1-web/src/core/network/ingestion/Handler.ts +++ b/fe1-web/src/core/network/ingestion/Handler.ts @@ -51,7 +51,6 @@ export function storeMessage(msg: ExtendedMessage) { } export function handleExtendedRpcRequests(req: ExtendedJsonRpcRequest) { - console.log(req); if (req.request.method === JsonRpcMethod.BROADCAST) { const broadcastParams = req.request.params as Broadcast; @@ -63,9 +62,7 @@ export function handleExtendedRpcRequests(req: ExtendedJsonRpcRequest) { ), ); } else if (req.request.method === JsonRpcMethod.PUBLISH) { - console.log("Here!"); const publishParams = req.request.params as Publish; - console.log(publishParams.message) storeMessage( ExtendedMessage.fromMessage(publishParams.message, req.receivedFrom, publishParams.channel), ); diff --git a/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts b/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts index 1217a4301e..6e4e0f3946 100644 --- a/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts +++ b/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts @@ -44,6 +44,7 @@ export enum ActionType { FEDERATION_INIT = 'init', FEDERATION_EXPECT = 'expect', FEDERATION_RESULT = 'result', + TOKENS_EXCHANGE = 'tokens_exchange', } /** Enumeration of all possible signatures of a message */ diff --git a/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts b/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts index 0b63f3d193..342f188629 100644 --- a/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts +++ b/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts @@ -33,6 +33,7 @@ const { FEDERATION_INIT, FEDERATION_EXPECT, FEDERATION_RESULT, + TOKENS_EXCHANGE, } = ActionType; const { KEYPAIR, POP_TOKEN } = SignatureType; @@ -109,6 +110,7 @@ export class MessageRegistry { [k(FEDERATION, FEDERATION_INIT), { signature: KEYPAIR }], [k(FEDERATION, FEDERATION_EXPECT), { signature: KEYPAIR }], [k(FEDERATION, FEDERATION_RESULT), { signature: KEYPAIR }], + [k(FEDERATION, TOKENS_EXCHANGE), { signature: KEYPAIR }], ]); /** diff --git a/fe1-web/src/core/network/validation/Validator.ts b/fe1-web/src/core/network/validation/Validator.ts index 7a35062978..2a165ea539 100644 --- a/fe1-web/src/core/network/validation/Validator.ts +++ b/fe1-web/src/core/network/validation/Validator.ts @@ -80,6 +80,7 @@ const schemaIds: Record> = { [ActionType.FEDERATION_INIT]: 'dataFederationInit', [ActionType.FEDERATION_EXPECT]: 'dataFederationExpect', [ActionType.FEDERATION_RESULT]: 'dataFederationResult', + [ActionType.TOKENS_EXCHANGE]: 'dataFederationTokensExchange', }, }; diff --git a/fe1-web/src/core/network/validation/schemas/dataSchemas.ts b/fe1-web/src/core/network/validation/schemas/dataSchemas.ts index 03d9dba7cd..a82b4e4aeb 100644 --- a/fe1-web/src/core/network/validation/schemas/dataSchemas.ts +++ b/fe1-web/src/core/network/validation/schemas/dataSchemas.ts @@ -36,6 +36,7 @@ import dataFederationChallengeRequest from 'protocol/query/method/message/data/d import dataFederationExpect from 'protocol/query/method/message/data/dataFederationExpect.json'; import dataFederationInit from 'protocol/query/method/message/data/dataFederationInit.json'; import dataFederationResult from 'protocol/query/method/message/data/dataFederationResult.json'; +import dataFederationTokensExchange from 'protocol/query/method/message/data/dataFederationTokensExchange.json'; /* eslint-enable import/order */ const dataSchemas = [ @@ -75,6 +76,7 @@ const dataSchemas = [ dataFederationExpect, dataFederationInit, dataFederationResult, + dataFederationTokensExchange, ]; export default dataSchemas; diff --git a/fe1-web/src/features/home/screens/ConnectConfirm.tsx b/fe1-web/src/features/home/screens/ConnectConfirm.tsx index ecde70d6da..f7b5becb4f 100644 --- a/fe1-web/src/features/home/screens/ConnectConfirm.tsx +++ b/fe1-web/src/features/home/screens/ConnectConfirm.tsx @@ -10,7 +10,7 @@ import ScreenWrapper from 'core/components/ScreenWrapper'; import { AppParamList } from 'core/navigation/typing/AppParamList'; import { ConnectParamList } from 'core/navigation/typing/ConnectParamList'; import { getNetworkManager, subscribeToChannel } from 'core/network'; -import { Hash } from 'core/objects'; +import { getFederationChannel, Hash } from 'core/objects'; import { Typography } from 'core/styles'; import containerStyles from 'core/styles/stylesheets/containerStyles'; import { FOUR_SECONDS } from 'resources/const'; @@ -63,6 +63,7 @@ const ConnectConfirm = () => { } else { // subscribe to the lao channel on the new connection await subscribeToChannel(laoId, dispatch, laoChannel, [connection]); + await subscribeToChannel(laoId, dispatch, getFederationChannel(laoId)); } navigation.navigate(STRINGS.navigation_app_lao, { diff --git a/fe1-web/src/features/home/screens/ConnectScan.tsx b/fe1-web/src/features/home/screens/ConnectScan.tsx index 6c38a2ad4d..566916a21e 100644 --- a/fe1-web/src/features/home/screens/ConnectScan.tsx +++ b/fe1-web/src/features/home/screens/ConnectScan.tsx @@ -11,7 +11,7 @@ import QrCodeScanOverlay from 'core/components/QrCodeScanOverlay'; import { AppParamList } from 'core/navigation/typing/AppParamList'; import { ConnectParamList } from 'core/navigation/typing/ConnectParamList'; import { getNetworkManager, subscribeToChannel } from 'core/network'; -import { Channel } from 'core/objects'; +import { Channel, getFederationChannel } from 'core/objects'; import { Spacing, Typography } from 'core/styles'; import { FOUR_SECONDS } from 'resources/const'; import STRINGS from 'resources/strings'; @@ -164,6 +164,7 @@ const ConnectScan = () => { ); await connectToLaoAndSubscribe(connectToLao, laoChannel); + await subscribeToChannel(connectToLao.lao, dispatch, getFederationChannel(connectToLao.lao)); isProcessingScan.current = false; setIsConnecting(false); diff --git a/fe1-web/src/features/index.ts b/fe1-web/src/features/index.ts index 0af6921626..f453fcb5fe 100644 --- a/fe1-web/src/features/index.ts +++ b/fe1-web/src/features/index.ts @@ -16,7 +16,6 @@ import * as rollCall from './rollCall'; import * as social from './social'; import * as wallet from './wallet'; import * as witness from './witness'; -import { getRollCallById } from './rollCall/functions/RollCall'; export function configureFeatures() { const messageRegistry = new MessageRegistry(); diff --git a/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx b/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx index ef2d4aa9e4..24e00f0331 100644 --- a/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx +++ b/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx @@ -54,6 +54,7 @@ const AddLinkedOrganizationModal = () => { const navigation = useNavigation(); const toast = useToast(); const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); + const isOrganizer = LinkedOrganizationsHooks.useIsLaoOrganizer(laoId); const lao = LinkedOrganizationsHooks.useCurrentLao(); const challengeSelector = useMemo(() => makeChallengeSelector(laoId), [laoId]); const challengeState = useSelector(challengeSelector); @@ -121,6 +122,7 @@ const AddLinkedOrganizationModal = () => { console.log('Expect Federation successfull'); }) .catch((err) => { + console.log(err); toast.show(`Could not expect Federation, error: ${err}`, { type: 'danger', placement: 'bottom', @@ -139,6 +141,7 @@ const AddLinkedOrganizationModal = () => { console.log('Init Federation successfull'); }) .catch((err) => { + console.log(err); toast.show(`Could not init Federation, error: ${err}`, { type: 'danger', placement: 'bottom', @@ -180,7 +183,7 @@ const AddLinkedOrganizationModal = () => { }; useEffect(() => { - if (challengeState) { + if (challengeState && isOrganizer) { const challenge = Challenge.fromState(challengeState); const jsonObj = { lao_id: laoId, @@ -193,7 +196,7 @@ const AddLinkedOrganizationModal = () => { }; setQRCodeData(JSON.stringify(jsonObj)); } - }, [challengeState, laoId, lao.organizer, lao.server_addresses]); + }, [challengeState, laoId, lao.organizer, lao.server_addresses, isOrganizer]); return ( <> diff --git a/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx b/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx index ff1c3f408c..edcefb095a 100644 --- a/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx +++ b/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx @@ -1,44 +1,53 @@ -import React, { useEffect } from 'react'; -import { Hash } from 'core/objects'; -import { LinkedOrganizationsHooks } from '../hooks'; -import { catchup, subscribeToChannel } from 'core/network'; -import { dispatch } from 'core/redux'; +import React from 'react'; import { useToast } from 'react-native-toast-notifications'; + +import { Hash } from 'core/objects'; import { FOUR_SECONDS } from 'resources/const'; +import { LinkedOrganizationsHooks } from '../hooks'; +import { tokensExchange } from '../network'; + interface BroadcastLinkedOrgInfoProps { - linkedLaoId: Hash, + linkedLaoId: Hash; } - - -const BroadcastLinkedOrgInfo: React.FC = ({ - linkedLaoId -}) => { +const BroadcastLinkedOrgInfo: React.FC = ({ linkedLaoId }) => { const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); - console.log(LinkedOrganizationsHooks.useGetLaoById(laoId)) - const isOrganizer = LinkedOrganizationsHooks.useIsLaoOrganizer(laoId); const toast = useToast(); const fetchedLao = LinkedOrganizationsHooks.useGetLaoById(linkedLaoId); - if (fetchedLao?.last_roll_call_id === undefined) { - toast.show(`Data Exchange failed: Linked Organization has no last roll call id`, { + const fetchedRollCall = LinkedOrganizationsHooks.useGetRollCallById( + fetchedLao!.last_roll_call_id!, + ); + + if (fetchedRollCall === undefined) { + toast.show(`Data Exchange failed: RollCall ID is undefined`, { type: 'danger', placement: 'bottom', duration: FOUR_SECONDS, }); - return (null); + return null; } - const fetchedRollCall = LinkedOrganizationsHooks.useGetRollCallById(fetchedLao.last_roll_call_id); - if (fetchedRollCall === undefined) { - toast.show(`Data Exchange failed: RollCall ID is undefined`, { + if (fetchedRollCall.attendees === undefined) { + toast.show(`Data Exchange failed: RollCall Attendees is undefined`, { type: 'danger', placement: 'bottom', duration: FOUR_SECONDS, }); - return (null); + return null; } - console.log(fetchedRollCall.attendees) - return (null); + tokensExchange(laoId, linkedLaoId, fetchedRollCall.id, fetchedRollCall.attendees) + .then(() => { + console.log('Data Exchange successfull'); + }) + .catch((err) => { + console.log(err); + toast.show(`Could not exchange Tokens, error: ${err}`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + }); + return null; }; export default BroadcastLinkedOrgInfo; diff --git a/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts b/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts index d6d0606bae..10edae5f2b 100644 --- a/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts +++ b/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts @@ -35,10 +35,11 @@ export namespace LinkedOrganizationsHooks { */ export const useGetLaoById = (laoId: Hash) => useLinkedOrganizationsContext().getLaoById(laoId); - /** + /** * Gets the function to retrieve a rollcall by its id */ - export const useGetRollCallById = (id: Hash) => useLinkedOrganizationsContext().getRollCallById(id); + export const useGetRollCallById = (id: Hash) => + useLinkedOrganizationsContext().getRollCallById(id); /** * Gets whether the current user is organizer of the given lao diff --git a/fe1-web/src/features/linked-organizations/index.ts b/fe1-web/src/features/linked-organizations/index.ts index 5f8b01f173..e96effb965 100644 --- a/fe1-web/src/features/linked-organizations/index.ts +++ b/fe1-web/src/features/linked-organizations/index.ts @@ -13,7 +13,8 @@ import { challengeReducer, linkedOrganizationsReducer } from './reducer'; export function configure( configuration: LinkedOrganizationsConfiguration, ): LinkedOrganizationsInterface { - const { useCurrentLao, useCurrentLaoId, useIsLaoOrganizer, getLaoById, getRollCallById } = configuration; + const { useCurrentLao, useCurrentLaoId, useIsLaoOrganizer, getLaoById, getRollCallById } = + configuration; configureNetwork(configuration); return { identifier: LINKED_ORGANIZATIONS_FEATURE_IDENTIFIER, diff --git a/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts b/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts index cb1028e50b..6641bd5c54 100644 --- a/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts +++ b/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts @@ -1,10 +1,12 @@ +import { subscribeToChannel } from 'core/network'; import { ActionType, ObjectType, ProcessableMessage } from 'core/network/jsonrpc/messages'; -import { Base64UrlData } from 'core/objects'; +import { Base64UrlData, getReactionChannel, getUserSocialChannel } from 'core/objects'; import { dispatch } from 'core/redux'; import { LinkedOrganizationsConfiguration } from '../interface'; import { Challenge } from '../objects/Challenge'; import { addReceivedChallenge, setChallenge } from '../reducer'; +import { addLinkedLaoId } from '../reducer/LinkedOrganizationsReducer'; import { ChallengeRequest, ChallengeMessage, @@ -12,6 +14,7 @@ import { FederationInit, FederationResult, } from './messages'; +import { TokensExchange } from './messages/TokensExchange'; /** * Handler for linked organization messages @@ -28,7 +31,6 @@ export const handleChallengeMessage = () => (msg: ProcessableMessage) => { console.warn('handleRequestChallengeMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `challenge was not processed: ${err}`; // obtain the lao id from the channel @@ -64,7 +66,6 @@ export const handleChallengeRequestMessage = console.warn('handleRequestChallengeMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `challenge/request was not processed: ${err}`; const laoId = getCurrentLaoId(); @@ -94,7 +95,6 @@ export const handleFederationInitMessage = console.warn('handleFederationInitMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `federation/init was not processed: ${err}`; const laoId = getCurrentLaoId(); @@ -130,7 +130,6 @@ export const handleFederationExpectMessage = console.warn('handleFederationExpectMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `federation/expect was not processed: ${err}`; const laoId = getCurrentLaoId(); @@ -159,8 +158,6 @@ export const handleFederationExpectMessage = export const handleFederationResultMessage = (getCurrentLaoId: LinkedOrganizationsConfiguration['getCurrentLaoId']) => (msg: ProcessableMessage) => { - console.log('HANLDE FEDRES'); - console.log(msg); if ( msg.messageData.object !== ObjectType.FEDERATION || msg.messageData.action !== ActionType.FEDERATION_RESULT @@ -168,7 +165,6 @@ export const handleFederationResultMessage = console.warn('handleFederationResultMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `federation/result was not processed: ${err}`; const laoId = getCurrentLaoId(); @@ -203,3 +199,72 @@ export const handleFederationResultMessage = } return false; }; + +/** + * Handles an tokensExchange message. + */ +export const handleTokensExchangeMessage = + (getCurrentLaoId: LinkedOrganizationsConfiguration['getCurrentLaoId']) => + (msg: ProcessableMessage) => { + if ( + msg.messageData.object !== ObjectType.FEDERATION || + msg.messageData.action !== ActionType.TOKENS_EXCHANGE + ) { + console.warn('handleTokensExchangeMessage was called to process an unsupported message'); + return false; + } + const makeErr = (err: string) => `federation/tokensExchange was not processed: ${err}`; + + const laoId = getCurrentLaoId(); + if (!laoId) { + console.warn(makeErr('no Lao is currently active')); + return false; + } + + if (msg.messageData instanceof TokensExchange) { + const tokensExchange = msg.messageData as TokensExchange; + if ( + tokensExchange.lao_id && + tokensExchange.roll_call_id && + tokensExchange.tokens && + tokensExchange.timestamp + ) { + dispatch(addLinkedLaoId(laoId, tokensExchange.lao_id)); + const subscribeChannels = async (): Promise => { + const subscribePromises = tokensExchange.tokens.map(async (attendee) => { + try { + await subscribeToChannel( + tokensExchange.lao_id, + dispatch, + getUserSocialChannel(tokensExchange.lao_id, attendee), + ); + } catch (err) { + console.error( + `Could not subscribe to social channel of attendee with public key '${attendee}', error:`, + err, + ); + } + }); + + try { + await Promise.all(subscribePromises); + } catch (err) { + console.error('Error subscribing to one or more social channels:', err); + } + + try { + await subscribeToChannel( + tokensExchange.lao_id, + dispatch, + getReactionChannel(tokensExchange.lao_id), + ); + } catch (err) { + console.error('Could not subscribe to reaction channel, error:', err); + } + }; + subscribeChannels(); + return true; + } + } + return false; + }; diff --git a/fe1-web/src/features/linked-organizations/network/LinkedOrgMessageApi.ts b/fe1-web/src/features/linked-organizations/network/LinkedOrgMessageApi.ts index 9aac213ca5..3bb2d11d1e 100644 --- a/fe1-web/src/features/linked-organizations/network/LinkedOrgMessageApi.ts +++ b/fe1-web/src/features/linked-organizations/network/LinkedOrgMessageApi.ts @@ -4,6 +4,7 @@ import { getFederationChannel, Hash, PublicKey, Timestamp } from 'core/objects'; import { Challenge } from '../objects/Challenge'; import { ChallengeRequest, ChallengeMessage, FederationExpect, FederationInit } from './messages'; +import { TokensExchange } from './messages/TokensExchange'; /** * Contains all functions to send social media related messages. @@ -74,3 +75,23 @@ export async function expectFederation( }); return publish(channel, message); } + +/** + * Sends a query to the server to exchange tokens + * + */ +export async function tokensExchange( + lao_id: Hash, + linked_lao_id: Hash, + roll_call_id: Hash, + tokens: PublicKey[], +): Promise { + const message = new TokensExchange({ + lao_id: linked_lao_id, + roll_call_id: roll_call_id, + tokens: tokens, + timestamp: Timestamp.EpochNow(), + }); + const channel = getFederationChannel(lao_id); + return publish(channel, message); +} diff --git a/fe1-web/src/features/linked-organizations/network/index.ts b/fe1-web/src/features/linked-organizations/network/index.ts index bf86f29593..c48bcfb373 100644 --- a/fe1-web/src/features/linked-organizations/network/index.ts +++ b/fe1-web/src/features/linked-organizations/network/index.ts @@ -7,6 +7,7 @@ import { handleFederationExpectMessage, handleFederationInitMessage, handleFederationResultMessage, + handleTokensExchangeMessage, } from './LinkedOrgHandler'; import { ChallengeRequest, @@ -15,6 +16,7 @@ import { FederationInit, FederationResult, } from './messages'; +import { TokensExchange } from './messages/TokensExchange'; export * from './LinkedOrgMessageApi'; @@ -54,4 +56,10 @@ export function configureNetwork(configuration: LinkedOrganizationsConfiguration handleFederationResultMessage(configuration.getCurrentLaoId), FederationResult.fromJson, ); + configuration.messageRegistry.add( + ObjectType.FEDERATION, + ActionType.TOKENS_EXCHANGE, + handleTokensExchangeMessage(configuration.getCurrentLaoId), + TokensExchange.fromJson, + ); } diff --git a/fe1-web/src/features/linked-organizations/network/messages/TokensExchange.ts b/fe1-web/src/features/linked-organizations/network/messages/TokensExchange.ts new file mode 100644 index 0000000000..ec533e8b96 --- /dev/null +++ b/fe1-web/src/features/linked-organizations/network/messages/TokensExchange.ts @@ -0,0 +1,61 @@ +import { ActionType, MessageData, ObjectType } from 'core/network/jsonrpc/messages'; +import { validateDataObject } from 'core/network/validation'; +import { Hash, ProtocolError, PublicKey, Timestamp } from 'core/objects'; + +/** Data sent to exchange tokens */ +export class TokensExchange implements MessageData { + public readonly object: ObjectType = ObjectType.FEDERATION; + + public readonly action: ActionType = ActionType.TOKENS_EXCHANGE; + + public readonly lao_id: Hash; + + public readonly roll_call_id: Hash; + + public readonly tokens: PublicKey[]; + + public readonly timestamp: Timestamp; + + constructor(msg: Partial) { + if (!msg.lao_id) { + throw new ProtocolError("Undefined 'lao_id' parameter encountered during 'TokensExchange'"); + } + if (!msg.roll_call_id) { + throw new ProtocolError( + "Undefined 'roll_call_id' parameter encountered during 'TokensExchange'", + ); + } + if (!msg.tokens || msg.tokens.length === 0) { + throw new ProtocolError( + "Undefined or empty 'tokens' parameter encountered during 'TokensExchange'", + ); + } + if (!msg.timestamp) { + throw new ProtocolError( + "Undefined 'timestamp' parameter encountered during 'TokensExchange'", + ); + } + this.lao_id = msg.lao_id; + this.roll_call_id = msg.roll_call_id; + this.tokens = msg.tokens; + this.timestamp = msg.timestamp; + } + + /** + * Creates an TokensExchange object from a given object + * @param obj + */ + public static fromJson(obj: any): TokensExchange { + const { errors } = validateDataObject(ObjectType.FEDERATION, ActionType.TOKENS_EXCHANGE, obj); + if (errors !== null) { + throw new ProtocolError(`Invalid tokens exchange\n\n${errors}`); + } + + return new TokensExchange({ + lao_id: obj.lao_id, + roll_call_id: obj.roll_call_id, + tokens: obj.tokens, + timestamp: obj.timestamp, + }); + } +} diff --git a/fe1-web/src/features/linked-organizations/objects/Challenge.ts b/fe1-web/src/features/linked-organizations/objects/Challenge.ts index b562247625..1e00ea1002 100644 --- a/fe1-web/src/features/linked-organizations/objects/Challenge.ts +++ b/fe1-web/src/features/linked-organizations/objects/Challenge.ts @@ -46,7 +46,7 @@ export class Challenge { public static fromJson(obj: any): Challenge { return new Challenge({ value: new Hash(obj.value), - valid_until: obj.valid_until, + valid_until: new Timestamp(obj.valid_until), }); } diff --git a/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts b/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts index cf0a6d20ad..f9dfce44c3 100644 --- a/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts +++ b/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts @@ -66,6 +66,32 @@ const linkedOrganizationSlice = createSlice({ state.byLaoId[laoId].byLinkedLaoId[linkedOrganization.lao_id] = linkedOrganization; }, }, + addLinkedLaoId: { + prepare(laoId: Hash, linkedLaoId: Hash) { + return { + payload: { + laoId: laoId.valueOf(), + linkedLaoId: linkedLaoId.valueOf(), + }, + }; + }, + reducer(state, action: PayloadAction<{ laoId: string; linkedLaoId: string }>) { + const { laoId, linkedLaoId } = action.payload; + + if (state.byLaoId[laoId] === undefined) { + state.byLaoId[laoId] = { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [], + }; + } + + if (!state.byLaoId[laoId].allLaoIds.includes(linkedLaoId.valueOf())) { + state.byLaoId[laoId].allLaoIds.push(linkedLaoId); + } + }, + }, addScannedLinkedOrganization: { prepare(laoId: Hash, linkedOrganization: LinkedOrganizationState) { return { @@ -131,6 +157,7 @@ export const { addLinkedOrganization, addScannedLinkedOrganization, removeScannedLinkedOrganization, + addLinkedLaoId, } = linkedOrganizationSlice.actions; export const getLinkedOrganizationState = (state: any): LinkedOrganizationReducerState => @@ -157,16 +184,16 @@ export const makeSingleLinkedOrganizationSelector = (laoId: Hash, linked_lao_id: }; /** - * Retrives all linked organization state by lao id + * Retrives all linked organization ids by lao id * @param laoId The id of the lao - * @returns A list of linked organization state + * @returns A list of linked organization ids */ export const makeLinkedOrganizationSelector = (laoId: Hash) => { return createSelector( - // First input: a map containing all linked organizations + // First input: a map containing all linked organization ids (state: any) => getLinkedOrganizationState(state), - // Selector: returns the linked organization for a specific lao and linked_lao_id - (linkedOrganizationState: LinkedOrganizationReducerState): LinkedOrganizationState[] | [] => { + // Selector: returns the linked organization ids for a specific lao and linked_lao_id + (linkedOrganizationState: LinkedOrganizationReducerState): string[] | [] => { const serializedLaoId = laoId.valueOf(); if (!linkedOrganizationState) { return []; @@ -174,7 +201,7 @@ export const makeLinkedOrganizationSelector = (laoId: Hash) => { if (!linkedOrganizationState.byLaoId[serializedLaoId]) { return []; } - return linkedOrganizationState.byLaoId[serializedLaoId].allLaos; + return linkedOrganizationState.byLaoId[serializedLaoId].allLaoIds; }, ); }; diff --git a/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts b/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts index 71a9a921c2..036b9ea516 100644 --- a/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts +++ b/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts @@ -215,7 +215,7 @@ describe('makeLinkedOrganizationsSelector', () => { }, } as LinkedOrganizationReducerState, }), - ).toEqual([mockOrganizationState]); + ).toEqual([serializedMockLaoId]); }); it('returns empty array if the linked organization is not in the store', () => { diff --git a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx index ea4e268899..0e7fed6c49 100644 --- a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx +++ b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx @@ -6,12 +6,16 @@ import { useSelector } from 'react-redux'; import { PoPIcon } from 'core/components'; import ScreenWrapper from 'core/components/ScreenWrapper'; +import { catchup, subscribeToChannel } from 'core/network'; +import { channelFromIds, Hash } from 'core/objects'; import { dispatch } from 'core/redux'; import { List, Typography } from 'core/styles'; import { FOUR_SECONDS } from 'resources/const'; import STRINGS from 'resources/strings'; +import BroadcastLinkedOrgInfo from '../components/BroadcastLinkedOrgInfo'; import { LinkedOrganizationsHooks } from '../hooks'; +import { LinkedOrganization } from '../objects/LinkedOrganization'; import { makeChallengeReceveidSelector, removeReceivedChallenge } from '../reducer'; import { addLinkedOrganization, @@ -19,10 +23,6 @@ import { makeScannedLinkedOrganizationSelector, removeScannedLinkedOrganization, } from '../reducer/LinkedOrganizationsReducer'; -import { LinkedOrganization } from '../objects/LinkedOrganization'; -import { Hash } from 'core/objects'; -import BroadcastLinkedOrgInfo from '../components/BroadcastLinkedOrgInfo'; -import { catchup, subscribeToChannel } from 'core/network'; const styles = StyleSheet.create({ flexibleView: { @@ -30,13 +30,12 @@ const styles = StyleSheet.create({ } as ViewStyle, }); - const LinkedOrganizationsScreen = () => { const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); const toast = useToast(); const isOrganizer = LinkedOrganizationsHooks.useIsLaoOrganizer(laoId); const linkedOrganizationSelector = useMemo(() => makeLinkedOrganizationSelector(laoId), [laoId]); - const linkedOrganizationStates = useSelector(linkedOrganizationSelector); + const linkedOrganizationIds = useSelector(linkedOrganizationSelector); const recvChallengeSelector = useMemo(() => makeChallengeReceveidSelector(laoId), [laoId]); const recvChallengeState = useSelector(recvChallengeSelector); @@ -46,21 +45,23 @@ const LinkedOrganizationsScreen = () => { [laoId], ); const scannedLinkedOrgStates = useSelector(scannedLinkedOrgSelector); - const [linkedLaoId, setLinkedLaoId] = useState(null); - + const [linkedLaoId, setLinkedLaoId] = useState(null); useEffect(() => { - const fetchData = async (linkedLaoId: Hash) => { - await subscribeToChannel(linkedLaoId, dispatch, '/root/' + linkedLaoId.valueOf()); - await catchup('/root/' + linkedLaoId.valueOf()); - await new Promise(f => setTimeout(f, 1000)); + const fetchData = async (linkedOrgId: Hash) => { + const channel = channelFromIds(linkedOrgId); + await subscribeToChannel(linkedOrgId, dispatch, channel); + await catchup(channel); + // sometimes there are erros without the extra waiting time - temporary fix + await new Promise((f) => setTimeout(f, 1000)); setLinkedLaoId(linkedLaoId); }; if ( recvChallengeState && scannedLinkedOrgStates && recvChallengeState.length !== 0 && - scannedLinkedOrgStates.length !== 0 + scannedLinkedOrgStates.length !== 0 && + isOrganizer ) { try { for (const [challenge, publicKey] of recvChallengeState) { @@ -92,7 +93,7 @@ const LinkedOrganizationsScreen = () => { console.log(e); } } - }, [recvChallengeState, laoId, toast, scannedLinkedOrgStates]); + }, [recvChallengeState, laoId, linkedLaoId, toast, scannedLinkedOrgStates, isOrganizer]); return ( @@ -103,18 +104,18 @@ const LinkedOrganizationsScreen = () => { : STRINGS.linked_organizations_description} - {linkedOrganizationStates.map((linkedOrgState) => ( - + {linkedOrganizationIds.map((id) => ( + - {STRINGS.linked_organizations_LaoID} {linkedOrgState.lao_id.valueOf()} + {STRINGS.linked_organizations_LaoID} {id} ))} - {linkedLaoId && } + {linkedLaoId && isOrganizer && } ); diff --git a/fe1-web/src/features/witness/network/messages/WitnessRegistry.ts b/fe1-web/src/features/witness/network/messages/WitnessRegistry.ts index c26abb7ee0..0e4433ca45 100644 --- a/fe1-web/src/features/witness/network/messages/WitnessRegistry.ts +++ b/fe1-web/src/features/witness/network/messages/WitnessRegistry.ts @@ -22,6 +22,7 @@ const { FEDERATION_INIT, FEDERATION_EXPECT, FEDERATION_RESULT, + TOKENS_EXCHANGE, } = ActionType; export enum WitnessingType { @@ -78,6 +79,7 @@ const WITNESSING_TYPE_MAP = new Map([ [k(FEDERATION, FEDERATION_INIT), { type: WitnessingType.NO_WITNESSING }], [k(FEDERATION, FEDERATION_EXPECT), { type: WitnessingType.NO_WITNESSING }], [k(FEDERATION, FEDERATION_RESULT), { type: WitnessingType.NO_WITNESSING }], + [k(FEDERATION, TOKENS_EXCHANGE), { type: WitnessingType.NO_WITNESSING }], ]); const getWitnessRegistryEntry = (data: MessageData): WitnessEntry | undefined => { diff --git a/protocol/examples/answer/rumor_state_ans.json b/protocol/examples/answer/rumor_state_ans.json index d00ea14e9e..14a93d7353 100644 --- a/protocol/examples/answer/rumor_state_ans.json +++ b/protocol/examples/answer/rumor_state_ans.json @@ -5,6 +5,12 @@ { "sender_id": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", "rumor_id": 1, + "timestamp" : { + "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=": 1, + "RZOPi59Iy5gkpS2mkpfQJNl44HKc2jVbF0iTGm0RvfU=": 5, + "CfG2ByLhtLJH--T2BL9hZ6eGm11tpkE-5KuvysSCY0I=": 1, + "r8cG9HyJ1FGBke_5IblCdH19mvy39MvLFSArVmY3FpY=": 9 + }, "messages": { "/root/nLghr9_P406lfkMjaNWqyohLxOiGlQee8zad4qAfj18=/social/8qlv4aUT5-tBodKp4RszY284CFYVaoDZK6XKiw9isSw=": [ { @@ -20,6 +26,12 @@ { "sender_id": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", "rumor_id": 2, + "timestamp" : { + "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=": 2, + "RZOPi59Iy5gkpS2mkpfQJNl44HKc2jVbF0iTGm0RvfU=": 5, + "CfG2ByLhtLJH--T2BL9hZ6eGm11tpkE-5KuvysSCY0I=": 1, + "r8cG9HyJ1FGBke_5IblCdH19mvy39MvLFSArVmY3FpY=": 10 + }, "messages": { "/root/nLghr9_P406lfkMjaNWqyohLxOiGlQee8zad4qAfj18=/HnXDyvSSron676Icmvcjk5zXvGLkPJ1fVOaWOxItzBE=": [ { diff --git a/protocol/examples/messageData/federation_tokens_exchange/federation_tokens_exchange.json b/protocol/examples/messageData/federation_tokens_exchange/federation_tokens_exchange.json new file mode 100644 index 0000000000..4ea713bf4e --- /dev/null +++ b/protocol/examples/messageData/federation_tokens_exchange/federation_tokens_exchange.json @@ -0,0 +1,8 @@ +{ + "object": "federation", + "action": "tokens_exchange", + "lao_id": "fzJSZjKf-2cbXH7kds9H8NORuuFIRLkevJlN7qQemjo=", + "roll_call_id": "fEvAfdtNrykd9NPYl9ReHLX-6IP6SFLKTZJLeGUHZ_U=" , + "tokens": ["M5ZychEi5rwm22FjwjNuljL1qMJWD2sE7oX9fcHNMDU="], + "timestamp": 1712854874 +} \ No newline at end of file diff --git a/protocol/examples/query/rumor/rumor.json b/protocol/examples/query/rumor/rumor.json index bb8744fce7..1a2075f108 100644 --- a/protocol/examples/query/rumor/rumor.json +++ b/protocol/examples/query/rumor/rumor.json @@ -5,6 +5,12 @@ "params": { "sender_id": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", "rumor_id": 1, + "timestamp" : { + "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=": 1, + "RZOPi59Iy5gkpS2mkpfQJNl44HKc2jVbF0iTGm0RvfU=": 5, + "CfG2ByLhtLJH--T2BL9hZ6eGm11tpkE-5KuvysSCY0I=": 1, + "r8cG9HyJ1FGBke_5IblCdH19mvy39MvLFSArVmY3FpY=": 10 + }, "messages": { "/root/nLghr9_P406lfkMjaNWqyohLxOiGlQee8zad4qAfj18=/social/8qlv4aUT5-tBodKp4RszY284CFYVaoDZK6XKiw9isSw=": [ { diff --git a/protocol/examples/query/rumor/wrong_rumor_additional_params.json b/protocol/examples/query/rumor/wrong_rumor_additional_params.json index abae1290f8..893f1fddc6 100644 --- a/protocol/examples/query/rumor/wrong_rumor_additional_params.json +++ b/protocol/examples/query/rumor/wrong_rumor_additional_params.json @@ -5,6 +5,12 @@ "params": { "sender_id": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", "rumor_id": 1, + "timestamp" : { + "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=": 1, + "RZOPi59Iy5gkpS2mkpfQJNl44HKc2jVbF0iTGm0RvfU=": 5, + "CfG2ByLhtLJH--T2BL9hZ6eGm11tpkE-5KuvysSCY0I=": 1, + "r8cG9HyJ1FGBke_5IblCdH19mvy39MvLFSArVmY3FpY=": 10 + }, "messages": { "/root/nLghr9_P406lfkMjaNWqyohLxOiGlQee8zad4qAfj18=/social/8qlv4aUT5-tBodKp4RszY284CFYVaoDZK6XKiw9isSw=": [ { diff --git a/protocol/examples/query/rumor/wrong_rumor_missing_channel.json b/protocol/examples/query/rumor/wrong_rumor_missing_channel.json index 5aff3ebfda..9ee547ff6b 100644 --- a/protocol/examples/query/rumor/wrong_rumor_missing_channel.json +++ b/protocol/examples/query/rumor/wrong_rumor_missing_channel.json @@ -5,6 +5,12 @@ "params": { "sender_id": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", "rumor_id": 1, + "timestamp" : { + "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=": 1, + "RZOPi59Iy5gkpS2mkpfQJNl44HKc2jVbF0iTGm0RvfU=": 5, + "CfG2ByLhtLJH--T2BL9hZ6eGm11tpkE-5KuvysSCY0I=": 1, + "r8cG9HyJ1FGBke_5IblCdH19mvy39MvLFSArVmY3FpY=": 10 + }, "messages": { "": [ { diff --git a/protocol/examples/query/rumor/wrong_rumor_missing_messages.json b/protocol/examples/query/rumor/wrong_rumor_missing_messages.json index 5eec6bf0c9..e1f0c7025d 100644 --- a/protocol/examples/query/rumor/wrong_rumor_missing_messages.json +++ b/protocol/examples/query/rumor/wrong_rumor_missing_messages.json @@ -4,6 +4,12 @@ "method": "rumor", "params": { "sender_id": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", - "rumor_id": 1 + "rumor_id": 1, + "timestamp" : { + "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=": 1, + "RZOPi59Iy5gkpS2mkpfQJNl44HKc2jVbF0iTGm0RvfU=": 5, + "CfG2ByLhtLJH--T2BL9hZ6eGm11tpkE-5KuvysSCY0I=": 1, + "r8cG9HyJ1FGBke_5IblCdH19mvy39MvLFSArVmY3FpY=": 10 + } } } diff --git a/protocol/examples/query/rumor/wrong_rumor_missing_rumor_id.json b/protocol/examples/query/rumor/wrong_rumor_missing_rumor_id.json index 3085736401..60d03d8657 100644 --- a/protocol/examples/query/rumor/wrong_rumor_missing_rumor_id.json +++ b/protocol/examples/query/rumor/wrong_rumor_missing_rumor_id.json @@ -4,6 +4,12 @@ "method": "rumor", "params": { "sender_id": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", + "timestamp" : { + "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=": 1, + "RZOPi59Iy5gkpS2mkpfQJNl44HKc2jVbF0iTGm0RvfU=": 5, + "CfG2ByLhtLJH--T2BL9hZ6eGm11tpkE-5KuvysSCY0I=": 1, + "r8cG9HyJ1FGBke_5IblCdH19mvy39MvLFSArVmY3FpY=": 10 + }, "messages": { "/root/nLghr9_P406lfkMjaNWqyohLxOiGlQee8zad4qAfj18=/social/8qlv4aUT5-tBodKp4RszY284CFYVaoDZK6XKiw9isSw=": [ { diff --git a/protocol/examples/query/rumor/wrong_rumor_missing_sender_id.json b/protocol/examples/query/rumor/wrong_rumor_missing_sender_id.json index 0fa68d0e9d..fa0e6e76ee 100644 --- a/protocol/examples/query/rumor/wrong_rumor_missing_sender_id.json +++ b/protocol/examples/query/rumor/wrong_rumor_missing_sender_id.json @@ -4,6 +4,12 @@ "method": "rumor", "params": { "rumor_id": 1, + "timestamp" : { + "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=": 1, + "RZOPi59Iy5gkpS2mkpfQJNl44HKc2jVbF0iTGm0RvfU=": 5, + "CfG2ByLhtLJH--T2BL9hZ6eGm11tpkE-5KuvysSCY0I=": 1, + "r8cG9HyJ1FGBke_5IblCdH19mvy39MvLFSArVmY3FpY=": 10 + }, "messages": { "/root/nLghr9_P406lfkMjaNWqyohLxOiGlQee8zad4qAfj18=/social/8qlv4aUT5-tBodKp4RszY284CFYVaoDZK6XKiw9isSw=": [ { diff --git a/protocol/examples/query/rumor/wrong_rumor_missing_timestamp.json b/protocol/examples/query/rumor/wrong_rumor_missing_timestamp.json new file mode 100644 index 0000000000..0fa68d0e9d --- /dev/null +++ b/protocol/examples/query/rumor/wrong_rumor_missing_timestamp.json @@ -0,0 +1,35 @@ +{ + "jsonrpc": "2.0", + "id": 4, + "method": "rumor", + "params": { + "rumor_id": 1, + "messages": { + "/root/nLghr9_P406lfkMjaNWqyohLxOiGlQee8zad4qAfj18=/social/8qlv4aUT5-tBodKp4RszY284CFYVaoDZK6XKiw9isSw=": [ + { + "data": "eyJvYmplY3QiOiJyb2xsX2NhbGwiLCJhY3Rpb24iOiJjcmVhdGUiLCJuYW1lIjoiUm9sbCBDYWxsIiwiY3JlYXRpb24iOjE2MzMwMzYxMjAsInByb3Bvc2VkX3N0YXJ0IjoxNjMzMDM2Mzg4LCJwcm9wb3NlZF9lbmQiOjE2MzMwMzk2ODgsImxvY2F0aW9uIjoiRVBGTCIsImlkIjoial9kSmhZYnpubXZNYnVMc0ZNQ2dzYlB5YjJ6Nm1vZ2VtSmFON1NWaHVVTT0ifQ==", + "sender": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", + "signature": "FFqBXhZSaKvBnTvrDNIeEYMpFKI5oIa5SAewquxIBHTTEyTIDnUgmvkwgccV9NrujPwDnRt1f4CIEqzXqhbjCw==", + "message_id": "DCBX48EuNO6q-Sr42ONqsj7opKiNeXyRzrjqTbZ_aMI=", + "witness_signatures": [] + } + ], + "/root/nLghr9_P406lfkMjaNWqyohLxOiGlQee8zad4qAfj18=/HnXDyvSSron676Icmvcjk5zXvGLkPJ1fVOaWOxItzBE=": [ + { + "data": "eyJvYmplY3QiOiJyb2xsX2NhbGwiLCJhY3Rpb24iOiJjcmVhdGUiLCJuYW1lIjoiUm9sbCBDYWxsIiwiY3JlYXRpb24iOjE2MzMwMzYxMjAsInByb3Bvc2VkX3N0YXJ0IjoxNjMzMDM2Mzg4LCJwcm9wb3NlZF9lbmQiOjE2MzMwMzk2ODgsImxvY2F0aW9uIjoiRVBGTCIsImlkIjoial9kSmhZYnpubXZNYnVMc0ZNQ2dzYlB5YjJ6Nm1vZ2VtSmFON1NWaHVVTT0ifQ==", + "sender": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", + "signature": "FFqBXhZSaKvBnTvrDNIeEYMpFKI5oIa5SAewquxIBHTTEyTIDnUgmvkwgccV9NrujPwDnRt1f4CIEqzXqhbjCw==", + "message_id": "z6SbjJ0Hw36k8L09-GVRq4PNmi06yQX4e8aZRSbUDwc=", + "witness_signatures": [] + }, + { + "data": "eyJvYmplY3QiOiJyb2xsX2NhbGwiLCJhY3Rpb24iOiJjcmVhdGUiLCJuYW1lIjoiUm9sbCBDYWxsIiwiY3JlYXRpb24iOjE2MzMwMzYxMjAsInByb3Bvc2VkX3N0YXJ0IjoxNjMzMDM2Mzg4LCJwcm9wb3NlZF9lbmQiOjE2MzMwMzk2ODgsImxvY2F0aW9uIjoiRVBGTCIsImlkIjoial9kSmhZYnpubXZNYnVMc0ZNQ2dzYlB5YjJ6Nm1vZ2VtSmFON1NWaHVVTT0ifQ==", + "sender": "J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=", + "signature": "FFqBXhZSaKvBnTvrDNIeEYMpFKI5oIa5SAewquxIBHTTEyTIDnUgmvkwgccV9NrujPwDnRt1f4CIEqzXqhbjCw==", + "message_id": "txbTmVMwCDkZdoaAiEYfAKozVizZzkeMkeOlzq5qMlg=", + "witness_signatures": [] + } + ] + } + } +} diff --git a/protocol/query/method/message/data/data.json b/protocol/query/method/message/data/data.json index 382d5eb1d0..a72b62effa 100644 --- a/protocol/query/method/message/data/data.json +++ b/protocol/query/method/message/data/data.json @@ -67,6 +67,9 @@ { "$ref": "dataFederationResult.json" }, + { + "$ref": "dataFederationTokensExchange.json" + }, { "$ref": "dataWitnessMessage.json" }, diff --git a/protocol/query/method/message/data/dataFederationTokensExchange.json b/protocol/query/method/message/data/dataFederationTokensExchange.json new file mode 100644 index 0000000000..cfb4d7b90e --- /dev/null +++ b/protocol/query/method/message/data/dataFederationTokensExchange.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/dedis/popstellar/master/protocol/query/method/message/data/dataFederationTokensExchange.json", + "description": "Sent by an organizer client to its server, to broadcast the Pop tokens", + "type": "object", + "properties": { + "object": { + "const": "federation" + }, + "action": { + "const": "tokens_exchange" + }, + "lao_id": { + "type": "string", + "contentEncoding": "base64", + "$comment": "Hash : HashLen(organizer, creation, name)" + }, + "roll_call_id": { + "type": "string", + "contentEncoding": "base64", + "$comment": "last roll call id" + }, + "tokens": { + "description": "[Array[Base64String]] list of Pop tokens", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "contentEncoding": "base64" + }, + "$comment": "List must be sorted according to byte encoding: -,0...9,A...Z,_,a...z" + }, + "timestamp": { + "type": "integer", + "description": "[Timestamp] of the tokens' exchange", + "minimum": 0 + } + }, + "additionalProperties": false, + "required": [ + "object", + "action", + "lao_id", + "roll_call_id", + "tokens", + "timestamp" + ] +} diff --git a/protocol/query/method/object/rumor.json b/protocol/query/method/object/rumor.json index 5f6b38a148..0f7c5cec9a 100644 --- a/protocol/query/method/object/rumor.json +++ b/protocol/query/method/object/rumor.json @@ -15,6 +15,10 @@ "description": "[Integer] ID of the rumor", "type": "integer" }, + "timestamp" : { + "description": "Rumor state in which this message has been sent", + "$ref": "./rumor_state.json" + }, "messages": { "description": "Key-value of channels and messages per channel", "type": "object", @@ -24,6 +28,7 @@ "required": [ "sender_id", "rumor_id", - "messages" + "messages", + "timestamp" ] } \ No newline at end of file diff --git a/protocol/test/main.js b/protocol/test/main.js index 12fbbe16a9..ab5cf35ac3 100644 --- a/protocol/test/main.js +++ b/protocol/test/main.js @@ -49,6 +49,7 @@ const message_data_federation_expect_schema = require("../query/method/message/d const message_data_federation_challenge_request_schema = require("../query/method/message/data/dataFederationChallengeRequest.json") const message_data_federation_challenge_schema = require("../query/method/message/data/dataFederationChallenge.json") const message_data_federation_result_schema = require("../query/method/message/data/dataFederationResult.json") +const message_data_federation_tokens_exchange_schema = require("../query/method/message/data/dataFederationTokensExchange.json") const message_data_chirp_add_schema = require("../query/method/message/data/dataAddChirp.json"); const message_data_chirp_notify_add_schema = require("../query/method/message/data/dataNotifyAddChirp.json"); @@ -124,6 +125,7 @@ ajv.addSchema([ message_data_federation_challenge_request_schema, message_data_federation_challenge_schema, message_data_federation_result_schema, + message_data_federation_tokens_exchange_schema, message_data_chirp_notify_add_schema, message_data_chirp_add_schema, diff --git a/protocol/test/main.test.js b/protocol/test/main.test.js index 0d4cc46c3d..df64e6cd42 100644 --- a/protocol/test/main.test.js +++ b/protocol/test/main.test.js @@ -270,6 +270,9 @@ test("message data: federation", () => { federation_result = require("../examples/messageData/federation_result/federation_result.json"); expect(federation_result).toBeValid(messageDataSchema); + federation_tokens_exchange = require("../examples/messageData/federation_tokens_exchange/federation_tokens_exchange.json"); + expect(federation_tokens_exchange).toBeValid(messageDataSchema); + }); test("message data: chirp", () => { From dd1f4e737400521149e2937e75a90255e5f7290e Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Fri, 28 Jun 2024 14:37:39 +0200 Subject: [PATCH 09/11] fix tsdocs --- .../components/__tests__/AddLinkedOrganizationButton.test.tsx | 2 ++ .../screens/__tests__/LinkedOrganizationsScreen.test.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/fe1-web/src/features/linked-organizations/components/__tests__/AddLinkedOrganizationButton.test.tsx b/fe1-web/src/features/linked-organizations/components/__tests__/AddLinkedOrganizationButton.test.tsx index 75d15452c1..3a5385f561 100644 --- a/fe1-web/src/features/linked-organizations/components/__tests__/AddLinkedOrganizationButton.test.tsx +++ b/fe1-web/src/features/linked-organizations/components/__tests__/AddLinkedOrganizationButton.test.tsx @@ -26,6 +26,8 @@ const contextValue = { useCurrentLaoId: () => mockLaoId, useIsLaoOrganizer: () => false, useCurrentLao: () => mockLaoServerAddress, + getLaoById: () => undefined, + getRollCallById: () => undefined, } as LinkedOrganizationsReactContext, }; diff --git a/fe1-web/src/features/linked-organizations/screens/__tests__/LinkedOrganizationsScreen.test.tsx b/fe1-web/src/features/linked-organizations/screens/__tests__/LinkedOrganizationsScreen.test.tsx index ba3cbb8f9e..12fa7f4103 100644 --- a/fe1-web/src/features/linked-organizations/screens/__tests__/LinkedOrganizationsScreen.test.tsx +++ b/fe1-web/src/features/linked-organizations/screens/__tests__/LinkedOrganizationsScreen.test.tsx @@ -21,6 +21,8 @@ const mockLinkedOrganizationsContextValue = (isOrganizer: boolean) => ({ useConnectedToLao: () => true, useIsLaoOrganizer: () => isOrganizer, useCurrentLao: () => mockLao, + getLaoById: () => undefined, + getRollCallById: () => undefined, } as LinkedOrganizationsReactContext, }); From b49e5d6ef227af2d92759082c53c2a73a9b6b4d7 Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Fri, 28 Jun 2024 15:45:11 +0200 Subject: [PATCH 10/11] fix bug --- .../screens/LinkedOrganizationsScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx index 0e7fed6c49..f7eecaafaf 100644 --- a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx +++ b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx @@ -54,7 +54,7 @@ const LinkedOrganizationsScreen = () => { await catchup(channel); // sometimes there are erros without the extra waiting time - temporary fix await new Promise((f) => setTimeout(f, 1000)); - setLinkedLaoId(linkedLaoId); + setLinkedLaoId(linkedOrgId); }; if ( recvChallengeState && @@ -115,7 +115,7 @@ const LinkedOrganizationsScreen = () => { ))} - {linkedLaoId && isOrganizer && } + {linkedLaoId && } ); From 6cb8579f4b4606d970665451ab3ba8cf33bbbb92 Mon Sep 17 00:00:00 2001 From: ljankoschek Date: Fri, 28 Jun 2024 22:25:10 +0200 Subject: [PATCH 11/11] add tests --- .../__tests__/LinkedOrgHandler.test.ts | 68 ++++++++++++++++++- .../__tests__/LinkedOrgMessageApi.test.ts | 34 ++++++++++ .../reducer/LinkedOrganizationsReducer.ts | 5 +- 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts index 12ff2a9e15..3a42ec166f 100644 --- a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts +++ b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts @@ -1,7 +1,13 @@ import 'jest-extended'; import '__tests__/utils/matchers'; -import { mockAddress, mockKeyPair, mockLaoId, mockLaoServerAddress } from '__tests__/utils'; +import { + mockAddress, + mockKeyPair, + mockLaoId, + mockLaoId2, + mockLaoServerAddress, +} from '__tests__/utils'; import { ActionType, Message, ObjectType, ProcessableMessage } from 'core/network/jsonrpc/messages'; import { Base64UrlData, @@ -14,12 +20,14 @@ import { import { dispatch } from 'core/redux'; import { Challenge } from 'features/linked-organizations/objects/Challenge'; import { setChallenge } from 'features/linked-organizations/reducer'; +import { mockRollCall } from 'features/rollCall/__tests__/utils'; import { handleChallengeMessage, handleChallengeRequestMessage, handleFederationExpectMessage, handleFederationInitMessage, + handleTokensExchangeMessage, } from '../LinkedOrgHandler'; import { ChallengeRequest, @@ -28,6 +36,7 @@ import { FederationInit, FederationResult, } from '../messages'; +import { TokensExchange } from '../messages/TokensExchange'; jest.mock('core/network/jsonrpc/messages/Message', () => { return { @@ -87,6 +96,13 @@ const mockMessageData = { witness_signatures: [], }; +const mockTokensExchange = new TokensExchange({ + lao_id: mockLaoId2, + roll_call_id: mockRollCall.id, + tokens: mockRollCall.attendees, + timestamp: TIMESTAMP, +}); + const getCurrentLaoId = () => mockLaoId; jest.mock('core/redux', () => { @@ -380,3 +396,53 @@ describe('handleFederationResultMessage', () => { ).toBeFalse(); }); }); + +describe('handleTokensExchangeMessage', () => { + it('should return false if the object type is wrong', () => { + expect( + handleTokensExchangeMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.MEETING, + action: ActionType.TOKENS_EXCHANGE, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if the action type is wrong', () => { + expect( + handleTokensExchangeMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.ADD, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if there is an issue with the message data', () => { + expect( + handleTokensExchangeMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.TOKENS_EXCHANGE, + lao_id: undefined as unknown as Hash, + roll_call_id: undefined as unknown as Hash, + tokens: undefined as unknown as PublicKey[], + timestamp: undefined as unknown as Timestamp, + } as TokensExchange, + }), + ).toBeFalse(); + }); + it('should return false if there is both a public key and a reason in the message data', () => { + expect( + handleTokensExchangeMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: mockTokensExchange, + }), + ).toBeTrue(); + }); +}); diff --git a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts index 9385dfd217..276f8bac4d 100644 --- a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts +++ b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts @@ -14,11 +14,13 @@ import { publish as mockPublish } from 'core/network/JsonRpcApi'; import { Hash, Timestamp } from 'core/objects'; import { OpenedLaoStore } from 'features/lao/store'; import { Challenge, ChallengeState } from 'features/linked-organizations/objects/Challenge'; +import { mockRollCall } from 'features/rollCall/__tests__/utils'; import * as msApi from '../LinkedOrgMessageApi'; import { ChallengeRequest } from '../messages'; import { FederationExpect } from '../messages/FederationExpect'; import { FederationInit } from '../messages/FederationInit'; +import { TokensExchange } from '../messages/TokensExchange'; jest.mock('core/network/JsonRpcApi', () => { return { @@ -67,6 +69,18 @@ const checkDataFederationExpect = (obj: MessageData) => { expect(data.challenge).toBeInstanceOf(Message); }; +const checkDataTokensExchange = (obj: MessageData) => { + expect(obj.object).toBe(ObjectType.FEDERATION); + expect(obj.action).toBe(ActionType.TOKENS_EXCHANGE); + + const data: TokensExchange = obj as TokensExchange; + expect(data).toBeObject(); + expect(data.lao_id).toBeBase64Url(); + expect(data.roll_call_id).toBeBase64Url(); + expect(data.tokens).toBeArray(); + expect(data.timestamp).toBeNumberObject(); +}; + beforeAll(configureTestFeatures); beforeEach(() => { @@ -123,4 +137,24 @@ describe('LinkedOrgMessageApi', () => { expect(channel).toBe(`/root/${mockLaoId2.valueOf()}/federation`); checkDataFederationExpect(msgData); }); + + it('should create the correct request for tokenExchange', async () => { + const mockTokensExchange = new TokensExchange({ + lao_id: mockLaoId2, + roll_call_id: mockRollCall.id, + tokens: mockRollCall.attendees, + timestamp: VALID_TIMESTAMP, + }); + await msApi.tokensExchange( + mockLaoId, + mockTokensExchange.lao_id, + mockTokensExchange.roll_call_id, + mockTokensExchange.tokens, + ); + + expect(publishMock).toBeCalledTimes(1); + const [channel, msgData] = publishMock.mock.calls[0]; + expect(channel).toBe(`/root/${mockLaoId.valueOf()}/federation`); + checkDataTokensExchange(msgData); + }); }); diff --git a/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts b/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts index f9dfce44c3..502116e754 100644 --- a/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts +++ b/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts @@ -87,7 +87,10 @@ const linkedOrganizationSlice = createSlice({ }; } - if (!state.byLaoId[laoId].allLaoIds.includes(linkedLaoId.valueOf())) { + if ( + !state.byLaoId[laoId].allLaoIds.includes(linkedLaoId.valueOf()) && + linkedLaoId.valueOf() !== laoId.valueOf() + ) { state.byLaoId[laoId].allLaoIds.push(linkedLaoId); } },