Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Work fe1 ljankoschek poptoken mnemonic #1918

Merged
merged 6 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 @@ -94,6 +94,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
Loading