diff --git a/blockchain/contracts/PoBA.sol b/blockchain/contracts/PoBA.sol index 96cc114..588f383 100644 --- a/blockchain/contracts/PoBA.sol +++ b/blockchain/contracts/PoBA.sol @@ -5,31 +5,75 @@ contract PoBA { address public owner; address public signer; - mapping (address => string[]) public accounts; - - constructor() { + constructor() public { owner = msg.sender; signer = owner; } + struct BankAccount { + string accountNumber; + string bankName; + uint256 attestationDate; + bool attestationFact; + + uint256 creationBlock; + } + + struct User { + uint256 creationBlock; + BankAccount[] bankAccounts; + } + + mapping (address => User) public users; + function signerIsValid(bytes32 data, uint8 v, bytes32 r, bytes32 s) public constant returns (bool) { bytes memory prefix = "\x19Ethereum Signed Message:\n32"; - bytes32 prefixed = keccak256(prefix, data); + bytes32 prefixed = keccak256(abi.encodePacked(prefix, data)); return (ecrecover(prefixed, v, r, s) == signer); } - function register(string account, uint8 v, bytes32 r, bytes32 s) { + function userExists(address wallet) + public view returns (bool) + { + return (users[wallet].creationBlock > 0); + } + + function register( + string account, + string institution, + uint8 v, bytes32 r, bytes32 s) public { + require(bytes(account).length > 0); + require(bytes(institution).length > 0); + require(users[msg.sender].bankAccounts.length < 2**256-1); + + if (!userExists(msg.sender)) { + // new user + users[msg.sender].creationBlock = block.number; + } + + BankAccount memory ba; + + ba.accountNumber = account; + ba.bankName = institution; + ba.attestationDate = now; + ba.attestationFact = true; + ba.creationBlock = block.number; - bytes32 hash = keccak256(msg.sender, account); + bytes32 hash = keccak256( + abi.encodePacked( + msg.sender, + ba.accountNumber, + ba.bankName + )); require(signerIsValid(hash, v, r, s)); - accounts[msg.sender].push(account); + users[msg.sender].bankAccounts.push(ba); } function accountsLength(address _address) public constant returns (uint256) { - return accounts[_address].length; + return users[_address].bankAccounts.length; } } diff --git a/frontend/src/PoBA.js b/frontend/src/PoBA.js index f47911d..0b00217 100644 --- a/frontend/src/PoBA.js +++ b/frontend/src/PoBA.js @@ -14,8 +14,8 @@ const PobaContract = contract(pobaArtifact) const getBankAccounts = async token => { const result = await axios.get(`/api/accounts/bank-accounts/${token}`) - - return result.data.accounts.numbers + const { ach, eft } = result.data.accounts.numbers + return [...ach, ...eft] } const getSignedBankAccount = async (accountId, ethAccount, token) => { @@ -80,9 +80,16 @@ class PoBA extends Component { this.setState({ loading: true }) return getSignedBankAccount(accountId, ethAccount, token) .then(txData => { - return this.pobaContract.register(txData.account, txData.v, txData.r, txData.s, { - from: ethAccount - }) + return this.pobaContract.register( + txData.bankAccount.account, + txData.bankAccount.institution, + txData.v, + txData.r, + txData.s, + { + from: ethAccount + } + ) }) .then( () => successAlert(), diff --git a/frontend/src/ui/BankAccountList.js b/frontend/src/ui/BankAccountList.js index 3275231..3022ffe 100644 --- a/frontend/src/ui/BankAccountList.js +++ b/frontend/src/ui/BankAccountList.js @@ -22,11 +22,9 @@ const Button = glamorous.button(buttonStyles, { export default ({ bankAccounts, onClick }) => ( {bankAccounts.map((bankAccount, index) => ( -
+
Bank account number: {bankAccount.account} - +
))} diff --git a/server/controllers/accounts.js b/server/controllers/accounts.js index 08fa03e..b910635 100644 --- a/server/controllers/accounts.js +++ b/server/controllers/accounts.js @@ -20,11 +20,26 @@ const getBankAccounts = async accessToken => { } } +const getInstitutionById = async institutionId => { + try { + return await plaidClient.getInstitutionById(institutionId) + } catch (error) { + logger.error({ status: error.status_code }, 'Error getting institution') + throw Error(`[getInstitutionById] ${error.error_code}: ${error.error_message}`) + } +} + const getBankAccount = async (accessToken, accountId) => { const bankAccounts = await getBankAccounts(accessToken) - const numberBankAccount = bankAccounts.numbers.filter(account => account.account_id === accountId) - logger.info({ numberBankAccount }, 'Got bank account') - return numberBankAccount[0] + const { numbers, item } = bankAccounts + const ach = numbers.ach.filter(account => account.account_id === accountId) + const eft = numbers.eft.filter(account => account.account_id === accountId) + const number = [...ach, ...eft][0] + const institutionId = item.institution_id + const { institution } = await getInstitutionById(institutionId) + const bankAccount = { account: number.account, institution: institution.name } + logger.info(bankAccount, 'Got bank account') + return bankAccount } module.exports = { diff --git a/server/etc/plaid.js b/server/etc/plaid.js index 076d72f..cec1721 100644 --- a/server/etc/plaid.js +++ b/server/etc/plaid.js @@ -5,7 +5,8 @@ const plaidClient = new plaid.Client( PLAID_CLIENT_ID, PLAID_SECRET, PLAID_PUBLIC_KEY, - plaid.environments[PLAID_ENV] + plaid.environments[PLAID_ENV], + { version: '2018-05-22' } ) module.exports = plaidClient diff --git a/server/routes/accounts.js b/server/routes/accounts.js index 5b0f373..8b6c172 100644 --- a/server/routes/accounts.js +++ b/server/routes/accounts.js @@ -36,10 +36,10 @@ const signBankAccount = (req, res) => { const accessToken = await accountsController.getAccessToken(token) - const { account } = await accountsController.getBankAccount(accessToken, accountId) - const hash = web3.utils.sha3(ethAccount + Buffer.from(account).toString('hex')) + const bankAccount = await accountsController.getBankAccount(accessToken, accountId) + const hash = web3.utils.sha3(ethAccount + Buffer.from(bankAccount.account).toString('hex')) const { v, r, s } = web3.eth.accounts.sign(hash, PRIVATE_KEY) - return res.send({ account, v, r, s }) + return res.send({ bankAccount, v, r, s }) } catch (e) { logger.error(e.message, 'There was an error getting the transaction data') return res.status(400).send({ error: e.message }) diff --git a/server/tests/accounts.controller.spec.js b/server/tests/accounts.controller.spec.js index 14fad0e..2468647 100644 --- a/server/tests/accounts.controller.spec.js +++ b/server/tests/accounts.controller.spec.js @@ -7,6 +7,7 @@ const mockExchangePublicToken = mocks.exchangePublicToken const mockExchangePublicTokenError = mocks.exchangePublicTokenError const mockGetAuth = mocks.getAuth const mockGetAuthError = mocks.getAuthError +const mockGetInstitutionById = mocks.getInstitutionById jest.mock('../etc/plaid', () => ({ exchangePublicToken: jest.fn(publicToken => { @@ -16,7 +17,8 @@ jest.mock('../etc/plaid', () => ({ getAuth: jest.fn(accessToken => { if (accessToken === mockAccessToken) return mockGetAuth return mockGetAuthError - }) + }), + getInstitutionById: jest.fn(() => mockGetInstitutionById) })) describe('[controllers] accounts', () => { @@ -39,8 +41,12 @@ describe('[controllers] accounts', () => { }) }) it('should return only one bank account', async () => { - const accountId = mockGetAuth.numbers[0].account_id + const accountId = mockGetAuth.numbers.ach[0].account_id const account = await accountsController.getBankAccount(mockAccessToken, accountId) - expect(account).toEqual(mockGetAuth.numbers[0]) + const expected = { + account: mockGetAuth.numbers.ach[0].account, + institution: mockGetInstitutionById.institution.name + } + expect(account).toEqual(expected) }) }) diff --git a/server/tests/accounts.route.spec.js b/server/tests/accounts.route.spec.js index 6180df9..4802402 100644 --- a/server/tests/accounts.route.spec.js +++ b/server/tests/accounts.route.spec.js @@ -6,6 +6,14 @@ const mockEthAccount = '0x92970dbD5C0Ee6b439422bFd7cD71e1DDA921A03' const mockToken = 'public-token-de3ce8ef-33f8-452c-a685-8671031fc0f6' const mockAccessToken = mocks.exchangePublicToken.access_token const mockBankAccounts = mocks.getAuth +const mockBankAccount = { + bankAccount: { + account: '9900009606', + account_id: 'vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D', + routing: '011401533', + wire_routing: '021000021' + } +} const mockSign = { v: '0x1b', r: '0x9a4acff8fcc5fc48278482669d3db5a728a226c8f82bce2895208c59ca5637b9', @@ -22,8 +30,8 @@ jest.mock('../controllers/accounts', () => ({ throw new Error('[getBankAccounts] INVALID_TOKEN: Error getting bank accounts') }), getBankAccount: jest.fn((accessToken, accountId) => { - if (accessToken === mockAccessToken && accountId === mockBankAccounts.numbers[0].account_id) - return mockBankAccounts.numbers[0] + if (accessToken === mockAccessToken && accountId === mockBankAccounts.numbers.ach[0].account_id) + return mockBankAccounts.numbers.ach[0] throw new Error('There was an error getting the transaction data') }) })) @@ -47,7 +55,8 @@ describe('[routes] accounts', () => { .then(res => { const { accounts } = res.body expect(res.status).toEqual(200) - expect(accounts.numbers.length).toBeGreaterThanOrEqual(0) + expect(accounts.numbers.ach.length).toBeGreaterThanOrEqual(0) + expect(accounts.numbers.eft.length).toBeGreaterThanOrEqual(0) })) it('should return error', () => request(app) @@ -60,7 +69,7 @@ describe('[routes] accounts', () => { .post('/api/accounts/sign-account') .send({ token: mockToken, - accountId: mockBankAccounts.numbers[0].account_id + accountId: mockBankAccounts.numbers.ach[0].account_id }) .then(res => { expect(res.status).toEqual(404) @@ -70,7 +79,7 @@ describe('[routes] accounts', () => { .post('/api/accounts/sign-account') .send({ ethAccount: mockEthAccount, - accountId: mockBankAccounts.numbers[0].account_id + accountId: mockBankAccounts.numbers.ach[0].account_id }) .then(res => { expect(res.status).toEqual(404) @@ -91,7 +100,7 @@ describe('[routes] accounts', () => { .send({ ethAccount: mockEthAccount, token: `${mockToken}-fail`, - accountId: mockBankAccounts.numbers[0].account_id + accountId: mockBankAccounts.numbers.ach[0].account_id }) .then(res => { expect(res.status).toEqual(400) @@ -102,11 +111,10 @@ describe('[routes] accounts', () => { .send({ ethAccount: mockEthAccount, token: mockToken, - accountId: mockBankAccounts.numbers[0].account_id + accountId: mockBankAccounts.numbers.ach[0].account_id }) .then(res => { - const { account } = mockBankAccounts.numbers[0] - expect(res.body).toEqual({ account, ...mockSign }) + expect(res.body).toEqual({ ...mockBankAccount, ...mockSign }) expect(res.status).toEqual(200) })) }) diff --git a/server/tests/utils/mocks.js b/server/tests/utils/mocks.js index 5ff18b9..1939b2c 100644 --- a/server/tests/utils/mocks.js +++ b/server/tests/utils/mocks.js @@ -29,14 +29,17 @@ const data = { type: 'depository' } ], - numbers: [ - { - account: '9900009606', - account_id: 'vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D', - routing: '011401533', - wire_routing: '021000021' - } - ], + numbers: { + ach: [ + { + account: '9900009606', + account_id: 'vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D', + routing: '011401533', + wire_routing: '021000021' + } + ], + eft: [] + }, item: { Object }, request_id: '45QSn', status_code: 200 @@ -48,6 +51,28 @@ const data = { error_type: 'INVALID_REQUEST', request_id: 'qcaHk', http_code: 404 + }, + getInstitutionById: { + institution: { + credentials: [ + { + label: 'User ID', + name: 'username', + type: 'text' + }, + { + label: 'Password', + name: 'password', + type: 'password' + } + ], + has_mfa: true, + institution_id: 'ins_109512', + mfa: ['code', 'list', 'questions', 'selections'], + name: 'Houndstooth Bank', + products: ['auth', 'balance', 'identity', 'transactions'] + }, + request_id: '9VpnU' } }