diff --git a/packages/attestation-service/config/config.json b/packages/attestation-service/config/config.json new file mode 100644 index 00000000000..4a471de5f52 --- /dev/null +++ b/packages/attestation-service/config/config.json @@ -0,0 +1,21 @@ +{ + "development": { + "username": "root", + "password": null, + "database": "database_development", + "host": "db/dev.db", + "dialect": "sqlite", + "operatorsAliases": false + }, + "test": { + "username": "root", + "password": null, + "database": "database_test", + "host": "127.0.0.1", + "dialect": "sqlite", + "operatorsAliases": false + }, + "production": { + "use_env_variable": "DATABASE_URL" + } +} diff --git a/packages/celotool/src/cmds/account/verify.ts b/packages/celotool/src/cmds/account/verify.ts index fbb371a80a0..d8245931955 100644 --- a/packages/celotool/src/cmds/account/verify.ts +++ b/packages/celotool/src/cmds/account/verify.ts @@ -5,6 +5,7 @@ import { ActionableAttestation, AttestationsWrapper, } from '@celo/contractkit/lib/wrappers/Attestations' +import { concurrentMap } from '@celo/utils/lib/async' import { base64ToHex } from '@celo/utils/lib/attestations' import prompts from 'prompts' import { switchToClusterFromEnv } from 'src/lib/cluster' @@ -54,12 +55,13 @@ async function verifyCmd(argv: VerifyArgv) { const attestations = await kit.contracts.getAttestations() const accounts = await kit.contracts.getAccounts() await printCurrentCompletedAttestations(attestations, argv.phone, account) - let attestationsToComplete = await attestations.getActionableAttestations(argv.phone, account) // Request more attestations if (argv.num > attestationsToComplete.length) { - console.info(`Requesting ${argv.num - attestationsToComplete.length} attestations`) + console.info( + `Requesting ${argv.num - attestationsToComplete.length} attestations from the smart contract` + ) await requestMoreAttestations( attestations, argv.phone, @@ -78,9 +80,9 @@ async function verifyCmd(argv: VerifyArgv) { } attestationsToComplete = await attestations.getActionableAttestations(argv.phone, account) - // Find attestations we can reveal/verify - console.info(`Revealing ${attestationsToComplete.length} attestations`) - await revealAttestations(attestationsToComplete, attestations, argv.phone) + // Find attestations we can verify + console.info(`Requesting ${attestationsToComplete.length} attestations from issuers`) + await requestAttestationsFromIssuers(attestationsToComplete, attestations, argv.phone, account) await promptForCodeAndVerify(attestations, argv.phone, account) } @@ -115,18 +117,28 @@ async function requestMoreAttestations( await attestations.selectIssuers(phoneNumber).then((txo) => txo.sendAndWaitForReceipt()) } -async function revealAttestations( +async function requestAttestationsFromIssuers( attestationsToReveal: ActionableAttestation[], attestations: AttestationsWrapper, - phoneNumber: string + phoneNumber: string, + account: string ) { - return Promise.all( - attestationsToReveal.map(async (attestation) => - attestations - .reveal(phoneNumber, attestation.issuer) - .then((txo) => txo.sendAndWaitForReceipt()) - ) - ) + return concurrentMap(5, attestationsToReveal, async (attestation) => { + try { + const response = await attestations.revealPhoneNumberToIssuer( + phoneNumber, + account, + attestation.issuer, + attestation.attestationServiceURL + ) + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}: ${await response.text()}`) + } + } catch (error) { + console.error(`Error requesting attestations from issuer ${attestation.issuer}`) + console.error(error) + } + }) } async function verifyCode( diff --git a/packages/celotool/src/e2e-tests/attestations_tests.ts b/packages/celotool/src/e2e-tests/attestations_tests.ts index b557f2e83cc..4e30437bc01 100644 --- a/packages/celotool/src/e2e-tests/attestations_tests.ts +++ b/packages/celotool/src/e2e-tests/attestations_tests.ts @@ -61,8 +61,9 @@ describe('governance tests', () => { const stats = await Attestations.getAttestationStat(phoneNumber, validatorAddress) assert.equal(stats.total, 2) - const actionable = await Attestations.getActionableAttestations(phoneNumber, validatorAddress) - assert.lengthOf(actionable, 2) + + const issuers = await Attestations.getAttestationIssuers(phoneNumber, validatorAddress) + assert.lengthOf(issuers, 2) }) }) }) diff --git a/packages/contractkit/src/wrappers/Attestations.ts b/packages/contractkit/src/wrappers/Attestations.ts index 701cd14e68e..878a84a8561 100644 --- a/packages/contractkit/src/wrappers/Attestations.ts +++ b/packages/contractkit/src/wrappers/Attestations.ts @@ -1,10 +1,13 @@ -import { ECIES, PhoneNumberUtils, SignatureUtils } from '@celo/utils' -import { sleep } from '@celo/utils/lib/async' -import { zip3 } from '@celo/utils/lib/collections' +import { PhoneNumberUtils, SignatureUtils } from '@celo/utils' +import { concurrentMap, sleep } from '@celo/utils/lib/async' +import { notEmpty, zip3 } from '@celo/utils/lib/collections' +import { parseSolidityStringArray } from '@celo/utils/lib/parsing' import BigNumber from 'bignumber.js' +import fetch from 'cross-fetch' import * as Web3Utils from 'web3-utils' import { Address, CeloContract, NULL_ADDRESS } from '../base' import { Attestations } from '../generated/types/Attestations' +import { ClaimTypes, IdentityMetadataWrapper } from '../identity' import { BaseWrapper, proxyCall, @@ -45,16 +48,10 @@ export enum AttestationState { export interface ActionableAttestation { issuer: Address - attestationState: AttestationState blockNumber: number - publicKey: string + attestationServiceURL: string } -const parseAttestationInfo = (rawState: { 0: string; 1: string }) => ({ - attestationState: parseInt(rawState[0], 10), - blockNumber: parseInt(rawState[1], 10), -}) - function attestationMessageToSign(phoneHash: string, account: Address) { const messageHash: string = Web3Utils.soliditySha3( { type: 'bytes32', value: phoneHash }, @@ -63,6 +60,23 @@ function attestationMessageToSign(phoneHash: string, account: Address) { return messageHash } +interface GetCompletableAttestationsResponse { + 0: string[] + 1: string[] + 2: string[] + 3: string[] +} +function parseGetCompletableAttestations(response: GetCompletableAttestationsResponse) { + const metadataURLs = parseSolidityStringArray( + response[2].map(toNumber), + (response[3] as unknown) as string + ) + + return zip3(response[0].map(toNumber), response[1], metadataURLs).map( + ([blockNumber, issuer, metadataURL]) => ({ blockNumber, issuer, metadataURL }) + ) +} + const stringIdentity = (x: string) => x export class AttestationsWrapper extends BaseWrapper { /** @@ -129,6 +143,17 @@ export class AttestationsWrapper extends BaseWrapper { await sleep(pollDurationSeconds * 1000) } } + + /** + * Returns the issuers of attestations for a phoneNumber/account combo + * @param phoneNumber Phone Number + * @param account Account + */ + getAttestationIssuers = proxyCall( + this.contract.methods.getAttestationIssuers, + tupleParser(PhoneNumberUtils.getPhoneHash, (x: string) => x) + ) + /** * Returns the attestation state of a phone number/account/issuer tuple * @param phoneNumber Phone Number @@ -179,7 +204,8 @@ export class AttestationsWrapper extends BaseWrapper { } /** - * Returns an array of attestations that can be completed, along with the issuers public key + * Returns an array of attestations that can be completed, along with the issuers' attestation + * service urls * @param phoneNumber * @param account */ @@ -187,42 +213,37 @@ export class AttestationsWrapper extends BaseWrapper { phoneNumber: string, account: Address ): Promise { - const accounts = await this.kit.contracts.getAccounts() const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) - const expiryBlocks = await this.attestationExpiryBlocks() - const currentBlockNumber = await this.kit.web3.eth.getBlockNumber() - - const issuers = await this.contract.methods.getAttestationIssuers(phoneHash, account).call() - const issuerState = Promise.all( - issuers.map((issuer) => - this.contract.methods - .getAttestationState(phoneHash, account, issuer) - .call() - .then(parseAttestationInfo) - ) - ) - // Typechain is not properly typing getDataEncryptionKey - const publicKeys: Promise = Promise.all( - issuers.map((issuer) => accounts.getDataEncryptionKey(issuer) as any) + const result = await this.contract.methods.getCompletableAttestations(phoneHash, account).call() + + const withAttestationServiceURLs = await concurrentMap( + 5, + parseGetCompletableAttestations(result), + async ({ blockNumber, issuer, metadataURL }) => { + try { + const metadata = await IdentityMetadataWrapper.fetchFromURL(metadataURL) + const attestationServiceURLClaim = metadata.findClaim(ClaimTypes.ATTESTATION_SERVICE_URL) + + if (attestationServiceURLClaim === undefined) { + throw new Error(`No attestation service URL registered for ${issuer}`) + } + + // TODO: Once we have status indicators, we should check if service is up + // https://github.com/celo-org/celo-monorepo/issues/1586 + return { + blockNumber, + issuer, + attestationServiceURL: attestationServiceURLClaim.url, + } + } catch (error) { + console.error(error) + return null + } + } ) - const isIncomplete = (status: AttestationState) => status === AttestationState.Incomplete - const hasNotExpired = (blockNumber: number) => currentBlockNumber < blockNumber + expiryBlocks - const isValidKey = (key: string) => key !== null && key !== '0x0' - - return zip3(issuers, await issuerState, await publicKeys) - .filter( - ([_issuer, attestation, publicKey]) => - isIncomplete(attestation.attestationState) && - hasNotExpired(attestation.blockNumber) && - isValidKey(publicKey) - ) - .map(([issuer, attestation, publicKey]) => ({ - ...attestation, - issuer, - publicKey: publicKey.toString(), - })) + return withAttestationServiceURLs.filter(notEmpty) } /** @@ -350,35 +371,23 @@ export class AttestationsWrapper extends BaseWrapper { return toTransactionObject(this.kit, this.contract.methods.selectIssuers(phoneHash)) } - /** - * Reveals the phone number to the issuer of the attestation on-chain - * @param phoneNumber The phone number which requested attestation - * @param issuer The address of issuer of the attestation - */ - async reveal(phoneNumber: string, issuer: Address) { - const accounts = await this.kit.contracts.getAccounts() - const publicKey: string = (await accounts.getDataEncryptionKey(issuer)) as any - - if (!publicKey) { - throw new Error('Issuer data encryption key is null') - } - - const encryptedPhone: any = - '0x' + - ECIES.Encrypt( - Buffer.from(publicKey.slice(2), 'hex'), - Buffer.from(phoneNumber, 'utf8') - ).toString('hex') - - return toTransactionObject( - this.kit, - this.contract.methods.reveal( - PhoneNumberUtils.getPhoneHash(phoneNumber), - encryptedPhone, + async revealPhoneNumberToIssuer( + phoneNumber: string, + account: Address, + issuer: Address, + serviceURL: string + ) { + return fetch(serviceURL + '/attestations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + account, + phoneNumber, issuer, - true - ) - ) + }), + }) } /** diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index 4e5d5bbea03..027eba383e8 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -199,6 +199,36 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { return accounts[account].metadataURL; } + /** + * @notice Getter for the metadata of multiple accounts. + * @param accountsToQuery The addresses of the accounts to get the metadata for. + * @return (stringLengths[] - the length of each string in bytes + * data - all strings concatenated + * ) + */ + function batchGetMetadataURL(address[] calldata accountsToQuery) + external + view + returns (uint256[] memory, bytes memory) + { + uint256 totalSize = 0; + uint256[] memory sizes = new uint256[](accountsToQuery.length); + for (uint256 i = 0; i < accountsToQuery.length; i = i.add(1)) { + sizes[i] = bytes(accounts[accountsToQuery[i]].metadataURL).length; + totalSize = totalSize.add(sizes[i]); + } + + bytes memory data = new bytes(totalSize); + uint256 pointer = 0; + for (uint256 i = 0; i < accountsToQuery.length; i = i.add(1)) { + for (uint256 j = 0; j < sizes[i]; j = j.add(1)) { + data[pointer] = bytes(accounts[accountsToQuery[i]].metadataURL)[j]; + pointer = pointer.add(1); + } + } + return (sizes, data); + } + /** * @notice Getter for the data encryption key and version. * @param account The address of the account to get the key for diff --git a/packages/protocol/contracts/common/interfaces/IAccounts.sol b/packages/protocol/contracts/common/interfaces/IAccounts.sol index 3c01142c8b4..b197c66b549 100644 --- a/packages/protocol/contracts/common/interfaces/IAccounts.sol +++ b/packages/protocol/contracts/common/interfaces/IAccounts.sol @@ -20,5 +20,9 @@ interface IAccounts { function getDataEncryptionKey(address) external view returns (bytes memory); function getWalletAddress(address) external view returns (address); function getMetadataURL(address) external view returns (string memory); + function batchGetMetadataURL(address[] calldata) + external + view + returns (uint256[] memory, bytes memory); function getName(address) external view returns (string memory); } diff --git a/packages/protocol/contracts/identity/Attestations.sol b/packages/protocol/contracts/identity/Attestations.sol index 6e225afbfb7..1551fe80c10 100644 --- a/packages/protocol/contracts/identity/Attestations.sol +++ b/packages/protocol/contracts/identity/Attestations.sol @@ -75,10 +75,6 @@ contract Attestations is mapping(bytes32 => IdentifierState) identifiers; - // Address of the RequestAttestation precompiled contract. - // solhint-disable-next-line state-visibility - address constant REQUEST_ATTESTATION = address(0xff); - // The duration in blocks in which an attestation can be completed from the block in which the // attestation was requested. uint256 public attestationExpiryBlocks; @@ -115,10 +111,6 @@ contract Attestations is event Withdrawal(address indexed account, address indexed token, uint256 amount); event AttestationExpiryBlocksSet(uint256 value); event AttestationRequestFeeSet(address indexed token, uint256 value); - event AttestorAuthorized(address indexed account, address attestor); - event AccountDataEncryptionKeySet(address indexed account, bytes dataEncryptionKey); - event AccountMetadataURLSet(address indexed account, string metadataURL); - event AccountWalletAddressSet(address indexed account, address walletAddress); event SelectIssuersWaitBlocksSet(uint256 value); function initialize( @@ -222,43 +214,6 @@ contract Attestations is delete state.unselectedRequests[msg.sender]; } - /** - * @notice Reveal the encrypted phone number to the issuer. - * @param identifier The hash of the identifier to be attested. - * @param encryptedPhone The number ECIES encrypted with the issuer's public key. - * @param issuer The issuer of the attestation. - * @param sendSms Whether or not to send an SMS. For testing purposes. - */ - function reveal(bytes32 identifier, bytes calldata encryptedPhone, address issuer, bool sendSms) - external - { - Attestation storage attestation = identifiers[identifier].attestations[msg.sender] - .issuedAttestations[issuer]; - - require(attestation.status == AttestationStatus.Incomplete, "Attestation is not incomplete"); - - // solhint-disable-next-line not-rely-on-time - require(!isAttestationExpired(attestation.blockNumber), "Attestation timed out"); - - // Generate the yet-to-be-signed attestation code that will be signed and sent to the - // encrypted phone number via SMS via the 'RequestAttestation' precompiled contract. - if (sendSms) { - bool success; - // solhint-disable-next-line avoid-call-value - (success, ) = REQUEST_ATTESTATION.call.value(0).gas(gasleft())( - abi.encode( - identifier, - keccak256(abi.encodePacked(identifier, msg.sender)), - msg.sender, - issuer, - encryptedPhone - ) - ); - - require(success, "sending SMS failed"); - } - } - /** * @notice Submit the secret message sent by the issuer to complete the attestation request. * @param identifier The hash of the identifier for this attestation. @@ -449,6 +404,49 @@ contract Attestations is } + /** + * @notice Returns the state of all attestations that are completable + * @param identifier Hash of the identifier. + * @param account Address of the account. + * @return ( blockNumbers[] - Block number of request/completion the attestation, + * issuers[] - Address of the issuer, + * stringLengths[] - The length of each metadataURL string for each issuer, + * stringData - All strings concatenated + * ) + */ + function getCompletableAttestations(bytes32 identifier, address account) + external + view + returns (uint32[] memory, address[] memory, uint256[] memory, bytes memory) + { + AttestedAddress storage state = identifiers[identifier].attestations[account]; + address[] storage issuers = state.selectedIssuers; + + uint256 num = 0; + for (uint256 i = 0; i < issuers.length; i = i.add(1)) { + if (isAttestationCompletable(state.issuedAttestations[issuers[i]])) { + num = num.add(1); + } + } + + uint32[] memory blockNumbers = new uint32[](num); + address[] memory completableIssuers = new address[](num); + + uint256 pointer = 0; + for (uint256 i = 0; i < issuers.length; i = i.add(1)) { + if (isAttestationCompletable(state.issuedAttestations[issuers[i]])) { + blockNumbers[pointer] = state.issuedAttestations[issuers[i]].blockNumber; + completableIssuers[pointer] = issuers[i]; + pointer = pointer.add(1); + } + } + + uint256[] memory stringLengths; + bytes memory stringData; + (stringLengths, stringData) = getAccounts().batchGetMetadataURL(completableIssuers); + return (blockNumbers, completableIssuers, stringLengths, stringData); + } + /** * @notice Returns the fee set for a particular token. * @param token Address of the attestationRequestFeeToken. @@ -568,7 +566,7 @@ contract Attestations is .sender]; bytes32 seed = getRandom().getBlockRandomness( - unselectedRequest.blockNumber + selectIssuersWaitBlocks + uint256(unselectedRequest.blockNumber).add(selectIssuersWaitBlocks) ); uint256 numberValidators = numberValidatorsInCurrentSet(); @@ -587,7 +585,7 @@ contract Attestations is continue; } - currentIndex++; + currentIndex = currentIndex.add(1); attestation.status = AttestationStatus.Incomplete; attestation.blockNumber = unselectedRequest.blockNumber; attestation.attestationRequestFeeToken = unselectedRequest.attestationRequestFeeToken; @@ -598,4 +596,9 @@ contract Attestations is function isAttestationExpired(uint128 attestationRequestBlock) internal view returns (bool) { return block.number >= uint256(attestationRequestBlock).add(attestationExpiryBlocks); } + + function isAttestationCompletable(Attestation storage attestation) internal view returns (bool) { + return (attestation.status == AttestationStatus.Incomplete && + !isAttestationExpired(attestation.blockNumber)); + } } diff --git a/packages/protocol/contracts/identity/interfaces/IAttestations.sol b/packages/protocol/contracts/identity/interfaces/IAttestations.sol index 3263fa789fc..92c3bc0e8cc 100644 --- a/packages/protocol/contracts/identity/interfaces/IAttestations.sol +++ b/packages/protocol/contracts/identity/interfaces/IAttestations.sol @@ -6,7 +6,6 @@ interface IAttestations { function setAttestationRequestFee(address, uint256) external; function request(bytes32, uint256, address) external; function selectIssuers(bytes32) external; - function reveal(bytes32, bytes calldata, address, bool) external; function complete(bytes32, uint8, bytes32, bytes32) external; function revoke(bytes32, uint256) external; function withdraw(address) external; @@ -24,5 +23,8 @@ interface IAttestations { external view returns (uint8, uint32, address); - + function getCompletableAttestations(bytes32, address) + external + view + returns (uint32[] memory, address[] memory, uint256[] memory, bytes memory); } diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index edc93be38c8..6e409ed4543 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -1,3 +1,4 @@ +import { parseSolidityStringArray } from '@celo/utils/lib/parsing' import { upperFirst } from 'lodash' import { AccountsInstance } from 'types' import { getParsedSignatureOfAddress } from '../../lib/signing-utils' @@ -288,6 +289,26 @@ contract('Accounts', (accounts: string[]) => { }) }) + describe('#batchGetMetadataURL', () => { + it('returns multiple metadata URLs', async () => { + const randomStrings = accounts.map((_) => web3.utils.randomHex(20).slice(2)) + await Promise.all( + accounts.map(async (account, i) => { + await accountsInstance.createAccount({ from: account }) + await accountsInstance.setMetadataURL(randomStrings[i], { from: account }) + }) + ) + const [stringLengths, data] = await accountsInstance.batchGetMetadataURL(accounts) + const strings = parseSolidityStringArray( + stringLengths.map((x) => x.toNumber()), + (data as unknown) as string + ) + for (let i = 0; i < accounts.length; i++) { + assert.equal(strings[i], randomStrings[i]) + } + }) + }) + describe('#setName', async () => { describe('when the account has not been created', () => { it('should revert', async () => { diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index 33b38af0d94..6b712d66c50 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -11,9 +11,10 @@ import { } from '@celo/protocol/lib/test-utils' import { attestToIdentifier } from '@celo/utils' import { privateKeyToAddress } from '@celo/utils/lib/address' +import { parseSolidityStringArray } from '@celo/utils/lib/parsing' import { getPhoneHash } from '@celo/utils/lib/phoneNumbers' import BigNumber from 'bignumber.js' -import { uniq } from 'lodash' +import { range, uniq } from 'lodash' import { AccountsContract, AccountsInstance, @@ -340,7 +341,7 @@ contract('Attestations', (accounts: string[]) => { await attestations.request(phoneHash, attestationsRequested, mockStableToken.address) }) - describe('when the issuers have not yet been revealed', () => { + describe('when the issuers have not yet been selected', () => { it('should revert requesting more attestations', async () => { await assertRevert(attestations.request(phoneHash, 1, mockStableToken.address)) }) @@ -353,7 +354,7 @@ contract('Attestations', (accounts: string[]) => { }) }) - describe('when the issuers have been revealed', async () => { + describe('when the issuers have been selected', async () => { beforeEach(async () => { const requestBlockNumber = await web3.eth.getBlockNumber() await random.addTestRandomness(requestBlockNumber + selectIssuersWaitBlocks, '0x1') @@ -414,6 +415,40 @@ contract('Attestations', (accounts: string[]) => { ) }) + it('should return the attestations in getCompletableAttestations', async () => { + await Promise.all( + accounts.map((account) => + accountsInstance.setMetadataURL(`https://test.com/${account}`, { from: account }) + ) + ) + await attestations.selectIssuers(phoneHash) + const [ + attestationBlockNumbers, + attestationIssuers, + stringLengths, + stringData, + ] = await attestations.getCompletableAttestations(phoneHash, caller) + + const urls = parseSolidityStringArray( + stringLengths.map((x) => x.toNumber()), + (stringData as unknown) as string + ) + + assert.lengthOf(attestationBlockNumbers, attestationsRequested) + await Promise.all( + range(0, attestationsRequested).map(async (i) => { + const [status, requestBlock] = await attestations.getAttestationState( + phoneHash, + caller, + attestationIssuers[i]! + ) + assert.equal(status.toNumber(), 1) + assertEqualBN(requestBlock, attestationBlockNumbers[i]) + assert.equal(`https://test.com/${attestationIssuers[i]}`, urls[i]) + }) + ) + }) + it('should delete the unselected request', async () => { await attestations.selectIssuers(phoneHash) const [ @@ -439,6 +474,22 @@ contract('Attestations', (accounts: string[]) => { }, }) }) + + describe('after attestationExpiryBlocks', () => { + beforeEach(async () => { + await attestations.selectIssuers(phoneHash) + await advanceBlockNum(attestationExpiryBlocks, web3) + }) + + it('should no longer list the attestations in getCompletableAttestations', async () => { + const [ + attestationBlockNumbers, + _attestationIssuers, + ] = await attestations.getCompletableAttestations(phoneHash, caller) + + assert.lengthOf(attestationBlockNumbers, 0) + }) + }) }) it('should revert when selecting too soon', async () => { @@ -449,45 +500,12 @@ contract('Attestations', (accounts: string[]) => { }) describe('without requesting attestations before', () => { - it('should revert when revealing issuers', async () => { + it('should revert when selecting issuers', async () => { await assertRevert(attestations.selectIssuers(phoneHash)) }) }) }) - describe('#reveal()', () => { - let issuer: string - - beforeEach(async () => { - await requestAttestations() - issuer = (await attestations.getAttestationIssuers(phoneHash, caller))[0] - }) - - it('should allow a reveal', async () => { - // @ts-ignore - await attestations.reveal(phoneHash, phoneHash, issuer, false) - }) - - it('should revert if users reveal a non-existent attestation request', async () => { - // @ts-ignore - await assertRevert(attestations.reveal(phoneHash, phoneHash, await getNonIssuer(), false)) - }) - - it('should revert if a user reveals a request that has been completed', async () => { - const [v, r, s] = await getVerificationCodeSignature(caller, issuer) - await attestations.complete(phoneHash, v, r, s) - - // @ts-ignore - await assertRevert(attestations.reveal(phoneHash, phoneHash, issuer, false)) - }) - - it('should revert if the request as expired', async () => { - await advanceBlockNum(attestationExpiryBlocks, web3) - // @ts-ignore - await assertRevert(attestations.reveal(phoneHash, phoneHash, issuer, false)) - }) - }) - describe('#complete()', () => { let issuer: string let v: number @@ -556,6 +574,15 @@ contract('Attestations', (accounts: string[]) => { assert.equal(pendingWithdrawals.toString(), attestationFee.toString()) }) + it('should no longer list the attestation in getCompletableAttestationStats', async () => { + await attestations.complete(phoneHash, v, r, s) + const [ + _attestationBlockNumbers, + attestationIssuers, + ] = await attestations.getCompletableAttestations(phoneHash, caller) + assert.equal(attestationIssuers.indexOf(issuer), -1) + }) + it('should emit the AttestationCompleted event', async () => { const response = await attestations.complete(phoneHash, v, r, s) assert.lengthOf(response.logs, 1) diff --git a/packages/utils/package.json b/packages/utils/package.json index 125c0769bc9..d917d7d40c2 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -26,6 +26,7 @@ "futoin-hkdf": "^1.0.3", "google-libphonenumber": "^3.2.4", "lodash": "^4.17.14", + "numeral": "^2.0.6", "web3-utils": "1.0.0-beta.37", "keccak256": "^1.0.0", "buffer-reverse": "^1.0.1", diff --git a/packages/utils/src/parsing.ts b/packages/utils/src/parsing.ts index effa63fb343..29fa9775231 100644 --- a/packages/utils/src/parsing.ts +++ b/packages/utils/src/parsing.ts @@ -16,3 +16,26 @@ export const parseInputAmount = (inputString: string): BigNumber => { // https://github.com/MikeMcl/bignumber.js/#use return new BigNumber(numeral(inputString).value() || '0') } + +/** + * Parses an "array of strings" that is returned from a Solidity function + * + * @param stringLengths length of each string in bytes + * @param data 0x-prefixed, hex-encoded string data in utf-8 bytes + */ +export const parseSolidityStringArray = (stringLengths: number[], data: string) => { + if (data === null) { + data = '0x' + } + const ret: string[] = [] + let offset = 0 + // @ts-ignore + const rawData = Buffer.from(data.slice(2), 'hex') + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < stringLengths.length; i++) { + const string = rawData.toString('utf-8', offset, offset + stringLengths[i]) + offset += stringLengths[i] + ret.push(string) + } + return ret +}