Skip to content

Commit

Permalink
feat: add tokens and refacto custom ERC20s (#509)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjlevesque committed May 11, 2021
1 parent 67898bb commit 897bdac
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 200 deletions.
3 changes: 2 additions & 1 deletion packages/currency/package.json
Expand Up @@ -37,7 +37,8 @@
"lint": "eslint \"src/**/*.ts\"",
"prepare": "yarn run build",
"test": "jest",
"test:watch": "yarn test --watch"
"test:watch": "yarn test --watch",
"add-erc20": "ts-node scripts/addErc20.ts"
},
"dependencies": {
"@metamask/contract-metadata": "1.25.0",
Expand Down
87 changes: 87 additions & 0 deletions packages/currency/scripts/addErc20.ts
@@ -0,0 +1,87 @@
/* eslint-disable import/no-extraneous-dependencies */
// @ts-check
import { ethers } from 'ethers';
import { exec } from 'child_process';
import { promisify } from 'util';
import { getDefaultProvider } from '@requestnetwork/payment-detection';
import ora from 'ora';
import yargs from 'yargs';

const erc20Abi = [
// Read-Only Functions
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
'function name() view returns (string)',
];

type IOptions = {
address: string;
network: string;
skipUpgrade: boolean;
};

const handler = async ({ address, network, skipUpgrade }: IOptions) => {
const spinner = ora('Initializing...').start();
if (!skipUpgrade) {
spinner.text = 'upgrading @metamask/contract-metadata';
await promisify(exec)('yarn add @metamask/contract-metadata --exact');
}

spinner.text = 'Fetch contract metadata';

const checksumAddress = ethers.utils.getAddress(address);

// check if the token exists in the list
// eslint-disable-next-line @typescript-eslint/no-var-requires
const metamaskContractMap = require('@metamask/contract-metadata');
if (metamaskContractMap[checksumAddress]) {
console.log(`This token is already listed on @metamask/contract-metadata`);
return;
}

const provider = getDefaultProvider(network);
const erc20 = new ethers.Contract(address, erc20Abi, provider);

const decimals = await erc20.decimals();
const symbol = await erc20.symbol();
const name = await erc20.name();

spinner.stop();

console.log(
JSON.stringify(
{
[checksumAddress]: {
name,
symbol,
decimals,
},
},
null,
2,
),
);
};

yargs(process.argv.slice(2)).command<IOptions>(
'$0 [address]',
'Fetch info about the desired ERC20 token, for the given network',
(builder) => {
return builder
.positional('address', {
describe: 'The token address',
type: 'string',
})
.option('network', {
describe: 'The network of the token (mainnet, rinkeby, matic...)',
default: 'mainnet',
})
.option('skipUpgrade', {
describe: 'Skips the @metamask/contract-metadata upgrade',
default: false,
type: 'boolean',
})
.demandOption('address');
},
handler,
).argv;
95 changes: 51 additions & 44 deletions packages/currency/src/erc20/index.ts
@@ -1,5 +1,30 @@
import { RequestLogicTypes } from '@requestnetwork/types';
import { supportedNetworks, supportedNetworksDetails, ERC20SymbolDetails } from './networks';
import { ethers } from 'ethers';
import { supportedNetworks } from './networks';

const getTokenInfoFromSymbol = (symbol: string, network?: string) => {
for (const networkKey of Object.keys(supportedNetworks).filter(
(net) => !network || net === network,
)) {
const token = Object.entries(supportedNetworks[networkKey]).find(
([, info]) => info.symbol === symbol,
);
if (token) {
return {
address: token[0],
network: networkKey,
...token[1],
};
}
}
return null;
};

const getTokenInfoFromCurrency = (currency: RequestLogicTypes.ICurrency) => {
const network = currency.network || 'mainnet';
const address = ethers.utils.getAddress(currency.value.toLowerCase());
return supportedNetworks[network]?.[address];
};

/**
* Returns a Currency object for an ERC20, if found
Expand All @@ -10,20 +35,14 @@ export function getErc20Currency(
symbol: string,
network?: string,
): RequestLogicTypes.ICurrency | undefined {
// Check if it's on one of the other supported networks
if (network) {
if (network in supportedNetworks && supportedNetworks[network].has(symbol)) {
return supportedNetworks[network].get(symbol);
}
return;
}
for (network of Object.keys(supportedNetworks)) {
if (supportedNetworks[network].has(symbol)) {
return supportedNetworks[network].get(symbol);
}
const info = getTokenInfoFromSymbol(symbol, network);
if (info) {
return {
type: RequestLogicTypes.CURRENCY.ERC20,
value: info.address,
network: info.network,
};
}

return;
}

/**
Expand All @@ -33,20 +52,11 @@ export function getErc20Currency(
* @returns The number of decimals for the ERC20 currency
*/
export function getErc20Decimals(currency: RequestLogicTypes.ICurrency): number {
const network = currency.network || 'mainnet';
let erc20Token;

// Get the decimals from one of the supported ERC20 networks
if (network in supportedNetworksDetails) {
erc20Token = Object.values(supportedNetworksDetails[network]).find(
({ address }) => address.toLowerCase() === currency.value.toLowerCase(),
);
}
const erc20Token = getTokenInfoFromCurrency(currency);

if (erc20Token) {
return erc20Token.decimals;
}

// If no supported ERC20 is found, throw error
throw new Error(`Unsupported ERC20 address: ${currency.value}`);
}
Expand All @@ -58,25 +68,19 @@ export function getErc20Decimals(currency: RequestLogicTypes.ICurrency): number
* @returns the ERC20 currency symbol string
*/
export function getErc20Symbol(currency: RequestLogicTypes.ICurrency): string | null {
const network = currency.network || 'mainnet';

if (currency.type !== RequestLogicTypes.CURRENCY.ERC20) {
throw new Error('Can only get symbol for ERC20 currencies');
}

// Find ERC20 symbol in one of the other supported ERC20 networks
if (network in supportedNetworks) {
const entry = [...supportedNetworks[network].entries()].find(
([, obj]) => currency.value.toLowerCase() === obj.value.toLowerCase(),
);
return entry ? entry[0] : null;
}

return null;
const erc20Token = getTokenInfoFromCurrency(currency);
return erc20Token ? erc20Token.symbol : null;
}

interface ERC20TokenDetails extends ERC20SymbolDetails {
interface ERC20TokenDetails {
address: string;
decimals: number;
name: string;
symbol: string;
network: string;
}

/**
Expand All @@ -85,13 +89,16 @@ interface ERC20TokenDetails extends ERC20SymbolDetails {
* @returns List of supported ERC20 tokens
*/
export function getSupportedERC20Tokens(): ERC20TokenDetails[] {
return Object.entries(supportedNetworksDetails).reduce(
return Object.entries(supportedNetworks).reduce(
(acc: ERC20TokenDetails[], [networkName, supportedCurrencies]) => {
return [
...acc,
...Object.entries(supportedCurrencies).map(([symbol, token]) => ({
...token,
symbol: `${symbol}${networkName !== 'mainnet' ? `-${networkName}` : ''}`,
...Object.entries(supportedCurrencies).map(([address, token]) => ({
address,
network: networkName,
decimals: token.decimals,
name: token.name,
symbol: `${token.symbol}${networkName !== 'mainnet' ? `-${networkName}` : ''}`,
})),
];
},
Expand All @@ -105,13 +112,13 @@ export function getSupportedERC20Tokens(): ERC20TokenDetails[] {
* @returns List of supported ERC20 currencies
*/
export function getSupportedERC20Currencies(): RequestLogicTypes.ICurrency[] {
return Object.entries(supportedNetworksDetails).reduce(
return Object.entries(supportedNetworks).reduce(
(acc: RequestLogicTypes.ICurrency[], [networkName, supportedCurrencies]) => {
return [
...acc,
...Object.entries(supportedCurrencies).map(([, token]) => ({
...Object.keys(supportedCurrencies).map((address) => ({
network: networkName,
value: token.address,
value: address,
type: RequestLogicTypes.CURRENCY.ERC20,
})),
];
Expand Down
21 changes: 4 additions & 17 deletions packages/currency/src/erc20/networks/celo.ts
@@ -1,23 +1,10 @@
import { RequestLogicTypes } from '@requestnetwork/types';
import { TokenMap } from './types';

// List of the supported celo network tokens
export const supportedCeloERC20 = new Map([
export const supportedCeloERC20: TokenMap = {
// cUSD token (https://explorer.celo.org/address/0x765de816845861e75a25fca122bb6898b8b1282a/transactions)
[
'CUSD',
{
network: 'celo',
type: RequestLogicTypes.CURRENCY.ERC20,
value: '0x765DE816845861e75A25fCA122bb6898B8B1282a',
},
],
]);

// Additional details about the supported rinkeby ERC20 tokens.
export const supportedCeloERC20Details = {
// Request Central Bank token, used for testing on rinkeby.
CUSD: {
address: '0x765DE816845861e75A25fCA122bb6898B8B1282a',
'0x765DE816845861e75A25fCA122bb6898B8B1282a': {
symbol: 'CUSD',
decimals: 18,
name: 'Celo Dollar',
},
Expand Down
45 changes: 7 additions & 38 deletions packages/currency/src/erc20/networks/index.ts
@@ -1,45 +1,14 @@
import { RequestLogicTypes } from '@requestnetwork/types';
import { supportedRinkebyERC20, supportedRinkebyERC20Details } from './rinkeby';
import { supportedMainnetERC20, supportedMainnetERC20Details } from './mainnet';
import { supportedCeloERC20, supportedCeloERC20Details } from './celo';
import { supportedMaticERC20, supportedMaticERC20Details } from './matic';
import { supportedRinkebyERC20 } from './rinkeby';
import { supportedMainnetERC20 } from './mainnet';
import { supportedCeloERC20 } from './celo';
import { supportedMaticERC20 } from './matic';
import type { TokenMap } from './types';

/**
* ERC20 Symbol details type
*/
export interface ERC20SymbolDetails {
address: string;
decimals: number;
name: string;
}

interface ISupportedNetworksMap {
[network: string]: Map<
string,
{
network: string;
type: RequestLogicTypes.CURRENCY;
value: string;
}
>;
}

interface ISupportedNetworksDetails {
[network: string]: {
[symbol: string]: ERC20SymbolDetails;
};
}

export const supportedNetworks: ISupportedNetworksMap = {
export const supportedNetworks: Record<string, TokenMap> = {
celo: supportedCeloERC20,
rinkeby: supportedRinkebyERC20,
mainnet: supportedMainnetERC20,
matic: supportedMaticERC20,
};

export const supportedNetworksDetails: ISupportedNetworksDetails = {
celo: supportedCeloERC20Details,
rinkeby: supportedRinkebyERC20Details,
mainnet: supportedMainnetERC20Details,
matic: supportedMaticERC20Details,
};
export type { TokenMap };

0 comments on commit 897bdac

Please sign in to comment.