Skip to content

Commit

Permalink
fix: racing condition in SendModal
Browse files Browse the repository at this point in the history
  • Loading branch information
irisdv committed Nov 27, 2024
1 parent 3949fbd commit be04c2e
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 53 deletions.
7 changes: 4 additions & 3 deletions packages/starknet-snap/src/rpcs/get-addr-from-starkname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type GetAddrFromStarkNameResponse = Infer<
>;

/**
* The RPC handler to add a ERC20 asset.
* The RPC handler to get a StarkName by a Starknet address.
*/
export class GetAddrFromStarkNameRpc extends RpcController<
GetAddrFromStarkNameParams,
Expand Down Expand Up @@ -81,9 +81,10 @@ export class GetAddrFromStarkNameRpc extends RpcController<
const { chainId, starkName } = params;

const network = await this.getNetworkFromChainId(chainId);
const getAddressResp = await getAddrFromStarkNameUtil(network, starkName);

return getAddressResp;
const address = await getAddrFromStarkNameUtil(network, starkName);

return address;
}
}

Expand Down
34 changes: 34 additions & 0 deletions packages/starknet-snap/src/utils/starknetUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,37 @@ describe('getEstimatedFees', () => {
});
});
});

describe('isValidStarkName', () => {
it.each([
{ starkName: 'valid.stark', expected: true },
{ starkName: 'valid-name.stark', expected: true },
{ starkName: 'valid123.stark', expected: true },
{ starkName: 'valid-name123.stark', expected: true },
{ starkName: 'valid.subdomain.stark', expected: true },
{ starkName: '1-valid.stark', expected: true },
{
starkName: 'valid-name-with-many-subdomains.valid.subdomain.stark',
expected: true,
},
{
starkName: 'too-long-stark-domain-name-more-than-48-characters.stark',
expected: false,
},
{ starkName: 'invalid..stark', expected: false },
{ starkName: 'invalid@stark', expected: false },
{ starkName: 'invalid_name.stark', expected: false },
{ starkName: 'invalid space.stark', expected: false },
{ starkName: 'invalid.starknet', expected: false },
{ starkName: '.invalid.stark', expected: false },
{ starkName: 'invalid.', expected: false },
{ starkName: 'invalid.stark.', expected: false },
{ starkName: '', expected: false },
])(
'validates `$starkName` correctly and returns $expected',
({ starkName, expected }) => {
const result = starknetUtils.isValidStarkName(starkName);
expect(result).toBe(expected);
},
);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { KeyboardEvent, ChangeEvent } from 'react';
import { KeyboardEvent, ChangeEvent, useEffect } from 'react';
import {
InputHTMLAttributes,
useRef,
Expand All @@ -24,32 +24,42 @@ import {
Wrapper,
} from './AddressInput.style';
import { STARKNET_ADDRESS_LENGTH } from 'utils/constants';
import { useStarkNetSnap } from 'services';
import { useAppSelector } from 'hooks/redux';

interface Props extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
setIsValidAddress?: Dispatch<SetStateAction<boolean>>;
onResolvedAddress?: (address: string) => void;
resolvedAddress?: string;
}

export const AddressInputView = ({
disabled,
onChange,
label,
setIsValidAddress,
onResolvedAddress,
resolvedAddress,
...otherProps
}: Props) => {
const networks = useAppSelector((state) => state.networks);
const chainId = networks?.items[networks.activeNetwork]?.chainId;
const { getAddrFromStarkName } = useStarkNetSnap();
const [focused, setFocused] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const [error, setError] = useState('');
const [valid, setValid] = useState(false);
const [info, setInfo] = useState('');

useEffect(() => {
if (!inputRef.current || !resolvedAddress) {
return;
}

const { valid, error, info } = validateAddress(
inputRef.current.value,
resolvedAddress,
);

setValid(valid);
setError(error);
setInfo(info);
}, [resolvedAddress]);

const displayIcon = () => {
return valid || error !== '';
};
Expand All @@ -68,39 +78,37 @@ export const AddressInputView = ({
//Check if valid address
onChange && onChange(event);

if (!inputRef.current) {
return;
}
const isValid =
inputRef.current.value !== '' && isValidAddress(inputRef.current.value);
if (isValid) {
setValid(true);
setError('');
onResolvedAddress?.(inputRef.current.value);
} else if (isValidStarkName(inputRef.current.value)) {
setValid(false);
setError('');
if (!inputRef.current) return;

getAddrFromStarkName(inputRef.current.value, chainId).then((address) => {
if (isValidAddress(address)) {
setValid(true);
setError('');
setInfo(shortenAddress(address as string, 12) as string);
onResolvedAddress?.(address as string);
} else {
setValid(false);
setError('.stark name not found');
setInfo('');
}
});
} else {
setValid(false);
setError('Invalid address format');
setInfo('');
}
const { valid, error, info } = validateAddress(
inputRef.current.value,
resolvedAddress,
);

setValid(valid);
setError(error);
setInfo(info);

if (setIsValidAddress) {
setIsValidAddress(isValid);
setIsValidAddress(valid);
}
};

const validateAddress = (value: string, addr: string | undefined) => {
if (value !== '' && isValidAddress(value)) {
return { valid: true, error: '', info: '' };
} else if (isValidStarkName(value)) {
if (addr && isValidAddress(addr)) {
return {
valid: true,
error: '',
info: shortenAddress(addr, 12) as string,
};
} else {
return { valid: false, error: '.stark name not found', info: '' };
}
} else {
return { valid: false, error: 'Invalid address format', info: '' };
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useRef, useState } from 'react';
import { AmountInput } from 'components/ui/molecule/AmountInput';
import { SendSummaryModal } from '../SendSummaryModal';
import {
Expand Down Expand Up @@ -43,6 +43,7 @@ export const SendModalView = ({ closeModal }: Props) => {
});
const [errors, setErrors] = useState({ amount: '', address: '' });
const [resolvedAddress, setResolvedAddress] = useState('');
const controllerRef = useRef<AbortController | null>(null);

const handleChange = (fieldName: string, fieldValue: string) => {
//Check if input amount does not exceed user balance
Expand All @@ -69,19 +70,46 @@ export const SendModalView = ({ closeModal }: Props) => {
case 'address':
if (fieldValue !== '') {
if (isValidAddress(fieldValue)) {
setResolvedAddress(fieldValue);
break;
} else if (isValidStarkName(fieldValue)) {
getAddrFromStarkName(fieldValue, chainId).then((address) => {
if (isValidAddress(address)) {
setResolvedAddress(address);
} else {
setErrors((prevErrors) => ({
...prevErrors,
address: '.stark name doesn’t exist',
}));
}
});
// Cancel previous request
controllerRef.current?.abort();

// Create a new AbortController for this request
const abortController = new AbortController();
controllerRef.current = abortController;

getAddrFromStarkNameQuery(fieldValue, {
signal: abortController.signal,
})
.then((address) => {
if (abortController.signal.aborted) return;
if (isValidAddress(address)) {
setResolvedAddress(address);
} else {
setResolvedAddress('');
setErrors((prevErrors) => ({
...prevErrors,
address: '.stark name doesn’t exist',
}));
}
})
.catch((error) => {
if (error.name !== 'AbortError') {
setResolvedAddress('');
setErrors((prevErrors) => ({
...prevErrors,
address: '.stark name doesn’t exist',
}));
}
})
.finally(() => {
if (controllerRef.current === abortController)
controllerRef.current = null;
});
} else {
setResolvedAddress('');
setErrors((prevErrors) => ({
...prevErrors,
address: 'Invalid address format',
Expand Down Expand Up @@ -111,6 +139,33 @@ export const SendModalView = ({ closeModal }: Props) => {
);
};

const getAddrFromStarkNameQuery = async (
starkName: string,
options?: { signal?: AbortSignal },
): Promise<string> => {
const { signal } = options || {};

return new Promise((resolve, reject) => {
// Check if the request is already aborted
if (signal?.aborted) {
return reject(new DOMException('Request aborted', 'AbortError'));
}

const abortHandler = () => {
reject(new DOMException('Request aborted', 'AbortError'));
};

signal?.addEventListener('abort', abortHandler);

getAddrFromStarkName(starkName, chainId)
.then(resolve)
.catch(reject)
.finally(() => {
signal?.removeEventListener('abort', abortHandler);
});
});
};

return (
<>
{!summaryModalOpen && (
Expand All @@ -127,7 +182,7 @@ export const SendModalView = ({ closeModal }: Props) => {
label="To"
placeholder="Paste recipient address or .stark name here"
onChange={(value) => handleChange('address', value.target.value)}
onResolvedAddress={(address) => setResolvedAddress(address)}
resolvedAddress={resolvedAddress}
/>
<SeparatorSmall />
<MessageAlert
Expand Down

0 comments on commit be04c2e

Please sign in to comment.