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

Feature: search ups by name #10

Merged
merged 9 commits into from
Dec 7, 2023
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
3 changes: 3 additions & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,9 @@ const schema = yup
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
NEXT_PUBLIC_UNIVERSAL_PROFILES_API_URL: yup.string(),
NEXT_PUBLIC_ALGOLIA_APP_ID: yup.string(),
NEXT_PUBLIC_ALGOLIA_API_KEY: yup.string(),
NEXT_PUBLIC_ALGOLIA_INDEX_NAME: yup.string(),

// Misc
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
Expand Down
13 changes: 8 additions & 5 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,14 @@ Settings for meta tags and OG tags

#### Address views

| Variable | Type | Description | Compulsoriness | Default value | Example value |
|---------------------------------------------|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|------------------------------------|
| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie" \| "universal_profile\|[identicon]"` | Type of identicon displayed next to addresses. In case of universal_profile you can provide a second identicon type that will be displayed when no universal profile is found | Style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar), [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) and [LUKSO Universal Profile](https://universalprofile.cloud/) | - | `jazzicon` | `gradient_avatar` |
| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array<AddressViewId>` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` |
| NEXT_PUBLIC_UNIVERSAL_PROFILES_API_URL | `string` | LUKSO UNIVERSAL PROFILE API URL used for getting various universal profile data | - | - | `https://api.universalprofile.com` |
| Variable | Type | Description | Compulsoriness | Default value | Example value |
|------------------------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|------------------------------------|
| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie" \| "universal_profile\|[identicon]"` | Type of identicon displayed next to addresses. In case of universal_profile you can provide a second identicon type that will be displayed when no universal profile is found | Style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar), [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) and [LUKSO Universal Profile](https://universalprofile.cloud/) | - | `jazzicon` | `gradient_avatar` |
| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array<AddressViewId>` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` |
| NEXT_PUBLIC_UNIVERSAL_PROFILES_API_URL | `string` | LUKSO UNIVERSAL PROFILE API URL used for getting various universal profile data | - | - | `https://api.universalprofile.com` |
| NEXT_PUBLIC_ALGOLIA_APP_ID | `string` | Algolia App's ID | - | - | `ABCD1234` |
| NEXT_PUBLIC_ALGOLIA_API_KEY | `string` | Algolia API key used for authenticating requests | - | - | `abcd1234defg5678` |
| NEXT_PUBLIC_ALGOLIA_INDEX_NAME | `string` | Index name from where data should be requested | - | - | `prod_mainnet_data` |

##### Address views list
| Id | Description |
Expand Down
11 changes: 11 additions & 0 deletions lib/api/buildUniversalProfileUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import algoliasearch from 'algoliasearch';

import { getEnvValue } from '../../configs/app/utils';

const algolia = {
appId: getEnvValue('NEXT_PUBLIC_ALGOLIA_APP_ID') || '',
apiKey: getEnvValue('NEXT_PUBLIC_ALGOLIA_API_KEY') || '',
index: getEnvValue('NEXT_PUBLIC_ALGOLIA_INDEX_NAME') || '',
};

export const algoliaIndex = algoliasearch(algolia.appId, algolia.apiKey).initIndex(algolia.index);
6 changes: 6 additions & 0 deletions lib/api/isUniversalProfileEnabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { getEnvValue } from '../../configs/app/utils';

export const isUniversalProfileEnabled = () => {
const env = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE');
return env === undefined ? false : env.includes('universal_profile');
};
4 changes: 4 additions & 0 deletions lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@ export const RESOURCES = {
api_v2_key: {
path: '/api/v2/key',
},
universal_profile: {
path: '',
},

// API V1
csv_export_txs: {
Expand Down Expand Up @@ -635,6 +638,7 @@ Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
Q extends 'universal_profile' ? Array<SearchResultItem> :
Q extends 'token' ? TokenInfo :
Q extends 'token_verified_info' ? TokenVerifiedInfo :
Q extends 'token_counters' ? TokenCounters :
Expand Down
35 changes: 35 additions & 0 deletions lib/api/useUniversalProfileApiFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';

import type { SearchResultAddressOrContractOrUniversalProfile } from '../../types/api/search';
import type { UniversalProfileProxyResponse } from '../../types/api/universalProfile';

import type { Params as FetchParams } from 'lib/hooks/useFetch';

import { algoliaIndex } from './buildUniversalProfileUrl';
import type { ResourceName, ResourcePathParams } from './resources';

export interface Params<R extends ResourceName> {
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
}

export default function useUniversalProfileApiFetch() {
return React.useCallback(async(queryParams: string,
) => {
try {
const { hits } = await algoliaIndex.search(queryParams);
return hits.map<SearchResultAddressOrContractOrUniversalProfile>((hit) => {
const hitAsUp = hit as unknown as UniversalProfileProxyResponse;
return {
type: 'universal_profile',
name: hitAsUp.hasProfileName ? hitAsUp.LSP3Profile.name : null,
address: hit.objectID,
is_smart_contract_verified: false,
};
});
} catch (error) {
return error;
}
}, []);
}
21 changes: 21 additions & 0 deletions lib/api/useUniversalProfileQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useQuery } from '@tanstack/react-query';

import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params } from './useApiQuery';
import { getResourceKey } from './useApiQuery';
import useUniversalProfileApiFetch from './useUniversalProfileApiFetch';

export default function useUniversalProfileQuery<R extends ResourceName, E = unknown>(
resource: R,
{ queryOptions, pathParams, queryParams }: Params<R, E> = {},
) {
const upFetch = useUniversalProfileApiFetch();
return useQuery<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: getResourceKey(resource, { pathParams, queryParams }),
queryFn: async() => {
return await upFetch(queryParams?.q as string) as Promise<ResourcePayload<R>>;
},
...queryOptions,
});
}
13 changes: 10 additions & 3 deletions mocks/search/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { SearchResultToken, SearchResultBlock, SearchResultAddressOrContract, SearchResultTx, SearchResultLabel, SearchResult } from 'types/api/search';
import type {
SearchResultToken,
SearchResultBlock,
SearchResultAddressOrContractOrUniversalProfile,
SearchResultTx,
SearchResultLabel,
SearchResult,
} from 'types/api/search';

export const token1: SearchResultToken = {
address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
Expand Down Expand Up @@ -47,15 +54,15 @@ export const block2: SearchResultBlock = {
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2',
};

export const address1: SearchResultAddressOrContract = {
export const address1: SearchResultAddressOrContractOrUniversalProfile = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: null,
type: 'address' as const,
is_smart_contract_verified: false,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};

export const contract1: SearchResultAddressOrContract = {
export const contract1: SearchResultAddressOrContractOrUniversalProfile = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'Unknown contract in this network',
type: 'contract' as const,
Expand Down
2 changes: 2 additions & 0 deletions nextjs/csp/policies/universalprofile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export function universalProfile(): CspDev.DirectiveDescriptor {
return {
'connect-src': [
'api.universalprofile.cloud',
'*.algolianet.com',
'*.algolia.net',
],
};
}
11 changes: 8 additions & 3 deletions types/api/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export interface SearchResultToken {
is_smart_contract_verified: boolean;
}

export interface SearchResultAddressOrContract {
type: 'address' | 'contract';
export interface SearchResultAddressOrContractOrUniversalProfile {
type: 'address' | 'contract' | 'universal_profile';
name: string | null;
address: string;
is_smart_contract_verified: boolean;
Expand Down Expand Up @@ -49,7 +49,12 @@ export interface SearchResultTx {
url?: string; // not used by the frontend, we build the url ourselves
}

export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel;
export type SearchResultItem =
SearchResultToken |
SearchResultAddressOrContractOrUniversalProfile |
SearchResultBlock |
SearchResultTx |
SearchResultLabel

export interface SearchResult {
items: Array<SearchResultItem>;
Expand Down
16 changes: 15 additions & 1 deletion types/api/universalProfile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
export type UPResponse = {
export type UniversalProfileProxyResponse = {
type: string;
hasProfileName: boolean;
hasProfileImage: boolean;
LSP3Profile: {
name: string;
profileImage: {
[key: number]: {
url: string;
};
};
};
}

export type UniversalProfileAlgoliaResponse = {
type: string;
hasProfileName: boolean;
hasProfileImage: boolean;
Expand Down
2 changes: 2 additions & 0 deletions ui/address/contract/ContractSourceCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
) : null;

const copyToClipboard = activeContractData?.length === 1 ?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<CopyToClipboard text={ activeContractData[0].source_code } isLoading={ isLoading } ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}/> :
null;

Expand Down
8 changes: 2 additions & 6 deletions ui/shared/HashStringShorten.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';

import shortenString from 'lib/shortenString';

import { getEnvValue } from '../../configs/app/utils';
import { isUniversalProfileEnabled } from '../../lib/api/isUniversalProfileEnabled';
import shortenUniversalProfile from '../../lib/shortenUniversalProfile';

interface Props {
Expand All @@ -16,11 +16,7 @@ interface Props {
const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span' }: Props) => {
const [ shortenedString, setShortenedString ] = useState(shortenString(hash));
useEffect(() => {
const identiconType = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE');
if (identiconType === undefined) {
return undefined;
}
if (identiconType.includes('universal_profile') && hash.includes(' (')) {
if (isUniversalProfileEnabled() && hash.includes(' (')) {
setShortenedString(shortenUniversalProfile(hash));
}
}, [ hash ]);
Expand Down
13 changes: 4 additions & 9 deletions ui/shared/HashStringShortenDynamic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import useFontFaceObserver from 'use-font-face-observer';

import { BODY_TYPEFACE, HEADING_TYPEFACE } from 'theme/foundations/typography';

import { getEnvValue } from '../../configs/app/utils';
import { isUniversalProfileEnabled } from '../../lib/api/isUniversalProfileEnabled';

const TAIL_LENGTH = 4;
const HEAD_MIN_LENGTH = 4;
Expand Down Expand Up @@ -69,21 +69,16 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled,
}

// if we get #, this means that we got a valid universal profile in format of @name#0x1234 - we can split this data and return username component.
const identiconType = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE');
if (identiconType === undefined) {
return;
}
if (identiconType.includes('universal_profile') && hash.includes(' (')) {
if (isUniversalProfileEnabled() && hash.includes(' (')) {
const upParts = hash.split(' (');
const hashHead = '#' + upParts[1].slice(2, 6); // change (0x1234...5678) -> #1234
const name = upParts[0];
const slicedName = name.slice(0, rightI - 3);
const displayed = rightI - 3 > name.length ? name + hashHead : slicedName + '...' + hashHead;
setDisplayedString(displayed);

return;
} else {
setDisplayedString(hash.slice(0, rightI - 1) + '...' + tail);
}
setDisplayedString(hash.slice(0, rightI - 1) + '...' + tail);
} else {
setDisplayedString(hash);
}
Expand Down
Loading
Loading