Skip to content

Commit

Permalink
feat: transfer to a stark name
Browse files Browse the repository at this point in the history
  • Loading branch information
irisdv committed Nov 25, 2024
1 parent 6a0236b commit ab2753b
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 5 deletions.
34 changes: 34 additions & 0 deletions packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,40 @@
}
},
"errors": []
},
{
"name": "starkNet_getAddrFromStarkName",
"summary": "Get address from a stark name",
"paramStructure": "by-name",
"params": [
{
"name": "starkName",
"summary": "stark name of the user",
"description": "stark name of the user",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "chainId",
"summary": "Id of the target Starknet network",
"description": "Id of the target Starknet network (default to Starknet Goerli Testnet)",
"required": false,
"schema": {
"$ref": "#/components/schemas/CHAIN_ID"
}
}
],
"result": {
"name": "result",
"summary": "Address of the given stark name",
"description": "Address of the given stark name",
"schema": {
"$ref": "#/components/schemas/ADDRESS"
}
},
"errors": []
}
],
"components": {
Expand Down
32 changes: 32 additions & 0 deletions packages/starknet-snap/src/getAddrFromStarkName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { toJson } from './utils/serializer';
import { getAddrFromStarkNameUtil } from '../src/utils/starknetUtils';
import { ApiParams, GetAddrFromStarkNameRequestParam } from './types/snapApi';
import { getNetworkFromChainId } from './utils/snapUtils';
import { logger } from './utils/logger';

export async function getAddrFromStarkName(params: ApiParams) {
try {
const { state, requestParams } = params;
const requestParamsObj = requestParams as GetAddrFromStarkNameRequestParam;

if (!requestParamsObj.starkName) {
throw new Error(
`The given stark name need to be non-empty string, got: ${toJson(
requestParamsObj,
)}`,
);
}

const starkName = requestParamsObj.starkName;

const network = getNetworkFromChainId(state, requestParamsObj.chainId);

const resp = await getAddrFromStarkNameUtil(network, starkName);
logger.log(`getAddrFromStarkName: addr:\n${toJson(resp)}`);

return resp;
} catch (err) {
logger.error(`Problem found: ${err}`);
throw err;
}
}
4 changes: 4 additions & 0 deletions packages/starknet-snap/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { extractPublicKey } from './extractPublicKey';
import { getCurrentNetwork } from './getCurrentNetwork';
import { getErc20TokenBalance } from './getErc20TokenBalance';
import { getStarkName } from './getStarkName';
import { getAddrFromStarkName } from './getAddrFromStarkName';
import { getStoredErc20Tokens } from './getStoredErc20Tokens';
import { getStoredNetworks } from './getStoredNetworks';
import { getStoredTransactions } from './getStoredTransactions';
Expand Down Expand Up @@ -289,6 +290,9 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
apiParams.requestParams as unknown as GetDeploymentDataParams,
);

case 'starkNet_getAddrFromStarkName':
return await getAddrFromStarkName(apiParams);

default:
throw new MethodNotFoundError() as unknown as Error;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/starknet-snap/src/types/snapApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,9 @@ export enum FeeTokenUnit {
ETH = 'wei',
STRK = 'fri',
}

export type GetAddrFromStarkNameRequestParam = {
starkName: string;
} & BaseRequestParams;

/* eslint-enable */
8 changes: 8 additions & 0 deletions packages/starknet-snap/src/utils/starknetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1367,3 +1367,11 @@ export const validateAccountRequireUpgradeOrDeploy = async (
throw new DeployRequiredError();
}
};

export const getAddrFromStarkNameUtil = async (
network: Network,
starkName: string,
) => {
const provider = getProvider(network);
return Account.getAddressFromStarkName(provider, starkName);
};
100 changes: 100 additions & 0 deletions packages/starknet-snap/test/src/getAddrFromStarkName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { WalletMock } from '../wallet.mock.test';
import * as utils from '../../src/utils/starknetUtils';
import { SnapState } from '../../src/types/snapState';
import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants';
import { Mutex } from 'async-mutex';
import {
ApiParams,
GetAddrFromStarkNameRequestParam,
} from '../../src/types/snapApi';
import { getAddrFromStarkName } from '../../src/getAddrFromStarkName';

chai.use(sinonChai);
const sandbox = sinon.createSandbox();

describe('Test function: getAddrFromStarkName', function () {
const walletStub = new WalletMock();
const state: SnapState = {
accContracts: [],
erc20Tokens: [],
networks: [STARKNET_SEPOLIA_TESTNET_NETWORK],
transactions: [],
};
const apiParams: ApiParams = {
state,
requestParams: {},
wallet: walletStub,
saveMutex: new Mutex(),
};

afterEach(function () {
walletStub.reset();
sandbox.restore();
});

it('should retrieve the address from stark name successfully', async function () {
sandbox.stub(utils, 'getAddrFromStarkNameUtil').callsFake(async () => {
return '0x01c744953f1d671673f46a9179a58a7e58d9299499b1e076cdb908e7abffe69f';
});
const requestObject: GetAddrFromStarkNameRequestParam = {
starkName: 'testName.stark',
};
apiParams.requestParams = requestObject;
const result = await getAddrFromStarkName(apiParams);
expect(result).to.be.eq(
'0x01c744953f1d671673f46a9179a58a7e58d9299499b1e076cdb908e7abffe69f',
);
});

it('should throw error if getAddrFromStarkNameUtil failed', async function () {
sandbox.stub(utils, 'getAddrFromStarkNameUtil').throws(new Error());
const requestObject: GetAddrFromStarkNameRequestParam = {
starkName: 'testName.stark',
};
apiParams.requestParams = requestObject;

let result;
try {
await getAddrFromStarkName(apiParams);
} catch (err) {
result = err;
} finally {
expect(result).to.be.an('Error');
}
});

it('should throw error if the stark name is empty', async function () {
const requestObject: GetAddrFromStarkNameRequestParam = {
starkName: '',
};
apiParams.requestParams = requestObject;

let result;
try {
await getAddrFromStarkName(apiParams);
} catch (err) {
result = err;
} finally {
expect(result).to.be.an('Error');
}
});

it('should throw error if the user address is invalid', async function () {
const requestObject: GetAddrFromStarkNameRequestParam = {
starkName: 'invalidName',
};
apiParams.requestParams = requestObject;

let result;
try {
await getAddrFromStarkName(apiParams);
} catch (err) {
result = err;
} finally {
expect(result).to.be.an('Error');
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,11 @@ export const Icon = styled(FontAwesomeIcon).attrs<IIcon>((props) => ({
? props.theme.palette.error.main
: props.theme.palette.success.main,
}))<IIcon>``;

export const InfoText = styled.div`
font-size: ${(props) => props.theme.typography.p2.fontSize};
font-family: ${(props) => props.theme.typography.p2.fontFamily};
color: ${(props) => props.theme.palette.grey.black};
padding-top: ${(props) => props.theme.spacing.tiny};
padding-left: ${(props) => props.theme.spacing.small};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,49 @@ import {
Dispatch,
SetStateAction,
} from 'react';
import { isSpecialInputKey, isValidAddress } from 'utils/utils';
import {
isSpecialInputKey,
isValidAddress,
isValidStarkName,
shortenAddress,
} from 'utils/utils';
import { HelperText } from 'components/ui/atom/HelperText';
import { Label } from 'components/ui/atom/Label';
import {
Icon,
InfoText,
Input,
InputContainer,
Left,
RowWrapper,
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;
}

export const AddressInputView = ({
disabled,
onChange,
label,
setIsValidAddress,
onResolvedAddress,
...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('');

const displayIcon = () => {
return valid || error !== '';
Expand Down Expand Up @@ -62,9 +76,27 @@ export const AddressInputView = ({
if (isValid) {
setValid(true);
setError('');
onResolvedAddress?.(inputRef.current.value);
} else if (isValidStarkName(inputRef.current.value)) {
setValid(false);
setError('');

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('');
}

if (setIsValidAddress) {
Expand Down Expand Up @@ -106,6 +138,7 @@ export const AddressInputView = ({
</Left>
</InputContainer>
{error && <HelperText>{error}</HelperText>}
{info && <InfoText>{info}</InfoText>}
</Wrapper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ import {
import { useAppSelector } from 'hooks/redux';
import { ethers } from 'ethers';
import { AddressInput } from 'components/ui/molecule/AddressInput';
import { isValidAddress } from 'utils/utils';
import { isValidAddress, isValidStarkName } from 'utils/utils';
import { Bold, Normal } from '../../ConnectInfoModal/ConnectInfoModal.style';
import { DropDown } from 'components/ui/molecule/DropDown';
import { DEFAULT_FEE_TOKEN } from 'utils/constants';
import { FeeToken } from 'types';
import { useStarkNetSnap } from 'services';

interface Props {
closeModal?: () => void;
}

export const SendModalView = ({ closeModal }: Props) => {
const networks = useAppSelector((state) => state.networks);
const chainId = networks?.items[networks.activeNetwork]?.chainId;
const wallet = useAppSelector((state) => state.wallet);
const { getAddrFromStarkName } = useStarkNetSnap();
const [summaryModalOpen, setSummaryModalOpen] = useState(false);
const [fields, setFields] = useState({
amount: '',
Expand All @@ -39,6 +42,7 @@ export const SendModalView = ({ closeModal }: Props) => {
feeToken: DEFAULT_FEE_TOKEN, // Default fee token
});
const [errors, setErrors] = useState({ amount: '', address: '' });
const [resolvedAddress, setResolvedAddress] = useState('');

const handleChange = (fieldName: string, fieldValue: string) => {
//Check if input amount does not exceed user balance
Expand All @@ -64,7 +68,20 @@ export const SendModalView = ({ closeModal }: Props) => {
break;
case 'address':
if (fieldValue !== '') {
if (!isValidAddress(fieldValue)) {
if (isValidAddress(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',
}));
}
});
} else {
setErrors((prevErrors) => ({
...prevErrors,
address: 'Invalid address format',
Expand Down Expand Up @@ -108,8 +125,9 @@ export const SendModalView = ({ closeModal }: Props) => {
</Network>
<AddressInput
label="To"
placeholder="Paste recipient address here"
placeholder="Paste recipient address or .stark name here"
onChange={(value) => handleChange('address', value.target.value)}
onResolvedAddress={(address) => setResolvedAddress(address)}
/>
<SeparatorSmall />
<MessageAlert
Expand Down Expand Up @@ -161,7 +179,7 @@ export const SendModalView = ({ closeModal }: Props) => {
{summaryModalOpen && (
<SendSummaryModal
closeModal={closeModal}
address={fields.address}
address={resolvedAddress}
amount={fields.amount}
chainId={fields.chainId}
selectedFeeToken={fields.feeToken} // Pass the selected fee token
Expand Down
Loading

0 comments on commit ab2753b

Please sign in to comment.