Skip to content

Commit

Permalink
chore: add utils to manage snap ui (#404)
Browse files Browse the repository at this point in the history
  • Loading branch information
stanleyyconsensys authored Oct 25, 2024
1 parent 774cfb1 commit 90f94a5
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 1 deletion.
14 changes: 14 additions & 0 deletions packages/starknet-snap/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { constants } from 'starknet';

import type { Erc20Token, Network } from './types/snapState';
import {
SnapEnv,
Expand All @@ -20,6 +22,9 @@ export type SnapConfig = {
defaultNetwork: Network;
availableNetworks: Network[];
preloadTokens: Erc20Token[];
explorer: {
[key: string]: string;
};
};

export const Config: SnapConfig = {
Expand All @@ -35,6 +40,15 @@ export const Config: SnapConfig = {
STARKNET_SEPOLIA_TESTNET_NETWORK,
],

explorer: {
[constants.StarknetChainId.SN_MAIN]:
// eslint-disable-next-line no-template-curly-in-string
'https://voyager.online/contract/${address}',
[constants.StarknetChainId.SN_SEPOLIA]:
// eslint-disable-next-line no-template-curly-in-string
'https://sepolia.voyager.online/contract/${address}',
},

preloadTokens: [
ETHER_MAINNET,
ETHER_SEPOLIA_TESTNET,
Expand Down
27 changes: 27 additions & 0 deletions packages/starknet-snap/src/utils/explorer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { constants } from 'starknet';

import { getExplorerUrl } from './explorer';

describe('getExplorerUrl', () => {
const address =
'0x074aaeb168bbd155d41290e6be09d80c9e937ee3d775eac19519a2fcc76fc61c';

it('returns a sepolia testnet explorer url', () => {
const result = getExplorerUrl(
address,
constants.StarknetChainId.SN_SEPOLIA,
);
expect(result).toBe(`https://sepolia.voyager.online/contract/${address}`);
});

it('returns a mainnet explorer url', () => {
const result = getExplorerUrl(address, constants.StarknetChainId.SN_MAIN);
expect(result).toBe(`https://voyager.online/contract/${address}`);
});

it('throws `Invalid Chain ID` error if the given Chain ID is not support', () => {
expect(() => getExplorerUrl(address, 'some Chain ID')).toThrow(
'Invalid Chain ID',
);
});
});
30 changes: 30 additions & 0 deletions packages/starknet-snap/src/utils/explorer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { constants } from 'starknet';

import { Config } from '../config';

/**
* Gets the explorer URL for a given address and Chain ID.
*
* @param address - The address to get the explorer URL for.
* @param chainId - The Chain ID.
* @returns The explorer URL as a string.
* @throws An error if an invalid scope is provided.
*/
export function getExplorerUrl(address: string, chainId: string): string {
switch (chainId) {
case constants.StarknetChainId.SN_MAIN:
return Config.explorer[constants.StarknetChainId.SN_MAIN].replace(
// eslint-disable-next-line no-template-curly-in-string
'${address}',
address,
);
case constants.StarknetChainId.SN_SEPOLIA:
return Config.explorer[constants.StarknetChainId.SN_SEPOLIA].replace(
// eslint-disable-next-line no-template-curly-in-string
'${address}',
address,
);
default:
throw new Error('Invalid Chain ID');
}
}
2 changes: 2 additions & 0 deletions packages/starknet-snap/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export * from './snap-state';
export * from './url';
export * from './string';
export * from './token';
export * from './snap-ui';
export * from './explorer';
// TODO: add other utils
130 changes: 130 additions & 0 deletions packages/starknet-snap/src/utils/snap-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { divider, heading, row, text } from '@metamask/snaps-sdk';

import { getExplorerUrl } from './explorer';
import { toJson } from './serializer';
import { shortenAddress } from './string';

/**
* Build a row component.
*
* @param params - The parameters.
* @param params.label - The label of the row component.
* @param params.value - The value of the row component.
* @returns A row component.
*/
export function rowUI({ label, value }: { label: string; value: string }) {
return row(
label,
text({
value,
markdown: false,
}),
);
}

/**
* Build a row component with the address.
*
* @param params - The parameters.
* @param params.label - The label.
* @param params.address - The address.
* @param [params.chainId] - The chain ID, when the chain ID is set, a exploder URL markdown will be generated.
* @param [params.shortern] - Whether to shorten the address. Default is true.
* @returns A row component with the address.
*/
export function addressUI({
label,
address,
chainId,
shortern = true,
}: {
label: string;
address: string;
chainId?: string;
shortern?: boolean;
}) {
let value = address;

if (shortern) {
value = shortenAddress(address);
}

if (chainId) {
value = `[${value}](${getExplorerUrl(address, chainId)})`;
}
return rowUI({
label,
value,
});
}

/**
* Build a row component with the network name.
*
* @param params - The parameters.
* @param params.networkName - The network name.
* @returns A row component with the network name.
*/
export function networkUI({ networkName }: { networkName: string }) {
return rowUI({
label: 'Network',
value: networkName,
});
}

/**
* Build a heading component.
*
* @param value - The header.
* @returns A heading component.
*/
export function headerUI(value: string) {
return heading(value);
}

/**
* Build a divider component
*
* @returns A divider component.
*/
export function dividerUI() {
return divider();
}

/**
* Build a row component with the signer address.
*
* @param params - The parameters.
* @param params.address - The signer address.
* @param params.chainId - The chain ID.
* @returns A row component with the signer address.
*/
export function signerUI({
address,
chainId,
}: {
address: string;
chainId: string;
}) {
return addressUI({
label: 'Signer Address',
address,
chainId,
shortern: true,
});
}

/**
* Build a row component with the JSON data.
*
* @param params - The parameters.
* @param params.data - The JSON data.
* @param params.label - The label.
* @returns A row component with the JSON data.
*/
export function jsonDataUI({ data, label }: { data: any; label: string }) {
return rowUI({
label,
value: toJson(data),
});
}
45 changes: 44 additions & 1 deletion packages/starknet-snap/src/utils/string.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { isAsciiString, isValidAsciiStrField } from './string';
import {
isAsciiString,
isValidAsciiStrField,
replaceMiddleChar,
shortenAddress,
} from './string';

describe('isAsciiString', () => {
it('returns true for a ASCII string', () => {
Expand Down Expand Up @@ -33,3 +38,41 @@ describe('isValidAsciiStrField', () => {
},
);
});

describe('replaceMiddleChar', () => {
const str =
'0x074aaeb168bbd155d41290e6be09d80c9e937ee3d775eac19519a2fcc76fc61c';
it('replaces the middle of a string', () => {
expect(replaceMiddleChar(str, 5, 3)).toBe('0x074...61c');
});

it('does not replace if the string is empty', () => {
expect(replaceMiddleChar('', 5, 3)).toBe('');
});

it('throws `Indexes must be positives` error if headLength or tailLength is negative value', () => {
expect(() => replaceMiddleChar(str, -1, 20)).toThrow(
'Indexes must be positives',
);
expect(() => replaceMiddleChar(str, 20, -10)).toThrow(
'Indexes must be positives',
);
});

it('throws `Indexes out of bounds` error if headLength + tailLength is out of bounds', () => {
expect(() => replaceMiddleChar(str, 100, 0)).toThrow(
'Indexes out of bounds',
);
expect(() => replaceMiddleChar(str, 0, 100)).toThrow(
'Indexes out of bounds',
);
});
});

describe('shortenAddress', () => {
const str =
'0x074aaeb168bbd155d41290e6be09d80c9e937ee3d775eac19519a2fcc76fc61c';
it('shorten an address', () => {
expect(shortenAddress(str)).toBe('0x074...c61c');
});
});
42 changes: 42 additions & 0 deletions packages/starknet-snap/src/utils/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,45 @@ export function isValidAsciiStrField(value: string, maxLength: number) {
isAsciiString(value) && value.trim().length > 0 && value.length <= maxLength
);
}

/**
* Replaces the middle characters of a string with a given string.
*
* @param str - The string to replace.
* @param headLength - The length of the head of the string that should not be replaced.
* @param tailLength - The length of the tail of the string that should not be replaced.
* @param replaceStr - The string to replace the middle characters with. Default is '...'.
* @returns The formatted string.
* @throws An error if the given headLength and tailLength cannot be replaced.
*/
export function replaceMiddleChar(
str: string,
headLength: number,
tailLength: number,
replaceStr = '...',
) {
if (!str) {
return str;
}
// Enforces indexes to be positive to avoid parameter swapping in `.substring`
if (headLength < 0 || tailLength < 0) {
throw new Error('Indexes must be positives');
}
// Check upper bound (using + is safe here, since we know that both lengths are positives)
if (headLength + tailLength > str.length) {
throw new Error('Indexes out of bounds');
}
return `${str.substring(0, headLength)}${replaceStr}${str.substring(
str.length - tailLength,
)}`;
}

/**
* Format the address in shorten string.
*
* @param address - The address to format.
* @returns The formatted address.
*/
export function shortenAddress(address: string) {
return replaceMiddleChar(address, 5, 4);
}

0 comments on commit 90f94a5

Please sign in to comment.