Skip to content

Commit

Permalink
Merge pull request #23 from ArtemKolodko/value_at_transfer_time
Browse files Browse the repository at this point in the history
Add transaction USD value at time of transfer
  • Loading branch information
DenSmolonski authored Jan 22, 2025
2 parents 555d39a + 382ec47 commit 6ddb901
Show file tree
Hide file tree
Showing 16 changed files with 574 additions and 343 deletions.
132 changes: 132 additions & 0 deletions lib/contexts/tokenPrice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { useEffect, useState } from 'react';

import useFetch from '../hooks/useFetch';
import isBrowser from '../isBrowser';

const TOKEN_PRICE_LS_KEY = 'token_price_cache';
const BinanceRatesUrl = 'https://api.binance.com/api/v3/klines?symbol=ONEUSDT&interval=1d&limit=1000';
const DayMs = 24 * 60 * 60 * 1000;

interface TokenPrice {
price: number;
timestamp: number;
}

interface ProviderProps {
children: React.ReactNode;
}

type TimestampValue = number | string | null
type BinanceKline = [
number,
string,
string,
string,
string,
string,
number,
string,
number,
string,
string,
string
];

interface ITokenPriceContext {
getPriceByTimestamp: (timestamp: TimestampValue) => number | null;
}

const getValuesFromLS = () => {
try {
const data = localStorage.getItem(TOKEN_PRICE_LS_KEY);
if (data) {
return JSON.parse(data) as Array<TokenPrice>;
}
} catch (e) {}
return [];
};

const saveValuesToLs = (values: Array<TokenPrice>) => {
localStorage.setItem(TOKEN_PRICE_LS_KEY, JSON.stringify(values));
};

export const TokenPriceContext = React.createContext<ITokenPriceContext>({
getPriceByTimestamp: () => {
return null;
},
});

export function TokenPriceProvider({ children }: ProviderProps) {
const [ values, setValues ] = useState<Array<TokenPrice>>([]);
const fetch = useFetch();

const browser = isBrowser();

useEffect(() => {
const updateCache = async() => {
try {
const valuesFromLS = getValuesFromLS();
const lastValueFromLS = valuesFromLS[valuesFromLS.length - 1];
// Fetch token prices history from Binance
if (
!lastValueFromLS ||
(Date.now() - lastValueFromLS.timestamp > DayMs)
) {
const result = await fetch<Array<BinanceKline>, unknown>(BinanceRatesUrl);
if (Array.isArray(result)) {
const newValues: Array<TokenPrice> = result.map(([ timestamp, price ]) => {
return {
timestamp,
price: Number(price),
};
});
setValues(newValues);
saveValuesToLs(newValues);
}
} else {
// Restore values from LS
setValues(valuesFromLS);
}
} catch (e) {
setValues([]);
}
};

if (browser) {
updateCache();
}
}, [ browser, fetch ]);

const getPriceByTimestamp = (timestamp: TimestampValue) => {
if (timestamp) {
const unixTimestamp = new Date(timestamp).valueOf();
const item = values.find(item => Math.abs(unixTimestamp - item.timestamp) <= 2 * DayMs);
if (item) {
return item.price;
}
}
return null;
};

return (
<TokenPriceContext.Provider
value={{
getPriceByTimestamp,
}}
>
{ children }
</TokenPriceContext.Provider>
);
}

export function useTokenPrice() {
const context = React.useContext(TokenPriceContext);
if (context === undefined) {
return {
getPriceByTimestamp: () => {
return null;
},
};
}
return context;
}
4 changes: 2 additions & 2 deletions lib/getCurrencyValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
usdBn = valueCurr.times(exchangeRateBn);
if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat(2) : usdBnDp.toFormat(2);
} else {
usdResult = usdBn.toFormat();
usdResult = usdBn.toFormat(2);
}
}

Expand Down
1 change: 1 addition & 0 deletions nextjs/csp/generateCspPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function generateCspPolicy() {
descriptors.safe(),
descriptors.sentry(),
descriptors.walletConnect(),
descriptors.binance(),
);

return makePolicyString(policyDescriptor);
Expand Down
9 changes: 9 additions & 0 deletions nextjs/csp/policies/binance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type CspDev from 'csp-dev';

export function binance(): CspDev.DirectiveDescriptor {
return {
'connect-src': [
'api.binance.com',
],
};
}
1 change: 1 addition & 0 deletions nextjs/csp/policies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { monaco } from './monaco';
export { safe } from './safe';
export { sentry } from './sentry';
export { walletConnect } from './walletConnect';
export { binance } from './binance';
25 changes: 14 additions & 11 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Layout from 'ui/shared/layout/Layout';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';

import 'lib/setLocale';
import { TokenPriceProvider } from '../lib/contexts/tokenPrice';
// import 'focus-visible/dist/focus-visible';

type AppPropsWithLayout = AppProps & {
Expand Down Expand Up @@ -104,17 +105,19 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<Web3ModalProvider>
<AppContextProvider pageProps={ pageProps }>
<AddressFormatProvider>
<QueryClientProvider client={ queryClient }>
<GrowthBookProvider growthbook={ growthBook }>
<ScrollDirectionProvider>
<SocketProvider url={ wsUrl }>
{ getLayout(<Component { ...pageProps }/>) }
</SocketProvider>
</ScrollDirectionProvider>
</GrowthBookProvider>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/>
</QueryClientProvider>
<TokenPriceProvider>
<QueryClientProvider client={ queryClient }>
<GrowthBookProvider growthbook={ growthBook }>
<ScrollDirectionProvider>
<SocketProvider url={ wsUrl }>
{ getLayout(<Component { ...pageProps }/>) }
</SocketProvider>
</ScrollDirectionProvider>
</GrowthBookProvider>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/>
</QueryClientProvider>
</TokenPriceProvider>
</AddressFormatProvider>
</AppContextProvider>
</Web3ModalProvider>
Expand Down
24 changes: 12 additions & 12 deletions types/api/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ export type AddressNFT = TokenInstance & {
token: TokenInfo;
token_type: Omit<TokenType, 'ERC-20'>;
value: string;
};
}

export type AddressCollection = {
token: TokenInfo;
amount: string;
token_instances: Array<Omit<AddressNFT, 'token'>>;
};
}

export interface AddressTokensResponse {
items: Array<AddressTokenBalance>;
Expand Down Expand Up @@ -116,13 +116,13 @@ export interface AddressStakingTransactionsResponse {
} | null;
}

export const AddressFromToFilterValues = [ 'from', 'to', 'staking_transactions' ] as const;
export const AddressFromToFilterValues = [ 'from', 'to' ] as const;

export type AddressFromToFilter = (typeof AddressFromToFilterValues)[number] | undefined;
export type AddressFromToFilter = typeof AddressFromToFilterValues[number] | undefined;

export type AddressTxsFilters = {
filter: AddressFromToFilter;
};
}

export interface AddressTokenTransferResponse {
items: Array<TokenTransfer>;
Expand All @@ -133,15 +133,15 @@ export type AddressTokenTransferFilters = {
filter?: AddressFromToFilter;
type?: Array<TokenType>;
token?: string;
};
}

export type AddressTokensFilter = {
type: TokenType;
};
}

export type AddressNFTTokensFilter = {
type: Array<NFTTokenType> | undefined;
};
}

export interface AddressCoinBalanceHistoryItem {
block_number: number;
Expand All @@ -162,7 +162,7 @@ export interface AddressCoinBalanceHistoryResponse {
export type AddressCoinBalanceHistoryChart = Array<{
date: string;
value: string;
}>;
}>

export interface AddressBlocksValidatedResponse {
items: Array<Block>;
Expand All @@ -187,15 +187,15 @@ export type AddressWithdrawalsResponse = {
index: number;
items_count: number;
};
};
}

export type AddressWithdrawalsItem = {
amount: string;
block_number: number;
index: number;
timestamp: string;
validator_index: number;
};
}

export type AddressTabsCounters = {
internal_txs_count: number | null;
Expand All @@ -206,4 +206,4 @@ export type AddressTabsCounters = {
staking_transactions_count: number | null;
validations_count: number | null;
withdrawals_count: number | null;
};
}
3 changes: 1 addition & 2 deletions types/api/stakingTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,4 @@ export type StakingTransaction = {
msg_delegator_address: string;
msg_slot_pub_key_to_add: string;
msg_slot_pub_key_to_remove: string;
claimed_reward: string | null;
};
}
48 changes: 15 additions & 33 deletions types/api/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,12 @@ import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction';

export type TransactionRevertReason =
| {
raw: string;
}
| DecodedInput;
export type TransactionRevertReason = {
raw: string;
} | DecodedInput;

type WrappedTransactionFields =
| 'decoded_input'
| 'fee'
| 'gas_limit'
| 'gas_price'
| 'hash'
| 'max_fee_per_gas'
| 'max_priority_fee_per_gas'
| 'method'
| 'nonce'
| 'raw_input'
| 'to'
| 'type'
| 'value';
type WrappedTransactionFields = 'decoded_input' | 'fee' | 'gas_limit' | 'gas_price' | 'hash' | 'max_fee_per_gas' |
'max_priority_fee_per_gas' | 'method' | 'nonce' | 'raw_input' | 'to' | 'type' | 'value';

export interface OpWithdrawal {
l1_transaction_hash: string;
Expand Down Expand Up @@ -92,7 +78,7 @@ export type Transaction = {
// zkEvm fields
zkevm_verify_hash?: string;
zkevm_batch_number?: number;
zkevm_status?: (typeof ZKEVM_L2_TX_STATUSES)[number];
zkevm_status?: typeof ZKEVM_L2_TX_STATUSES[number];
zkevm_sequence_hash?: string;
// blob tx fields
blob_versioned_hashes?: Array<string>;
Expand All @@ -102,10 +88,7 @@ export type Transaction = {
max_fee_per_blob_gas?: string;
shard_id?: string;
to_shard_id?: string;
claimed_reward?: string;
delegated_amount?: string;
undelegated_amount?: string;
};
}

export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ];

Expand Down Expand Up @@ -148,15 +131,14 @@ export interface TransactionsResponseWatchlist {
} | null;
}

export type TransactionType =
| 'rootstock_remasc'
| 'rootstock_bridge'
| 'token_transfer'
| 'contract_creation'
| 'contract_call'
| 'token_creation'
| 'coin_transfer'
| 'blob_transaction';
export type TransactionType = 'rootstock_remasc' |
'rootstock_bridge' |
'token_transfer' |
'contract_creation' |
'contract_call' |
'token_creation' |
'coin_transfer' |
'blob_transaction'

export type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;

Expand Down
10 changes: 8 additions & 2 deletions ui/address/AddressTxsFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Menu, MenuButton, MenuList, MenuOptionGroup, MenuItemOption, useDisclosure } from '@chakra-ui/react';
import {
Menu,
MenuButton,
MenuList,
MenuOptionGroup,
MenuItemOption,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';

import type { AddressFromToFilter } from 'types/api/address';
Expand Down Expand Up @@ -33,7 +40,6 @@ const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }
<MenuItemOption value="all">All</MenuItemOption>
<MenuItemOption value="from">Outgoing transactions</MenuItemOption>
<MenuItemOption value="to">Incoming transactions</MenuItemOption>
<MenuItemOption value="staking_transactions">Staking transactions</MenuItemOption>
</MenuOptionGroup>
</MenuList>
</Menu>
Expand Down
Loading

0 comments on commit 6ddb901

Please sign in to comment.