Skip to content

Commit

Permalink
Merge pull request #1918 from dedis/work-fe1-ljankoschek-poptoken-mne…
Browse files Browse the repository at this point in the history
…monic

Work fe1 ljankoschek poptoken mnemonic
  • Loading branch information
K1li4nL authored Jun 30, 2024
2 parents 14b1cdd + 489d25e commit 04a581e
Show file tree
Hide file tree
Showing 23 changed files with 580 additions and 53 deletions.
110 changes: 110 additions & 0 deletions fe1-web/src/core/functions/Mnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { randomInt, createHash } from 'crypto';

import base64url from 'base64url';
import * as bip39 from 'bip39';

import { Base64UrlData } from 'core/objects';

/**
* This function computes the hashcode of a buffer according to the
* Java Arrays.hashcode(bytearray) function to get the same results as in fe2.
* @param buf
* @returns computed hash code as a number
*/
function hashCode(buf: Buffer): number {
if (buf === null) {
return 0;
}
let result = 1;
for (let element of buf) {
if (element >= 2 ** 7) {
element -= 2 ** 8;
}
result = 31 * result + (element == null ? 0 : element);
result %= 2 ** 32;
if (result >= 2 ** 31) {
result -= 2 ** 32;
}
if (result < -(2 ** 31)) {
result += 2 ** 32;
}
}
return result;
}

/**
* This function converts a buffer into mnemomic words from the english dictionary.
* @param data buffer containing a base64 string
* @returns array containing the mnemonic words
*/
function generateMnemonic(data: Buffer): string[] {
try {
const digest = createHash('sha256').update(data).digest();
let mnemonic = '';
bip39.setDefaultWordlist('english');
mnemonic = bip39.entropyToMnemonic(digest);
return mnemonic.split(' ').filter((word) => word.length > 0);
} catch (e) {
console.error(
`Error generating the mnemonic for the base64 string ${base64url.encode(data)}`,
e,
);
return [];
}
}

/**
* This function converts a buffer of a base64 string into a given number of mnemonic words.
*
* Disclaimer: there's no guarantee that different base64 inputs map to 2 different words. The
* reason is that the representation space is limited. However, since the amount of messages is
* low is practically improbable to have conflicts
*
* @param data Buffer of a base64 string
* @param numberOfWords number of mnemonic words we want to generate
* @return given number of mnemonic words concatenated with ' '
*/
function generateMnemonicFromBase64(data: Buffer, numberOfWords: number): string {
// Generate the mnemonic words from the input data
const mnemonicWords = generateMnemonic(data);
if (mnemonicWords.length === 0) {
return 'none';
}

let result = '';
const hc = hashCode(data);
for (let i = 0; i < numberOfWords; i += 1) {
const wordIndex = Math.abs(hc + i) % mnemonicWords.length;
result = `${result} ${mnemonicWords[wordIndex]}`;
}

return result.substring(1, result.length);
}

/**
* This function filters all non digits characters and returns the first nbDigits
* @param b64 base64 string containing numbers
* @param nbDigits numbers of digitis to extract from input
* @return string containing all the extracted numbers
*/
function getFirstNumberDigits(b64: string, nbDigits: number): string {
const digits = b64.replace(/\D/g, '');
return digits.slice(0, nbDigits).padStart(nbDigits, '0');
}

/**
* This function generates a unique and memorable username from a base64 string.
*
* @param input base64 string.
* @return a username composed of truncated mnemonic words and a numerical suffix.
*/
export function generateUsernameFromBase64(input: string): string {
const words = generateMnemonicFromBase64(new Base64UrlData(input).toBuffer(), 2).split(' ');
if (words.length < 2) {
return `defaultUsername${randomInt(0, 10000000).toString().padStart(4, '0')}`;
}
const number = getFirstNumberDigits(input, 4);
const word1 = words[0].charAt(0).toUpperCase() + words[0].slice(1);
const word2 = words[1].charAt(0).toUpperCase() + words[1].slice(1);
return `${word1}${word2}${number}`;
}
19 changes: 19 additions & 0 deletions fe1-web/src/core/functions/__tests__/Mnemonic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { generateUsernameFromBase64 } from '../Mnemonic';

test('generateUsernameFromBase64 should generate the correct username for the token: d_xeXEsurEnyWOp04mrrMxC3m4cS-3jK_9_Aw-UYfww=', () => {
const popToken = 'd_xeXEsurEnyWOp04mrrMxC3m4cS-3jK_9_Aw-UYfww=';
const result = 'SpoonIssue0434';
expect(generateUsernameFromBase64(popToken)).toBe(result);
});

test('generateUsernameFromBase64 should generate the correct username for the token: e5YJ5Q6x39u_AIK78_puEE2X5wy7Y7iYZLeuZx1lnZI=', () => {
const popToken = 'e5YJ5Q6x39u_AIK78_puEE2X5wy7Y7iYZLeuZx1lnZI=';
const result = 'BidTrial5563';
expect(generateUsernameFromBase64(popToken)).toBe(result);
});

test('generateUsernameFromBase64 should generate the correct username for the token: Y5ZAd_7Ba31uu4EUIYbG2AVnthR623-NdPyYhtyechE=', () => {
const popToken = 'Y5ZAd_7Ba31uu4EUIYbG2AVnthR623-NdPyYhtyechE=';
const result = 'FigureDevote5731';
expect(generateUsernameFromBase64(popToken)).toBe(result);
});
3 changes: 2 additions & 1 deletion fe1-web/src/features/digital-cash/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mockKeyPair, mockLaoId } from '__tests__/utils';
import { mockKeyPair, mockLao, mockLaoId } from '__tests__/utils';
import { Hash, PopToken, RollCallToken } from 'core/objects';
import { COINBASE_HASH, SCRIPT_TYPE } from 'resources/const';

Expand Down Expand Up @@ -26,6 +26,7 @@ export const mockDigitalCashContextValue = (isOrganizer: boolean) => ({
}),
useRollCallTokensByLaoId: () => [mockRollCallToken],
useRollCallTokenByRollCallId: () => mockRollCallToken,
useCurrentLao: () => mockLao,
} as DigitalCashReactContext,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Modal, View } from 'react-native';
import { ScrollView, TouchableWithoutFeedback } from 'react-native-gesture-handler';

import ModalHeader from 'core/components/ModalHeader';
import { generateUsernameFromBase64 } from 'core/functions/Mnemonic';
import { Hash, RollCallToken } from 'core/objects';
import { List, ModalStyles, Typography } from 'core/styles';
import { COINBASE_HASH } from 'resources/const';
Expand All @@ -24,6 +25,8 @@ const TransactionHistory = ({ laoId, rollCallTokens }: IPropTypes) => {
const [selectedTransaction, setSelectedTransaction] = useState<Transaction | null>(null);
const [showModal, setShowModal] = useState<boolean>(false);

const currentLao = DigitalCashHooks.useCurrentLao();

// If we want to show all transactions, just use DigitalCashHooks.useTransactions(laoId)
const transactions: Transaction[] = DigitalCashHooks.useTransactionsByRollCallTokens(
laoId,
Expand Down Expand Up @@ -135,7 +138,10 @@ const TransactionHistory = ({ laoId, rollCallTokens }: IPropTypes) => {
<ListItem.Title
style={[Typography.base, Typography.code]}
numberOfLines={1}>
{input.script.publicKey.valueOf()}
{input.script.publicKey.valueOf() === currentLao.organizer.valueOf()
? input.script.publicKey.valueOf() +
STRINGS.digital_cash_wallet_transaction_history_organizer
: generateUsernameFromBase64(input.script.publicKey.valueOf())}
</ListItem.Title>
<ListItem.Subtitle>
{input.txOutHash.valueOf() === COINBASE_HASH &&
Expand Down
5 changes: 5 additions & 0 deletions fe1-web/src/features/digital-cash/hooks/DigitalCashHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export namespace DigitalCashHooks {
export const useRollCallsByLaoId = (laoId: Hash) =>
useDigitalCashContext().useRollCallsByLaoId(laoId);

/**
* Gets the current lao , throws an error if there is none
*/
export const useCurrentLao = () => useDigitalCashContext().useCurrentLao();

/**
* Gets the list of all transactions that happened in this LAO
* To use only in a React component
Expand Down
1 change: 1 addition & 0 deletions fe1-web/src/features/digital-cash/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function compose(
useRollCallById: configuration.useRollCallById,
useCurrentLaoId: configuration.useCurrentLaoId,
useConnectedToLao: configuration.useConnectedToLao,
useCurrentLao: configuration.useCurrentLao,
useIsLaoOrganizer: configuration.useIsLaoOrganizer,
useRollCallTokensByLaoId: configuration.useRollCallTokensByLaoId,
useRollCallTokenByRollCallId: configuration.useRollCallTokenByRollCallId,
Expand Down
7 changes: 7 additions & 0 deletions fe1-web/src/features/digital-cash/interface/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ export interface DigitalCashCompositionConfiguration {
* @returns the RollCallToken or undefined if not found in the roll call
*/
useRollCallTokenByRollCallId: (laoId: Hash, rollCallId?: Hash) => RollCallToken | undefined;

/**
* Gets the current lao
* @returns The current lao
*/
useCurrentLao: () => DigitalCashFeature.Lao;
}

/**
Expand All @@ -102,6 +108,7 @@ export type DigitalCashReactContext = Pick<
| 'useCurrentLaoId'
| 'useIsLaoOrganizer'
| 'useConnectedToLao'
| 'useCurrentLao'

/* roll call */
| 'useRollCallById'
Expand Down
1 change: 1 addition & 0 deletions fe1-web/src/features/digital-cash/interface/Feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Hash, PopToken, PublicKey } from 'core/objects';
export namespace DigitalCashFeature {
export interface Lao {
id: Hash;
organizer: PublicKey;
}

export interface LaoScreen extends NavigationDrawerScreen {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CompositeScreenProps, useNavigation, useRoute } from '@react-navigation/core';
import { StackScreenProps } from '@react-navigation/stack';
import * as Clipboard from 'expo-clipboard';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Modal, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native';
import {
ScrollView,
Expand All @@ -21,6 +21,7 @@ import {
} from 'core/components';
import ModalHeader from 'core/components/ModalHeader';
import ScreenWrapper from 'core/components/ScreenWrapper';
import { generateUsernameFromBase64 } from 'core/functions/Mnemonic';
import { KeyPairStore } from 'core/keypair';
import { AppParamList } from 'core/navigation/typing/AppParamList';
import { DigitalCashParamList } from 'core/navigation/typing/DigitalCashParamList';
Expand Down Expand Up @@ -83,19 +84,34 @@ const SendReceive = () => {

const selectedRollCall = DigitalCashHooks.useRollCallById(selectedRollCallId);

const [beneficiary, setBeneficiary] = useState('');
const [beneficiary, setBeneficiaryState] = useState('');
const [beneficiaryFocused, setBeneficiaryFocused] = useState(false);
const [amount, setAmount] = useState('');
const [error, setError] = useState('');

const setBeneficiary = useCallback(
(newBeneficiary: string) => {
if (rollCall?.attendees) {
for (let i = 0; i < rollCall!.attendees!.length; i += 1) {
if (generateUsernameFromBase64(rollCall!.attendees![i].valueOf()) === newBeneficiary) {
setBeneficiaryState(rollCall!.attendees![i].valueOf());
return;
}
}
}
setBeneficiaryState(newBeneficiary);
},
[rollCall],
);

const suggestedBeneficiaries = useMemo(() => {
// do not show any suggestions if no text has been entered
if (!beneficiaryFocused && beneficiary.trim().length === 0) {
return [];
}

return (rollCall?.attendees || [])
.map((key) => key.toString())
.map((key) => generateUsernameFromBase64(key.valueOf()))
.filter((key) => key.startsWith(beneficiary));
}, [beneficiary, beneficiaryFocused, rollCall]);

Expand Down Expand Up @@ -123,7 +139,7 @@ const SendReceive = () => {
if (scannedPoPToken) {
setBeneficiary(scannedPoPToken);
}
}, [scannedPoPToken]);
}, [scannedPoPToken, setBeneficiary]);

const checkBeneficiaryValidity = (): boolean => {
if (!isCoinbase && beneficiary === '') {
Expand Down Expand Up @@ -179,7 +195,6 @@ const SendReceive = () => {
if (!rollCallToken) {
throw new Error('The roll call token is not defined');
}

return requestSendTransaction(
rollCallToken.token,
new PublicKey(beneficiary),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ exports[`SendReceive renders correctly 1`] = `
}
}
>
You can send cash by entering the public key of the beneficiary below and choosing the amount of cash you would like to transfer. To receive money you canshow your PoP token to the sender. To access the QR code of your PoP token, tab the QRcode icon in the top right of this screen.
You can send cash by entering the username of the beneficiary below and choosing the amount of cash you would like to transfer. To receive money you canshow your PoP token to the sender. To access the QR code of your PoP token, tab the QRcode icon in the top right of this screen.
</Text>
<View
style={
Expand Down Expand Up @@ -1038,7 +1038,7 @@ exports[`SendReceive renders correctly with passed scanned pop token 1`] = `
}
}
>
You can send cash by entering the public key of the beneficiary below and choosing the amount of cash you would like to transfer. To receive money you canshow your PoP token to the sender. To access the QR code of your PoP token, tab the QRcode icon in the top right of this screen.
You can send cash by entering the username of the beneficiary below and choosing the amount of cash you would like to transfer. To receive money you canshow your PoP token to the sender. To access the QR code of your PoP token, tab the QRcode icon in the top right of this screen.
</Text>
<View
style={
Expand Down
1 change: 1 addition & 0 deletions fe1-web/src/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function configureFeatures() {
useConnectedToLao: laoConfiguration.hooks.useConnectedToLao,
useIsLaoOrganizer: laoConfiguration.hooks.useIsLaoOrganizer,
getLaoOrganizer: laoConfiguration.functions.getLaoOrganizer,
useCurrentLao: laoConfiguration.hooks.useCurrentLao,
useRollCallById: rollCallConfiguration.hooks.useRollCallById,
useRollCallsByLaoId: rollCallConfiguration.hooks.useRollCallsByLaoId,
useRollCallTokensByLaoId: rollCallConfiguration.hooks.useRollCallTokensByLaoId,
Expand Down
7 changes: 6 additions & 1 deletion fe1-web/src/features/rollCall/components/AttendeeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Color, Icon, List, Typography } from 'core/styles';
import { FOUR_SECONDS } from 'resources/const';
import STRINGS from 'resources/strings';

import { generateUsernameFromBase64 } from '../../../core/functions/Mnemonic';

const styles = StyleSheet.create({
tokenHighlight: {
backgroundColor: Color.lightPopBlue,
Expand Down Expand Up @@ -72,8 +74,11 @@ const AttendeeList = ({ popTokens, personalToken }: IPropTypes) => {
numberOfLines={1}
testID={`attendee_${idx}`}
selectable>
{token.valueOf()}
{generateUsernameFromBase64(token.valueOf())}
</ListItem.Title>
<ListItem.Subtitle style={[Typography.small, Typography.code]} numberOfLines={1}>
{STRINGS.popToken} {token.valueOf()}
</ListItem.Subtitle>
</ListItem.Content>
</ListItem>
);
Expand Down
Loading

0 comments on commit 04a581e

Please sign in to comment.