From 9ab8913c6ed2005ef3e022012cfb9d2af627ec9d Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Mon, 30 Apr 2018 10:50:11 -0700 Subject: [PATCH 01/54] Remove unused variables --- contracts/test/identity/TestClaimHolderPresigned.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contracts/test/identity/TestClaimHolderPresigned.js b/contracts/test/identity/TestClaimHolderPresigned.js index 442452d1..2e9c0fed 100644 --- a/contracts/test/identity/TestClaimHolderPresigned.js +++ b/contracts/test/identity/TestClaimHolderPresigned.js @@ -9,11 +9,6 @@ const signature_2 = "0x061ef9cdd7707d90d7a7d95b53ddbd94905cb05dfe4734f97744c7976 const dataHash_1 = "0x4f32f7a7d40b4d65a917926cbfd8fd521483e7472bcc4d024179735622447dc9" const dataHash_2 = "0xa183d4eb3552e730c2dd3df91384426eb88879869b890ad12698320d8b88cb48" -// number of bytes in a hex string: -// num characters (without "0x") divided by 2 -const numBytesInSignature = 65 -const numBytesInDataHash = 32 - contract("ClaimHolderPresigned", accounts => { let attestation_1 = { claimType: 1, From 2ff0ecb71554cf082b9fae424a775b58011ca099 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Mon, 30 Apr 2018 11:35:43 -0700 Subject: [PATCH 02/54] Move getBytes to ClaimHolder contract and make it internal vs private This will allow it to be accessed from both ClaimHolder and ClaimHolderPresigned --- contracts/contracts/identity/ClaimHolder.sol | 13 +++++++++++++ .../contracts/identity/ClaimHolderPresigned.sol | 14 -------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/contracts/contracts/identity/ClaimHolder.sol b/contracts/contracts/identity/ClaimHolder.sol index 6bc41fc2..1ea52b8f 100644 --- a/contracts/contracts/identity/ClaimHolder.sol +++ b/contracts/contracts/identity/ClaimHolder.sol @@ -104,4 +104,17 @@ contract ClaimHolder is KeyHolder, ERC735 { return claimsByType[_claimType]; } + function getBytes(bytes _str, uint256 _offset, uint256 _length) + internal + pure + returns (bytes) + { + bytes memory sig = new bytes(_length); + uint256 j = 0; + for (uint256 k = _offset; k< _offset + _length; k++) { + sig[j] = _str[k]; + j++; + } + return sig; + } } diff --git a/contracts/contracts/identity/ClaimHolderPresigned.sol b/contracts/contracts/identity/ClaimHolderPresigned.sol index b85f7dca..dc867f47 100644 --- a/contracts/contracts/identity/ClaimHolderPresigned.sol +++ b/contracts/contracts/identity/ClaimHolderPresigned.sol @@ -32,18 +32,4 @@ contract ClaimHolderPresigned is ClaimHolder { offset += _offsets[i]; } } - - function getBytes(bytes _str, uint256 _offset, uint256 _length) - private - pure - returns (bytes) - { - bytes memory sig = new bytes(_length); - uint256 j = 0; - for (uint256 k = _offset; k< _offset + _length; k++) { - sig[j] = _str[k]; - j++; - } - return sig; - } } From faf0745c9b122789e2a26ff164bf9898298ee157 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Mon, 30 Apr 2018 13:09:35 -0700 Subject: [PATCH 03/54] Implement addClaims method on ClaimHolder contract --- contracts/contracts/identity/ClaimHolder.sol | 20 ++++++++ contracts/test/identity/TestClaimHolder.js | 52 +++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/identity/ClaimHolder.sol b/contracts/contracts/identity/ClaimHolder.sol index 1ea52b8f..5e84b811 100644 --- a/contracts/contracts/identity/ClaimHolder.sol +++ b/contracts/contracts/identity/ClaimHolder.sol @@ -51,6 +51,26 @@ contract ClaimHolder is KeyHolder, ERC735 { return claimId; } + function addClaims( + uint256[] _claimType, + address[] _issuer, + bytes _signature, + bytes _data + ) + public + { + for (uint8 i = 0; i < _claimType.length; i++) { + addClaim( + _claimType[i], + 1, + _issuer[i], + getBytes(_signature, (i * 65), 65), + getBytes(_data, (i * 32), 32), + "" + ); + } + } + function removeClaim(bytes32 _claimId) public returns (bool success) { if (msg.sender != address(this)) { require(keyHasPurpose(keccak256(msg.sender), 1), "Sender does not have management key"); diff --git a/contracts/test/identity/TestClaimHolder.js b/contracts/test/identity/TestClaimHolder.js index c377e38c..e2447a83 100644 --- a/contracts/test/identity/TestClaimHolder.js +++ b/contracts/test/identity/TestClaimHolder.js @@ -3,18 +3,28 @@ const web3Utils = require('web3-utils') const contractDefinition = artifacts.require("ClaimHolder") const signature_1 = "0xeb6123e537e17e2c67b67bbc0b93e6b25ea9eae276c4c2ab353bd7e853ebad2446cc7e91327f3737559d7a9a90fc88529a6b72b770a612f808ab0ba57a46866e1c" +const signature_2 = "0x061ef9cdd7707d90d7a7d95b53ddbd94905cb05dfe4734f97744c7976f2776145fef298fd0e31afa43a103cd7f5b00e3b226b0d62e4c492d54bec02eb0c2a0901b" const dataHash_1 = "0x4f32f7a7d40b4d65a917926cbfd8fd521483e7472bcc4d024179735622447dc9" +const dataHash_2 = "0xa183d4eb3552e730c2dd3df91384426eb88879869b890ad12698320d8b88cb48" contract("ClaimHolder", accounts => { let instance let attestation_1 = { claimType: 1, - scheme: 11, + scheme: 1, issuer: accounts[1], signature: signature_1, data: dataHash_1, - uri: "https://foo.bar/attestation1" + uri: "" + } + let attestation_2 = { + claimType: 2, + scheme: 1, + issuer: accounts[2], + signature: signature_2, + data: dataHash_2, + uri: "" } beforeEach(async function() { @@ -45,4 +55,42 @@ contract("ClaimHolder", accounts => { assert.equal(data, attestation_1.data) assert.equal(uri, attestation_1.uri) }) + + it("can batch add claims", async function() { + await instance.addClaims( + [ attestation_1.claimType, attestation_2.claimType ], + [ attestation_1.issuer, attestation_2.issuer ], + attestation_1.signature + attestation_2.signature.slice(2), + attestation_1.data + attestation_2.data.slice(2), + { from: accounts[0] } + ) + + let claimId_1 = web3Utils.soliditySha3( + attestation_1.issuer, + attestation_1.claimType + ) + let fetchedClaim_1 = await instance.getClaim(claimId_1, { from: accounts[0] }) + assert.ok(fetchedClaim_1) + let [ claimType_1, scheme_1, issuer_1, signature_1, data_1, uri_1 ] = fetchedClaim_1 + assert.equal(claimType_1.toNumber(), attestation_1.claimType) + assert.equal(scheme_1.toNumber(), attestation_1.scheme) + assert.equal(issuer_1, attestation_1.issuer) + assert.equal(signature_1, attestation_1.signature) + assert.equal(data_1, attestation_1.data) + assert.equal(uri_1, attestation_1.uri) + + let claimId_2 = web3Utils.soliditySha3( + attestation_2.issuer, + attestation_2.claimType + ) + let fetchedClaim_2 = await instance.getClaim(claimId_2, { from: accounts[0] }) + assert.ok(fetchedClaim_2) + let [ claimType_2, scheme_2, issuer_2, signature_2, data_2, uri_2 ] = fetchedClaim_2 + assert.equal(claimType_2.toNumber(), attestation_2.claimType) + assert.equal(scheme_2.toNumber(), attestation_2.scheme) + assert.equal(issuer_2, attestation_2.issuer) + assert.equal(signature_2, attestation_2.signature) + assert.equal(data_2, attestation_2.data) + assert.equal(uri_2, attestation_2.uri) + }) }) From 1c520757439f5c203750b919623b865ee6126022 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Mon, 30 Apr 2018 19:04:49 -0700 Subject: [PATCH 04/54] Create attestations resource --- src/resources/attestations.js | 116 ++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/resources/attestations.js diff --git a/src/resources/attestations.js b/src/resources/attestations.js new file mode 100644 index 00000000..eadccb61 --- /dev/null +++ b/src/resources/attestations.js @@ -0,0 +1,116 @@ +import fetch from "cross-fetch" + +const appendSlash = (url) => { + return (url.substr(-1) === "/") ? url : url + "/" +} + +class AttestationObject { + constructor({ claimType, data, signature, issuer }) { + this.claimType = claimType + this.data = data + this.signature = signature, + this.issuer = issuer + } +} + +let responseToUrl = (resp = {}) => { + return resp['url'] +} + +let http = async (baseUrl, url, body, successFn, method) => { + let response = await fetch( + appendSlash(baseUrl) + url, + { + method, + body: body ? JSON.stringify(body) : undefined, + headers: { "content-type": "application/json" } + } + ) + let json = await response.json() + if (response.ok) { + return successFn ? successFn(json) : json + } + return Promise.reject(JSON.stringify(json)) +} + +class Attestations { + constructor({ serverUrl, issuer }) { + this.serverUrl = serverUrl + + this.responseToAttestation = (resp = {}) => { + return new AttestationObject({ + claimType: resp['claim-type'], + data: resp['data'], + signature: resp['signature'], + issuer + }) + } + } + + async post(url, body, successFn) { + return await http(this.serverUrl, url, body, successFn, 'POST') + } + + async get(url, successFn) { + return await http(this.serverUrl, url, undefined, successFn, 'GET') + } + + async phoneGenerateCode({ phone }) { + return await this.post("phone/generate-code", { phone }) + } + + async phoneVerify({ identity, phone, code }) { + return await this.post( + "phone/verify", + { identity, phone, code }, + this.responseToAttestation + ) + } + + async emailGenerateCode({ email }) { + return await this.post("email/generate-code", { email }) + } + + async emailVerify({ identity, email, code }) { + return await this.post( + "email/verify", + { identity, email, code }, + this.responseToAttestation + ) + } + + async facebookAuthUrl({ redirectUrl }) { + return await this.get( + `facebook/auth-url?redirect-url=${redirectUrl}`, + responseToUrl + ) + } + + async facebookVerify({ identity, redirectUrl, code }) { + return await this.post( + "facebook/verify", + { identity, "redirect-url": redirectUrl, code }, + this.responseToAttestation + ) + } + + async twitterAuthUrl() { + return await this.get( + `twitter/auth-url`, + responseToUrl + ) + } + + async twitterVerify({ identity, oauthVerifier }) { + return await this.post( + "twitter/verify", + { identity, "oauth-verifier": oauthVerifier }, + this.responseToAttestation + ) + } +} + +module.exports = { + AttestationObject, + Attestations +} From bcf1fb3af379e41b01c39748f3115c4688296096 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 00:32:13 -0700 Subject: [PATCH 05/54] Rename user registry methods The name `create` conflicts with a Truffle method (it conflicts with something anyway - I am guessing it's Truffle) --- contracts/contracts/UserRegistry.sol | 4 ++-- contracts/test/TestUserRegistry.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/UserRegistry.sol b/contracts/contracts/UserRegistry.sol index e5f875a1..60890dc2 100644 --- a/contracts/contracts/UserRegistry.sol +++ b/contracts/contracts/UserRegistry.sol @@ -25,7 +25,7 @@ contract UserRegistry { */ /// @dev create(): Create a user - function create() public + function createUser() public { ClaimHolder _identity = new ClaimHolder(); users[msg.sender] = _identity; @@ -34,7 +34,7 @@ contract UserRegistry { /// @dev createWithClaims(): Create a user with presigned claims // Params correspond to params of ClaimHolderPresigned - function createWithClaims( + function createUserWithClaims( uint256[] _claimType, address[] _issuer, bytes _signature, diff --git a/contracts/test/TestUserRegistry.js b/contracts/test/TestUserRegistry.js index 1f43450f..f637a9f7 100644 --- a/contracts/test/TestUserRegistry.js +++ b/contracts/test/TestUserRegistry.js @@ -33,7 +33,7 @@ contract("UserRegistry", accounts => { }) it("should be able to create a user", async function() { - let create = await userRegistry.create({ from: accounts[1] }) + let create = await userRegistry.createUser({ from: accounts[1] }) let identityAddress = await userRegistry.users(accounts[1]) let newUserEvent = create.logs.find(e => e.event == "NewUser") assert.ok(identityAddress) @@ -45,7 +45,7 @@ contract("UserRegistry", accounts => { }) it("should be able to create a user with claims", async function() { - let createWithClaims = await userRegistry.createWithClaims( + let createWithClaims = await userRegistry.createUserWithClaims( [attestation_1.claimType], [attestation_1.issuer], attestation_1.signature, From e3bdb8140ba2f9509d8378cd32ad63979138d198 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 00:33:06 -0700 Subject: [PATCH 06/54] Make claim holder contract available in contract service --- src/contract-service.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/contract-service.js b/src/contract-service.js index a619f86b..797ffbab 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -1,3 +1,4 @@ +import ClaimHolderContract from "./../contracts/build/contracts/ClaimHolder.json" import ListingsRegistryContract from "./../contracts/build/contracts/ListingsRegistry.json" import ListingContract from "./../contracts/build/contracts/Listing.json" import PurchaseContract from "./../contracts/build/contracts/Purchase.json" @@ -19,7 +20,8 @@ class ContractService { listingsRegistryContract: ListingsRegistryContract, listingContract: ListingContract, purchaseContract: PurchaseContract, - userRegistryContract: UserRegistryContract + userRegistryContract: UserRegistryContract, + claimHolderContract: ClaimHolderContract } for (let name in contracts) { this[name] = contracts[name] From 6677875b6ab0a7390a7485054a4e10e906639d08 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 00:33:13 -0700 Subject: [PATCH 07/54] Create users resource --- src/resources/users.js | 115 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/resources/users.js diff --git a/src/resources/users.js b/src/resources/users.js new file mode 100644 index 00000000..3f3088dd --- /dev/null +++ b/src/resources/users.js @@ -0,0 +1,115 @@ +import ResourceBase from "../ResourceBase" +import { AttestationObject } from "./attestations" +import userSchema from '../schemas/user.json' + +var Ajv = require('ajv') +var ajv = new Ajv() + +const selfAttestationClaimType = 13 // TODO: use the correct number here + +let validateUser = (data) => { + let validate = ajv.compile(userSchema) + if (!validate(data)) { + throw new Error('Invalid user data') + } else { + return data + } +} + +class Users extends ResourceBase { + constructor({ contractService, ipfsService }) { + super(...arguments) + } + + async set({ profile, attestations = [] }) { + let account = await this.contractService.currentAccount() + let userRegistry = await this.contractService.userRegistryContract.deployed() + + if (profile && validateUser(profile)) { + // Submit to IPFS + let ipfsHash = await this.ipfsService.submitFile(profile) + let asBytes32 = this.contractService.getBytes32FromIpfsHash(ipfsHash) + // For now we'll ignore issuer & signature for self attestations + // If it's a self-attestation, then no validation is necessary + // A signature would be an extra UI step, so we don't want to add it if not necessary + let selfAttestation = new AttestationObject({ + claimType: selfAttestationClaimType, + data: asBytes32, + issuer: "0x0000000000000000000000000000000000000000", + signature: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }) + attestations.push(selfAttestation) + } + + if (attestations.length) { + let web3 = this.contractService.web3 + let claimTypes = attestations.map(({ claimType }) => claimType) + let issuers = attestations.map(({ issuer }) => issuer) + let sigs = "0x" + attestations.map(({ signature }) => { + return signature.substr(2) + }).join("") + let data = "0x" + attestations.map(({ data }) => { + return web3.sha3(data).substr(2) + }).join("") + return await userRegistry.createUserWithClaims( + claimTypes, + issuers, + sigs, + data, + { from: account, gas: 4000000 } + ) + } else { + return await userRegistry.createUser({ from: account, gas: 4000000 }) + } + } + + async get(address) { + let account = await this.contractService.currentAccount() + let userRegistry = await this.contractService.userRegistryContract.deployed() + address = address || account + let identityAddress = await userRegistry.users(address) + let claims = await this.getClaims(identityAddress) + return claims + } + + async getClaims(identityAddress) { + let identity = this.contractService.claimHolderContract.at(identityAddress) + let allEvents = identity.allEvents({fromBlock: 0, toBlock: 'latest'}) + let claims = await new Promise((resolve, reject) => { + allEvents.get((err, events) => { + let claimAddedEvents = events.filter(({ event }) => event === "ClaimAdded" ) + let mapped = claimAddedEvents.map(({ args }) => { + return { + claimId: args.claimId, + claimType: args.claimType.toNumber(), + data: args.data, + issuer: args.issuer, + scheme: args.scheme.toNumber(), + signature: args.signature, + uri: args.uri + } + }) + resolve(mapped) + }) + }) + let profileClaims = claims.filter(({ claimType }) => claimType === selfAttestationClaimType ) + let nonProfileClaims = claims.filter(({ claimType }) => claimType !== selfAttestationClaimType ) + let profile = {} + if (profileClaims.length) { + let bytes32 = profileClaims[0].data + let ipfsHash = this.contractService.getIpfsHashFromBytes32(bytes32) + profile = await this.ipfsService.getFile(ipfsHash) + } + return { + profile, + attestations: validAttestations(nonProfileClaims) + } + } + + validAttestations(attestations) { + // TODO + return attestations + } +} + +module.exports = Users From 76703da114465dac5dd34d8f3025d035e6e6d56d Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 00:36:09 -0700 Subject: [PATCH 08/54] Expose attestation and user resources on origin-js --- src/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index baf144df..5af9d0c0 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,10 @@ import ContractService from "./contract-service" import IpfsService from "./ipfs-service" var resources = { + attestations: require("./resources/attestations").Attestations, listings: require("./resources/listings"), - purchases: require("./resources/purchases") + purchases: require("./resources/purchases"), + users: require("./resources/users") } class Origin { From 1b5b13afb7566086f07c1f7327e0330705a90acd Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 11:35:58 -0700 Subject: [PATCH 09/54] Properly initialize attestation resource This one is different than the other resources. Takes in different config options. --- src/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 5af9d0c0..4842596d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,8 @@ import ContractService from "./contract-service" import IpfsService from "./ipfs-service" +import { Attestations } from "./resources/attestations" var resources = { - attestations: require("./resources/attestations").Attestations, listings: require("./resources/listings"), purchases: require("./resources/purchases"), users: require("./resources/users") @@ -13,7 +13,9 @@ class Origin { ipfsDomain, ipfsApiPort, ipfsGatewayPort, - ipfsGatewayProtocol + ipfsGatewayProtocol, + attestationServerUrl, + attestationIssuer } = {}) { this.contractService = new ContractService() this.ipfsService = new IpfsService({ @@ -22,6 +24,11 @@ class Origin { ipfsGatewayPort, ipfsGatewayProtocol }) + this.attestations = new Attestations({ + serverUrl: attestationServerUrl, + issuer: attestationIssuer, + contractService: this.contractService + }) // Instantiate each resource and give it access to contracts and IPFS for (let resourceName in resources) { From dbc8eb72a347183599ac5c59f479160734564f8f Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 12:17:10 -0700 Subject: [PATCH 10/54] Add rlp dependency --- package-lock.json | 3 +-- package.json | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a170074e..b478efed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16190,8 +16190,7 @@ "rlp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.0.0.tgz", - "integrity": "sha1-nbOE/0uJqPYVY9kjldhiWxjzr7A=", - "dev": true + "integrity": "sha1-nbOE/0uJqPYVY9kjldhiWxjzr7A=" }, "rsa-pem-to-jwk": { "version": "1.1.3", diff --git a/package.json b/package.json index 22864e89..03c4d311 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "form-data": "^2.3.2", "ipfs-api": "^20.2.0", "map-cache": "^0.2.2", + "rlp": "^2.0.0", "truffle-contract": "^3.0.5", "util.promisify": "^1.0.0", "web3": "^1.0.0-beta.34" From 50bd6cbea15538c5fe722fc4418a96909ccd3127 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 12:18:58 -0700 Subject: [PATCH 11/54] Add alternative `wallet` param and use it predict identity address We can predict where a contract will be deployed, thus presign claims for an identity that does not exist yet. See: https://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa --- src/resources/attestations.js | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index eadccb61..42b5b56b 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -1,4 +1,6 @@ import fetch from "cross-fetch" +import RLP from "rlp" +import web3Utils from "web3-utils" const appendSlash = (url) => { return (url.substr(-1) === "/") ? url : url + "/" @@ -34,8 +36,9 @@ let http = async (baseUrl, url, body, successFn, method) => { } class Attestations { - constructor({ serverUrl, issuer }) { + constructor({ serverUrl, issuer, contractService }) { this.serverUrl = serverUrl + this.contractService = contractService this.responseToAttestation = (resp = {}) => { return new AttestationObject({ @@ -55,11 +58,24 @@ class Attestations { return await http(this.serverUrl, url, undefined, successFn, 'GET') } + async predictIdentityAddress(wallet) { + let web3 = this.contractService.web3 + let nonce = await new Promise((resolve, reject) => { + web3.eth.getTransactionCount(wallet, (err, count) => { + resolve(count) + }) + }) + return "0x" + web3Utils.sha3(RLP.encode([wallet, nonce])).substring(26, 66) + } + async phoneGenerateCode({ phone }) { return await this.post("phone/generate-code", { phone }) } - async phoneVerify({ identity, phone, code }) { + async phoneVerify({ identity, wallet, phone, code }) { + if (wallet && !identity) { + identity = await this.predictIdentityAddress(wallet) + } return await this.post( "phone/verify", { identity, phone, code }, @@ -71,7 +87,10 @@ class Attestations { return await this.post("email/generate-code", { email }) } - async emailVerify({ identity, email, code }) { + async emailVerify({ identity, wallet, email, code }) { + if (wallet && !identity) { + identity = await this.predictIdentityAddress(wallet) + } return await this.post( "email/verify", { identity, email, code }, @@ -86,7 +105,10 @@ class Attestations { ) } - async facebookVerify({ identity, redirectUrl, code }) { + async facebookVerify({ identity, wallet, redirectUrl, code }) { + if (wallet && !identity) { + identity = await this.predictIdentityAddress(wallet) + } return await this.post( "facebook/verify", { identity, "redirect-url": redirectUrl, code }, @@ -101,7 +123,10 @@ class Attestations { ) } - async twitterVerify({ identity, oauthVerifier }) { + async twitterVerify({ identity, wallet, oauthVerifier }) { + if (wallet && !identity) { + identity = await this.predictIdentityAddress(wallet) + } return await this.post( "twitter/verify", { identity, "oauth-verifier": oauthVerifier }, From 3518b908c37c6c8b42d52a802debe7bfd5401a8c Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 13:47:19 -0700 Subject: [PATCH 12/54] Add ethereumjs-util dependency --- package-lock.json | 5 +---- package.json | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b478efed..ad46c937 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2913,7 +2913,7 @@ }, "compression": { "version": "1.7.2", - "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.2.tgz", "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", "dev": true, "requires": { @@ -4636,7 +4636,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", - "dev": true, "requires": { "bn.js": "4.11.8", "create-hash": "1.2.0", @@ -4765,7 +4764,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.4.tgz", "integrity": "sha1-HItoeSV0RO9NPz+7rC3tEs2ZfZM=", - "dev": true, "requires": { "is-hex-prefixed": "1.0.0", "strip-hex-prefix": "1.0.0" @@ -10556,7 +10554,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", - "dev": true, "requires": { "bindings": "1.3.0", "inherits": "2.0.3", diff --git a/package.json b/package.json index 03c4d311..dac80274 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "babel-polyfill": "^6.26.0", "bs58": "^4.0.1", "cross-fetch": "^2.1.1", + "ethereumjs-util": "^5.2.0", "form-data": "^2.3.2", "ipfs-api": "^20.2.0", "map-cache": "^0.2.2", From 611e5ccc708e5227004f9bdf4f5cb089af2f073f Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 14:02:56 -0700 Subject: [PATCH 13/54] Ensure identity addresses we send to bridge server are checksummed --- src/resources/attestations.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 42b5b56b..5bb95c45 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -78,7 +78,11 @@ class Attestations { } return await this.post( "phone/verify", - { identity, phone, code }, + { + identity: web3Utils.toChecksumAddress(identity), + phone, + code + }, this.responseToAttestation ) } @@ -93,7 +97,11 @@ class Attestations { } return await this.post( "email/verify", - { identity, email, code }, + { + identity: web3Utils.toChecksumAddress(identity), + email, + code + }, this.responseToAttestation ) } @@ -111,7 +119,11 @@ class Attestations { } return await this.post( "facebook/verify", - { identity, "redirect-url": redirectUrl, code }, + { + identity: web3Utils.toChecksumAddress(identity), + "redirect-url": redirectUrl, + code + }, this.responseToAttestation ) } @@ -129,7 +141,10 @@ class Attestations { } return await this.post( "twitter/verify", - { identity, "oauth-verifier": oauthVerifier }, + { + identity: web3Utils.toChecksumAddress(identity), + "oauth-verifier": oauthVerifier + }, this.responseToAttestation ) } From b26b7ef0205106d507e75ad587da606a98a15044 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 14:15:19 -0700 Subject: [PATCH 14/54] Bug fix: don't hash data that is already a hash :) --- src/resources/users.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resources/users.js b/src/resources/users.js index 3f3088dd..b50b138b 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -49,7 +49,8 @@ class Users extends ResourceBase { return signature.substr(2) }).join("") let data = "0x" + attestations.map(({ data }) => { - return web3.sha3(data).substr(2) + let hashed = (data.substr(0, 2) === "0x") ? data : web3.sha3(data) + return hashed.substr(2) }).join("") return await userRegistry.createUserWithClaims( claimTypes, From 286ec28964c05f1740c10f30a3626181fff9a0e9 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Tue, 1 May 2018 15:29:09 -0700 Subject: [PATCH 15/54] Add web3-eth-accounts dependency --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index dac80274..0af6db78 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "rlp": "^2.0.0", "truffle-contract": "^3.0.5", "util.promisify": "^1.0.0", - "web3": "^1.0.0-beta.34" + "web3": "^1.0.0-beta.34", + "web3-eth-accounts": "^1.0.0-beta.34" }, "devDependencies": { "babel-cli": "^6.26.0", From b463e6f17bac057f87f7efddadd16772bd315d39 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 00:51:19 -0700 Subject: [PATCH 16/54] Validate attestations, client-side origin-js should validate all attestations, and only return the ones that are valid. --- src/resources/users.js | 45 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/resources/users.js b/src/resources/users.js index b50b138b..9ecdd07d 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -1,6 +1,15 @@ import ResourceBase from "../ResourceBase" import { AttestationObject } from "./attestations" -import userSchema from '../schemas/user.json' +import userSchema from "../schemas/user.json" +import { + fromRpcSig, + ecrecover, + toBuffer, + bufferToHex, + pubToAddress +} from "ethereumjs-util" +import web3Utils from "web3-utils" // not necessary with web3 1.0 +import Web3EthAccounts from "web3-eth-accounts" // not necessary with web3 1.0 var Ajv = require('ajv') var ajv = new Ajv() @@ -17,8 +26,10 @@ let validateUser = (data) => { } class Users extends ResourceBase { - constructor({ contractService, ipfsService }) { - super(...arguments) + constructor({ contractService, ipfsService, issuer }) { + super({ contractService, ipfsService }) + this.issuer = issuer + this.web3EthAccounts = new Web3EthAccounts() } async set({ profile, attestations = [] }) { @@ -49,7 +60,7 @@ class Users extends ResourceBase { return signature.substr(2) }).join("") let data = "0x" + attestations.map(({ data }) => { - let hashed = (data.substr(0, 2) === "0x") ? data : web3.sha3(data) + let hashed = (data.substr(0, 2) === "0x") ? data : web3Utils.sha3(data) return hashed.substr(2) }).join("") return await userRegistry.createUserWithClaims( @@ -103,13 +114,31 @@ class Users extends ResourceBase { } return { profile, - attestations: validAttestations(nonProfileClaims) + attestations: this.validAttestations(identityAddress, nonProfileClaims) } } - validAttestations(attestations) { - // TODO - return attestations + validAttestations(identityAddress, attestations) { + return attestations.filter(({ claimType, issuer, data, signature }) => { + if (issuer.toLowerCase() !== this.issuer.toLowerCase()) { + // TODO: we should be checking that issuer has key purpose on a master origin identity contract + // (rather than hard-coding a single issuer) + return false + } + let msg = web3Utils.soliditySha3( + identityAddress, + claimType, + data + ) + let prefixedMsg = this.web3EthAccounts.hashMessage(msg) + let dataBuf = toBuffer(prefixedMsg) + let sig = fromRpcSig(signature) + let recovered = ecrecover(dataBuf, sig.v, sig.r, sig.s) + let recoveredBuf = pubToAddress(recovered) + let recoveredHex = bufferToHex(recoveredBuf) + let hashedRecovered = web3Utils.soliditySha3(recoveredHex) + return recoveredHex.toLowerCase() === issuer.toLowerCase() + }) } } From 9ca2fdb8e04f7106867ddc72e44257bb13f2e7c2 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 00:51:49 -0700 Subject: [PATCH 17/54] Instantiate users separately from other resources This is required for now so that issuer will be passed in. We can refactor this later. --- src/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 4842596d..21022492 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,11 @@ import ContractService from "./contract-service" import IpfsService from "./ipfs-service" import { Attestations } from "./resources/attestations" +import Users from "./resources/users" var resources = { listings: require("./resources/listings"), - purchases: require("./resources/purchases"), - users: require("./resources/users") + purchases: require("./resources/purchases") } class Origin { @@ -29,6 +29,11 @@ class Origin { issuer: attestationIssuer, contractService: this.contractService }) + this.users = new Users({ + contractService: this.contractService, + ipfsService: this.ipfsService, + issuer: attestationIssuer + }) // Instantiate each resource and give it access to contracts and IPFS for (let resourceName in resources) { From 34218e69680e5e639ae8e84b2fdf75ac0e4b70e4 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 02:17:02 -0700 Subject: [PATCH 18/54] Refactor `user registry`/`claim holder` relationship Now the user directly calls the identity contracts, and the identity contracts add themselves to the user registry. This seems cleaner and simpler, but the real motivation to refactor was to allow identity contract addresses to be reliably predicted before they are actually deployed (for creating presigned claims). If the identity contract is deployed directly by the user, then its address can be predicted safely. If it is deployed by a shared user registry contract though, that opens the door to race conditions where other people might be deploying identities at the same time. --- contracts/contracts/UserRegistry.sol | 32 ++------- contracts/contracts/identity/ClaimHolder.sol | 10 +++ .../identity/ClaimHolderPresigned.sol | 2 + contracts/test/TestUserRegistry.js | 71 ++----------------- contracts/test/identity/TestClaimHolder.js | 24 ++++--- .../test/identity/TestClaimHolderPresigned.js | 8 +++ src/contract-service.js | 4 +- src/resources/users.js | 14 ++-- 8 files changed, 57 insertions(+), 108 deletions(-) diff --git a/contracts/contracts/UserRegistry.sol b/contracts/contracts/UserRegistry.sol index 60890dc2..a086002b 100644 --- a/contracts/contracts/UserRegistry.sol +++ b/contracts/contracts/UserRegistry.sol @@ -24,33 +24,11 @@ contract UserRegistry { * Public functions */ - /// @dev create(): Create a user - function createUser() public + /// @dev registerUser(): Add a user to the registry + function registerUser() + public { - ClaimHolder _identity = new ClaimHolder(); - users[msg.sender] = _identity; - emit NewUser(msg.sender, _identity); - } - - /// @dev createWithClaims(): Create a user with presigned claims - // Params correspond to params of ClaimHolderPresigned - function createUserWithClaims( - uint256[] _claimType, - address[] _issuer, - bytes _signature, - bytes _data, - uint256[] _offsets - ) - public - { - ClaimHolderPresigned _identity = new ClaimHolderPresigned( - _claimType, - _issuer, - _signature, - _data, - _offsets - ); - users[msg.sender] = _identity; - emit NewUser(msg.sender, _identity); + users[tx.origin] = msg.sender; + emit NewUser(tx.origin, msg.sender); } } diff --git a/contracts/contracts/identity/ClaimHolder.sol b/contracts/contracts/identity/ClaimHolder.sol index 5e84b811..d6a3a1be 100644 --- a/contracts/contracts/identity/ClaimHolder.sol +++ b/contracts/contracts/identity/ClaimHolder.sol @@ -2,9 +2,19 @@ pragma solidity ^0.4.23; import './ERC735.sol'; import './KeyHolder.sol'; +import '../UserRegistry.sol'; contract ClaimHolder is KeyHolder, ERC735 { + constructor ( + address _userRegistryAddress + ) + public + { + UserRegistry userRegistry = UserRegistry(_userRegistryAddress); + userRegistry.registerUser(); + } + bytes32 claimId; mapping (bytes32 => Claim) claims; mapping (uint256 => bytes32[]) claimsByType; diff --git a/contracts/contracts/identity/ClaimHolderPresigned.sol b/contracts/contracts/identity/ClaimHolderPresigned.sol index dc867f47..25bceeff 100644 --- a/contracts/contracts/identity/ClaimHolderPresigned.sol +++ b/contracts/contracts/identity/ClaimHolderPresigned.sol @@ -11,12 +11,14 @@ import './ClaimHolder.sol'; contract ClaimHolderPresigned is ClaimHolder { constructor( + address _userRegistryAddress, uint256[] _claimType, address[] _issuer, bytes _signature, bytes _data, uint256[] _offsets ) + ClaimHolder(_userRegistryAddress) public { uint offset = 0; diff --git a/contracts/test/TestUserRegistry.js b/contracts/test/TestUserRegistry.js index f637a9f7..4325f890 100644 --- a/contracts/test/TestUserRegistry.js +++ b/contracts/test/TestUserRegistry.js @@ -1,77 +1,18 @@ -const web3Utils = require("web3-utils") - const UserRegistry = artifacts.require("./UserRegistry.sol") -const ClaimHolderPresigned = artifacts.require( - "./dentity/ClaimHolderPresigned.sol" -) - -// Used to assert error cases -const isEVMError = function(err) { - let str = err.toString() - return str.includes("revert") -} - -const signature_1 = - "0xeb6123e537e17e2c67b67bbc0b93e6b25ea9eae276c4c2ab353bd7e853ebad2446cc7e91327f3737559d7a9a90fc88529a6b72b770a612f808ab0ba57a46866e1c" - -const ipfsHash_1 = - "0x4f32f7a7d40b4d65a917926cbfd8fd521483e7472bcc4d024179735622447dc9" contract("UserRegistry", accounts => { let userRegistry - let attestation_1 = { - claimType: 1, - scheme: 1, - issuer: accounts[1], - signature: signature_1, - data: ipfsHash_1, - uri: "" - } beforeEach(async () => { userRegistry = await UserRegistry.new({ from: accounts[0] }) }) - it("should be able to create a user", async function() { - let create = await userRegistry.createUser({ from: accounts[1] }) + it("should be able to register a user", async function() { + let register = await userRegistry.registerUser({ from: accounts[1] }) let identityAddress = await userRegistry.users(accounts[1]) - let newUserEvent = create.logs.find(e => e.event == "NewUser") - assert.ok(identityAddress) - assert.notEqual( - identityAddress, - "0x0000000000000000000000000000000000000000" - ) - assert.equal(newUserEvent.args["_identity"], identityAddress) - }) - - it("should be able to create a user with claims", async function() { - let createWithClaims = await userRegistry.createUserWithClaims( - [attestation_1.claimType], - [attestation_1.issuer], - attestation_1.signature, - attestation_1.data, - [32], - { from: accounts[1] } - ) - let identityAddress = await userRegistry.users(accounts[1]) - let newUserEvent = createWithClaims.logs.find(e => e.event == "NewUser") - assert.ok(identityAddress) - assert.equal(newUserEvent.args["_identity"], identityAddress) - - // Check that claim was added - let identity = ClaimHolderPresigned.at(identityAddress) - let claimId = web3Utils.soliditySha3( - attestation_1.issuer, - attestation_1.claimType - ) - let fetchedClaim = await identity.getClaim(claimId, { from: accounts[1] }) - assert.ok(fetchedClaim) - let [claimType, scheme, issuer, signature, data, uri] = fetchedClaim - assert.equal(claimType.toNumber(), attestation_1.claimType) - assert.equal(scheme.toNumber(), attestation_1.scheme) - assert.equal(issuer, attestation_1.issuer) - assert.equal(signature, attestation_1.signature) - assert.equal(data, attestation_1.data) - assert.equal(uri, attestation_1.uri) + let newUserEvent = register.logs.find(e => e.event == "NewUser") + assert.equal(identityAddress, accounts[1]) + assert.equal(newUserEvent.args["_address"], accounts[1]) + assert.equal(newUserEvent.args["_identity"], accounts[1]) }) }) diff --git a/contracts/test/identity/TestClaimHolder.js b/contracts/test/identity/TestClaimHolder.js index e2447a83..80e01326 100644 --- a/contracts/test/identity/TestClaimHolder.js +++ b/contracts/test/identity/TestClaimHolder.js @@ -1,6 +1,7 @@ const web3Utils = require('web3-utils') -const contractDefinition = artifacts.require("ClaimHolder") +const ClaimHolder = artifacts.require("ClaimHolder") +const UserRegistry = artifacts.require("UserRegistry") const signature_1 = "0xeb6123e537e17e2c67b67bbc0b93e6b25ea9eae276c4c2ab353bd7e853ebad2446cc7e91327f3737559d7a9a90fc88529a6b72b770a612f808ab0ba57a46866e1c" const signature_2 = "0x061ef9cdd7707d90d7a7d95b53ddbd94905cb05dfe4734f97744c7976f2776145fef298fd0e31afa43a103cd7f5b00e3b226b0d62e4c492d54bec02eb0c2a0901b" @@ -9,7 +10,7 @@ const dataHash_1 = "0x4f32f7a7d40b4d65a917926cbfd8fd521483e7472bcc4d024179735622 const dataHash_2 = "0xa183d4eb3552e730c2dd3df91384426eb88879869b890ad12698320d8b88cb48" contract("ClaimHolder", accounts => { - let instance + let claimHolder, userRegistry let attestation_1 = { claimType: 1, scheme: 1, @@ -28,7 +29,8 @@ contract("ClaimHolder", accounts => { } beforeEach(async function() { - instance = await contractDefinition.new({ from: accounts[0] }) + userRegistry = await UserRegistry.new( { from: accounts[1] } ) + claimHolder = await ClaimHolder.new(userRegistry.address, { from: accounts[0] }) }) it("can add and get claim", async function() { @@ -36,7 +38,7 @@ contract("ClaimHolder", accounts => { attestation_1.issuer, attestation_1.claimType ) - await instance.addClaim( + await claimHolder.addClaim( attestation_1.claimType, attestation_1.scheme, attestation_1.issuer, @@ -45,7 +47,7 @@ contract("ClaimHolder", accounts => { attestation_1.uri, { from: accounts[0] } ) - let fetchedClaim = await instance.getClaim(claimId, { from: accounts[0] }) + let fetchedClaim = await claimHolder.getClaim(claimId, { from: accounts[0] }) assert.ok(fetchedClaim) let [ claimType, scheme, issuer, signature, data, uri ] = fetchedClaim assert.equal(claimType.toNumber(), attestation_1.claimType) @@ -57,7 +59,7 @@ contract("ClaimHolder", accounts => { }) it("can batch add claims", async function() { - await instance.addClaims( + await claimHolder.addClaims( [ attestation_1.claimType, attestation_2.claimType ], [ attestation_1.issuer, attestation_2.issuer ], attestation_1.signature + attestation_2.signature.slice(2), @@ -69,7 +71,7 @@ contract("ClaimHolder", accounts => { attestation_1.issuer, attestation_1.claimType ) - let fetchedClaim_1 = await instance.getClaim(claimId_1, { from: accounts[0] }) + let fetchedClaim_1 = await claimHolder.getClaim(claimId_1, { from: accounts[0] }) assert.ok(fetchedClaim_1) let [ claimType_1, scheme_1, issuer_1, signature_1, data_1, uri_1 ] = fetchedClaim_1 assert.equal(claimType_1.toNumber(), attestation_1.claimType) @@ -83,7 +85,7 @@ contract("ClaimHolder", accounts => { attestation_2.issuer, attestation_2.claimType ) - let fetchedClaim_2 = await instance.getClaim(claimId_2, { from: accounts[0] }) + let fetchedClaim_2 = await claimHolder.getClaim(claimId_2, { from: accounts[0] }) assert.ok(fetchedClaim_2) let [ claimType_2, scheme_2, issuer_2, signature_2, data_2, uri_2 ] = fetchedClaim_2 assert.equal(claimType_2.toNumber(), attestation_2.claimType) @@ -93,4 +95,10 @@ contract("ClaimHolder", accounts => { assert.equal(data_2, attestation_2.data) assert.equal(uri_2, attestation_2.uri) }) + + it("registers the user", async function() { + let identityAddress = await userRegistry.users(accounts[0]) + assert.ok(identityAddress) + assert.notEqual(identityAddress, "0x0000000000000000000000000000000000000000") + }) }) diff --git a/contracts/test/identity/TestClaimHolderPresigned.js b/contracts/test/identity/TestClaimHolderPresigned.js index 2e9c0fed..2b4f080f 100644 --- a/contracts/test/identity/TestClaimHolderPresigned.js +++ b/contracts/test/identity/TestClaimHolderPresigned.js @@ -2,6 +2,7 @@ var web3Utils = require('web3-utils') const ClaimHolder = artifacts.require("ClaimHolder") const ClaimHolderPresigned = artifacts.require("ClaimHolderPresigned") +const UserRegistry = artifacts.require("UserRegistry") const signature_1 = "0xeb6123e537e17e2c67b67bbc0b93e6b25ea9eae276c4c2ab353bd7e853ebad2446cc7e91327f3737559d7a9a90fc88529a6b72b770a612f808ab0ba57a46866e1c" const signature_2 = "0x061ef9cdd7707d90d7a7d95b53ddbd94905cb05dfe4734f97744c7976f2776145fef298fd0e31afa43a103cd7f5b00e3b226b0d62e4c492d54bec02eb0c2a0901b" @@ -28,7 +29,9 @@ contract("ClaimHolderPresigned", accounts => { } it("should deploy identity with attestations", async function() { + let userRegistry = await UserRegistry.new( { from: accounts[3] } ) let instance = await ClaimHolderPresigned.new( + userRegistry.address, [ attestation_1.claimType, attestation_2.claimType ], [ attestation_1.issuer, attestation_2.issuer ], attestation_1.signature + attestation_2.signature.slice(2), @@ -60,5 +63,10 @@ contract("ClaimHolderPresigned", accounts => { assert.equal(signature_2, attestation_2.signature) assert.equal(data_2, attestation_2.data) assert.equal(uri_2, attestation_2.uri) + + // Check user registry + let identityAddress = await userRegistry.users(accounts[0]) + assert.ok(identityAddress) + assert.notEqual(identityAddress, "0x0000000000000000000000000000000000000000") }) }) diff --git a/src/contract-service.js b/src/contract-service.js index 797ffbab..136fd993 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -1,4 +1,5 @@ import ClaimHolderContract from "./../contracts/build/contracts/ClaimHolder.json" +import ClaimHolderPresignedContract from "./../contracts/build/contracts/ClaimHolderPresigned.json" import ListingsRegistryContract from "./../contracts/build/contracts/ListingsRegistry.json" import ListingContract from "./../contracts/build/contracts/Listing.json" import PurchaseContract from "./../contracts/build/contracts/Purchase.json" @@ -21,7 +22,8 @@ class ContractService { listingContract: ListingContract, purchaseContract: PurchaseContract, userRegistryContract: UserRegistryContract, - claimHolderContract: ClaimHolderContract + claimHolderContract: ClaimHolderContract, + claimHolderPresignedContract: ClaimHolderPresignedContract } for (let name in contracts) { this[name] = contracts[name] diff --git a/src/resources/users.js b/src/resources/users.js index 9ecdd07d..9876da12 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -63,7 +63,8 @@ class Users extends ResourceBase { let hashed = (data.substr(0, 2) === "0x") ? data : web3Utils.sha3(data) return hashed.substr(2) }).join("") - return await userRegistry.createUserWithClaims( + return await this.contractService.claimHolderPresignedContract.new( + userRegistry.address, claimTypes, issuers, sigs, @@ -71,7 +72,10 @@ class Users extends ResourceBase { { from: account, gas: 4000000 } ) } else { - return await userRegistry.createUser({ from: account, gas: 4000000 }) + return await this.contractService.claimHolderContract.new( + userRegistry.address, + { from: account, gas: 4000000 } + ) } } @@ -125,11 +129,7 @@ class Users extends ResourceBase { // (rather than hard-coding a single issuer) return false } - let msg = web3Utils.soliditySha3( - identityAddress, - claimType, - data - ) + let msg = web3Utils.soliditySha3(identityAddress, claimType, data) let prefixedMsg = this.web3EthAccounts.hashMessage(msg) let dataBuf = toBuffer(prefixedMsg) let sig = fromRpcSig(signature) From efc12683af0cfb5812ad4edad07b1fdfa92a02cb Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 04:02:46 -0700 Subject: [PATCH 19/54] Only accept wallet as param for verify methods We can and should just accept wallet as input - users of origin-js should not have to worry about the identity contract itself (for now). We're managing that behind the scenes. --- src/resources/attestations.js | 46 +++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 5bb95c45..2e1eafc4 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -65,21 +65,31 @@ class Attestations { resolve(count) }) }) - return "0x" + web3Utils.sha3(RLP.encode([wallet, nonce])).substring(26, 66) + let address = "0x" + web3Utils.sha3(RLP.encode([wallet, nonce])).substring(26, 66) + return web3Utils.toChecksumAddress(address) + } + + async getIdentityAddress(wallet) { + let userRegistry = await this.contractService.userRegistryContract.deployed() + let identityAddress = await userRegistry.users(wallet) + let hasRegisteredIdentity = identityAddress !== "0x0000000000000000000000000000000000000000" + if (hasRegisteredIdentity) { + return web3Utils.toChecksumAddress(identityAddress) + } else { + return this.predictIdentityAddress(wallet) + } } async phoneGenerateCode({ phone }) { return await this.post("phone/generate-code", { phone }) } - async phoneVerify({ identity, wallet, phone, code }) { - if (wallet && !identity) { - identity = await this.predictIdentityAddress(wallet) - } + async phoneVerify({ wallet, phone, code }) { + let identity = await this.getIdentityAddress(wallet) return await this.post( "phone/verify", { - identity: web3Utils.toChecksumAddress(identity), + identity, phone, code }, @@ -91,14 +101,12 @@ class Attestations { return await this.post("email/generate-code", { email }) } - async emailVerify({ identity, wallet, email, code }) { - if (wallet && !identity) { - identity = await this.predictIdentityAddress(wallet) - } + async emailVerify({ wallet, email, code }) { + let identity = await this.getIdentityAddress(wallet) return await this.post( "email/verify", { - identity: web3Utils.toChecksumAddress(identity), + identity, email, code }, @@ -113,14 +121,12 @@ class Attestations { ) } - async facebookVerify({ identity, wallet, redirectUrl, code }) { - if (wallet && !identity) { - identity = await this.predictIdentityAddress(wallet) - } + async facebookVerify({ wallet, redirectUrl, code }) { + let identity = await this.getIdentityAddress(wallet) return await this.post( "facebook/verify", { - identity: web3Utils.toChecksumAddress(identity), + identity, "redirect-url": redirectUrl, code }, @@ -135,14 +141,12 @@ class Attestations { ) } - async twitterVerify({ identity, wallet, oauthVerifier }) { - if (wallet && !identity) { - identity = await this.predictIdentityAddress(wallet) - } + async twitterVerify({ wallet, oauthVerifier }) { + let identity = await this.getIdentityAddress(wallet) return await this.post( "twitter/verify", { - identity: web3Utils.toChecksumAddress(identity), + identity, "oauth-verifier": oauthVerifier }, this.responseToAttestation From 0148631e1da054ea787a6d7ac887d9f4e6fd7fe1 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 04:21:16 -0700 Subject: [PATCH 20/54] Don't create identity if one already exists --- src/resources/users.js | 93 +++++++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/src/resources/users.js b/src/resources/users.js index 9876da12..b5741dc9 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -33,27 +33,44 @@ class Users extends ResourceBase { } async set({ profile, attestations = [] }) { - let account = await this.contractService.currentAccount() - let userRegistry = await this.contractService.userRegistryContract.deployed() - if (profile && validateUser(profile)) { - // Submit to IPFS - let ipfsHash = await this.ipfsService.submitFile(profile) - let asBytes32 = this.contractService.getBytes32FromIpfsHash(ipfsHash) - // For now we'll ignore issuer & signature for self attestations - // If it's a self-attestation, then no validation is necessary - // A signature would be an extra UI step, so we don't want to add it if not necessary - let selfAttestation = new AttestationObject({ - claimType: selfAttestationClaimType, - data: asBytes32, - issuer: "0x0000000000000000000000000000000000000000", - signature: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }) + let selfAttestation = await this.profileAttestation(profile) attestations.push(selfAttestation) } + this.addAttestations(attestations) + } + + async get(address) { + let account = await this.contractService.currentAccount() + let userRegistry = await this.contractService.userRegistryContract.deployed() + address = address || account + let identityAddress = await userRegistry.users(address) + let claims = await this.getClaims(identityAddress) + return claims + } + + async profileAttestation(profile) { + // Submit to IPFS + let ipfsHash = await this.ipfsService.submitFile(profile) + let asBytes32 = this.contractService.getBytes32FromIpfsHash(ipfsHash) + // For now we'll ignore issuer & signature for self attestations + // If it's a self-attestation, then no validation is necessary + // A signature would be an extra UI step, so we don't want to add it if not necessary + return new AttestationObject({ + claimType: selfAttestationClaimType, + data: asBytes32, + issuer: "0x0000000000000000000000000000000000000000", + signature: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }) + } + async addAttestations(attestations) { + let account = await this.contractService.currentAccount() + let userRegistry = await this.contractService.userRegistryContract.deployed() if (attestations.length) { - let web3 = this.contractService.web3 + // format params for solidity methods to batch add claims + let identityAddress = await userRegistry.users(account) + let hasRegisteredIdentity = identityAddress !== "0x0000000000000000000000000000000000000000" let claimTypes = attestations.map(({ claimType }) => claimType) let issuers = attestations.map(({ issuer }) => issuer) let sigs = "0x" + attestations.map(({ signature }) => { @@ -63,15 +80,32 @@ class Users extends ResourceBase { let hashed = (data.substr(0, 2) === "0x") ? data : web3Utils.sha3(data) return hashed.substr(2) }).join("") - return await this.contractService.claimHolderPresignedContract.new( - userRegistry.address, - claimTypes, - issuers, - sigs, - data, - { from: account, gas: 4000000 } - ) - } else { + let dataOffsets = attestations.map(() => 32) // all data hashes will be 32 bytes + + if (hasRegisteredIdentity) { + // batch add claims to existing identity + let claimHolder = await this.contractService.claimHolderContract.at(identityAddress) + return await claimHolder.addClaims( + claimTypes, + issuers, + sigs, + data, + { from: account, gas: 4000000 } + ) + } else { + // create identity with presigned claims + return await this.contractService.claimHolderPresignedContract.new( + userRegistry.address, + claimTypes, + issuers, + sigs, + data, + dataOffsets, + { from: account, gas: 4000000 } + ) + } + } else if (!hasRegisteredIdentity) { + // create identity return await this.contractService.claimHolderContract.new( userRegistry.address, { from: account, gas: 4000000 } @@ -79,15 +113,6 @@ class Users extends ResourceBase { } } - async get(address) { - let account = await this.contractService.currentAccount() - let userRegistry = await this.contractService.userRegistryContract.deployed() - address = address || account - let identityAddress = await userRegistry.users(address) - let claims = await this.getClaims(identityAddress) - return claims - } - async getClaims(identityAddress) { let identity = this.contractService.claimHolderContract.at(identityAddress) let allEvents = identity.allEvents({fromBlock: 0, toBlock: 'latest'}) From 948d2200c829bc61521106f7e45086772ba97068 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 11:41:21 -0700 Subject: [PATCH 21/54] Refactor claim holder contract inheritance structure ClaimHolder -> ClaimHolderRegistered -> ClaimHolderPresigned This way we can use ClaimHolder on its own without a constructor that tries to add itself to a user registry. --- contracts/contracts/identity/ClaimHolder.sol | 10 ---------- .../identity/ClaimHolderPresigned.sol | 6 +++--- .../identity/ClaimHolderRegistered.sol | 16 ++++++++++++++++ ...mHolder.js => TestClaimHolderRegistered.js} | 18 +++++++++--------- src/contract-service.js | 4 ++-- src/resources/users.js | 6 +++--- 6 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 contracts/contracts/identity/ClaimHolderRegistered.sol rename contracts/test/identity/{TestClaimHolder.js => TestClaimHolderRegistered.js} (84%) diff --git a/contracts/contracts/identity/ClaimHolder.sol b/contracts/contracts/identity/ClaimHolder.sol index d6a3a1be..5e84b811 100644 --- a/contracts/contracts/identity/ClaimHolder.sol +++ b/contracts/contracts/identity/ClaimHolder.sol @@ -2,19 +2,9 @@ pragma solidity ^0.4.23; import './ERC735.sol'; import './KeyHolder.sol'; -import '../UserRegistry.sol'; contract ClaimHolder is KeyHolder, ERC735 { - constructor ( - address _userRegistryAddress - ) - public - { - UserRegistry userRegistry = UserRegistry(_userRegistryAddress); - userRegistry.registerUser(); - } - bytes32 claimId; mapping (bytes32 => Claim) claims; mapping (uint256 => bytes32[]) claimsByType; diff --git a/contracts/contracts/identity/ClaimHolderPresigned.sol b/contracts/contracts/identity/ClaimHolderPresigned.sol index 25bceeff..6b1eb4ef 100644 --- a/contracts/contracts/identity/ClaimHolderPresigned.sol +++ b/contracts/contracts/identity/ClaimHolderPresigned.sol @@ -1,6 +1,6 @@ pragma solidity ^0.4.23; -import './ClaimHolder.sol'; +import './ClaimHolderRegistered.sol'; /** * NOTE: This contract exists as a convenience for deploying an identity with @@ -8,7 +8,7 @@ import './ClaimHolder.sol'; * instead. */ -contract ClaimHolderPresigned is ClaimHolder { +contract ClaimHolderPresigned is ClaimHolderRegistered { constructor( address _userRegistryAddress, @@ -18,7 +18,7 @@ contract ClaimHolderPresigned is ClaimHolder { bytes _data, uint256[] _offsets ) - ClaimHolder(_userRegistryAddress) + ClaimHolderRegistered(_userRegistryAddress) public { uint offset = 0; diff --git a/contracts/contracts/identity/ClaimHolderRegistered.sol b/contracts/contracts/identity/ClaimHolderRegistered.sol new file mode 100644 index 00000000..a3cd13e8 --- /dev/null +++ b/contracts/contracts/identity/ClaimHolderRegistered.sol @@ -0,0 +1,16 @@ +pragma solidity ^0.4.23; + +import './ClaimHolder.sol'; +import '../UserRegistry.sol'; + +contract ClaimHolderRegistered is ClaimHolder { + + constructor ( + address _userRegistryAddress + ) + public + { + UserRegistry userRegistry = UserRegistry(_userRegistryAddress); + userRegistry.registerUser(); + } +} diff --git a/contracts/test/identity/TestClaimHolder.js b/contracts/test/identity/TestClaimHolderRegistered.js similarity index 84% rename from contracts/test/identity/TestClaimHolder.js rename to contracts/test/identity/TestClaimHolderRegistered.js index 80e01326..422c3743 100644 --- a/contracts/test/identity/TestClaimHolder.js +++ b/contracts/test/identity/TestClaimHolderRegistered.js @@ -1,6 +1,6 @@ const web3Utils = require('web3-utils') -const ClaimHolder = artifacts.require("ClaimHolder") +const ClaimHolderRegistered = artifacts.require("ClaimHolderRegistered") const UserRegistry = artifacts.require("UserRegistry") const signature_1 = "0xeb6123e537e17e2c67b67bbc0b93e6b25ea9eae276c4c2ab353bd7e853ebad2446cc7e91327f3737559d7a9a90fc88529a6b72b770a612f808ab0ba57a46866e1c" @@ -9,8 +9,8 @@ const signature_2 = "0x061ef9cdd7707d90d7a7d95b53ddbd94905cb05dfe4734f97744c7976 const dataHash_1 = "0x4f32f7a7d40b4d65a917926cbfd8fd521483e7472bcc4d024179735622447dc9" const dataHash_2 = "0xa183d4eb3552e730c2dd3df91384426eb88879869b890ad12698320d8b88cb48" -contract("ClaimHolder", accounts => { - let claimHolder, userRegistry +contract("ClaimHolderRegistered", accounts => { + let claimHolderRegistered, userRegistry let attestation_1 = { claimType: 1, scheme: 1, @@ -30,7 +30,7 @@ contract("ClaimHolder", accounts => { beforeEach(async function() { userRegistry = await UserRegistry.new( { from: accounts[1] } ) - claimHolder = await ClaimHolder.new(userRegistry.address, { from: accounts[0] }) + claimHolderRegistered = await ClaimHolderRegistered.new(userRegistry.address, { from: accounts[0] }) }) it("can add and get claim", async function() { @@ -38,7 +38,7 @@ contract("ClaimHolder", accounts => { attestation_1.issuer, attestation_1.claimType ) - await claimHolder.addClaim( + await claimHolderRegistered.addClaim( attestation_1.claimType, attestation_1.scheme, attestation_1.issuer, @@ -47,7 +47,7 @@ contract("ClaimHolder", accounts => { attestation_1.uri, { from: accounts[0] } ) - let fetchedClaim = await claimHolder.getClaim(claimId, { from: accounts[0] }) + let fetchedClaim = await claimHolderRegistered.getClaim(claimId, { from: accounts[0] }) assert.ok(fetchedClaim) let [ claimType, scheme, issuer, signature, data, uri ] = fetchedClaim assert.equal(claimType.toNumber(), attestation_1.claimType) @@ -59,7 +59,7 @@ contract("ClaimHolder", accounts => { }) it("can batch add claims", async function() { - await claimHolder.addClaims( + await claimHolderRegistered.addClaims( [ attestation_1.claimType, attestation_2.claimType ], [ attestation_1.issuer, attestation_2.issuer ], attestation_1.signature + attestation_2.signature.slice(2), @@ -71,7 +71,7 @@ contract("ClaimHolder", accounts => { attestation_1.issuer, attestation_1.claimType ) - let fetchedClaim_1 = await claimHolder.getClaim(claimId_1, { from: accounts[0] }) + let fetchedClaim_1 = await claimHolderRegistered.getClaim(claimId_1, { from: accounts[0] }) assert.ok(fetchedClaim_1) let [ claimType_1, scheme_1, issuer_1, signature_1, data_1, uri_1 ] = fetchedClaim_1 assert.equal(claimType_1.toNumber(), attestation_1.claimType) @@ -85,7 +85,7 @@ contract("ClaimHolder", accounts => { attestation_2.issuer, attestation_2.claimType ) - let fetchedClaim_2 = await claimHolder.getClaim(claimId_2, { from: accounts[0] }) + let fetchedClaim_2 = await claimHolderRegistered.getClaim(claimId_2, { from: accounts[0] }) assert.ok(fetchedClaim_2) let [ claimType_2, scheme_2, issuer_2, signature_2, data_2, uri_2 ] = fetchedClaim_2 assert.equal(claimType_2.toNumber(), attestation_2.claimType) diff --git a/src/contract-service.js b/src/contract-service.js index 136fd993..96f2feb3 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -1,4 +1,4 @@ -import ClaimHolderContract from "./../contracts/build/contracts/ClaimHolder.json" +import ClaimHolderRegisteredContract from "./../contracts/build/contracts/ClaimHolderRegistered.json" import ClaimHolderPresignedContract from "./../contracts/build/contracts/ClaimHolderPresigned.json" import ListingsRegistryContract from "./../contracts/build/contracts/ListingsRegistry.json" import ListingContract from "./../contracts/build/contracts/Listing.json" @@ -22,7 +22,7 @@ class ContractService { listingContract: ListingContract, purchaseContract: PurchaseContract, userRegistryContract: UserRegistryContract, - claimHolderContract: ClaimHolderContract, + claimHolderRegisteredContract: ClaimHolderRegisteredContract, claimHolderPresignedContract: ClaimHolderPresignedContract } for (let name in contracts) { diff --git a/src/resources/users.js b/src/resources/users.js index b5741dc9..442e0d0a 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -84,7 +84,7 @@ class Users extends ResourceBase { if (hasRegisteredIdentity) { // batch add claims to existing identity - let claimHolder = await this.contractService.claimHolderContract.at(identityAddress) + let claimHolder = await this.contractService.claimHolderRegisteredContract.at(identityAddress) return await claimHolder.addClaims( claimTypes, issuers, @@ -106,7 +106,7 @@ class Users extends ResourceBase { } } else if (!hasRegisteredIdentity) { // create identity - return await this.contractService.claimHolderContract.new( + return await this.contractService.claimHolderRegisteredContract.new( userRegistry.address, { from: account, gas: 4000000 } ) @@ -114,7 +114,7 @@ class Users extends ResourceBase { } async getClaims(identityAddress) { - let identity = this.contractService.claimHolderContract.at(identityAddress) + let identity = this.contractService.claimHolderRegisteredContract.at(identityAddress) let allEvents = identity.allEvents({fromBlock: 0, toBlock: 'latest'}) let claims = await new Promise((resolve, reject) => { allEvents.get((err, events) => { From 983769d6dfebb30fc2542488728d24705366409b Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 11:45:28 -0700 Subject: [PATCH 22/54] Create OriginIdentity contract for issuing attestations --- .../contracts/identity/OriginIdentity.sol | 8 ++++++ contracts/migrations/2_deploy_contracts.js | 4 ++- contracts/migrations/4_add_sample_issuer.js | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 contracts/contracts/identity/OriginIdentity.sol create mode 100644 contracts/migrations/4_add_sample_issuer.js diff --git a/contracts/contracts/identity/OriginIdentity.sol b/contracts/contracts/identity/OriginIdentity.sol new file mode 100644 index 00000000..1fb5a549 --- /dev/null +++ b/contracts/contracts/identity/OriginIdentity.sol @@ -0,0 +1,8 @@ +pragma solidity ^0.4.23; + +import './ClaimHolder.sol'; + +// This will be deployed exactly once and represents Origin Protocol's +// own identity for use in signing attestations. + +contract OriginIdentity is ClaimHolder {} diff --git a/contracts/migrations/2_deploy_contracts.js b/contracts/migrations/2_deploy_contracts.js index 4d07653c..19210719 100644 --- a/contracts/migrations/2_deploy_contracts.js +++ b/contracts/migrations/2_deploy_contracts.js @@ -2,6 +2,7 @@ var ListingsRegistry = artifacts.require("./ListingsRegistry.sol"); var Listing = artifacts.require("./Listing.sol"); var UserRegistry = artifacts.require("./UserRegistry.sol"); var PurchaseLibrary = artifacts.require("./PurchaseLibrary.sol"); +var OriginIdentity = artifacts.require("./OriginIdentity.sol"); module.exports = function(deployer) { deployer.deploy(PurchaseLibrary); @@ -9,4 +10,5 @@ module.exports = function(deployer) { deployer.link(PurchaseLibrary, Listing) deployer.deploy(ListingsRegistry); deployer.deploy(UserRegistry); -}; \ No newline at end of file + deployer.deploy(OriginIdentity); +}; diff --git a/contracts/migrations/4_add_sample_issuer.js b/contracts/migrations/4_add_sample_issuer.js new file mode 100644 index 00000000..e8cc8071 --- /dev/null +++ b/contracts/migrations/4_add_sample_issuer.js @@ -0,0 +1,27 @@ +var OriginIdentityContract = require("../build/contracts/OriginIdentity.json") +var contract = require("truffle-contract") +var web3Utils = require("web3-utils") + +const issuer = "0x99C03fBb0C995ff1160133A8bd210D0E77bCD101" +const issuerHashed = web3Utils.soliditySha3(issuer) +const keyPurpose = 3 +const keyType = 1 + +module.exports = function(deployer, network) { + return add_sample_issuer(network) +} + +async function add_sample_issuer(network) { + let defaultAccount = web3.eth.accounts[0] + + let originIdentityContract = contract(OriginIdentityContract) + originIdentityContract.setProvider(web3.currentProvider) + let originIdentity = await originIdentityContract.deployed() + + return await originIdentity.addKey( + issuerHashed, + keyPurpose, + keyType, + { from: defaultAccount, gas: 4000000 } + ) +} From 8fdcdd20ab1440aaf494b6f02a47f41b80fd6db6 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 11:48:19 -0700 Subject: [PATCH 23/54] Properly verify attestation issuer by checking that it is a valid key Instead of hard-coding a single key, we have a single "hard-coded" identity contract that can have multiple keys - so we can use multiple keys to sign attestations if we want to or need to. This complies with the design of ERC725/ERC735. --- src/contract-service.js | 4 +++- src/resources/users.js | 15 ++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/contract-service.js b/src/contract-service.js index 96f2feb3..62b29ab0 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -4,6 +4,7 @@ import ListingsRegistryContract from "./../contracts/build/contracts/ListingsReg import ListingContract from "./../contracts/build/contracts/Listing.json" import PurchaseContract from "./../contracts/build/contracts/Purchase.json" import UserRegistryContract from "./../contracts/build/contracts/UserRegistry.json" +import OriginIdentityContract from "./../contracts/build/contracts/OriginIdentity.json" import bs58 from "bs58" import Web3 from "web3" @@ -23,7 +24,8 @@ class ContractService { purchaseContract: PurchaseContract, userRegistryContract: UserRegistryContract, claimHolderRegisteredContract: ClaimHolderRegisteredContract, - claimHolderPresignedContract: ClaimHolderPresignedContract + claimHolderPresignedContract: ClaimHolderPresignedContract, + originIdentityContract: OriginIdentityContract } for (let name in contracts) { this[name] = contracts[name] diff --git a/src/resources/users.js b/src/resources/users.js index 442e0d0a..a98013cf 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -141,19 +141,16 @@ class Users extends ResourceBase { let ipfsHash = this.contractService.getIpfsHashFromBytes32(bytes32) profile = await this.ipfsService.getFile(ipfsHash) } + let validAttestations = await this.validAttestations(identityAddress, nonProfileClaims) return { profile, - attestations: this.validAttestations(identityAddress, nonProfileClaims) + attestations: validAttestations } } - validAttestations(identityAddress, attestations) { - return attestations.filter(({ claimType, issuer, data, signature }) => { - if (issuer.toLowerCase() !== this.issuer.toLowerCase()) { - // TODO: we should be checking that issuer has key purpose on a master origin identity contract - // (rather than hard-coding a single issuer) - return false - } + async validAttestations(identityAddress, attestations) { + let originIdentity = await this.contractService.originIdentityContract.deployed() + return attestations.filter(async ({ claimType, issuer, data, signature }) => { let msg = web3Utils.soliditySha3(identityAddress, claimType, data) let prefixedMsg = this.web3EthAccounts.hashMessage(msg) let dataBuf = toBuffer(prefixedMsg) @@ -162,7 +159,7 @@ class Users extends ResourceBase { let recoveredBuf = pubToAddress(recovered) let recoveredHex = bufferToHex(recoveredBuf) let hashedRecovered = web3Utils.soliditySha3(recoveredHex) - return recoveredHex.toLowerCase() === issuer.toLowerCase() + return await originIdentity.keyHasPurpose(hashedRecovered, 3) }) } } From 97040ec87c30a39579177a95d0348f76aabc4c02 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 12:12:56 -0700 Subject: [PATCH 24/54] Fix the way we reference contracts in migrations We shouldn't be loading json files and using truffle-contract in the tests. Depending on how the tests are run, the files may not have the correct network addresses. The tests should be able to be run in an isolated environment, regardless of the state of the json files. --- .../migrations/3_create_sample_listings.js | 33 +++++++------------ contracts/migrations/4_add_sample_issuer.js | 7 ++-- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/contracts/migrations/3_create_sample_listings.js b/contracts/migrations/3_create_sample_listings.js index b350cd06..f7295863 100644 --- a/contracts/migrations/3_create_sample_listings.js +++ b/contracts/migrations/3_create_sample_listings.js @@ -1,7 +1,6 @@ -var ListingsRegistryContract = require("../build/contracts/ListingsRegistry.json") -var ListingContract = require("../build/contracts/Listing.json") -var PurchaseContract = require("../build/contracts/Purchase.json") -var contract = require("truffle-contract") +var ListingsRegistry = artifacts.require("./ListingsRegistry.sol"); +var Listing = artifacts.require("./Listing.sol"); +var Purchase = artifacts.require("./Purchase.sol"); module.exports = function(deployer, network) { return deploy_sample_contracts(network) @@ -15,56 +14,48 @@ async function deploy_sample_contracts(network) { const a_buyer_account = web3.eth.accounts[2] const another_buyer_account = web3.eth.accounts[3] - const listingRegistryContract = contract(ListingsRegistryContract) - listingRegistryContract.setProvider(web3.currentProvider) - const listingRegistry = await listingRegistryContract.deployed() - - const listingContract = contract(ListingContract) - listingContract.setProvider(web3.currentProvider) - - const purchaseContract = contract(PurchaseContract) - purchaseContract.setProvider(web3.currentProvider) + const listingsRegistry = await ListingsRegistry.deployed() const getListingContract = async transaction => { const index = transaction.logs.find(x => x.event == "NewListing").args._index - const info = await listingRegistry.getListing(index) + const info = await listingsRegistry.getListing(index) const address = info[0] - return listingContract.at(address) + return Listing.at(address) } const buyListing = async (listing, qty, from) => { const price = await listing.price() const transaction = await listing.buyListing(qty, { from: from, value: price, gas: 4476768 }) const address = transaction.logs.find(x => x.event == "ListingPurchased").args._purchaseContract - return purchaseContract.at(address) + return Purchase.at(address) } - await listingRegistry.create( + await listingsRegistry.create( "0x4f32f7a7d40b4d65a917926cbfd8fd521483e7472bcc4d024179735622447dc9", web3.toWei(3, "ether"), 1, { from: a_seller_account, gas: 4476768 } ) - await listingRegistry.create( + await listingsRegistry.create( "0xa183d4eb3552e730c2dd3df91384426eb88879869b890ad12698320d8b88cb48", web3.toWei(0.6, "ether"), 1, { from: default_account, gas: 4476768 } ) - await listingRegistry.create( + await listingsRegistry.create( "0xab92c0500ba26fa6f5244f8ba54746e15dd455a7c99a67f0e8f8868c8fab4a1a", web3.toWei(8.5, "ether"), 1, { from: a_seller_account, gas: 4476768 } ) - await listingRegistry.create( + await listingsRegistry.create( "0x6b14cac30356789cd0c39fec0acc2176c3573abdb799f3b17ccc6972ab4d39ba", web3.toWei(1.5, "ether"), 1, { from: default_account, gas: 4476768 } ) - const ticketsTransaction = await listingRegistry.create( + const ticketsTransaction = await listingsRegistry.create( "0xff5957ff4035d28dcee79e65aa4124a4de4dcc8cb028faca54c883a5497d8917", web3.toWei(0.3, "ether"), 27, diff --git a/contracts/migrations/4_add_sample_issuer.js b/contracts/migrations/4_add_sample_issuer.js index e8cc8071..c98e34ea 100644 --- a/contracts/migrations/4_add_sample_issuer.js +++ b/contracts/migrations/4_add_sample_issuer.js @@ -1,5 +1,4 @@ -var OriginIdentityContract = require("../build/contracts/OriginIdentity.json") -var contract = require("truffle-contract") +var OriginIdentity = artifacts.require("./OriginIdentity.sol"); var web3Utils = require("web3-utils") const issuer = "0x99C03fBb0C995ff1160133A8bd210D0E77bCD101" @@ -14,9 +13,7 @@ module.exports = function(deployer, network) { async function add_sample_issuer(network) { let defaultAccount = web3.eth.accounts[0] - let originIdentityContract = contract(OriginIdentityContract) - originIdentityContract.setProvider(web3.currentProvider) - let originIdentity = await originIdentityContract.deployed() + let originIdentity = await OriginIdentity.deployed() return await originIdentity.addKey( issuerHashed, From e710d73d760d827424d3563b34d021bb016297b8 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 15:03:35 -0700 Subject: [PATCH 25/54] Use `deployer.then` to make sure migrations run synchronously See: http://truffleframework.com/docs/getting_started/migrations#deployer-then-function- --- contracts/migrations/2_deploy_contracts.js | 22 ++++++++++++------- .../migrations/3_create_sample_listings.js | 4 +++- contracts/migrations/4_add_sample_issuer.js | 4 +++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/contracts/migrations/2_deploy_contracts.js b/contracts/migrations/2_deploy_contracts.js index 19210719..a417a595 100644 --- a/contracts/migrations/2_deploy_contracts.js +++ b/contracts/migrations/2_deploy_contracts.js @@ -4,11 +4,17 @@ var UserRegistry = artifacts.require("./UserRegistry.sol"); var PurchaseLibrary = artifacts.require("./PurchaseLibrary.sol"); var OriginIdentity = artifacts.require("./OriginIdentity.sol"); -module.exports = function(deployer) { - deployer.deploy(PurchaseLibrary); - deployer.link(PurchaseLibrary, ListingsRegistry) - deployer.link(PurchaseLibrary, Listing) - deployer.deploy(ListingsRegistry); - deployer.deploy(UserRegistry); - deployer.deploy(OriginIdentity); -}; +module.exports = function(deployer, network) { + return deployer.then(() => { + return deployContracts(deployer) + }) +} + +async function deployContracts(deployer) { + await deployer.deploy(PurchaseLibrary); + await deployer.link(PurchaseLibrary, ListingsRegistry) + await deployer.link(PurchaseLibrary, Listing) + await deployer.deploy(ListingsRegistry); + await deployer.deploy(UserRegistry); + await deployer.deploy(OriginIdentity); +} diff --git a/contracts/migrations/3_create_sample_listings.js b/contracts/migrations/3_create_sample_listings.js index f7295863..3aa1230a 100644 --- a/contracts/migrations/3_create_sample_listings.js +++ b/contracts/migrations/3_create_sample_listings.js @@ -3,7 +3,9 @@ var Listing = artifacts.require("./Listing.sol"); var Purchase = artifacts.require("./Purchase.sol"); module.exports = function(deployer, network) { - return deploy_sample_contracts(network) + return deployer.then(() => { + return deploy_sample_contracts(network) + }) } async function deploy_sample_contracts(network) { diff --git a/contracts/migrations/4_add_sample_issuer.js b/contracts/migrations/4_add_sample_issuer.js index c98e34ea..5ee8f0d1 100644 --- a/contracts/migrations/4_add_sample_issuer.js +++ b/contracts/migrations/4_add_sample_issuer.js @@ -7,7 +7,9 @@ const keyPurpose = 3 const keyType = 1 module.exports = function(deployer, network) { - return add_sample_issuer(network) + return deployer.then(() => { + return add_sample_issuer(network) + }) } async function add_sample_issuer(network) { From fb23a9bfddaff7efe8cf4a5105791fd1cab7271a Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 15:16:28 -0700 Subject: [PATCH 26/54] Remove unnecessary issuer config option We're using a master origin identity contract to keep up with issuers. No need for the config option now. (Eventually might allow people to pass in the address of one or more trusted attestation identities - for now we'll just manage that automatically.) --- src/index.js | 12 +++--------- src/resources/attestations.js | 3 +-- src/resources/users.js | 3 +-- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/index.js b/src/index.js index 21022492..eedad746 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,8 @@ import Users from "./resources/users" var resources = { listings: require("./resources/listings"), - purchases: require("./resources/purchases") + purchases: require("./resources/purchases"), + users: require("./resources/users") } class Origin { @@ -14,8 +15,7 @@ class Origin { ipfsApiPort, ipfsGatewayPort, ipfsGatewayProtocol, - attestationServerUrl, - attestationIssuer + attestationServerUrl } = {}) { this.contractService = new ContractService() this.ipfsService = new IpfsService({ @@ -26,14 +26,8 @@ class Origin { }) this.attestations = new Attestations({ serverUrl: attestationServerUrl, - issuer: attestationIssuer, contractService: this.contractService }) - this.users = new Users({ - contractService: this.contractService, - ipfsService: this.ipfsService, - issuer: attestationIssuer - }) // Instantiate each resource and give it access to contracts and IPFS for (let resourceName in resources) { diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 2e1eafc4..74fe14df 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -7,11 +7,10 @@ const appendSlash = (url) => { } class AttestationObject { - constructor({ claimType, data, signature, issuer }) { + constructor({ claimType, data, signature }) { this.claimType = claimType this.data = data this.signature = signature, - this.issuer = issuer } } diff --git a/src/resources/users.js b/src/resources/users.js index a98013cf..6801b657 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -26,9 +26,8 @@ let validateUser = (data) => { } class Users extends ResourceBase { - constructor({ contractService, ipfsService, issuer }) { + constructor({ contractService, ipfsService }) { super({ contractService, ipfsService }) - this.issuer = issuer this.web3EthAccounts = new Web3EthAccounts() } From dbfaa146452c2c1359741793defeccf1ceaeb254 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 15:56:08 -0700 Subject: [PATCH 27/54] Hash data in attestation resource rather than user resource Once data leaves the attestation resource, we'll assume that it's been hashed --- src/resources/attestations.js | 4 ++-- src/resources/users.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 74fe14df..5955b336 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -9,8 +9,8 @@ const appendSlash = (url) => { class AttestationObject { constructor({ claimType, data, signature }) { this.claimType = claimType - this.data = data - this.signature = signature, + this.data = web3Utils.sha3(data) + this.signature = signature } } diff --git a/src/resources/users.js b/src/resources/users.js index 6801b657..5baa4283 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -76,8 +76,7 @@ class Users extends ResourceBase { return signature.substr(2) }).join("") let data = "0x" + attestations.map(({ data }) => { - let hashed = (data.substr(0, 2) === "0x") ? data : web3Utils.sha3(data) - return hashed.substr(2) + return data.substr(2) }).join("") let dataOffsets = attestations.map(() => 32) // all data hashes will be 32 bytes From ba48ad6f80735e1f6a2cf9636fa8b8428deffcca Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 17:25:17 -0700 Subject: [PATCH 28/54] Never mind - don't hash data in the constructor This results in double hashing if your data is already hashed. That was a bad idea in the first place. --- src/resources/attestations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 5955b336..2d57eb38 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -9,7 +9,7 @@ const appendSlash = (url) => { class AttestationObject { constructor({ claimType, data, signature }) { this.claimType = claimType - this.data = web3Utils.sha3(data) + this.data = data this.signature = signature } } @@ -42,7 +42,7 @@ class Attestations { this.responseToAttestation = (resp = {}) => { return new AttestationObject({ claimType: resp['claim-type'], - data: resp['data'], + data: web3Utils.sha3(resp['data']), signature: resp['signature'], issuer }) From 932f2ccdb9965c935865402a6afbdf9231c97082 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 17:33:26 -0700 Subject: [PATCH 29/54] Only add new attestations in `set` method This will allow consumers of this library to simply pass in the state that they want (both the profile and attestations). In other words, this efficiently replaces the existing user with whatever is passed in. (The only exception to this is removing attestations. We won't worry about that for now.) --- src/resources/users.js | 46 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/resources/users.js b/src/resources/users.js index 5baa4283..29e07dee 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -15,6 +15,7 @@ var Ajv = require('ajv') var ajv = new Ajv() const selfAttestationClaimType = 13 // TODO: use the correct number here +const zeroAddress = "0x0000000000000000000000000000000000000000" let validateUser = (data) => { let validate = ajv.compile(userSchema) @@ -36,16 +37,27 @@ class Users extends ResourceBase { let selfAttestation = await this.profileAttestation(profile) attestations.push(selfAttestation) } - this.addAttestations(attestations) + let newAttestations = await this.newAttestations(attestations) + this.addAttestations(newAttestations) } async get(address) { + let identityAddress = await this.identityAddress(address) + if (identityAddress) { + return await this.getClaims(identityAddress) + } + return [] + } + + async identityAddress(address) { let account = await this.contractService.currentAccount() let userRegistry = await this.contractService.userRegistryContract.deployed() address = address || account - let identityAddress = await userRegistry.users(address) - let claims = await this.getClaims(identityAddress) - return claims + let result = await userRegistry.users(address) + if (String(result) === zeroAddress) { + } else { + return result + } } async profileAttestation(profile) { @@ -63,13 +75,31 @@ class Users extends ResourceBase { }) } + async newAttestations(attestations) { + let identityAddress = await this.identityAddress() + let existingAttestations = [] + if (identityAddress) { + let claims = await this.getClaims(identityAddress) + existingAttestations = claims.attestations + } + return attestations.filter((attestation) => { + let matchingAttestation = existingAttestations.filter((existingAttestation) => { + let claimTypeMatches = attestation.claimType === existingAttestation.claimType + let dataMatches = attestation.data === existingAttestation.data + let sigMatches = attestation.signature === existingAttestation.signature + return claimTypeMatches || dataMatches || sigMatches + }) + let isNew = matchingAttestation.length === 0 + return isNew + }) + } + async addAttestations(attestations) { let account = await this.contractService.currentAccount() let userRegistry = await this.contractService.userRegistryContract.deployed() + let identityAddress = await this.identityAddress() if (attestations.length) { // format params for solidity methods to batch add claims - let identityAddress = await userRegistry.users(account) - let hasRegisteredIdentity = identityAddress !== "0x0000000000000000000000000000000000000000" let claimTypes = attestations.map(({ claimType }) => claimType) let issuers = attestations.map(({ issuer }) => issuer) let sigs = "0x" + attestations.map(({ signature }) => { @@ -80,7 +110,7 @@ class Users extends ResourceBase { }).join("") let dataOffsets = attestations.map(() => 32) // all data hashes will be 32 bytes - if (hasRegisteredIdentity) { + if (identityAddress) { // batch add claims to existing identity let claimHolder = await this.contractService.claimHolderRegisteredContract.at(identityAddress) return await claimHolder.addClaims( @@ -102,7 +132,7 @@ class Users extends ResourceBase { { from: account, gas: 4000000 } ) } - } else if (!hasRegisteredIdentity) { + } else if (!identityAddress) { // create identity return await this.contractService.claimHolderRegisteredContract.new( userRegistry.address, From 97170b6874615d10bd284c6933a380c03e8483d3 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 17:35:01 -0700 Subject: [PATCH 30/54] Use last rather than first profile event as the current Because we're just fetching from the logs, we want to make sure to get the most recent so that profile updates work. Sooner rather than later we should move away from using the logs like this. --- src/resources/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/users.js b/src/resources/users.js index 29e07dee..52e96bc4 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -165,7 +165,7 @@ class Users extends ResourceBase { let nonProfileClaims = claims.filter(({ claimType }) => claimType !== selfAttestationClaimType ) let profile = {} if (profileClaims.length) { - let bytes32 = profileClaims[0].data + let bytes32 = profileClaims[profileClaims.length - 1].data let ipfsHash = this.contractService.getIpfsHashFromBytes32(bytes32) profile = await this.ipfsService.getFile(ipfsHash) } From b88a45a38f4f011b0ebbe1804dddbee7525af204 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 18:16:19 -0700 Subject: [PATCH 31/54] Add a `service` property to AttestationObject as a convenience --- src/resources/attestations.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 2d57eb38..b79a4625 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -2,6 +2,13 @@ import fetch from "cross-fetch" import RLP from "rlp" import web3Utils from "web3-utils" +const claimTypeMapping = { + 3: "facebook", + 4: "twitter", + 10: "phone", + 11: "email" +} + const appendSlash = (url) => { return (url.substr(-1) === "/") ? url : url + "/" } @@ -9,6 +16,7 @@ const appendSlash = (url) => { class AttestationObject { constructor({ claimType, data, signature }) { this.claimType = claimType + this.service = claimTypeMapping[claimType] this.data = data this.signature = signature } From 687e355cbce2619f721cc71b0db5c28e97c3797d Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 18:17:10 -0700 Subject: [PATCH 32/54] Return attestation objects in `get` method --- src/resources/users.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/resources/users.js b/src/resources/users.js index 52e96bc4..318dd7c3 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -170,10 +170,8 @@ class Users extends ResourceBase { profile = await this.ipfsService.getFile(ipfsHash) } let validAttestations = await this.validAttestations(identityAddress, nonProfileClaims) - return { - profile, - attestations: validAttestations - } + let attestations = validAttestations.map(att => new AttestationObject(att)) + return { profile, attestations } } async validAttestations(identityAddress, attestations) { From 75d0623b0105840588bb7e75782360e2d84931d8 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 18:20:51 -0700 Subject: [PATCH 33/54] Return updated user in `set` method --- src/resources/users.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resources/users.js b/src/resources/users.js index 318dd7c3..881b51ec 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -38,7 +38,8 @@ class Users extends ResourceBase { attestations.push(selfAttestation) } let newAttestations = await this.newAttestations(attestations) - this.addAttestations(newAttestations) + await this.addAttestations(newAttestations) + return await this.get() } async get(address) { From 348d494f53a4269af3fe95307157cfc60e8bc387 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 22:46:00 -0700 Subject: [PATCH 34/54] Pass fetch service into attestations constructor This allows it to be stubbed in tests. --- src/index.js | 4 ++- src/resources/attestations.js | 47 +++++++++++++++++------------------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index eedad746..4f55a65a 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import ContractService from "./contract-service" import IpfsService from "./ipfs-service" import { Attestations } from "./resources/attestations" import Users from "./resources/users" +import fetch from "cross-fetch" var resources = { listings: require("./resources/listings"), @@ -26,7 +27,8 @@ class Origin { }) this.attestations = new Attestations({ serverUrl: attestationServerUrl, - contractService: this.contractService + contractService: this.contractService, + fetch }) // Instantiate each resource and give it access to contracts and IPFS diff --git a/src/resources/attestations.js b/src/resources/attestations.js index b79a4625..29819482 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -1,4 +1,3 @@ -import fetch from "cross-fetch" import RLP from "rlp" import web3Utils from "web3-utils" @@ -26,43 +25,43 @@ let responseToUrl = (resp = {}) => { return resp['url'] } -let http = async (baseUrl, url, body, successFn, method) => { - let response = await fetch( - appendSlash(baseUrl) + url, - { - method, - body: body ? JSON.stringify(body) : undefined, - headers: { "content-type": "application/json" } - } - ) - let json = await response.json() - if (response.ok) { - return successFn ? successFn(json) : json - } - return Promise.reject(JSON.stringify(json)) -} - class Attestations { - constructor({ serverUrl, issuer, contractService }) { + constructor({ serverUrl, contractService, fetch }) { this.serverUrl = serverUrl this.contractService = contractService + this.fetch = fetch this.responseToAttestation = (resp = {}) => { return new AttestationObject({ claimType: resp['claim-type'], data: web3Utils.sha3(resp['data']), - signature: resp['signature'], - issuer + signature: resp['signature'] }) } } + async http(baseUrl, url, body, successFn, method) { + let response = await this.fetch( + appendSlash(baseUrl) + url, + { + method, + body: body ? JSON.stringify(body) : undefined, + headers: { "content-type": "application/json" } + } + ) + let json = await response.json() + if (response.ok) { + return successFn ? successFn(json) : json + } + return Promise.reject(JSON.stringify(json)) + } + async post(url, body, successFn) { - return await http(this.serverUrl, url, body, successFn, 'POST') + return await this.http(this.serverUrl, url, body, successFn, 'POST') } async get(url, successFn) { - return await http(this.serverUrl, url, undefined, successFn, 'GET') + return await this.http(this.serverUrl, url, undefined, successFn, 'GET') } async predictIdentityAddress(wallet) { @@ -77,8 +76,8 @@ class Attestations { } async getIdentityAddress(wallet) { - let userRegistry = await this.contractService.userRegistryContract.deployed() - let identityAddress = await userRegistry.users(wallet) + let userRegistry = await this.contractService.deployed(this.contractService.userRegistryContract) + let identityAddress = await userRegistry.methods.users(wallet).call() let hasRegisteredIdentity = identityAddress !== "0x0000000000000000000000000000000000000000" if (hasRegisteredIdentity) { return web3Utils.toChecksumAddress(identityAddress) From 245d0ef62b7e41c034e4f4fd4831fb989d9bd3d8 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 22:48:55 -0700 Subject: [PATCH 35/54] Move "private" methods to bottom of file Of course there's no real distinction between private and public, but it's easier to read if we keep the "public" methods at the top --- src/resources/attestations.js | 70 +++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 29819482..86a5f4e3 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -40,41 +40,6 @@ class Attestations { } } - async http(baseUrl, url, body, successFn, method) { - let response = await this.fetch( - appendSlash(baseUrl) + url, - { - method, - body: body ? JSON.stringify(body) : undefined, - headers: { "content-type": "application/json" } - } - ) - let json = await response.json() - if (response.ok) { - return successFn ? successFn(json) : json - } - return Promise.reject(JSON.stringify(json)) - } - - async post(url, body, successFn) { - return await this.http(this.serverUrl, url, body, successFn, 'POST') - } - - async get(url, successFn) { - return await this.http(this.serverUrl, url, undefined, successFn, 'GET') - } - - async predictIdentityAddress(wallet) { - let web3 = this.contractService.web3 - let nonce = await new Promise((resolve, reject) => { - web3.eth.getTransactionCount(wallet, (err, count) => { - resolve(count) - }) - }) - let address = "0x" + web3Utils.sha3(RLP.encode([wallet, nonce])).substring(26, 66) - return web3Utils.toChecksumAddress(address) - } - async getIdentityAddress(wallet) { let userRegistry = await this.contractService.deployed(this.contractService.userRegistryContract) let identityAddress = await userRegistry.methods.users(wallet).call() @@ -158,6 +123,41 @@ class Attestations { this.responseToAttestation ) } + + async http(baseUrl, url, body, successFn, method) { + let response = await this.fetch( + appendSlash(baseUrl) + url, + { + method, + body: body ? JSON.stringify(body) : undefined, + headers: { "content-type": "application/json" } + } + ) + let json = await response.json() + if (response.ok) { + return successFn ? successFn(json) : json + } + return Promise.reject(JSON.stringify(json)) + } + + async post(url, body, successFn) { + return await this.http(this.serverUrl, url, body, successFn, 'POST') + } + + async get(url, successFn) { + return await this.http(this.serverUrl, url, undefined, successFn, 'GET') + } + + async predictIdentityAddress(wallet) { + let web3 = this.contractService.web3 + let nonce = await new Promise((resolve, reject) => { + web3.eth.getTransactionCount(wallet, (err, count) => { + resolve(count) + }) + }) + let address = "0x" + web3Utils.sha3(RLP.encode([wallet, nonce])).substring(26, 66) + return web3Utils.toChecksumAddress(address) + } } module.exports = { From 5df02818d230851dcbd359ffc5df97d9915b1170 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 22:52:15 -0700 Subject: [PATCH 36/54] Add fetch-mock dependency This is a really nice library that lets us mock the fetch object for http requests. Makes it a breeze to write tests that make external http requests. --- package-lock.json | 24 ++++++++++++++++++++++++ package.json | 1 + 2 files changed, 25 insertions(+) diff --git a/package-lock.json b/package-lock.json index ad46c937..82ded073 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5353,6 +5353,30 @@ "pend": "1.2.0" } }, + "fetch-mock": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-6.3.0.tgz", + "integrity": "sha512-VDQ5dKhO91NzjrP/VtP1np9/sgdJTSvFTk4qiG2+VhpyN6d08xGuQ2YjoA6FvOuugNYQw4LkPMR5Q8UAhqhY9g==", + "dev": true, + "requires": { + "glob-to-regexp": "0.4.0", + "path-to-regexp": "2.2.1" + }, + "dependencies": { + "glob-to-regexp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.0.tgz", + "integrity": "sha512-fyPCII4vn9Gvjq2U/oDAfP433aiE64cyP/CJjRJcpVGjqqNdioUYn9+r0cSzT1XPwmGAHuTT7iv+rQT8u/YHKQ==", + "dev": true + }, + "path-to-regexp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", + "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==", + "dev": true + } + } + }, "fetch-ponyfill": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz", diff --git a/package.json b/package.json index 0af6db78..4eca388f 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "babel-preset-react": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "chai": "^4.1.2", + "fetch-mock": "^6.3.0", "flow-bin": "^0.71.0", "ganache-core": "^2.1.0", "html-webpack-plugin": "^3.2.0", From 3e3abb9cb78c00547a2bf2ec7979c89076f9bcd3 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 22:59:32 -0700 Subject: [PATCH 37/54] Test attestations resource This carefully stubs out the server response and asserts that each request uses the correct method, url, and params. As long as we keep the test expectations in line with the actual bridge server, we get decent confidence that this code will work with the bridge server. --- test/resource_attestations.test.js | 195 +++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 test/resource_attestations.test.js diff --git a/test/resource_attestations.test.js b/test/resource_attestations.test.js new file mode 100644 index 00000000..a27b074e --- /dev/null +++ b/test/resource_attestations.test.js @@ -0,0 +1,195 @@ +import { + Attestations, + AttestationObject +} from "../src/resources/attestations.js" +import ContractService from "../src/contract-service" +import { expect } from "chai" +import Web3 from "web3" +import fetchMock from "fetch-mock" + +const sampleWallet = "0x627306090abaB3A6e1400e9345bC60c78a8BEf57" +const sampleAttestation = { + "claim-type": 1, + data: "some data", + signature: "0x1a2b3c" +} + +let expectParams = (requestBody, params) => { + params.forEach(param => { + expect(requestBody[param], `Param ${param} should be in the request`).to + .exist + }) +} + +let expectAttestation = result => { + expect(result.signature).to.equal(sampleAttestation.signature) + expect(result.data).to.equal(Web3.utils.soliditySha3(sampleAttestation.data)) + expect(result.claimType).to.equal(sampleAttestation["claim-type"]) +} + +let setup = () => { + let provider = new Web3.providers.HttpProvider("http://localhost:8545") + let web3 = new Web3(provider) + let contractService = new ContractService({ web3 }) + return new Attestations({ contractService }) +} + +let setupWithServer = ({ + expectedMethod, + expectedPath, + expectedParams, + responseStub +}) => { + let provider = new Web3.providers.HttpProvider("http://localhost:8545") + let web3 = new Web3(provider) + let contractService = new ContractService({ web3 }) + let serverUrl = "http://fake.url/api/attestations" // fake url + let fetch = fetchMock.sandbox().mock( + (requestUrl, opts) => { + expect(opts.method).to.equal(expectedMethod) + expect(requestUrl).to.equal(serverUrl + "/" + expectedPath) + if (expectedParams) { + let requestBody = JSON.parse(opts.body) + expectParams(requestBody, expectedParams) + } + return true + }, + { body: JSON.stringify(responseStub) } + ) + return new Attestations({ fetch, serverUrl, contractService }) +} + +describe("Attestation Resource", function() { + this.timeout(5000) // default is 2000 + + describe("getIdentityAddress", () => { + it("should predict identity address from wallet", async () => { + let attestations = setup() + let wallet = await attestations.contractService.currentAccount() + let identityAddress = await attestations.getIdentityAddress(wallet) + expect(identityAddress).to.be.a("string") + }) + }) + + describe("phoneGenerateCode", () => { + it("should process the request", async () => { + let attestations = setupWithServer({ + expectedMethod: "POST", + expectedPath: "phone/generate-code", + responseStub: {} + }) + let response = await attestations.phoneGenerateCode({ + phone: "555-555-5555" + }) + expect(response).to.be.an("object") + }) + }) + + describe("phoneVerify", () => { + it("should process the request", async () => { + let attestations = setupWithServer({ + expectedMethod: "POST", + expectedPath: "phone/verify", + expectedParams: ["identity", "phone", "code"], + responseStub: sampleAttestation + }) + let response = await attestations.phoneVerify({ + wallet: sampleWallet, + phone: "555-555-5555", + code: "12345" + }) + expectAttestation(response) + }) + }) + + describe("emailGenerateCode", () => { + it("should process the request", async () => { + let attestations = setupWithServer({ + expectedMethod: "POST", + expectedPath: "email/generate-code", + expectedParams: ["email"], + responseStub: {} + }) + let response = await attestations.emailGenerateCode({ + email: "asdf@asdf.asdf" + }) + expect(response).to.be.an("object") + }) + }) + + describe("emailVerify", () => { + it("should process the request", async () => { + let attestations = setupWithServer({ + expectedMethod: "POST", + expectedPath: "email/verify", + expectedParams: ["identity", "email", "code"], + responseStub: sampleAttestation + }) + let response = await attestations.emailVerify({ + wallet: sampleWallet, + email: "asdf@asdf.asdf", + code: "12345" + }) + expectAttestation(response) + }) + }) + + describe("facebookAuthUrl", () => { + it("should process the request", async () => { + let attestations = setupWithServer({ + expectedMethod: "GET", + expectedPath: "facebook/auth-url?redirect-url=http://redirect.url", + responseStub: { url: "foo.bar" } + }) + let response = await attestations.facebookAuthUrl({ + redirectUrl: "http://redirect.url" + }) + expect(response).to.equal("foo.bar") + }) + }) + + describe("facebookVerify", () => { + it("should process the request", async () => { + let attestations = setupWithServer({ + expectedMethod: "POST", + expectedPath: "facebook/verify", + expectedParams: ["identity", "code", "redirect-url"], + responseStub: sampleAttestation + }) + let response = await attestations.facebookVerify({ + wallet: sampleWallet, + redirectUrl: "foo.bar", + code: "12345" + }) + expectAttestation(response) + }) + }) + + describe("twitterAuthUrl", () => { + it("should process the request", async () => { + let attestations = setupWithServer({ + expectedMethod: "GET", + expectedPath: "twitter/auth-url", + responseStub: { url: "foo.bar" } + }) + let response = await attestations.twitterAuthUrl() + expect(response).to.equal("foo.bar") + }) + }) + + describe("twitterVerify", () => { + it("should process the request", async () => { + let attestations = setupWithServer({ + expectedMethod: "POST", + expectedPath: "twitter/verify", + expectedParams: ["identity", "oauth-verifier"], + responseStub: sampleAttestation + }) + let response = await attestations.twitterVerify({ + wallet: sampleWallet, + oauthVerifier: "foo.bar" + }) + expectAttestation(response) + }) + }) +}) From 5e6195d5a4c50d97761d12101a7e4d8598e0e014 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 23:32:51 -0700 Subject: [PATCH 38/54] Directly use web3 rather than drop in pieces We can do this since we're on 1.0 now. :D --- contracts/migrations/4_add_sample_issuer.js | 4 ++-- contracts/test/identity/TestClaimHolderPresigned.js | 6 +++--- contracts/test/identity/TestClaimHolderRegistered.js | 8 ++++---- package.json | 3 +-- src/resources/attestations.js | 12 ++++++------ src/resources/users.js | 7 +++---- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/contracts/migrations/4_add_sample_issuer.js b/contracts/migrations/4_add_sample_issuer.js index 5ee8f0d1..ced64af6 100644 --- a/contracts/migrations/4_add_sample_issuer.js +++ b/contracts/migrations/4_add_sample_issuer.js @@ -1,8 +1,8 @@ var OriginIdentity = artifacts.require("./OriginIdentity.sol"); -var web3Utils = require("web3-utils") +var Web3 = require("web3") const issuer = "0x99C03fBb0C995ff1160133A8bd210D0E77bCD101" -const issuerHashed = web3Utils.soliditySha3(issuer) +const issuerHashed = Web3.utils.soliditySha3(issuer) const keyPurpose = 3 const keyType = 1 diff --git a/contracts/test/identity/TestClaimHolderPresigned.js b/contracts/test/identity/TestClaimHolderPresigned.js index 2b4f080f..a60c2c7c 100644 --- a/contracts/test/identity/TestClaimHolderPresigned.js +++ b/contracts/test/identity/TestClaimHolderPresigned.js @@ -1,4 +1,4 @@ -var web3Utils = require('web3-utils') +var Web3 = require("web3") const ClaimHolder = artifacts.require("ClaimHolder") const ClaimHolderPresigned = artifacts.require("ClaimHolderPresigned") @@ -41,7 +41,7 @@ contract("ClaimHolderPresigned", accounts => { ) // Check attestation 1 - let claimId_1 = web3Utils.soliditySha3(attestation_1.issuer, attestation_1.claimType) + let claimId_1 = Web3.utils.soliditySha3(attestation_1.issuer, attestation_1.claimType) let fetchedClaim_1 = await instance.getClaim(claimId_1, { from: accounts[0] }) assert.ok(fetchedClaim_1) let [ claimType_1, scheme_1, issuer_1, signature_1, data_1, uri_1 ] = fetchedClaim_1 @@ -53,7 +53,7 @@ contract("ClaimHolderPresigned", accounts => { assert.equal(uri_1, attestation_1.uri) // Check attestation 2 - let claimId_2 = web3Utils.soliditySha3(attestation_2.issuer, attestation_2.claimType) + let claimId_2 = Web3.utils.soliditySha3(attestation_2.issuer, attestation_2.claimType) let fetchedClaim_2 = await instance.getClaim(claimId_2, { from: accounts[0] }) assert.ok(fetchedClaim_2) let [ claimType_2, scheme_2, issuer_2, signature_2, data_2, uri_2 ] = fetchedClaim_2 diff --git a/contracts/test/identity/TestClaimHolderRegistered.js b/contracts/test/identity/TestClaimHolderRegistered.js index 422c3743..a6b14398 100644 --- a/contracts/test/identity/TestClaimHolderRegistered.js +++ b/contracts/test/identity/TestClaimHolderRegistered.js @@ -1,4 +1,4 @@ -const web3Utils = require('web3-utils') +var Web3 = require("web3") const ClaimHolderRegistered = artifacts.require("ClaimHolderRegistered") const UserRegistry = artifacts.require("UserRegistry") @@ -34,7 +34,7 @@ contract("ClaimHolderRegistered", accounts => { }) it("can add and get claim", async function() { - let claimId = web3Utils.soliditySha3( + let claimId = Web3.utils.soliditySha3( attestation_1.issuer, attestation_1.claimType ) @@ -67,7 +67,7 @@ contract("ClaimHolderRegistered", accounts => { { from: accounts[0] } ) - let claimId_1 = web3Utils.soliditySha3( + let claimId_1 = Web3.utils.soliditySha3( attestation_1.issuer, attestation_1.claimType ) @@ -81,7 +81,7 @@ contract("ClaimHolderRegistered", accounts => { assert.equal(data_1, attestation_1.data) assert.equal(uri_1, attestation_1.uri) - let claimId_2 = web3Utils.soliditySha3( + let claimId_2 = Web3.utils.soliditySha3( attestation_2.issuer, attestation_2.claimType ) diff --git a/package.json b/package.json index 4eca388f..3341c319 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,7 @@ "rlp": "^2.0.0", "truffle-contract": "^3.0.5", "util.promisify": "^1.0.0", - "web3": "^1.0.0-beta.34", - "web3-eth-accounts": "^1.0.0-beta.34" + "web3": "^1.0.0-beta.34" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 86a5f4e3..f60aa75b 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -1,5 +1,5 @@ import RLP from "rlp" -import web3Utils from "web3-utils" +import Web3 from "web3" const claimTypeMapping = { 3: "facebook", @@ -34,7 +34,7 @@ class Attestations { this.responseToAttestation = (resp = {}) => { return new AttestationObject({ claimType: resp['claim-type'], - data: web3Utils.sha3(resp['data']), + data: Web3.utils.soliditySha3(resp['data']), signature: resp['signature'] }) } @@ -45,7 +45,7 @@ class Attestations { let identityAddress = await userRegistry.methods.users(wallet).call() let hasRegisteredIdentity = identityAddress !== "0x0000000000000000000000000000000000000000" if (hasRegisteredIdentity) { - return web3Utils.toChecksumAddress(identityAddress) + return Web3.utils.toChecksumAddress(identityAddress) } else { return this.predictIdentityAddress(wallet) } @@ -123,7 +123,7 @@ class Attestations { this.responseToAttestation ) } - + async http(baseUrl, url, body, successFn, method) { let response = await this.fetch( appendSlash(baseUrl) + url, @@ -155,8 +155,8 @@ class Attestations { resolve(count) }) }) - let address = "0x" + web3Utils.sha3(RLP.encode([wallet, nonce])).substring(26, 66) - return web3Utils.toChecksumAddress(address) + let address = "0x" + Web3.utils.sha3(RLP.encode([wallet, nonce])).substring(26, 66) + return Web3.utils.toChecksumAddress(address) } } diff --git a/src/resources/users.js b/src/resources/users.js index 881b51ec..8ae90ce5 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -8,8 +8,7 @@ import { bufferToHex, pubToAddress } from "ethereumjs-util" -import web3Utils from "web3-utils" // not necessary with web3 1.0 -import Web3EthAccounts from "web3-eth-accounts" // not necessary with web3 1.0 +import Web3 from "web3" var Ajv = require('ajv') var ajv = new Ajv() @@ -29,7 +28,7 @@ let validateUser = (data) => { class Users extends ResourceBase { constructor({ contractService, ipfsService }) { super({ contractService, ipfsService }) - this.web3EthAccounts = new Web3EthAccounts() + this.web3EthAccounts = this.contractService.web3.eth.accounts } async set({ profile, attestations = [] }) { @@ -178,7 +177,7 @@ class Users extends ResourceBase { async validAttestations(identityAddress, attestations) { let originIdentity = await this.contractService.originIdentityContract.deployed() return attestations.filter(async ({ claimType, issuer, data, signature }) => { - let msg = web3Utils.soliditySha3(identityAddress, claimType, data) + let msg = Web3.utils.soliditySha3(identityAddress, claimType, data) let prefixedMsg = this.web3EthAccounts.hashMessage(msg) let dataBuf = toBuffer(prefixedMsg) let sig = fromRpcSig(signature) From b6ab69dd67ef208d21ff1d5e9ad1fdd2a483bab3 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Wed, 2 May 2018 23:58:00 -0700 Subject: [PATCH 39/54] Add `deploy` method to contract service --- src/contract-service.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/contract-service.js b/src/contract-service.js index 62b29ab0..9e7b4010 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -119,6 +119,14 @@ class ContractService { ) } + async deploy(contract, args, options) { + let deployed = await this.deployed(contract) + return await deployed.deploy({ + data: contract.bytecode, + arguments: args + }).send(options) + } + async getAllListingIds() { const range = (start, count) => Array.apply(0, Array(count)).map((element, index) => index + start) From c3e71703cbdb8beb3579566d11cf76c0c4c7ef61 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 00:17:38 -0700 Subject: [PATCH 40/54] Add optional addres param to deployed method --- src/contract-service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contract-service.js b/src/contract-service.js index 9e7b4010..b5989fe4 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -110,12 +110,12 @@ class ContractService { } } - async deployed(contract) { + async deployed(contract, addrs) { const net = await this.web3.eth.net.getId() - const addrs = contract.networks[net] + addrs = addrs || contract.networks[net].address || null return new this.web3.eth.Contract( contract.abi, - addrs ? addrs.address : null + addrs ) } From 4f3fc44db5551edf0973896a9dd777c099717f71 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 00:18:55 -0700 Subject: [PATCH 41/54] Update users resource to use 1.0 --- src/resources/users.js | 79 ++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/src/resources/users.js b/src/resources/users.js index 8ae90ce5..d9dfa407 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -51,9 +51,9 @@ class Users extends ResourceBase { async identityAddress(address) { let account = await this.contractService.currentAccount() - let userRegistry = await this.contractService.userRegistryContract.deployed() + let userRegistry = await this.contractService.deployed(this.contractService.userRegistryContract) address = address || account - let result = await userRegistry.users(address) + let result = await userRegistry.methods.users(address).call() if (String(result) === zeroAddress) { } else { return result @@ -96,7 +96,7 @@ class Users extends ResourceBase { async addAttestations(attestations) { let account = await this.contractService.currentAccount() - let userRegistry = await this.contractService.userRegistryContract.deployed() + let userRegistry = await this.contractService.deployed(this.contractService.userRegistryContract) let identityAddress = await this.identityAddress() if (attestations.length) { // format params for solidity methods to batch add claims @@ -112,23 +112,28 @@ class Users extends ResourceBase { if (identityAddress) { // batch add claims to existing identity - let claimHolder = await this.contractService.claimHolderRegisteredContract.at(identityAddress) - return await claimHolder.addClaims( + let claimHolder = await this.contractService.deployed( + this.contractService.claimHolderRegisteredContract, + identityAddress + ) + return await claimHolder.methods.addClaims( claimTypes, issuers, sigs, - data, - { from: account, gas: 4000000 } - ) + data + ).call({ from: account, gas: 4000000 }) } else { // create identity with presigned claims - return await this.contractService.claimHolderPresignedContract.new( - userRegistry.address, - claimTypes, - issuers, - sigs, - data, - dataOffsets, + return await this.contractService.deploy( + this.contractService.claimHolderPresignedContract, + [ + userRegistry.options.address, + claimTypes, + issuers, + sigs, + data, + dataOffsets + ], { from: account, gas: 4000000 } ) } @@ -142,27 +147,25 @@ class Users extends ResourceBase { } async getClaims(identityAddress) { - let identity = this.contractService.claimHolderRegisteredContract.at(identityAddress) - let allEvents = identity.allEvents({fromBlock: 0, toBlock: 'latest'}) - let claims = await new Promise((resolve, reject) => { - allEvents.get((err, events) => { - let claimAddedEvents = events.filter(({ event }) => event === "ClaimAdded" ) - let mapped = claimAddedEvents.map(({ args }) => { - return { - claimId: args.claimId, - claimType: args.claimType.toNumber(), - data: args.data, - issuer: args.issuer, - scheme: args.scheme.toNumber(), - signature: args.signature, - uri: args.uri - } - }) - resolve(mapped) - }) + let identity = await this.contractService.deployed( + this.contractService.claimHolderRegisteredContract, + identityAddress + ) + let allEvents = await identity.getPastEvents("allEvents", { fromBlock: 0 }) + let claimAddedEvents = allEvents.filter(({ event }) => event === "ClaimAdded" ) + let mapped = claimAddedEvents.map(({ returnValues }) => { + return { + claimId: returnValues.claimId, + claimType: Number(returnValues.claimType), + data: returnValues.data, + issuer: returnValues.issuer, + scheme: Number(returnValues.scheme), + signature: returnValues.signature, + uri: returnValues.uri + } }) - let profileClaims = claims.filter(({ claimType }) => claimType === selfAttestationClaimType ) - let nonProfileClaims = claims.filter(({ claimType }) => claimType !== selfAttestationClaimType ) + let profileClaims = mapped.filter(({ claimType }) => claimType === selfAttestationClaimType ) + let nonProfileClaims = mapped.filter(({ claimType }) => claimType !== selfAttestationClaimType ) let profile = {} if (profileClaims.length) { let bytes32 = profileClaims[profileClaims.length - 1].data @@ -175,7 +178,7 @@ class Users extends ResourceBase { } async validAttestations(identityAddress, attestations) { - let originIdentity = await this.contractService.originIdentityContract.deployed() + let originIdentity = await this.contractService.deployed(this.contractService.originIdentityContract) return attestations.filter(async ({ claimType, issuer, data, signature }) => { let msg = Web3.utils.soliditySha3(identityAddress, claimType, data) let prefixedMsg = this.web3EthAccounts.hashMessage(msg) @@ -184,8 +187,8 @@ class Users extends ResourceBase { let recovered = ecrecover(dataBuf, sig.v, sig.r, sig.s) let recoveredBuf = pubToAddress(recovered) let recoveredHex = bufferToHex(recoveredBuf) - let hashedRecovered = web3Utils.soliditySha3(recoveredHex) - return await originIdentity.keyHasPurpose(hashedRecovered, 3) + let hashedRecovered = Web3.utils.soliditySha3(recoveredHex) + return await originIdentity.methods.keyHasPurpose(hashedRecovered, 3).call() }) } } From 88fad96f8d2d46247cdb8a1f4fae1e0f1a4b42be Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 00:20:21 -0700 Subject: [PATCH 42/54] Coerce claimType to number in AttestationObject constructor Just to be safe --- src/resources/attestations.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index f60aa75b..6b4f93b4 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -14,6 +14,7 @@ const appendSlash = (url) => { class AttestationObject { constructor({ claimType, data, signature }) { + claimType = Number(claimType) this.claimType = claimType this.service = claimTypeMapping[claimType] this.data = data From 98caf41133f7174e4b2284879b34a7e40cb334fc Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 00:48:39 -0700 Subject: [PATCH 43/54] Contract service bug fix --- src/contract-service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/contract-service.js b/src/contract-service.js index b5989fe4..31690335 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -112,7 +112,8 @@ class ContractService { async deployed(contract, addrs) { const net = await this.web3.eth.net.getId() - addrs = addrs || contract.networks[net].address || null + let storedAddress = contract.networks[net] && contract.networks[net].address + addrs = addrs || storedAddress || null return new this.web3.eth.Contract( contract.abi, addrs From 498900699b0a6233416ab37b9c77dfcd31911056 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 00:49:28 -0700 Subject: [PATCH 44/54] Fix logic error - should use `and` instead of `or` for exact match --- src/resources/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/users.js b/src/resources/users.js index d9dfa407..0d96a59b 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -87,7 +87,7 @@ class Users extends ResourceBase { let claimTypeMatches = attestation.claimType === existingAttestation.claimType let dataMatches = attestation.data === existingAttestation.data let sigMatches = attestation.signature === existingAttestation.signature - return claimTypeMatches || dataMatches || sigMatches + return claimTypeMatches && dataMatches && sigMatches }) let isNew = matchingAttestation.length === 0 return isNew From d1a4359ab1a45f97e6a91998ed888ef3b3cb8d9b Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 02:34:04 -0700 Subject: [PATCH 45/54] Add a second sample issuer during migrations --- ...add_sample_issuer.js => 4_add_sample_issuers.js} | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) rename contracts/migrations/{4_add_sample_issuer.js => 4_add_sample_issuers.js} (62%) diff --git a/contracts/migrations/4_add_sample_issuer.js b/contracts/migrations/4_add_sample_issuers.js similarity index 62% rename from contracts/migrations/4_add_sample_issuer.js rename to contracts/migrations/4_add_sample_issuers.js index ced64af6..922f3b83 100644 --- a/contracts/migrations/4_add_sample_issuer.js +++ b/contracts/migrations/4_add_sample_issuers.js @@ -1,8 +1,8 @@ var OriginIdentity = artifacts.require("./OriginIdentity.sol"); var Web3 = require("web3") -const issuer = "0x99C03fBb0C995ff1160133A8bd210D0E77bCD101" -const issuerHashed = Web3.utils.soliditySha3(issuer) +const issuer_1 = "0x99C03fBb0C995ff1160133A8bd210D0E77bCD101" +const issuer_2 = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf" const keyPurpose = 3 const keyType = 1 @@ -17,8 +17,15 @@ async function add_sample_issuer(network) { let originIdentity = await OriginIdentity.deployed() + await originIdentity.addKey( + Web3.utils.soliditySha3(issuer_1), + keyPurpose, + keyType, + { from: defaultAccount, gas: 4000000 } + ) + return await originIdentity.addKey( - issuerHashed, + Web3.utils.soliditySha3(issuer_2), keyPurpose, keyType, { from: defaultAccount, gas: 4000000 } From 836a66ba86c634a92507b9a72adfc5e165379732 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 02:34:59 -0700 Subject: [PATCH 46/54] Create separate method for validating a single attestation --- src/resources/users.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/resources/users.js b/src/resources/users.js index 0d96a59b..4568d30d 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -177,19 +177,27 @@ class Users extends ResourceBase { return { profile, attestations } } - async validAttestations(identityAddress, attestations) { + async isValidAttestation({ claimType, data, signature }, identityAddress) { let originIdentity = await this.contractService.deployed(this.contractService.originIdentityContract) - return attestations.filter(async ({ claimType, issuer, data, signature }) => { - let msg = Web3.utils.soliditySha3(identityAddress, claimType, data) - let prefixedMsg = this.web3EthAccounts.hashMessage(msg) - let dataBuf = toBuffer(prefixedMsg) - let sig = fromRpcSig(signature) - let recovered = ecrecover(dataBuf, sig.v, sig.r, sig.s) - let recoveredBuf = pubToAddress(recovered) - let recoveredHex = bufferToHex(recoveredBuf) - let hashedRecovered = Web3.utils.soliditySha3(recoveredHex) - return await originIdentity.methods.keyHasPurpose(hashedRecovered, 3).call() + let msg = Web3.utils.soliditySha3(identityAddress, claimType, data) + let prefixedMsg = this.web3EthAccounts.hashMessage(msg) + let dataBuf = toBuffer(prefixedMsg) + let sig = fromRpcSig(signature) + let recovered = ecrecover(dataBuf, sig.v, sig.r, sig.s) + let recoveredBuf = pubToAddress(recovered) + let recoveredHex = bufferToHex(recoveredBuf) + let hashedRecovered = Web3.utils.soliditySha3(recoveredHex) + return await originIdentity.methods.keyHasPurpose(hashedRecovered, 3).call() + } + + async validAttestations(identityAddress, attestations) { + let promiseWithValidation = attestations.map(async (attestation) => { + let isValid = await this.isValidAttestation(attestation, identityAddress) + return { isValid, attestation } }) + let withValidation = await Promise.all(promiseWithValidation) + let filtered = withValidation.filter(({ isValid, attestation }) => isValid) + return filtered.map(({ attestation }) => attestation) } } From 2a738358dee5bea0d4883d217f5e7896624b39a5 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 02:35:18 -0700 Subject: [PATCH 47/54] Code format with `npm run format` --- src/contract-service.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/contract-service.js b/src/contract-service.js index 31690335..618074c4 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -114,18 +114,17 @@ class ContractService { const net = await this.web3.eth.net.getId() let storedAddress = contract.networks[net] && contract.networks[net].address addrs = addrs || storedAddress || null - return new this.web3.eth.Contract( - contract.abi, - addrs - ) + return new this.web3.eth.Contract(contract.abi, addrs) } async deploy(contract, args, options) { let deployed = await this.deployed(contract) - return await deployed.deploy({ - data: contract.bytecode, - arguments: args - }).send(options) + return await deployed + .deploy({ + data: contract.bytecode, + arguments: args + }) + .send(options) } async getAllListingIds() { From 9f03339a5ca84a5128af627c4967d5f43b31e706 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 11:46:19 -0700 Subject: [PATCH 48/54] Users resource bug fixes --- src/resources/users.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resources/users.js b/src/resources/users.js index 4568d30d..6e074b45 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -55,6 +55,7 @@ class Users extends ResourceBase { address = address || account let result = await userRegistry.methods.users(address).call() if (String(result) === zeroAddress) { + return false } else { return result } @@ -121,7 +122,7 @@ class Users extends ResourceBase { issuers, sigs, data - ).call({ from: account, gas: 4000000 }) + ).send({ from: account, gas: 4000000 }) } else { // create identity with presigned claims return await this.contractService.deploy( From 31007f3cffce493d381771da32519cf2ff7404cb Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 11:47:26 -0700 Subject: [PATCH 49/54] Add clearUser method to user registry It doesn't add any new functionality, strictly speaking. The same thing could be accomplished by creating a contract that does the same thing. I basically just created this as a convenience for clearing users between tests. We can refactor later if we think of a cleaner solution. --- contracts/contracts/UserRegistry.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/contracts/UserRegistry.sol b/contracts/contracts/UserRegistry.sol index a086002b..2cf5f16f 100644 --- a/contracts/contracts/UserRegistry.sol +++ b/contracts/contracts/UserRegistry.sol @@ -31,4 +31,11 @@ contract UserRegistry { users[tx.origin] = msg.sender; emit NewUser(tx.origin, msg.sender); } + + /// @dev clearUser(): Remove user from the registry + function clearUser() + public + { + users[msg.sender] = 0; + } } From 636aefc0d08942f5cc38ff54c7c0e56018cd4a07 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 11:48:12 -0700 Subject: [PATCH 50/54] Test the users resource --- test/resource_users.test.js | 171 ++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 test/resource_users.test.js diff --git a/test/resource_users.test.js b/test/resource_users.test.js new file mode 100644 index 00000000..04d37afd --- /dev/null +++ b/test/resource_users.test.js @@ -0,0 +1,171 @@ +import Users from "../src/resources/users.js" +import { + Attestations, + AttestationObject +} from "../src/resources/attestations.js" +import ContractService from "../src/contract-service.js" +import IpfsService from "../src/ipfs-service.js" +import { expect } from "chai" +import Web3 from "web3" + +const issuerPrivatekey = + "0000000000000000000000000000000000000000000000000000000000000001" +const issuerPublicKey = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf" +const issuerHashed = Web3.utils.soliditySha3(issuerPublicKey) + +let generateAttestation = async ({ + identityAddress, + web3, + claimType, + data +}) => { + data = Web3.utils.soliditySha3(data) + let msg = Web3.utils.soliditySha3(identityAddress, claimType, data) + let prefixedMsg = web3.eth.accounts.hashMessage(msg) + let signing = web3.eth.accounts.sign(msg, issuerPrivatekey) + let signature = signing.signature + return new AttestationObject({ claimType, data, signature }) +} + +const invalidAttestation = new AttestationObject({ + claimType: 123, + data: Web3.utils.sha3("gibberish"), + signature: + "0x4e8feba65cbd88fc246013da8dfb478e880518594d86349f54af9c8d5e2eac2b223222c4c6b93f18bd54fc88f4342f1b02a8ea764a411fc02823a3420574375c1c" +}) + +describe("User Resource", function() { + this.timeout(10000) // default is 2000 + let users + let phoneAttestation + let emailAttestation + let facebookAttestation + + beforeEach(async () => { + let provider = new Web3.providers.HttpProvider("http://localhost:8545") + let web3 = new Web3(provider) + let accounts = await web3.eth.getAccounts() + let contractService = new ContractService({ web3 }) + let originIdentity = await contractService.deployed( + contractService.originIdentityContract + ) + let ipfsService = new IpfsService({ + ipfsDomain: "127.0.0.1", + ipfsApiPort: "5002", + ipfsGatewayPort: "8080", + ipfsGatewayProtocol: "http" + }) + let attestations = new Attestations({ contractService }) + users = new Users({ contractService, ipfsService }) + + // clear user before each test because blockchain persists between tests + // sort of a hack to force clean state at beginning of each test + let userRegistry = await contractService.deployed( + contractService.userRegistryContract + ) + await userRegistry.methods.clearUser().send({ from: accounts[0] }) + + let identityAddress = await attestations.getIdentityAddress(accounts[0]) + phoneAttestation = await generateAttestation({ + identityAddress, + web3, + claimType: 10, + data: "phone verified" + }) + emailAttestation = await generateAttestation({ + identityAddress, + web3, + claimType: 11, + data: "email verified" + }) + return (facebookAttestation = await generateAttestation({ + identityAddress, + web3, + claimType: 3, + data: "facebook verified" + })) + }) + + describe("set", () => { + it("should be able to deploy new identity", async () => { + let user = await users.set({ + profile: { claims: { name: "Wonder Woman" } } + }) + + expect(user.attestations.length).to.equal(0) + expect(user.profile.claims.name).to.equal("Wonder Woman") + }) + + it("should be able to update profile and claims after creation", async () => { + let user = await users.set({ + profile: { claims: { name: "Iron Man" } } + }) + + expect(user.attestations.length).to.equal(0) + expect(user.profile.claims.name).to.equal("Iron Man") + + user = await users.set({ + profile: { claims: { name: "Black Panther" } }, + attestations: [phoneAttestation] + }) + + expect(user.attestations.length).to.equal(1) + expect(user.profile.claims.name).to.equal("Black Panther") + + user = await users.set({ + profile: { claims: { name: "Batman" } } + }) + + expect(user.attestations.length).to.equal(1) + expect(user.profile.claims.name).to.equal("Batman") + + user = await users.set({ + attestations: [phoneAttestation, emailAttestation] + }) + + expect(user.attestations.length).to.equal(2) + expect(user.profile.claims.name).to.equal("Batman") + }) + + it("should be able to deploy new identity with presigned claims", async () => { + let user = await users.set({ + profile: { claims: { name: "Black Widow" } }, + attestations: [phoneAttestation, emailAttestation] + }) + + expect(user.attestations.length).to.equal(2) + expect(user.profile.claims.name).to.equal("Black Widow") + }) + + it("should ignore invalid claims", async () => { + let user = await users.set({ + profile: { claims: { name: "Deadpool" } }, + attestations: [phoneAttestation, emailAttestation, invalidAttestation] + }) + + expect(user.attestations.length).to.equal(2) + expect(user.profile.claims.name).to.equal("Deadpool") + }) + }) + + describe("get", () => { + it("should reflect the current state of the user", async () => { + await users.set({ + profile: { claims: { name: "Groot" } } + }) + let user = await users.get() + + expect(user.attestations.length).to.equal(0) + expect(user.profile.claims.name).to.equal("Groot") + + await users.set({ + profile: { claims: { name: "Baby Groot" } }, + attestations: [phoneAttestation] + }) + user = await users.get() + + expect(user.attestations.length).to.equal(1) + expect(user.profile.claims.name).to.equal("Baby Groot") + }) + }) +}) From b3847fd5c84e2247380b53a36993724b98e98f42 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 16:11:34 -0700 Subject: [PATCH 51/54] Add attestationServerUrl config option to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 80791d1c..d3125c69 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ And buyers to: - `ipfsApiPort` - `ipfsGatewayPort` - `ipfsGatewayProtocol` + - `attestationServerUrl` ## IPFS From e0c97e0fa2cb17cbccfe415bba2de5c5c9e3e31e Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 16:13:55 -0700 Subject: [PATCH 52/54] Fall back to current account if wallet is not provided --- src/resources/attestations.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/resources/attestations.js b/src/resources/attestations.js index 6b4f93b4..0ccb48d4 100644 --- a/src/resources/attestations.js +++ b/src/resources/attestations.js @@ -42,6 +42,8 @@ class Attestations { } async getIdentityAddress(wallet) { + let currentAccount = await this.contractService.currentAccount() + wallet = wallet || currentAccount let userRegistry = await this.contractService.deployed(this.contractService.userRegistryContract) let identityAddress = await userRegistry.methods.users(wallet).call() let hasRegisteredIdentity = identityAddress !== "0x0000000000000000000000000000000000000000" From b3de5212eb395c2724bf56d3b5a3e3956a2ea5af Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 17:11:20 -0700 Subject: [PATCH 53/54] Update `deploy` method to wait until the deploy has finished --- src/contract-service.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/contract-service.js b/src/contract-service.js index 618074c4..33d1d02b 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -119,12 +119,19 @@ class ContractService { async deploy(contract, args, options) { let deployed = await this.deployed(contract) - return await deployed - .deploy({ - data: contract.bytecode, - arguments: args - }) - .send(options) + let transaction = await new Promise((resolve, reject) => { + deployed + .deploy({ + data: contract.bytecode, + arguments: args + }) + .send(options) + .on("receipt", receipt => { + resolve(receipt) + }) + .on("error", err => reject(err)) + }) + return await this.waitTransactionFinished(transaction.transactionHash) } async getAllListingIds() { From d18dddb8f6c8348896d5851d8da5d6c750b79e31 Mon Sep 17 00:00:00 2001 From: Tyler Yasaka Date: Thu, 3 May 2018 17:22:23 -0700 Subject: [PATCH 54/54] Return user object from usrs resource --- src/resources/users.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/resources/users.js b/src/resources/users.js index 6e074b45..fad4374b 100644 --- a/src/resources/users.js +++ b/src/resources/users.js @@ -25,6 +25,13 @@ let validateUser = (data) => { } } +class UserObject { + constructor({ profile, attestations } = {}) { + this.profile = profile + this.attestations = attestations + } +} + class Users extends ResourceBase { constructor({ contractService, ipfsService }) { super({ contractService, ipfsService }) @@ -44,9 +51,10 @@ class Users extends ResourceBase { async get(address) { let identityAddress = await this.identityAddress(address) if (identityAddress) { - return await this.getClaims(identityAddress) + let userData = await this.getClaims(identityAddress) + return new UserObject(userData) } - return [] + return new UserObject() } async identityAddress(address) {