Skip to content

Commit

Permalink
Merge pull request #1069 from tonwhales/release/v2.1.15
Browse files Browse the repository at this point in the history
Release/v2.1.15
  • Loading branch information
vzhovnitsky authored Sep 23, 2024
2 parents 35d2073 + 7a8aa83 commit 65ceb09
Show file tree
Hide file tree
Showing 25 changed files with 445 additions and 120 deletions.
2 changes: 1 addition & 1 deletion VERSION_CODE
Original file line number Diff line number Diff line change
@@ -1 +1 @@
208
209
4 changes: 3 additions & 1 deletion app/components/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const ItemSwitch = memo((props: {
onChange: (value: boolean) => void,
leftIcon?: ImageSourcePropType,
leftIconComponent?: any,
titleStyle?: StyleProp<TextStyle>
titleStyle?: StyleProp<TextStyle>,
style?: StyleProp<TextStyle>,
disabled?: boolean,
}) => {
const theme = useTheme();
Expand All @@ -61,6 +62,7 @@ export const ItemSwitch = memo((props: {
minHeight: 72
},
Platform.select({ android: { opacity: props.disabled ? 0.8 : 1 } }),
props.style
]}
disabled={props.disabled}
>
Expand Down
30 changes: 30 additions & 0 deletions app/engine/api/fetchJettonPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import axios from "axios";
import { z } from "zod";

const jettonPayloadScheme = z.object({
customPayload: z.string().optional().nullable(),
stateInit: z.string().optional().nullable()
});

export type JettonPayload = z.infer<typeof jettonPayloadScheme>;

export async function fetchJettonPayload(account: string, jettonMaster: string, customPayloadApiUri?: string | null): Promise<JettonPayload> {
const endpoint = `https://connect.tonhubapi.com/mintless/jettons/`;
const path = `${jettonMaster}/transfer/${account}/payload`;

const searchParams = new URLSearchParams();
if (customPayloadApiUri) {
searchParams.append('customPayloadApiUri', customPayloadApiUri);
}
const search = searchParams.toString();
const url = `${endpoint}${path}${search ? `?${search}` : ''}`;
const res = (await axios.get(url)).data;

const parsed = jettonPayloadScheme.safeParse(res);

if (!parsed.success) {
throw Error('Invalid jetton payload');
}

return parsed.data;
}
48 changes: 48 additions & 0 deletions app/engine/api/fetchMintlessHints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import axios from "axios";
import { z } from "zod";

const mintlessJettonScheme = z.object({
balance: z.string(),
walletAddress: z.object({
address: z.string(),
name: z.string().optional().nullable(),
icon: z.string().optional().nullable(),
isScam: z.boolean(),
isWallet: z.boolean()
}),
price: z.object({
prices: z.record(z.number()).optional().nullable(),
diff24h: z.record(z.string()).optional().nullable(),
diff7d: z.record(z.string()).optional().nullable(),
diff30d: z.record(z.string()).optional().nullable()
}).optional().nullable(),
jetton: z.object({
address: z.string(),
name: z.string(),
symbol: z.string(),
decimals: z.number(),
image: z.string(),
verification: z.string(),
customPayloadApiUri: z.string().nullable().optional()
}),
extensions: z.array(z.string()),
lock: z.object({
amount: z.string(),
till: z.number()
}).optional().nullable()
});
const mintlessJettonListScheme = z.array(mintlessJettonScheme);

export type MintlessJetton = z.infer<typeof mintlessJettonScheme>;

export async function fetchMintlessHints(address: string): Promise<MintlessJetton[]> {
let res = (await axios.get(`https://connect.tonhubapi.com/mintless/jettons/${address}`)).data;

const parsed = mintlessJettonListScheme.safeParse(res);

if (!parsed.success) {
throw Error('Invalid mintless hints');
}

return parsed.data;
}
41 changes: 41 additions & 0 deletions app/engine/hooks/jettons/useHints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Queries } from '../../queries';
import { fetchHints } from '../../api/fetchHints';
import { z } from "zod";
import { storagePersistence } from '../../../storage/storage';
import { fetchMintlessHints, MintlessJetton } from '../../api/fetchMintlessHints';
import { queryClient } from '../../clients';
import { StoredJettonWallet } from '../../metadata/StoredMetadata';
import { getQueryData } from '../../utils/getQueryData';

const txsHintsKey = 'txsHints';
const txsHintsCodec = z.array(z.string());
Expand Down Expand Up @@ -44,8 +48,45 @@ export function useHints(addressString?: string): string[] {
},
enabled: !!addressString,
refetchInterval: 10000,
refetchOnMount: true,
refetchOnWindowFocus: true,
});

return hints.data || [];
}

export function useMintlessHints(addressString?: string): MintlessJetton[] {
let hints = useQuery({
queryKey: Queries.Mintless(addressString || ''),
queryFn: async () => {
try {
const fetched = await fetchMintlessHints(addressString!);

const cache = queryClient.getQueryCache();
// update jetton wallets with mintless hints
fetched?.forEach(hint => {
const wallet = getQueryData<StoredJettonWallet | null>(cache, Queries.Account(hint.walletAddress.address).JettonWallet());

if (!wallet) {
queryClient.setQueryData<StoredJettonWallet | null>(Queries.Account(hint.walletAddress.address).JettonWallet(), {
balance: hint.balance,
owner: addressString!,
master: hint.jetton.address,
address: hint.walletAddress.address
});
}
});

return fetched;
} catch (e) {
console.warn('fetch mintless hints error', e);
}
},
enabled: !!addressString,
refetchInterval: 10000,
refetchOnMount: true,
refetchOnWindowFocus: true
});

return hints.data || [];
}
42 changes: 42 additions & 0 deletions app/engine/hooks/jettons/useJettonPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useQuery } from "@tanstack/react-query";
import { Queries } from "../../queries";
import { fetchJettonPayload } from "../../api/fetchJettonPayload";
import { queryClient } from "../../clients";
import { getQueryData } from "../../utils/getQueryData";
import { MintlessJetton } from "../../api/fetchMintlessHints";
import { Address } from "@ton/core";

export function useJettonPayload(account?: string, masterAddress?: string) {
const enabled = !!account && !!masterAddress;

const query = useQuery({
queryKey: Queries.Jettons().Address(account || '').WalletPayload(masterAddress || ''),
queryFn: async () => {
if (!account || !masterAddress) {
return null;
}

const queryCache = queryClient.getQueryCache();
const mintlessHints = getQueryData<MintlessJetton[]>(queryCache, Queries.Mintless(account!)) || [];
const mintlessJetton = mintlessHints.find(h => Address.parse(masterAddress).equals(Address.parse(h.jetton.address)))?.jetton;

if (!mintlessJetton) {
return null;
}

const customPayloadApiUri = mintlessJetton.customPayloadApiUri;
const res = await fetchJettonPayload(account!, masterAddress!, customPayloadApiUri);
return res;
},
enabled,
staleTime: 1000 * 5,
refetchOnMount: true,
refetchOnWindowFocus: true
});

return {
data: query.data,
loading: (query.isFetching || query.isLoading) && enabled,
isError: query.isError,
}
}
46 changes: 42 additions & 4 deletions app/engine/hooks/jettons/usePrefetchHints.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { useHints } from './useHints';
import { useHints, useMintlessHints } from './useHints';
import { useNetwork } from '../network/useNetwork';
import { Queries } from '../../queries';
import { fetchMetadata } from '../../metadata/fetchMetadata';
Expand All @@ -13,10 +13,13 @@ import { TonClient4 } from '@ton/ton';
import { QueryClient } from '@tanstack/react-query';
import { storage } from '../../../storage/storage';
import { create, keyResolver, windowedFiniteBatchScheduler } from "@yornaath/batshit";
import { clients } from '../../clients';
import { clients, queryClient } from '../../clients';
import { AsyncLock } from 'teslabot';
import memoize from '../../../utils/memoize';
import { tryGetJettonWallet } from '../../metadata/introspections/tryGetJettonWallet';
import { tryFetchJettonWalletIsClaimed } from '../../metadata/introspections/tryFetchJettonWalletIsClaimed';
import { fetchMintlessHints, MintlessJetton } from '../../api/fetchMintlessHints';
import { getQueryData } from '../../utils/getQueryData';

let jettonFetchersLock = new AsyncLock();

Expand Down Expand Up @@ -110,14 +113,33 @@ const walletBatcher = memoize((client: TonClient4, isTestnet: boolean) => {
await Promise.all(wallets.map(async (wallet) => {
try {
let address = Address.parse(wallet);
log(`[jetton-wallet] 🟡 batch ${wallet}`);

let data = await tryFetchJettonWallet(client, await getLastBlock(), address);
if (!data) {
return;
}

let isClaimed = await tryFetchJettonWalletIsClaimed(client, await getLastBlock(), address);
let mintlessBalance;

if (isClaimed === false) {
const owner = data.owner.toString({ testOnly: isTestnet });
const queryCache = queryClient.getQueryCache();
const queryKey = Queries.Mintless(owner);
let mintlessHints = getQueryData<MintlessJetton[]>(queryCache, queryKey) || [];

if (!mintlessHints) {
try {
mintlessHints = await fetchMintlessHints(owner);
} catch { }
}

mintlessBalance = mintlessHints.find(hint => hint.walletAddress.address === wallet)?.balance;
}

result.push({
balance: data.balance.toString(10),
balance: mintlessBalance || data.balance.toString(10),
master: data.master.toString({ testOnly: isTestnet }),
owner: data.owner.toString({ testOnly: isTestnet }),
address: wallet
Expand Down Expand Up @@ -218,6 +240,7 @@ function invalidateJettonsDataIfVersionChanged(queryClient: QueryClient) {

export function usePrefetchHints(queryClient: QueryClient, address?: string) {
const hints = useHints(address);
const mintlessHints = useMintlessHints(address);
const { isTestnet } = useNetwork();

useEffect(() => {
Expand Down Expand Up @@ -280,8 +303,23 @@ export function usePrefetchHints(queryClient: QueryClient, address?: string) {
});
}
}));

// Prefetch mintless jettons
await Promise.all(mintlessHints.map(async hint => {
let result = queryClient.getQueryData<JettonMasterState>(Queries.Jettons().MasterContent(hint.jetton.address));
if (!result) {
await queryClient.prefetchQuery({
queryKey: Queries.Jettons().MasterContent(hint.jetton.address),
queryFn: jettonMasterContentQueryFn(hint.jetton.address, isTestnet),
});
await queryClient.prefetchQuery({
queryKey: Queries.Jettons().Address(address).Wallet(hint.walletAddress.address),
queryFn: jettonWalletAddressQueryFn(hint.walletAddress.address, address, isTestnet)
});
}
}));
})().catch((e) => {
console.warn(e);
});
}, [address, hints]);
}, [address, hints, mintlessHints]);
}
42 changes: 36 additions & 6 deletions app/engine/hooks/jettons/useSortedHintsWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useCallback, useEffect } from "react";
import { getSortedHints, useSortedHintsState } from "./useSortedHints";
import { useNetwork } from "..";
import { compareHints, filterHint, getHint } from "../../../utils/hintSortFilter";
import { compareHints, filterHint, getHint, getMintlessHint } from "../../../utils/hintSortFilter";
import { queryClient } from "../../clients";
import { QueryCacheNotifyEvent } from "@tanstack/react-query";
import { Queries } from "../../queries";
import { getQueryData } from "../../utils/getQueryData";
import { throttle } from "../../../utils/throttle";
import { MintlessJetton } from "../../api/fetchMintlessHints";

// check if two arrays are equal by content invariant of the order
function areArraysEqualByContent<T>(a: T[], b: T[]): boolean {
Expand Down Expand Up @@ -77,19 +78,48 @@ function useSubToHintChange(
}, [owner, reSortHints]);
}

enum HintType {
Hint = 'hint',
Mintless = 'mintless'
}

type SortableHint = { hintType: HintType.Hint, address: string }
| { hintType: HintType.Mintless, address: string, hint: MintlessJetton };

export function useSortedHintsWatcher(address?: string) {
const { isTestnet } = useNetwork();
const [, setSortedHints] = useSortedHintsState(address);

const resyncAllHintsWeights = useCallback(throttle(() => {
const cache = queryClient.getQueryCache();
const hints = getQueryData<string[]>(cache, Queries.Hints(address ?? ''));
if (!hints) {
return;
}
const mintlessHints = getQueryData<MintlessJetton[]>(cache, Queries.Mintless(address ?? ''));

const allHints = [
...(hints || []).map((h) => ({ hintType: HintType.Hint, address: h })),
...(mintlessHints || []).map((h) => ({ hintType: HintType.Mintless, address: h.walletAddress.address, hint: h }))
]

const allHintsSet = new Set([...hints ?? [], ...mintlessHints?.map((h) => h.walletAddress.address) ?? []]);

const noDups: SortableHint[] = Array.from(allHintsSet).map((a) => {
const hint = allHints.find((h) => h.address === a);

if (!hint) {
return null;
}

return hint;
}).filter((x) => !!x) as SortableHint[];

const sorted = noDups
.map((h) => {
if (h.hintType === HintType.Hint) {
return getHint(cache, h.address, isTestnet);
}

const sorted = hints
.map((h) => getHint(cache, h, isTestnet))
return getMintlessHint(cache, h.hint, isTestnet);
})
.sort(compareHints).filter(filterHint([])).map((x) => x.address);

setSortedHints(sorted);
Expand Down
2 changes: 0 additions & 2 deletions app/engine/hooks/metadata/useContractMetadatas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import { useQueries } from '@tanstack/react-query';
import { Queries } from '../../queries';
import { contractMetadataQueryFn } from '../jettons/usePrefetchHints';
import { useNetwork } from '../network/useNetwork';
import { useClient4 } from '../network/useClient4';

export function useContractMetadatas(contracts: string[]) {
const { isTestnet } = useNetwork();
const client = useClient4(isTestnet);

return useQueries({
queries: contracts.map(m => ({
Expand Down
Loading

0 comments on commit 65ceb09

Please sign in to comment.