diff --git a/src/components/cards/NavMenuCard/NavMenuCard.tsx b/src/components/cards/NavMenuCard/NavMenuCard.tsx index 775115509..d41cbc87a 100644 --- a/src/components/cards/NavMenuCard/NavMenuCard.tsx +++ b/src/components/cards/NavMenuCard/NavMenuCard.tsx @@ -100,7 +100,6 @@ function NavMenuCard() { // const arBalance = await queryClient.fetchQuery( // buildARBalanceQuery({ // address: walletAddress, - // provider: arweaveDataProvider, // meta: [gateway], // }), // ); diff --git a/src/components/forms/DomainSettings/DescriptionRow.tsx b/src/components/forms/DomainSettings/DescriptionRow.tsx index 0b30882e9..f943dd868 100644 --- a/src/components/forms/DomainSettings/DescriptionRow.tsx +++ b/src/components/forms/DomainSettings/DescriptionRow.tsx @@ -1,11 +1,11 @@ import ValidationInput from '@src/components/inputs/text/ValidationInput/ValidationInput'; import ConfirmTransactionModal from '@src/components/modals/ConfirmTransactionModal/ConfirmTransactionModal'; -import { useGlobalState } from '@src/state/contexts/GlobalState'; import { ANT_INTERACTION_TYPES, ContractInteraction, VALIDATION_INPUT_TYPES, } from '@src/types'; +import { validateArweaveId } from '@src/utils'; import eventEmitter from '@src/utils/events'; import { Skeleton } from 'antd'; import { useEffect, useState } from 'react'; @@ -25,7 +25,6 @@ export default function DescriptionRow({ const [newDescription, setNewDescription] = useState( description ?? '', ); - const [{ arweaveDataProvider }] = useGlobalState(); const [showModal, setShowModal] = useState(false); useEffect(() => { @@ -76,8 +75,9 @@ export default function DescriptionRow({ setValue={(e) => setNewDescription(e)} validationPredicates={{ [VALIDATION_INPUT_TYPES.ARWEAVE_ID]: { - fn: (id: string) => - arweaveDataProvider.validateArweaveId(id), + fn: async (id: string) => { + return validateArweaveId(id); + }, }, }} maxCharLength={(str) => str.length <= 512} diff --git a/src/components/forms/DomainSettings/LogoRow.tsx b/src/components/forms/DomainSettings/LogoRow.tsx index 0243ecea1..ac8d00ad5 100644 --- a/src/components/forms/DomainSettings/LogoRow.tsx +++ b/src/components/forms/DomainSettings/LogoRow.tsx @@ -4,12 +4,12 @@ import ArweaveID, { } from '@src/components/layout/ArweaveID/ArweaveID'; import ConfirmTransactionModal from '@src/components/modals/ConfirmTransactionModal/ConfirmTransactionModal'; import { ArweaveTransactionID } from '@src/services/arweave/ArweaveTransactionID'; -import { useGlobalState } from '@src/state/contexts/GlobalState'; import { ANT_INTERACTION_TYPES, ContractInteraction, VALIDATION_INPUT_TYPES, } from '@src/types'; +import { validateArweaveId } from '@src/utils'; import { isArweaveTransactionID } from '@src/utils'; import { ARNS_TX_ID_ENTRY_REGEX } from '@src/utils/constants'; import eventEmitter from '@src/utils/events'; @@ -29,7 +29,6 @@ export default function LogoRow({ }) { const [editing, setEditing] = useState(false); const [newLogoTxId, setNewLogoTxId] = useState(logoTxId ?? ''); - const [{ arweaveDataProvider }] = useGlobalState(); const [showModal, setShowModal] = useState(false); useEffect(() => { @@ -86,8 +85,9 @@ export default function LogoRow({ setValue={(e) => setNewLogoTxId(e)} validationPredicates={{ [VALIDATION_INPUT_TYPES.ARWEAVE_ID]: { - fn: (id: string) => - arweaveDataProvider.validateArweaveId(id), + fn: async (id: string) => { + return validateArweaveId(id); + }, }, }} maxCharLength={(str) => str.length <= 43} diff --git a/src/components/forms/DomainSettings/TargetIDRow.tsx b/src/components/forms/DomainSettings/TargetIDRow.tsx index 37471e3ab..727934fe3 100644 --- a/src/components/forms/DomainSettings/TargetIDRow.tsx +++ b/src/components/forms/DomainSettings/TargetIDRow.tsx @@ -4,12 +4,12 @@ import ArweaveID, { } from '@src/components/layout/ArweaveID/ArweaveID'; import ConfirmTransactionModal from '@src/components/modals/ConfirmTransactionModal/ConfirmTransactionModal'; import { ArweaveTransactionID } from '@src/services/arweave/ArweaveTransactionID'; -import { useGlobalState } from '@src/state/contexts/GlobalState'; import { ANT_INTERACTION_TYPES, ContractInteraction, VALIDATION_INPUT_TYPES, } from '@src/types'; +import { validateArweaveId } from '@src/utils'; import { isArweaveTransactionID } from '@src/utils'; import { ARNS_TX_ID_ENTRY_REGEX } from '@src/utils/constants'; import eventEmitter from '@src/utils/events'; @@ -29,7 +29,6 @@ export default function TargetIDRow({ }) { const [editing, setEditing] = useState(false); const [newTargetId, setNewTargetId] = useState(targetId ?? ''); - const [{ arweaveDataProvider }] = useGlobalState(); const [showModal, setShowModal] = useState(false); useEffect(() => { @@ -86,8 +85,9 @@ export default function TargetIDRow({ setValue={(e) => setNewTargetId(e)} validationPredicates={{ [VALIDATION_INPUT_TYPES.ARWEAVE_ID]: { - fn: (id: string) => - arweaveDataProvider.validateArweaveId(id), + fn: async (id: string) => { + return validateArweaveId(id); + }, }, }} maxCharLength={(str) => str.length <= 43} diff --git a/src/components/inputs/text/NameTokenSelector/NameTokenSelector.tsx b/src/components/inputs/text/NameTokenSelector/NameTokenSelector.tsx index 869a19664..c42e99681 100644 --- a/src/components/inputs/text/NameTokenSelector/NameTokenSelector.tsx +++ b/src/components/inputs/text/NameTokenSelector/NameTokenSelector.tsx @@ -1,5 +1,7 @@ import { ANT, AOProcess, AoArNSNameData } from '@ar.io/sdk/web'; import Tooltip from '@src/components/Tooltips/Tooltip'; +import { validateArweaveId } from '@src/utils'; +import { buildArNSRecordsQuery, queryClient } from '@src/utils/network'; import { Pagination, PaginationProps } from 'antd'; import { useEffect, useRef, useState } from 'react'; @@ -31,7 +33,8 @@ function NameTokenSelector({ }: { selectedTokenCallback: (id: ArweaveTransactionID | undefined) => void; }) { - const [{ arweaveDataProvider, antAoClient, hyperbeamUrl }] = useGlobalState(); + const [{ antAoClient, hyperbeamUrl, arioContract, arioProcessId }] = + useGlobalState(); const [{ walletAddress }] = useWalletState(); const [searchText, setSearchText] = useState(); @@ -146,11 +149,12 @@ function NameTokenSelector({ } const processIds = fetchedprocessIds.concat(validImports); - const associatedRecords = await arweaveDataProvider.getRecords({ - filters: { - processId: processIds, - }, - }); + const associatedRecords = await queryClient.fetchQuery( + buildArNSRecordsQuery({ + arioContract: arioContract, + meta: [arioProcessId.toString()], + }), + ); const contracts: { processId: ArweaveTransactionID; @@ -376,8 +380,8 @@ function NameTokenSelector({ } validationPredicates={{ [VALIDATION_INPUT_TYPES.ARWEAVE_ID]: { - fn: (id: string) => { - return arweaveDataProvider.validateArweaveId(id); + fn: async (id: string) => { + return validateArweaveId(id); }, }, }} diff --git a/src/components/layout/ArPrice/ArPrice.tsx b/src/components/layout/ArPrice/ArPrice.tsx index b4909942c..1122e9109 100644 --- a/src/components/layout/ArPrice/ArPrice.tsx +++ b/src/components/layout/ArPrice/ArPrice.tsx @@ -1,30 +1,18 @@ -import { useEffect, useState } from 'react'; +import { useArPrice } from '@src/hooks'; +import { useEffect } from 'react'; -import { useGlobalState } from '../../../state/contexts/GlobalState'; import eventEmitter from '../../../utils/events'; function ArPrice({ dataSize }: { dataSize: number }) { - const [{ arweaveDataProvider }] = useGlobalState(); + const { data, error } = useArPrice(dataSize); - const [price, setPrice] = useState(0); useEffect(() => { - getPrice(); - }, [dataSize]); - - async function getPrice() { - const result = await arweaveDataProvider.getArPrice(dataSize); - try { - if (!result) { - throw new Error('Could not get price on gas fee'); - } - setPrice(result); - } catch (error: any) { - eventEmitter.emit(error); - setPrice(0); + if (error) { + eventEmitter.emit('error', error); } - } + }, [error]); - return <>{`${price.toPrecision(8)} AR`}; + return <>{`${(data ?? 0).toPrecision(8)} AR`}; } export default ArPrice; diff --git a/src/components/layout/BlockHeightCounter/BlockHeightCounter.tsx b/src/components/layout/BlockHeightCounter/BlockHeightCounter.tsx index 415306455..abedf3aaa 100644 --- a/src/components/layout/BlockHeightCounter/BlockHeightCounter.tsx +++ b/src/components/layout/BlockHeightCounter/BlockHeightCounter.tsx @@ -1,3 +1,4 @@ +import { useArweaveBlockHeight } from '@src/hooks'; import Countdown from 'antd/lib/statistic/Countdown'; import { ReactNode, useEffect, useState } from 'react'; @@ -10,17 +11,20 @@ const BlockHeightCounter = ({ }: { prefixText?: ReactNode; }) => { - const [ - { blockHeight, lastBlockUpdateTimestamp, arweaveDataProvider }, - dispatchGlobalState, - ] = useGlobalState(); + const [{ blockHeight, lastBlockUpdateTimestamp }, dispatchGlobalState] = + useGlobalState(); + const { refetch } = useArweaveBlockHeight(); const [timeUntilUpdate, setTimeUntilUpdate] = useState(0); const updateBlockHeight = async () => { try { - const blockHeight = await arweaveDataProvider.getCurrentBlockHeight(); - dispatchGlobalState({ type: 'setBlockHeight', payload: blockHeight }); + const blockHeight = await refetch(); + if (blockHeight.data) + dispatchGlobalState({ + type: 'setBlockHeight', + payload: blockHeight.data, + }); } catch (error) { eventEmitter.emit('error', error); } diff --git a/src/components/modals/ConnectWalletModal/ConnectWalletModal.tsx b/src/components/modals/ConnectWalletModal/ConnectWalletModal.tsx index 4cd17f47d..3b50e7783 100644 --- a/src/components/modals/ConnectWalletModal/ConnectWalletModal.tsx +++ b/src/components/modals/ConnectWalletModal/ConnectWalletModal.tsx @@ -82,7 +82,6 @@ function ConnectWalletModal(): JSX.Element { try { setConnecting(true); await walletConnector.connect(); - const address = await walletConnector.getWalletAddress(); dispatchWalletState({ type: 'setWalletAddress', diff --git a/src/components/modals/ant-management/EditUndernameModal/EditUndernameModal.tsx b/src/components/modals/ant-management/EditUndernameModal/EditUndernameModal.tsx index 99a449fba..ce50ea325 100644 --- a/src/components/modals/ant-management/EditUndernameModal/EditUndernameModal.tsx +++ b/src/components/modals/ant-management/EditUndernameModal/EditUndernameModal.tsx @@ -1,4 +1,5 @@ import { ANT, AOProcess, AoANTRecord } from '@ar.io/sdk/web'; +import { validateArweaveId } from '@src/utils'; import { clamp } from 'lodash'; import { useEffect, useRef, useState } from 'react'; @@ -32,7 +33,7 @@ function EditUndernameModal({ closeModal: () => void; payloadCallback: (payload: SetRecordPayload) => void; }) { - const [{ arweaveDataProvider, antAoClient, hyperbeamUrl }] = useGlobalState(); + const [{ antAoClient, hyperbeamUrl }] = useGlobalState(); const isMobile = useIsMobile(); const targetIdRef = useRef(null); const ttlRef = useRef(null); @@ -126,8 +127,9 @@ function EditUndernameModal({ customPattern={ARNS_TX_ID_ENTRY_REGEX} validationPredicates={{ [VALIDATION_INPUT_TYPES.ARWEAVE_ID]: { - fn: (id: string) => - arweaveDataProvider.validateArweaveId(id), + fn: async (id: string) => { + return validateArweaveId(id); + }, }, }} /> diff --git a/src/components/pages/Register/Register.tsx b/src/components/pages/Register/Register.tsx index 52d293c6e..b780568ed 100644 --- a/src/components/pages/Register/Register.tsx +++ b/src/components/pages/Register/Register.tsx @@ -29,6 +29,7 @@ import { formatARIOWithCommas, formatDate, isArweaveTransactionID, + validateArweaveId, } from '../../../utils'; import { MAX_LEASE_DURATION, @@ -45,15 +46,8 @@ import PageLoader from '../../layout/progress/PageLoader/PageLoader'; import './styles.css'; function RegisterNameForm() { - const [ - { - arweaveDataProvider, - arioTicker, - arioProcessId, - antAoClient, - hyperbeamUrl, - }, - ] = useGlobalState(); + const [{ arioTicker, arioProcessId, antAoClient, hyperbeamUrl }] = + useGlobalState(); const [ { domain, leaseDuration, registrationType, antID, targetId }, dispatchRegisterState, @@ -468,8 +462,9 @@ function RegisterNameForm() { placeholder={'Arweave Transaction ID (Target ID)'} validationPredicates={{ [VALIDATION_INPUT_TYPES.ARWEAVE_ID]: { - fn: (id: string) => - arweaveDataProvider.validateArweaveId(id), + fn: async (id: string) => { + return validateArweaveId(id); + }, }, }} showValidationChecklist={false} diff --git a/src/components/pages/Settings/ArNSSettings.tsx b/src/components/pages/Settings/ArNSSettings.tsx index 31fe4f988..b355253e5 100644 --- a/src/components/pages/Settings/ArNSSettings.tsx +++ b/src/components/pages/Settings/ArNSSettings.tsx @@ -8,14 +8,12 @@ import { import ArweaveID, { ArweaveIdTypes, } from '@src/components/layout/ArweaveID/ArweaveID'; -import { ArweaveCompositeDataProvider } from '@src/services/arweave/ArweaveCompositeDataProvider'; +import { useArweaveBlockHeight } from '@src/hooks'; import { ArweaveTransactionID } from '@src/services/arweave/ArweaveTransactionID'; -import { SimpleArweaveDataProvider } from '@src/services/arweave/SimpleArweaveDataProvider'; import { useGlobalState, useWalletState } from '@src/state'; import { isArweaveTransactionID } from '@src/utils'; import { ARIO_PROCESS_ID } from '@src/utils/constants'; import { Input } from 'antd'; -import Arweave from 'arweave'; import { RotateCcw } from 'lucide-react'; import { useEffect, useState } from 'react'; @@ -31,6 +29,7 @@ function ArNSSettings() { arioProcessId?.toString(), ); const [isValidAddress, setIsValidAddress] = useState(true); + const { data: blockHeight } = useArweaveBlockHeight(); useEffect(() => { setRegistryAddress(arioProcessId?.toString()); @@ -55,22 +54,17 @@ function ArNSSettings() { type: 'setArIOContract', payload: arIOContract, }); - - const arweave = new Arweave({ - host: gateway, - protocol: 'https', - }); - const arweaveDataProvider = new SimpleArweaveDataProvider(arweave); - - const provider = new ArweaveCompositeDataProvider({ - contract: arIOContract, - arweave: arweaveDataProvider, - }); - dispatchGlobalState({ type: 'setGateway', - payload: { gateway, provider }, + payload: { gateway }, }); + // TODO: why do we need to set the block height here? + if (blockHeight) { + dispatchGlobalState({ + type: 'setBlockHeight', + payload: blockHeight || 0, + }); + } } } diff --git a/src/components/pages/Settings/NetworkSettings.tsx b/src/components/pages/Settings/NetworkSettings.tsx index 12683a6c9..6809a4084 100644 --- a/src/components/pages/Settings/NetworkSettings.tsx +++ b/src/components/pages/Settings/NetworkSettings.tsx @@ -36,14 +36,7 @@ import './styles.css'; function NetworkSettings() { const [ - { - gateway, - aoNetwork, - arioProcessId, - arioContract, - turboNetwork, - hyperbeamUrl, - }, + { gateway, aoNetwork, arioProcessId, turboNetwork, hyperbeamUrl }, dispatchGlobalState, ] = useGlobalState(); const [newGateway, setNewGateway] = useState(gateway); @@ -161,7 +154,7 @@ function NetworkSettings() { throw new Error('Gateway not available: ' + gate); }); // Always try to update gateway - dispatchNewGateway(gate, arioContract, dispatchGlobalState); + dispatchNewGateway(gate, dispatchGlobalState); } catch (error) { eventEmitter.emit('error', error); eventEmitter.emit('error', { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 7d828b0e0..461b66bf2 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,4 +2,9 @@ export * from './useIsMobile/useIsMobile'; export * from './useLongPress/useLongPress'; export * from './useIsFocused/useIsFocused'; export * from './useRegistrationStatus/useRegistrationStatus'; +export * from './useArPrice'; +export * from './useArBalance'; +export * from './useArweaveBlockHeight'; +export * from './useArNSRecord'; +export * from './useArNSReserved'; export * from './useSyncSettings/useSyncSettings'; diff --git a/src/hooks/useArBalance.tsx b/src/hooks/useArBalance.tsx new file mode 100644 index 000000000..a31a34add --- /dev/null +++ b/src/hooks/useArBalance.tsx @@ -0,0 +1,20 @@ +import { ArweaveTransactionID } from '@src/services/arweave/ArweaveTransactionID'; +import { useGlobalState } from '@src/state'; +import { useQuery } from '@tanstack/react-query'; +import Arweave from 'arweave'; + +export function useArBalance(address?: ArweaveTransactionID) { + const [{ gateway }] = useGlobalState(); + + return useQuery({ + queryKey: ['ar-balance', address?.toString(), gateway], + queryFn: async () => { + if (!address) throw new Error('No address provided'); + const arweave = new Arweave({ host: gateway, protocol: 'https' }); + const winston = await arweave.wallets.getBalance(address.toString()); + return +arweave.ar.winstonToAr(winston); + }, + enabled: !!address, + staleTime: 1000 * 60 * 60, + }); +} diff --git a/src/hooks/useArNSReserved.tsx b/src/hooks/useArNSReserved.tsx new file mode 100644 index 000000000..c364411aa --- /dev/null +++ b/src/hooks/useArNSReserved.tsx @@ -0,0 +1,16 @@ +import { useGlobalState } from '@src/state'; +import { useQuery } from '@tanstack/react-query'; + +export function useArNSReserved(domain?: string) { + const [{ arioContract, arioProcessId }] = useGlobalState(); + + return useQuery({ + queryKey: ['arns-reserved', domain, arioProcessId.toString()], + queryFn: async () => { + if (!domain) return null; + return arioContract.getArNSReservedName({ name: domain }); + }, + enabled: !!domain, + staleTime: Infinity, + }); +} diff --git a/src/hooks/useArPrice.tsx b/src/hooks/useArPrice.tsx new file mode 100644 index 000000000..d81f75c2d --- /dev/null +++ b/src/hooks/useArPrice.tsx @@ -0,0 +1,17 @@ +import { useGlobalState } from '@src/state'; +import { useQuery } from '@tanstack/react-query'; +import Arweave from 'arweave'; + +export function useArPrice(dataSize: number) { + const [{ gateway }] = useGlobalState(); + + return useQuery({ + queryKey: ['ar-price', dataSize, gateway], + queryFn: async () => { + const arweave = new Arweave({ host: gateway, protocol: 'https' }); + const { data } = await arweave.api.get(`/price/${dataSize}`); + return +arweave.ar.winstonToAr(data, { formatted: true }); + }, + staleTime: Infinity, + }); +} diff --git a/src/hooks/useArweaveBlockHeight.tsx b/src/hooks/useArweaveBlockHeight.tsx new file mode 100644 index 000000000..f48c0b1bd --- /dev/null +++ b/src/hooks/useArweaveBlockHeight.tsx @@ -0,0 +1,16 @@ +import { useGlobalState } from '@src/state'; +import { useQuery } from '@tanstack/react-query'; +import Arweave from 'arweave'; + +export function useArweaveBlockHeight() { + const [{ gateway }] = useGlobalState(); + + return useQuery({ + queryKey: ['block-height', gateway], + queryFn: async () => { + const arweave = new Arweave({ host: gateway, protocol: 'https' }); + return (await arweave.blocks.getCurrent()).height; + }, + staleTime: 1000 * 60, + }); +} diff --git a/src/services/arweave/ArweaveCompositeDataProvider.ts b/src/services/arweave/ArweaveCompositeDataProvider.ts deleted file mode 100644 index 50a5f514c..000000000 --- a/src/services/arweave/ArweaveCompositeDataProvider.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { - AoARIORead, - AoArNSNameData, - fetchAllArNSRecords, - mARIOToken, -} from '@ar.io/sdk/web'; -import { lowerCaseDomain } from '@src/utils'; - -import { AoAddress, ArweaveDataProvider } from '../../types'; -import { ArweaveTransactionID } from './ArweaveTransactionID'; - -export class ArweaveCompositeDataProvider implements ArweaveDataProvider { - // NOTE: this class should not have any logic for performing queries itself, but rather logic for getting results from - // an array of providers, using different strategies such as Promise.race or Promise.all. - private contract: AoARIORead; - private arweave: ArweaveDataProvider; - - // TODO: implement strategy methods - constructor({ - contract, - arweave, - }: { - arweave: ArweaveDataProvider; - contract: AoARIORead; - }) { - this.contract = contract; - this.arweave = arweave; - } - - async getArBalance(wallet: AoAddress): Promise { - return wallet instanceof ArweaveTransactionID - ? this.arweave.getArBalance(wallet) - : 0; - } - - async getContractBalanceForWallet( - wallet: ArweaveTransactionID, - ): Promise { - return this.contract - .getBalance({ - address: wallet.toString(), - }) - .then((balance: number) => new mARIOToken(balance).toARIO().valueOf()); - } - - async getTransactionStatus( - ids: ArweaveTransactionID[] | ArweaveTransactionID, - blockheight?: number, - ): Promise> { - return this.arweave.getTransactionStatus(ids, blockheight); - } - - async getTransactionTags( - id: ArweaveTransactionID, - ): Promise<{ [x: string]: string }> { - return this.arweave.getTransactionTags(id); - } - - async validateTransactionTags(params: { - id: string; - numberOfConfirmations?: number; - requiredTags?: { - [x: string]: string[]; // allowed values - }; - }) { - return this.arweave.validateTransactionTags(params); - } - - async validateArweaveId(id: string): Promise { - return this.arweave.validateArweaveId(id); - } - - async validateConfirmations(id: string): Promise { - return this.arweave.validateConfirmations(id); - } - - async validateArweaveAddress(address: string): Promise { - return this.arweave.validateArweaveAddress(address); - } - - async getArPrice(data: number): Promise { - return await this.arweave.getArPrice(data); - } - - async getCurrentBlockHeight(): Promise { - return await this.arweave.getCurrentBlockHeight(); - } - - // TODO: implement arns service query for the following 3 functions - async isDomainReserved({ - domain, - }: { - domain: string; - }): Promise<{ isReserved: boolean; reservedFor?: string }> { - const reserved = await this.contract.getArNSReservedName({ name: domain }); - return { - isReserved: !!reserved, - reservedFor: reserved?.target, - }; - } - - async isDomainAvailable({ domain }: { domain: string }): Promise { - const [record, reserved] = await Promise.all([ - this.getRecord({ domain }), - this.isDomainReserved({ domain }), - ]); - return !record && !reserved.isReserved; - } - - async getRecord({ - domain, - }: { - domain: string; - }): Promise { - const record = await this.contract.getArNSRecord({ - name: lowerCaseDomain(domain), - }); - return record; - } - - async getRecords({ - filters, - }: { - filters: { - processId?: ArweaveTransactionID[]; - }; - }): Promise> { - // TODO: check the cache for existing records and only fetch new ones - const records: Record = await fetchAllArNSRecords({ - contract: this.contract, - }); - - // filter by processId - return Object.fromEntries( - Object.entries(records).filter( - ([, record]) => - filters.processId === undefined || - filters.processId.includes( - new ArweaveTransactionID(record.processId), - ), - ), - ); - } - - async getTokenBalance(address: AoAddress): Promise { - return this.contract - .getBalance({ - address: address.toString(), - }) - .then((balance) => new mARIOToken(balance).toARIO().valueOf()); - } -} diff --git a/src/services/arweave/SimpleArweaveDataProvider.ts b/src/services/arweave/SimpleArweaveDataProvider.ts deleted file mode 100644 index 4a4b4eac1..000000000 --- a/src/services/arweave/SimpleArweaveDataProvider.ts +++ /dev/null @@ -1,297 +0,0 @@ -import Arweave from 'arweave/node'; -import Ar from 'arweave/node/ar'; -import { ResponseWithData } from 'arweave/node/lib/api'; - -import { ArweaveDataProvider, TransactionHeaders } from '../../types'; -import { tagsToObject, withExponentialBackoff } from '../../utils'; -import { - RECOMMENDED_TRANSACTION_CONFIRMATIONS, - transactionByOwnerQuery, -} from '../../utils/constants'; -import { ArweaveTransactionID } from './ArweaveTransactionID'; - -const ACCEPTABLE_STATUSES = new Set([200, 202]); -export class SimpleArweaveDataProvider implements ArweaveDataProvider { - private _arweave: Arweave; - private _ar: Ar = new Ar(); - - constructor(arweave: Arweave) { - this._arweave = arweave; - } - - async getArBalance(wallet: ArweaveTransactionID): Promise { - const winstonBalance = await withExponentialBackoff({ - fn: () => this._arweave.wallets.getBalance(wallet.toString()), - shouldRetry: (balance) => !balance, - initialDelay: 500, - maxTries: 3, - }); - return +this._ar.winstonToAr(winstonBalance); - } - - async getTransactionStatus( - ids: ArweaveTransactionID[] | ArweaveTransactionID, - currentBlockHeight?: number, - ): Promise> { - if (Array.isArray(ids)) { - if (!currentBlockHeight) { - throw new Error( - `Current blockheight is required when fetching multiple transactions`, - ); - } - - const queryIds = (cursor?: string) => ({ - query: ` { - transactions( - first:100 - ids: [${ids.map((id) => `"${id.toString()}"`)}] - ${cursor ? `after: "${cursor}"` : ''} - ) { - pageInfo { - hasNextPage - } - - edges { - cursor - node { - id - block { - height - } - } - } - } - }`, - }); - - const transactions = ids.length - ? await this.fetchPaginatedData(queryIds) - : ids; - const statuses = transactions.reduce( - ( - acc: Record, - tx: any, - ) => { - // not guaranteed - if (tx?.node?.id && tx?.node?.block?.height) { - acc[tx.node.id] = { - confirmations: currentBlockHeight - tx.node.block.height, - blockHeight: tx.node.block.height, - }; - } - return acc; - }, - {}, - ); - - return statuses; - } - - const { status, data } = await this._arweave.api.get(`/tx/${ids}/status`); - if (!ACCEPTABLE_STATUSES.has(status)) { - throw Error('Failed fetch confirmations for transaction id.'); - } - return { - [ids.toString()]: { - confirmations: +data.number_of_confirmations, - blockHeight: data.block_height, - }, - }; - } - - async getTransactionTags( - id: ArweaveTransactionID, - ): Promise<{ [x: string]: string }> { - const { data: encodedTags } = await this._arweave.api.get( - `/tx/${id.toString()}/tags`, - ); - const decodedTags = tagsToObject(encodedTags); - return decodedTags; - } - - async getTransactionHeaders( - id: ArweaveTransactionID, - ): Promise { - const { - status, - data: headers, - }: { status: number; data: TransactionHeaders } = - await this._arweave.api.get(`/tx/${id.toString()}`); - if (!ACCEPTABLE_STATUSES.has(status)) { - throw Error(`Transaction ID not found. Try again. Status: ${status}`); - } - return headers; - } - - async validateTransactionTags({ - id, - requiredTags = {}, - }: { - id: string; - requiredTags?: { [x: string]: string[] }; - }): Promise { - const txID = await this.validateArweaveId(id); - - // fetch the headers to confirm transaction actually exists - await this.getTransactionHeaders(txID); - - // validate tags - if (requiredTags) { - const tags = await this.getTransactionTags(txID); - // check that all required tags exist, and their values are allowed - Object.entries(requiredTags).map(([requiredTag, allowedValues]) => { - if (Object.keys(tags).includes(requiredTag)) { - if (allowedValues.includes(tags[requiredTag])) { - // allowed tag! - return true; - } - throw Error( - `${requiredTag} tag is present, but as an invalid value: ${tags[requiredTag]}. Allowed values: ${allowedValues}`, - ); - } - throw Error(`Contract is missing required tag: ${requiredTag}`); - }); - } - } - async validateArweaveId(id: string): Promise { - // a simple promise that throws on a poorly formatted transaction id - return new Promise((resolve, reject) => { - try { - const txId = new ArweaveTransactionID(id); - resolve(txId); - } catch (error: any) { - reject(error); - } - }); - } - - async validateArweaveAddress(address: string): Promise { - try { - const targetAddress = new ArweaveTransactionID(address); - - const txPromise = this._arweave.api - .get(`/tx/${targetAddress.toString()}`) - .then((res: ResponseWithData) => - ACCEPTABLE_STATUSES.has(res.status) ? res.data : undefined, - ); - - const balancePromise = this._arweave.api - .get(`/wallet/${targetAddress.toString()}/balance`) - .then((res: ResponseWithData) => - ACCEPTABLE_STATUSES.has(res.status) ? res.data > 0 : undefined, - ); - - const gqlPromise = this._arweave.api - .post(`/graphql`, transactionByOwnerQuery(targetAddress)) - .then((res: ResponseWithData) => - ACCEPTABLE_STATUSES.has(res.status) - ? res.data.data.transactions.edges - : [], - ); - - const [isTransaction, balance, hasTransactions] = await Promise.all([ - txPromise, - balancePromise, - gqlPromise, - ]); - - if (hasTransactions.length || balance) { - return true; - } - - if (isTransaction) { - const tags = tagsToObject(isTransaction.tags); - - const isContract = Object.values(tags).includes('SmartWeaveContract'); - - throw new Error( - `Provided address (${targetAddress.toString()} is a ${ - isContract ? 'Smartweave Contract' : 'transaction ID' - }.`, - ); - } - // test address : ceN9pWPt4IdPWj6ujt_CCuOOHGLpKu0MMrpu9a0fJNM - // must be connected to a gateway that fetches L2 to perform this check - if (!hasTransactions || !hasTransactions.length) { - throw new Error(`Address has no transactions`); - } - return true; - } catch (error) { - throw new Error(`Unable to verify this is an arweave address.`); - } - } - - async validateConfirmations( - id: string, - requiredNumberOfConfirmations = RECOMMENDED_TRANSACTION_CONFIRMATIONS, - ): Promise { - const txId = await this.validateArweaveId(id); - - // fetch the headers to confirm transaction actually exists - await this.getTransactionHeaders(txId); - - // validate confirmations - if (requiredNumberOfConfirmations > 0) { - const confirmations = await this.getTransactionStatus(txId); - if ( - confirmations[txId.toString()].confirmations < - requiredNumberOfConfirmations - ) { - throw Error( - `Process ID does not have required number of confirmations. Current confirmations: ${confirmations}. Required number of confirmations: ${requiredNumberOfConfirmations}.`, - ); - } - } - } - - async getArPrice(dataSize: number): Promise { - try { - const result = await this._arweave.api.get(`/price/${dataSize}`); - - return +this._arweave.ar.winstonToAr(result.data, { formatted: true }); - } catch (error) { - console.error(error); - return 0; - } - } - - async getCurrentBlockHeight(): Promise { - return (await this._arweave.blocks.getCurrent()).height; - } - - async fetchPaginatedData(query: (c?: string) => Record) { - let hasNextPage = true; - let afterCursor: string | undefined = undefined; - let allData: any[] = []; - - while (hasNextPage) { - try { - const response = await withExponentialBackoff({ - fn: () => - this._arweave.api.post( - '/graphql', - query(afterCursor), - ) as Promise, - shouldRetry: (error) => error?.status === 429, - initialDelay: 100, - maxTries: 30, - }); - const transactions = response?.data?.data?.transactions; - - if (transactions?.edges.length) { - allData = [...allData, ...transactions.edges]; - } - hasNextPage = transactions?.pageInfo?.hasNextPage; - afterCursor = transactions?.edges?.at(-1)?.cursor; - if (!afterCursor) { - hasNextPage = false; - } - } catch (error) { - console.error('Error fetching paginated data:', error); - hasNextPage = false; - } - } - - return allData; - } -} diff --git a/src/state/actions/dispatchNewGateway.ts b/src/state/actions/dispatchNewGateway.ts index 8dadd37cc..72412f841 100644 --- a/src/state/actions/dispatchNewGateway.ts +++ b/src/state/actions/dispatchNewGateway.ts @@ -1,38 +1,17 @@ -import { AoARIORead, AoARIOWrite } from '@ar.io/sdk/web'; -import Arweave from 'arweave'; import { Dispatch } from 'react'; -import { ArweaveCompositeDataProvider } from '../../services/arweave/ArweaveCompositeDataProvider'; -import { SimpleArweaveDataProvider } from '../../services/arweave/SimpleArweaveDataProvider'; import eventEmitter from '../../utils/events'; import { GlobalAction } from '../reducers'; export async function dispatchNewGateway( gateway: string, - contract: AoARIORead | AoARIOWrite, dispatch: Dispatch, ): Promise { try { - const arweave = new Arweave({ - host: gateway, - protocol: 'https', - }); - - const arweaveDataProvider = new SimpleArweaveDataProvider(arweave); - const provider = new ArweaveCompositeDataProvider({ - arweave: arweaveDataProvider, - contract: contract, - }); - const blockHeight = await provider.getCurrentBlockHeight(); - dispatch({ - type: 'setBlockHeight', - payload: blockHeight, - }); dispatch({ type: 'setGateway', payload: { gateway, - provider, }, }); } catch (error) { diff --git a/src/state/contexts/GlobalState.tsx b/src/state/contexts/GlobalState.tsx index e52f8df8b..f8d19556e 100644 --- a/src/state/contexts/GlobalState.tsx +++ b/src/state/contexts/GlobalState.tsx @@ -21,14 +21,11 @@ import React, { useReducer, } from 'react'; -import { ArweaveCompositeDataProvider } from '../../services/arweave/ArweaveCompositeDataProvider'; -import { SimpleArweaveDataProvider } from '../../services/arweave/SimpleArweaveDataProvider'; import { APP_VERSION, ARIO_AO_CU_URL, ARIO_PROCESS_ID, ARWEAVE_HOST, - DEFAULT_ARWEAVE, NETWORK_DEFAULTS, } from '../../utils/constants'; import type { GlobalAction } from '../reducers/GlobalReducer'; @@ -67,8 +64,6 @@ function loadSettingsFromStorage(): { return null; } -export const defaultArweave = new SimpleArweaveDataProvider(DEFAULT_ARWEAVE); - // Load saved settings or use defaults const savedSettings = loadSettingsFromStorage(); const initialGateway = savedSettings?.gateway || ARWEAVE_HOST; @@ -99,7 +94,6 @@ export type GlobalState = { arioProcessId: string; blockHeight?: number; lastBlockUpdateTimestamp?: number; - arweaveDataProvider: ArweaveCompositeDataProvider; arioContract: AoARIORead | AoARIOWrite; hyperbeamUrl?: string; }; @@ -121,10 +115,6 @@ const initialState: GlobalState = { hyperbeamUrl: initialHyperbeamUrl, blockHeight: undefined, lastBlockUpdateTimestamp: undefined, - arweaveDataProvider: new ArweaveCompositeDataProvider({ - arweave: defaultArweave, - contract: defaultArIO, - }), arioContract: defaultArIO, }; @@ -138,21 +128,14 @@ export const useGlobalState = (): [GlobalState, Dispatch] => type StateProviderProps = { reducer: React.Reducer; children: React.ReactNode; - arweaveDataProvider?: ArweaveCompositeDataProvider; }; /** Create provider to wrap app in */ export function GlobalStateProvider({ reducer, children, - arweaveDataProvider, }: StateProviderProps): JSX.Element { - const [state, dispatchGlobalState] = useReducer( - reducer, - arweaveDataProvider - ? { ...initialState, arweaveDataProvider } - : initialState, - ); + const [state, dispatchGlobalState] = useReducer(reducer, initialState); useEffect(() => { async function updateTicker() { diff --git a/src/state/contexts/WalletState.tsx b/src/state/contexts/WalletState.tsx index c6e7eecb0..6a55f7f05 100644 --- a/src/state/contexts/WalletState.tsx +++ b/src/state/contexts/WalletState.tsx @@ -1,5 +1,6 @@ -import { AOProcess, ARIO } from '@ar.io/sdk/web'; +import { AOProcess, ARIO, mARIOToken } from '@ar.io/sdk/web'; import { ArweaveAppError, BeaconError } from '@src/utils/errors'; +import Arweave from 'arweave'; import React, { Dispatch, createContext, @@ -64,12 +65,13 @@ export function WalletStateProvider({ const [ { - arweaveDataProvider, blockHeight, arioTicker, arioProcessId, aoClient, turboNetwork, + gateway, + arioContract, }, dispatchGlobalState, ] = useGlobalState(); @@ -169,10 +171,15 @@ export function WalletStateProvider({ async function updateBalances(address: AoAddress, arioTicker: string) { try { + const arweave = new Arweave({ host: gateway, protocol: 'https' }); const [arioBalance, arBalance] = await Promise.all([ - arweaveDataProvider.getTokenBalance(address), + arioContract + .getBalance({ address: address.toString() }) + .then((b) => new mARIOToken(b).toARIO().valueOf()), address instanceof ArweaveTransactionID - ? arweaveDataProvider.getArBalance(address) + ? arweave.wallets + .getBalance(address.toString()) + .then((b) => +arweave.ar.winstonToAr(b)) : Promise.resolve(0), ]); diff --git a/src/state/reducers/GlobalReducer.ts b/src/state/reducers/GlobalReducer.ts index ba39ca358..ad303c766 100644 --- a/src/state/reducers/GlobalReducer.ts +++ b/src/state/reducers/GlobalReducer.ts @@ -1,7 +1,6 @@ import { AoARIORead, AoARIOWrite, AoClient } from '@ar.io/sdk/web'; import { NETWORK_DEFAULTS } from '@src/utils/constants'; -import { ArweaveCompositeDataProvider } from '../../services/arweave/ArweaveCompositeDataProvider'; import { GlobalState } from '../contexts/GlobalState'; export type GlobalAction = @@ -9,7 +8,6 @@ export type GlobalAction = type: 'setGateway'; payload: { gateway: string; - provider: ArweaveCompositeDataProvider; }; } | { @@ -58,7 +56,6 @@ export const reducer = ( return { ...state, gateway: action.payload.gateway, - arweaveDataProvider: action.payload.provider, }; case 'setAONetwork': return { diff --git a/src/types.ts b/src/types.ts index 62b574b80..fa5d10721 100644 --- a/src/types.ts +++ b/src/types.ts @@ -110,32 +110,6 @@ export interface KVCache { clean(): void; } -export interface ArweaveDataProvider { - // add isAddress method - getTransactionStatus( - ids: ArweaveTransactionID[] | ArweaveTransactionID, - blockheight?: number, - ): Promise>; - getTransactionTags( - id: ArweaveTransactionID, - ): Promise<{ [x: string]: string }>; - validateTransactionTags(params: { - id: string; - requiredTags?: { - [x: string]: string[] | ArweaveTransactionID[]; // allowed values - }; - }): Promise; - validateArweaveId(id: string): Promise; - validateConfirmations( - id: string, - numberOfConfirmations?: number, - ): Promise; - validateArweaveAddress(address: string): Promise; - getArBalance(wallet: ArweaveTransactionID): Promise; - getArPrice(data: number): Promise; - getCurrentBlockHeight(): Promise; -} - export type ConnectWalletModalProps = { setShowModal: Dispatch>; }; diff --git a/src/utils/arweave.ts b/src/utils/arweave.ts new file mode 100644 index 000000000..555c0cceb --- /dev/null +++ b/src/utils/arweave.ts @@ -0,0 +1,30 @@ +import { ArweaveTransactionID } from '@src/services/arweave/ArweaveTransactionID'; +import Arweave from 'arweave'; + +import { ARWEAVE_HOST } from './constants'; + +export function validateArweaveId(id: string): ArweaveTransactionID { + return new ArweaveTransactionID(id); +} + +export async function validateArweaveAddress( + address: string, + gateway: string = ARWEAVE_HOST, +): Promise { + const arweave = new Arweave({ host: gateway, protocol: 'https' }); + try { + const status = await arweave.api + .get(`/tx/${address}/status`) + .then((res) => res.status); + + // if tx exists, this is not an address + if (status === 200 || status === 202) { + throw new Error('Provided string is a transaction id'); + } + + await arweave.wallets.getBalance(address); + return true; + } catch { + throw new Error('Unable to verify this is an arweave address.'); + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index fa1936c53..b225fbcff 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './common/common'; export * from './searchUtils/searchUtils'; export * from './transactionUtils/transactionUtils'; +export * from './arweave'; diff --git a/src/utils/network.ts b/src/utils/network.ts index 44f0e843b..57f5940ab 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -8,8 +8,6 @@ import { fetchAllArNSRecords, mARIOToken, } from '@ar.io/sdk/web'; -import { ArweaveCompositeDataProvider } from '@src/services/arweave/ArweaveCompositeDataProvider'; -import { AoAddress } from '@src/types'; import { QueryClient } from '@tanstack/react-query'; import { PersistedClient, @@ -102,27 +100,6 @@ export function buildIOBalanceQuery({ staleTime: 1000 * 60 * 60, // one hour }; } -export function buildARBalanceQuery({ - provider, - address, - meta, -}: { - provider: ArweaveCompositeDataProvider; - address: AoAddress; - meta?: string[]; -}): { - queryKey: ['ar-balance', string] | string[]; - queryFn: () => Promise; - staleTime: number; -} { - return { - queryKey: ['ar-balance', address.toString(), ...(meta || [])], - queryFn: async () => { - return await provider.getArBalance(address).catch(() => 0); - }, - staleTime: 1000 * 60 * 60, // one hour - }; -} export function buildArNSRecordsQuery({ arioContract, diff --git a/tests/common/mocks/ArweaveCompositeDataProviderMock.ts b/tests/common/mocks/ArweaveCompositeDataProviderMock.ts deleted file mode 100644 index a14d48c3c..000000000 --- a/tests/common/mocks/ArweaveCompositeDataProviderMock.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { AoArNSNameData } from '@ar.io/sdk/web'; - -const ArweaveCompositeDataProviderMock = jest.fn(() => ({ - getArBalance: jest.fn(() => Promise.resolve()), - getContractState: jest.fn(() => Promise.resolve()), - writeTransaction: jest.fn(() => Promise.resolve()), - getContractBalanceForWallet: jest.fn(() => Promise.resolve()), - getContractsForWallet: jest.fn(() => Promise.resolve()), - getTransactionStatus: jest.fn(() => Promise.resolve()), - getTransactionTags: jest.fn(() => Promise.resolve()), - validateTransactionTags: jest.fn(() => Promise.resolve()), - validateArweaveId: jest.fn(() => Promise.resolve()), - validateConfirmations: jest.fn(() => Promise.resolve()), - validateArweaveAddress: jest.fn(() => Promise.resolve()), - deployContract: jest.fn(() => Promise.resolve()), - registerAtomicName: jest.fn(() => Promise.resolve()), - getArPrice: jest.fn(() => Promise.resolve()), - getCurrentBlockHeight: jest.fn(() => Promise.resolve(1)), - getContractInteractions: jest.fn(() => Promise.resolve()), - getPendingContractInteractions: jest.fn(() => Promise.resolve()), - getConfirmedContractInteractions: jest.fn(() => Promise.resolve()), - getFailedContractInteractions: jest.fn(() => Promise.resolve()), - getSuccessfulContractInteractions: jest.fn(() => Promise.resolve()), - getContractInteraction: jest.fn(() => Promise.resolve()), - getContractInteractionsByAddress: jest.fn(() => Promise.resolve()), - getContractInteractionsByBlockHeight: jest.fn(() => Promise.resolve()), - getContractInteractionsByTimestamp: jest.fn(() => Promise.resolve()), - getContractInteractionsByTransactionId: jest.fn(() => Promise.resolve()), - getContractInteractionsByWalletAddress: jest.fn(() => Promise.resolve()), - getRecord: jest.fn, any[]>(() => { - throw new Error('Not implemented'); - }), - getRecords: jest.fn(() => Promise.resolve()), - isDomainReserved: jest.fn(() => Promise.resolve()), - isDomainAvailable: jest.fn(() => Promise.resolve()), - getTokenBalance: jest.fn(() => Promise.resolve()), -})); - -export default ArweaveCompositeDataProviderMock;