[OP-15] ethers switch for account tab data (#105)

pull/106/head
Chris Mckay 5 years ago committed by GitHub
parent abe344869d
commit dfcb606198
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      package.json
  2. 14
      src/chain/contracts/AccountIngress.ts
  3. 16
      src/chain/contracts/AccountRules.ts
  4. 16
      src/chain/contracts/Admin.ts
  5. 14
      src/chain/contracts/NodeIngress.ts
  6. 16
      src/chain/contracts/NodeRules.ts
  7. 21
      src/chain/provider.ts
  8. 2
      src/containers/Tabs/Account.tsx
  9. 16
      src/context/__test__/__snapshots__/accountData.test.tsx.snap
  10. 16
      src/context/__test__/__snapshots__/adminData.test.tsx.snap
  11. 15
      src/context/__test__/accountData.test.tsx
  12. 4
      src/context/__test__/adminData.test.tsx
  13. 2
      src/context/__test__/network.test.tsx
  14. 82
      src/context/accountData.tsx
  15. 48
      src/context/network.tsx
  16. 24
      src/util/configLoader.ts
  17. 16
      yarn.lock

@ -17,13 +17,16 @@
"coverage:contracts": "yarn solidity-coverage",
"coverage": "yarn run coverage:contracts",
"build:app": "yarn react-scripts build",
"build:contracts": "yarn truffle compile && yarn typechain --target truffle --outDir ./src/chain/@types 'src/chain/abis/*.json'",
"build:contracts": "yarn truffle compile && yarn typechain --target ethers --outDir ./src/chain/@types 'src/chain/abis/*.json'",
"build": "yarn run build:contracts && yarn run build:app",
"start": "yarn react-scripts start",
"eject": "yarn react-scripts eject"
},
"eslintConfig": {
"extends": ["react-app", "plugin:prettier/recommended"]
"extends": [
"react-app",
"plugin:prettier/recommended"
]
},
"browserslist": {
"production": [
@ -50,6 +53,7 @@
"dotenv": "^7.0.0",
"drizzle": "^1.4.0",
"drizzle-react": "^1.3.0",
"ethers": "^4.0.32",
"idx": "^2.5.6",
"jest-junit": "^6.4.0",
"node-sass": "^4.12.0",

@ -0,0 +1,14 @@
import { Contract } from 'ethers';
import { Provider } from 'ethers/providers';
import AccountIngressAbi from '../abis/AccountIngress.json';
import { AccountIngress } from '../@types/AccountIngress';
import { Config } from '../../util/configLoader';
let instance: AccountIngress | null = null;
export const accountIngressFactory = async (config: Config, provider: Provider) => {
if (instance) return instance;
instance = new Contract(config.accountIngressAddress, AccountIngressAbi.abi, provider) as AccountIngress;
return instance;
};

@ -0,0 +1,16 @@
import { Contract } from 'ethers';
import AccountRulesAbi from '../abis/AccountRules.json';
import { AccountIngress } from '../@types/AccountIngress';
import { AccountRules } from '../@types/AccountRules';
let instance: AccountRules | null = null;
export const accountRulesFactory = async (ingressInstance: AccountIngress) => {
if (instance) return instance;
const ruleContractName = await ingressInstance.functions.RULES_CONTRACT();
const accountRulesAddress = await ingressInstance.functions.getContractAddress(ruleContractName);
instance = new Contract(accountRulesAddress, AccountRulesAbi.abi, ingressInstance.provider) as AccountRules;
return instance;
};

@ -0,0 +1,16 @@
import { Contract } from 'ethers';
import AdminAbi from '../abis/Admin.json';
import { AccountIngress } from '../@types/AccountIngress';
import { NodeIngress } from '../@types/NodeIngress';
import { Admin } from '../@types/Admin';
let instance: Admin | null = null;
export default async (ingressInstance: AccountIngress | NodeIngress) => {
if (instance) return instance;
const adminAddress = await ingressInstance.functions.getContractAddress('ADMIN');
instance = new Contract(adminAddress, AdminAbi.abi, ingressInstance.provider) as Admin;
return instance;
};

@ -0,0 +1,14 @@
import { Contract } from 'ethers';
import { Provider } from 'ethers/providers';
import NodeIngressAbi from '../abis/NodeIngress.json';
import { NodeIngress } from '../@types/NodeIngress';
import { Config } from '../../util/configLoader';
let instance: NodeIngress | null = null;
export const nodeIngressFactory = async (config: Config, provider: Provider) => {
if (instance) return instance;
instance = new Contract(config.nodeIngressAddress, NodeIngressAbi.abi, provider) as NodeIngress;
return instance;
};

@ -0,0 +1,16 @@
import { Contract } from 'ethers';
import NodeRulesAbi from '../abis/NodeRules.json';
import { NodeIngress } from '../@types/NodeIngress';
import { NodeRules } from '../@types/NodeRules';
let instance: NodeRules | null = null;
export default async (ingressInstance: NodeIngress) => {
if (instance) return instance;
const ruleContractName = await ingressInstance.functions.RULES_CONTRACT();
const nodeRulesAddress = await ingressInstance.functions.getContractAddress(ruleContractName);
instance = new Contract(nodeRulesAddress, NodeRulesAbi.abi, ingressInstance.provider) as NodeRules;
return instance;
};

@ -0,0 +1,21 @@
import { Provider } from 'ethers/providers';
import { ethers } from 'ethers';
import Web3 from 'web3';
let provider: Provider | undefined = undefined;
let web3: Web3 | undefined = undefined;
const web3Factory = async () => {
if (web3) return web3;
web3 = new Web3(Web3.givenProvider);
return web3;
};
export const providerFactory = async () => {
if (provider) return provider;
const web3 = await web3Factory();
provider = new ethers.providers.Web3Provider(web3.currentProvider);
return provider;
};

@ -153,7 +153,7 @@ const AccountTabContainer: React.FC<AccountTabContainerProps> = ({ isOpen }) =>
deleteTransaction={deleteTransaction}
isValid={isValidAccount}
isOpen={isOpen}
isReadOnly={isReadOnly}
isReadOnly={isReadOnly!}
pendingLock={!!transactions.get('lock')}
/>
);

@ -1,16 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AccountDataProvider /> matches snapshot 1`] = `
<ContextProvider
value={
Object {
"accountWhitelist": Array [],
"setAccountWhitelist": [Function],
}
}
>
<div
className="test"
/>
</ContextProvider>
`;

@ -1,16 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AdminDataProvider /> matches snapshot 1`] = `
<ContextProvider
value={
Object {
"admins": Array [],
"setAdmins": [Function],
}
}
>
<div
className="test"
/>
</ContextProvider>
`;

@ -1,13 +1,22 @@
// Libs
import React from 'react';
import toJson from 'enzyme-to-json';
import { mocked } from 'ts-jest/utils';
import { shallow, ShallowWrapper } from 'enzyme';
// Components
import { AccountDataProvider } from '../accountData';
import { useNetwork } from '../network';
jest.mock('../network', () => ({
useNetwork: jest.fn()
}));
describe('<AccountDataProvider />', () => {
let wrapper: ShallowWrapper;
beforeAll(() => {
mocked(useNetwork).mockImplementation((): any => ({}));
});
beforeEach(() => {
wrapper = shallow(
<AccountDataProvider>
@ -19,8 +28,4 @@ describe('<AccountDataProvider />', () => {
it('renders children when passed in', () => {
expect(wrapper.contains(<div className="test" />)).toEqual(true);
});
it('matches snapshot', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
});

@ -19,8 +19,4 @@ describe('<AdminDataProvider />', () => {
it('renders children when passed in', () => {
expect(wrapper.contains(<div className="test" />)).toEqual(true);
});
it('matches snapshot', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
});

@ -51,7 +51,7 @@ describe('useNetwork', () => {
networkId: undefined,
status: ''
}));
mocked(useConfig).mockImplementation(() => ({}));
mocked(useConfig).mockImplementation((): any => ({}));
testHook(() => (network = useNetwork()), NetworkProvider);
});

@ -1,12 +1,19 @@
import React, { createContext, useContext, useEffect, useState, useMemo } from 'react';
import { drizzleReactHooks } from 'drizzle-react';
import { AccountRules } from '../chain/@types/AccountRules';
import { accountRulesFactory } from '../chain/contracts/AccountRules';
import { useNetwork } from './network';
type Account = { address: string };
type ContextType =
| {
accountWhitelist: Account[];
setAccountWhitelist: (account: Account[]) => void;
setAccountWhitelist: React.Dispatch<React.SetStateAction<Account[]>>;
accountReadOnly?: boolean;
setAccountReadOnly: React.Dispatch<React.SetStateAction<boolean | undefined>>;
accountRulesContract?: AccountRules;
setAccountRulesContract: React.Dispatch<React.SetStateAction<AccountRules | undefined>>;
}
| undefined;
@ -21,7 +28,38 @@ const AccountDataContext = createContext<ContextType>(undefined);
*/
export const AccountDataProvider: React.FC = (props: React.Props<{}>) => {
const [accountWhitelist, setAccountWhitelist] = useState<Account[]>([]);
const value = useMemo(() => ({ accountWhitelist, setAccountWhitelist }), [accountWhitelist, setAccountWhitelist]);
const [accountReadOnly, setAccountReadOnly] = useState<boolean | undefined>(undefined);
const [accountRulesContract, setAccountRulesContract] = useState<AccountRules | undefined>(undefined);
const value = useMemo(
() => ({
accountWhitelist,
setAccountWhitelist,
accountReadOnly,
setAccountReadOnly,
accountRulesContract,
setAccountRulesContract
}),
[
accountWhitelist,
setAccountWhitelist,
accountReadOnly,
setAccountReadOnly,
accountRulesContract,
setAccountRulesContract
]
);
const { accountIngressContract } = useNetwork();
useEffect(() => {
if (accountIngressContract === undefined) {
setAccountRulesContract(undefined);
} else {
accountRulesFactory(accountIngressContract).then(contract => setAccountRulesContract(contract));
}
}, [accountIngressContract]);
return <AccountDataContext.Provider value={value} {...props} />;
};
@ -41,30 +79,28 @@ export const useAccountData = () => {
throw new Error('useAccountData must be used within an AccountDataProvider.');
}
const { accountWhitelist, setAccountWhitelist } = context;
const { drizzle, useCacheCall } = drizzleReactHooks.useDrizzle();
const accountIsReadOnly: boolean = useCacheCall('AccountRules', 'isReadOnly');
const accountWhitelistSize: number = useCacheCall('AccountRules', 'getSize');
const { getByIndex: getAccountByIndex } = drizzle.contracts.AccountRules.methods;
const { accountWhitelist, setAccountWhitelist, accountReadOnly, setAccountReadOnly, accountRulesContract } = context;
const { userAddress } = drizzleReactHooks.useDrizzleState((drizzleState: any) => ({
userAddress: drizzleState.accounts[0]
}));
useEffect(() => {
const promises = [];
for (let index = 0; index < accountWhitelistSize; index++) {
promises.push(getAccountByIndex(index).call());
if (accountRulesContract === undefined) {
setAccountWhitelist([]);
setAccountReadOnly(undefined);
} else {
accountRulesContract.functions.isReadOnly().then(isReadOnly => setAccountReadOnly(isReadOnly));
accountRulesContract.functions.getSize().then(whitelistSize => {
const whitelistElementsPromises = [];
for (let i = 0; whitelistSize.gt(i); i++) {
whitelistElementsPromises.push(accountRulesContract.functions.getByIndex(i));
}
Promise.all(whitelistElementsPromises).then(responses => {
setAccountWhitelist(responses.map(address => ({ address })));
});
});
}
Promise.all(promises).then(responses => {
const updatedAccountWhitelist = responses.map((address: string) => ({ address }));
setAccountWhitelist(updatedAccountWhitelist);
});
}, [accountWhitelistSize, setAccountWhitelist, getAccountByIndex]);
const dataReady = useMemo(() => typeof accountIsReadOnly === 'boolean' && Array.isArray(accountWhitelist), [
accountIsReadOnly,
accountWhitelist
]);
}, [accountRulesContract, setAccountWhitelist, setAccountReadOnly]);
const formattedAccountWhitelist = useMemo(() => {
return accountWhitelist
@ -76,10 +112,14 @@ export const useAccountData = () => {
.reverse();
}, [accountWhitelist]);
const dataReady = useMemo(() => {
return accountRulesContract !== undefined && accountReadOnly !== undefined && accountWhitelist !== undefined;
}, [accountRulesContract, accountReadOnly, accountWhitelist]);
return {
userAddress,
dataReady,
whitelist: formattedAccountWhitelist,
isReadOnly: accountIsReadOnly
isReadOnly: accountReadOnly
};
};

@ -11,12 +11,24 @@ import Admin from '../chain/abis/Admin.json';
import { getAllowedNetworks } from '../util/contracts';
import { useConfig } from '../context/configData';
import { providerFactory } from '../chain/provider';
import { AccountIngress } from '../chain/@types/AccountIngress';
import { accountIngressFactory } from '../chain/contracts/AccountIngress';
import { NodeIngress } from '../chain/@types/NodeIngress';
import { nodeIngressFactory } from '../chain/contracts/NodeIngress';
type ContextType =
| {
isCorrectNetwork?: boolean;
setIsCorrectNetwork: React.Dispatch<React.SetStateAction<boolean | undefined>>;
web3Initialized: boolean;
setWeb3Initialized: React.Dispatch<React.SetStateAction<boolean>>;
contracts: {
accountIngressContract?: AccountIngress;
setAccountIngressContract: React.Dispatch<React.SetStateAction<AccountIngress | undefined>>;
nodeIngressContract?: NodeIngress;
setNodeIngressContract: React.Dispatch<React.SetStateAction<NodeIngress | undefined>>;
};
}
| undefined;
@ -37,6 +49,8 @@ const NetworkContext = createContext<ContextType>(undefined);
export const NetworkProvider: React.FC<{}> = props => {
const [isCorrectNetwork, setIsCorrectNetwork] = useState<boolean | undefined>(undefined);
const [web3Initialized, setWeb3Initialized] = useState<boolean>(false);
const [accountIngressContract, setAccountIngressContract] = useState<AccountIngress | undefined>(undefined);
const [nodeIngressContract, setNodeIngressContract] = useState<NodeIngress | undefined>(undefined);
const config = useConfig();
const [drizzle] = useState<Drizzle>(() => {
@ -46,14 +60,36 @@ export const NetworkProvider: React.FC<{}> = props => {
return new Drizzle(options, drizzleStore);
});
useEffect(() => {
providerFactory().then(provider => {
accountIngressFactory(config, provider).then(accountIngress => setAccountIngressContract(accountIngress));
nodeIngressFactory(config, provider).then(nodeIngress => setNodeIngressContract(nodeIngress));
});
}, [config]);
const value = useMemo(
() => ({
isCorrectNetwork,
setIsCorrectNetwork,
web3Initialized,
setWeb3Initialized
setWeb3Initialized,
contracts: {
accountIngressContract,
setAccountIngressContract,
nodeIngressContract,
setNodeIngressContract
}
}),
[isCorrectNetwork, setIsCorrectNetwork, web3Initialized, setWeb3Initialized]
[
isCorrectNetwork,
setIsCorrectNetwork,
web3Initialized,
setWeb3Initialized,
accountIngressContract,
setAccountIngressContract,
nodeIngressContract,
setNodeIngressContract
]
);
return (
@ -75,10 +111,10 @@ export const NetworkProvider: React.FC<{}> = props => {
export const useNetwork = () => {
const context = useContext(NetworkContext);
if (!context) {
throw new Error('useData must be used within a DataProvider.');
throw new Error('useNetwork must be used within a DataProvider.');
}
const { isCorrectNetwork, setIsCorrectNetwork, web3Initialized, setWeb3Initialized } = context;
const { isCorrectNetwork, setIsCorrectNetwork, web3Initialized, setWeb3Initialized, contracts } = context;
const { networkId, status } = drizzleReactHooks.useDrizzleState((drizzleState: any) => ({
status: drizzleState.web3.status,
@ -99,6 +135,8 @@ export const useNetwork = () => {
return {
isCorrectNetwork,
networkId,
web3Initialized
web3Initialized,
accountIngressContract: contracts.accountIngressContract,
nodeIngressContract: contracts.nodeIngressContract
};
};

@ -1,5 +1,9 @@
import AccountIngress from '../chain/abis/AccountIngress.json';
import NodeIngress from '../chain/abis/NodeIngress.json';
export type Config = {
AccountRulesAddress?: string;
accountIngressAddress: string;
nodeIngressAddress: string;
};
const loadConfig = async (): Promise<Config> => {
@ -10,15 +14,27 @@ const loadConfig = async (): Promise<Config> => {
if (response.ok) {
return response.json().catch((reason: any) => {
console.log('config parsing failed with error:', reason);
return {};
throw new Error('Config parsing failed with error: ' + reason);
});
} else {
console.log('Failed to load config file');
return {};
throw new Error('Config file not found');
}
// development defaults
} else {
return {};
const accountIngressNetworks = Object.values(AccountIngress.networks);
if (accountIngressNetworks.length === 0) {
throw new Error("Account Ingress Contract abi doesn't contain any networks, probably not deployed");
}
const accountIngressAddress = (accountIngressNetworks[0] as { address: string }).address;
const nodeIngressNetworks = Object.values(NodeIngress.networks);
if (nodeIngressNetworks.length === 0) {
throw new Error("Node Ingress Contract abi doesn't contain any networks, probably not deployed");
}
const nodeIngressAddress = (nodeIngressNetworks[0] as { address: string }).address;
return { accountIngressAddress, nodeIngressAddress };
}
};

@ -5211,6 +5211,22 @@ ethers@^4.0.27:
uuid "2.0.1"
xmlhttprequest "1.8.0"
ethers@^4.0.32:
version "4.0.32"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.32.tgz#46378864cb3bf29b57c2effd17508b560743abf6"
integrity sha512-r0k2tBNF6MYEsvwmINeP3VPppD/7eAZyiOk/ifDDawXGCKqr3iEQkPq6OZSDVD+4Jie38WPteS9thXzpn2+A5Q==
dependencies:
"@types/node" "^10.3.2"
aes-js "3.0.0"
bn.js "^4.4.0"
elliptic "6.3.3"
hash.js "1.1.3"
js-sha3 "0.5.7"
scrypt-js "2.0.4"
setimmediate "1.0.4"
uuid "2.0.1"
xmlhttprequest "1.8.0"
ethjs-unit@0.1.6, ethjs-unit@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699"

Loading…
Cancel
Save