-
Notifications
You must be signed in to change notification settings - Fork 375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimized Attestation view calls and removal of the reveal TX #1578
Changes from 17 commits
c6030f5
63d0bcb
31f9628
16ffc20
74f329b
975a8eb
75ffa26
e155778
5aa4c55
f56acd7
9268525
47cc6db
0b5d736
03991da
e576e79
9ef5d97
4cd276d
bc3a01f
6b624c9
eb9d87b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Attestations> { | ||
/** | ||
|
@@ -129,6 +143,17 @@ export class AttestationsWrapper extends BaseWrapper<Attestations> { | |
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,50 +204,46 @@ export class AttestationsWrapper extends BaseWrapper<Attestations> { | |
} | ||
|
||
/** | ||
* 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 | ||
*/ | ||
async getActionableAttestations( | ||
phoneNumber: string, | ||
account: Address | ||
): Promise<ActionableAttestation[]> { | ||
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<string[]> = Promise.all( | ||
issuers.map((issuer) => accounts.getDataEncryptionKey(issuer) as any) | ||
const result = await this.contract.methods.getCompletableAttestations(phoneHash, account).call() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please define a helper method that parses this into a more readable object |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider allowing the error to propagate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about it, but it seemed to me that a validator not or incorrectly registering an attestation service url should not result in an error for a client who just wants to complete attestations. Thus, I favored returning the attestations that are completable, by intent of the function. |
||
} | ||
} | ||
) | ||
|
||
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<Attestations> { | |
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 | ||
) | ||
) | ||
}), | ||
}) | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't review this