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 diff --git a/contracts/contracts/UserRegistry.sol b/contracts/contracts/UserRegistry.sol index e5f875a1..2cf5f16f 100644 --- a/contracts/contracts/UserRegistry.sol +++ b/contracts/contracts/UserRegistry.sol @@ -24,33 +24,18 @@ contract UserRegistry { * Public functions */ - /// @dev create(): Create a user - function create() 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); + users[tx.origin] = msg.sender; + emit NewUser(tx.origin, msg.sender); } - /// @dev createWithClaims(): Create a user with presigned claims - // Params correspond to params of ClaimHolderPresigned - function createWithClaims( - uint256[] _claimType, - address[] _issuer, - bytes _signature, - bytes _data, - uint256[] _offsets - ) - public + /// @dev clearUser(): Remove user from the registry + function clearUser() + public { - ClaimHolderPresigned _identity = new ClaimHolderPresigned( - _claimType, - _issuer, - _signature, - _data, - _offsets - ); - users[msg.sender] = _identity; - emit NewUser(msg.sender, _identity); + users[msg.sender] = 0; } } diff --git a/contracts/contracts/identity/ClaimHolder.sol b/contracts/contracts/identity/ClaimHolder.sol index 6bc41fc2..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"); @@ -104,4 +124,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..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,15 +8,17 @@ import './ClaimHolder.sol'; * instead. */ -contract ClaimHolderPresigned is ClaimHolder { +contract ClaimHolderPresigned is ClaimHolderRegistered { constructor( + address _userRegistryAddress, uint256[] _claimType, address[] _issuer, bytes _signature, bytes _data, uint256[] _offsets ) + ClaimHolderRegistered(_userRegistryAddress) public { uint offset = 0; @@ -32,18 +34,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; - } } 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/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..a417a595 100644 --- a/contracts/migrations/2_deploy_contracts.js +++ b/contracts/migrations/2_deploy_contracts.js @@ -2,11 +2,19 @@ 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); - deployer.link(PurchaseLibrary, ListingsRegistry) - deployer.link(PurchaseLibrary, Listing) - deployer.deploy(ListingsRegistry); - deployer.deploy(UserRegistry); -}; \ No newline at end of file +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 b350cd06..3aa1230a 100644 --- a/contracts/migrations/3_create_sample_listings.js +++ b/contracts/migrations/3_create_sample_listings.js @@ -1,10 +1,11 @@ -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) + return deployer.then(() => { + return deploy_sample_contracts(network) + }) } async function deploy_sample_contracts(network) { @@ -15,56 +16,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_issuers.js b/contracts/migrations/4_add_sample_issuers.js new file mode 100644 index 00000000..922f3b83 --- /dev/null +++ b/contracts/migrations/4_add_sample_issuers.js @@ -0,0 +1,33 @@ +var OriginIdentity = artifacts.require("./OriginIdentity.sol"); +var Web3 = require("web3") + +const issuer_1 = "0x99C03fBb0C995ff1160133A8bd210D0E77bCD101" +const issuer_2 = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf" +const keyPurpose = 3 +const keyType = 1 + +module.exports = function(deployer, network) { + return deployer.then(() => { + return add_sample_issuer(network) + }) +} + +async function add_sample_issuer(network) { + let defaultAccount = web3.eth.accounts[0] + + let originIdentity = await OriginIdentity.deployed() + + await originIdentity.addKey( + Web3.utils.soliditySha3(issuer_1), + keyPurpose, + keyType, + { from: defaultAccount, gas: 4000000 } + ) + + return await originIdentity.addKey( + Web3.utils.soliditySha3(issuer_2), + keyPurpose, + keyType, + { from: defaultAccount, gas: 4000000 } + ) +} diff --git a/contracts/test/TestUserRegistry.js b/contracts/test/TestUserRegistry.js index 1f43450f..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.create({ 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.createWithClaims( - [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 deleted file mode 100644 index c377e38c..00000000 --- a/contracts/test/identity/TestClaimHolder.js +++ /dev/null @@ -1,48 +0,0 @@ -const web3Utils = require('web3-utils') - -const contractDefinition = artifacts.require("ClaimHolder") - -const signature_1 = "0xeb6123e537e17e2c67b67bbc0b93e6b25ea9eae276c4c2ab353bd7e853ebad2446cc7e91327f3737559d7a9a90fc88529a6b72b770a612f808ab0ba57a46866e1c" - -const dataHash_1 = "0x4f32f7a7d40b4d65a917926cbfd8fd521483e7472bcc4d024179735622447dc9" - -contract("ClaimHolder", accounts => { - let instance - let attestation_1 = { - claimType: 1, - scheme: 11, - issuer: accounts[1], - signature: signature_1, - data: dataHash_1, - uri: "https://foo.bar/attestation1" - } - - beforeEach(async function() { - instance = await contractDefinition.new({ from: accounts[0] }) - }) - - it("can add and get claim", async function() { - let claimId = web3Utils.soliditySha3( - attestation_1.issuer, - attestation_1.claimType - ) - await instance.addClaim( - attestation_1.claimType, - attestation_1.scheme, - attestation_1.issuer, - attestation_1.signature, - attestation_1.data, - attestation_1.uri, - { from: accounts[0] } - ) - let fetchedClaim = await instance.getClaim(claimId, { from: accounts[0] }) - 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) - }) -}) diff --git a/contracts/test/identity/TestClaimHolderPresigned.js b/contracts/test/identity/TestClaimHolderPresigned.js index 442452d1..a60c2c7c 100644 --- a/contracts/test/identity/TestClaimHolderPresigned.js +++ b/contracts/test/identity/TestClaimHolderPresigned.js @@ -1,7 +1,8 @@ -var web3Utils = require('web3-utils') +var Web3 = require("web3") const ClaimHolder = artifacts.require("ClaimHolder") const ClaimHolderPresigned = artifacts.require("ClaimHolderPresigned") +const UserRegistry = artifacts.require("UserRegistry") const signature_1 = "0xeb6123e537e17e2c67b67bbc0b93e6b25ea9eae276c4c2ab353bd7e853ebad2446cc7e91327f3737559d7a9a90fc88529a6b72b770a612f808ab0ba57a46866e1c" const signature_2 = "0x061ef9cdd7707d90d7a7d95b53ddbd94905cb05dfe4734f97744c7976f2776145fef298fd0e31afa43a103cd7f5b00e3b226b0d62e4c492d54bec02eb0c2a0901b" @@ -9,11 +10,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, @@ -33,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), @@ -43,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 @@ -55,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 @@ -65,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/contracts/test/identity/TestClaimHolderRegistered.js b/contracts/test/identity/TestClaimHolderRegistered.js new file mode 100644 index 00000000..a6b14398 --- /dev/null +++ b/contracts/test/identity/TestClaimHolderRegistered.js @@ -0,0 +1,104 @@ +var Web3 = require("web3") + +const ClaimHolderRegistered = artifacts.require("ClaimHolderRegistered") +const UserRegistry = artifacts.require("UserRegistry") + +const signature_1 = "0xeb6123e537e17e2c67b67bbc0b93e6b25ea9eae276c4c2ab353bd7e853ebad2446cc7e91327f3737559d7a9a90fc88529a6b72b770a612f808ab0ba57a46866e1c" +const signature_2 = "0x061ef9cdd7707d90d7a7d95b53ddbd94905cb05dfe4734f97744c7976f2776145fef298fd0e31afa43a103cd7f5b00e3b226b0d62e4c492d54bec02eb0c2a0901b" + +const dataHash_1 = "0x4f32f7a7d40b4d65a917926cbfd8fd521483e7472bcc4d024179735622447dc9" +const dataHash_2 = "0xa183d4eb3552e730c2dd3df91384426eb88879869b890ad12698320d8b88cb48" + +contract("ClaimHolderRegistered", accounts => { + let claimHolderRegistered, userRegistry + let attestation_1 = { + claimType: 1, + scheme: 1, + issuer: accounts[1], + signature: signature_1, + data: dataHash_1, + uri: "" + } + let attestation_2 = { + claimType: 2, + scheme: 1, + issuer: accounts[2], + signature: signature_2, + data: dataHash_2, + uri: "" + } + + beforeEach(async function() { + userRegistry = await UserRegistry.new( { from: accounts[1] } ) + claimHolderRegistered = await ClaimHolderRegistered.new(userRegistry.address, { from: accounts[0] }) + }) + + it("can add and get claim", async function() { + let claimId = Web3.utils.soliditySha3( + attestation_1.issuer, + attestation_1.claimType + ) + await claimHolderRegistered.addClaim( + attestation_1.claimType, + attestation_1.scheme, + attestation_1.issuer, + attestation_1.signature, + attestation_1.data, + attestation_1.uri, + { 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) + 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) + }) + + it("can batch add claims", async function() { + await claimHolderRegistered.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 = Web3.utils.soliditySha3( + attestation_1.issuer, + attestation_1.claimType + ) + 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) + 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 = Web3.utils.soliditySha3( + attestation_2.issuer, + attestation_2.claimType + ) + 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) + 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) + }) + + it("registers the user", async function() { + let identityAddress = await userRegistry.users(accounts[0]) + assert.ok(identityAddress) + assert.notEqual(identityAddress, "0x0000000000000000000000000000000000000000") + }) +}) diff --git a/package-lock.json b/package-lock.json index a170074e..82ded073 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" @@ -5355,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", @@ -10556,7 +10578,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", @@ -16190,8 +16211,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..3341c319 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,11 @@ "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", + "rlp": "^2.0.0", "truffle-contract": "^3.0.5", "util.promisify": "^1.0.0", "web3": "^1.0.0-beta.34" @@ -50,6 +52,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", diff --git a/src/contract-service.js b/src/contract-service.js index a619f86b..33d1d02b 100644 --- a/src/contract-service.js +++ b/src/contract-service.js @@ -1,7 +1,10 @@ +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" 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" @@ -19,7 +22,10 @@ class ContractService { listingsRegistryContract: ListingsRegistryContract, listingContract: ListingContract, purchaseContract: PurchaseContract, - userRegistryContract: UserRegistryContract + userRegistryContract: UserRegistryContract, + claimHolderRegisteredContract: ClaimHolderRegisteredContract, + claimHolderPresignedContract: ClaimHolderPresignedContract, + originIdentityContract: OriginIdentityContract } for (let name in contracts) { this[name] = contracts[name] @@ -104,13 +110,28 @@ class ContractService { } } - async deployed(contract) { + async deployed(contract, addrs) { const net = await this.web3.eth.net.getId() - const addrs = contract.networks[net] - return new this.web3.eth.Contract( - contract.abi, - addrs ? addrs.address : null - ) + let storedAddress = contract.networks[net] && contract.networks[net].address + addrs = addrs || storedAddress || null + return new this.web3.eth.Contract(contract.abi, addrs) + } + + async deploy(contract, args, options) { + let deployed = await this.deployed(contract) + 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() { diff --git a/src/index.js b/src/index.js index baf144df..4f55a65a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,13 @@ 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"), - purchases: require("./resources/purchases") + purchases: require("./resources/purchases"), + users: require("./resources/users") } class Origin { @@ -11,7 +15,8 @@ class Origin { ipfsDomain, ipfsApiPort, ipfsGatewayPort, - ipfsGatewayProtocol + ipfsGatewayProtocol, + attestationServerUrl } = {}) { this.contractService = new ContractService() this.ipfsService = new IpfsService({ @@ -20,6 +25,11 @@ class Origin { ipfsGatewayPort, ipfsGatewayProtocol }) + this.attestations = new Attestations({ + serverUrl: attestationServerUrl, + contractService: this.contractService, + fetch + }) // 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 new file mode 100644 index 00000000..0ccb48d4 --- /dev/null +++ b/src/resources/attestations.js @@ -0,0 +1,169 @@ +import RLP from "rlp" +import Web3 from "web3" + +const claimTypeMapping = { + 3: "facebook", + 4: "twitter", + 10: "phone", + 11: "email" +} + +const appendSlash = (url) => { + return (url.substr(-1) === "/") ? url : url + "/" +} + +class AttestationObject { + constructor({ claimType, data, signature }) { + claimType = Number(claimType) + this.claimType = claimType + this.service = claimTypeMapping[claimType] + this.data = data + this.signature = signature + } +} + +let responseToUrl = (resp = {}) => { + return resp['url'] +} + +class Attestations { + constructor({ serverUrl, contractService, fetch }) { + this.serverUrl = serverUrl + this.contractService = contractService + this.fetch = fetch + + this.responseToAttestation = (resp = {}) => { + return new AttestationObject({ + claimType: resp['claim-type'], + data: Web3.utils.soliditySha3(resp['data']), + signature: resp['signature'] + }) + } + } + + 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" + if (hasRegisteredIdentity) { + return Web3.utils.toChecksumAddress(identityAddress) + } else { + return this.predictIdentityAddress(wallet) + } + } + + async phoneGenerateCode({ phone }) { + return await this.post("phone/generate-code", { phone }) + } + + async phoneVerify({ wallet, phone, code }) { + let identity = await this.getIdentityAddress(wallet) + return await this.post( + "phone/verify", + { + identity, + phone, + code + }, + this.responseToAttestation + ) + } + + async emailGenerateCode({ email }) { + return await this.post("email/generate-code", { email }) + } + + async emailVerify({ wallet, email, code }) { + let identity = await this.getIdentityAddress(wallet) + 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({ wallet, redirectUrl, code }) { + let identity = await this.getIdentityAddress(wallet) + 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({ wallet, oauthVerifier }) { + let identity = await this.getIdentityAddress(wallet) + return await this.post( + "twitter/verify", + { + identity, + "oauth-verifier": oauthVerifier + }, + 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" + Web3.utils.sha3(RLP.encode([wallet, nonce])).substring(26, 66) + return Web3.utils.toChecksumAddress(address) + } +} + +module.exports = { + AttestationObject, + Attestations +} diff --git a/src/resources/users.js b/src/resources/users.js new file mode 100644 index 00000000..fad4374b --- /dev/null +++ b/src/resources/users.js @@ -0,0 +1,213 @@ +import ResourceBase from "../ResourceBase" +import { AttestationObject } from "./attestations" +import userSchema from "../schemas/user.json" +import { + fromRpcSig, + ecrecover, + toBuffer, + bufferToHex, + pubToAddress +} from "ethereumjs-util" +import Web3 from "web3" + +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) + if (!validate(data)) { + throw new Error('Invalid user data') + } else { + return data + } +} + +class UserObject { + constructor({ profile, attestations } = {}) { + this.profile = profile + this.attestations = attestations + } +} + +class Users extends ResourceBase { + constructor({ contractService, ipfsService }) { + super({ contractService, ipfsService }) + this.web3EthAccounts = this.contractService.web3.eth.accounts + } + + async set({ profile, attestations = [] }) { + if (profile && validateUser(profile)) { + let selfAttestation = await this.profileAttestation(profile) + attestations.push(selfAttestation) + } + let newAttestations = await this.newAttestations(attestations) + await this.addAttestations(newAttestations) + return await this.get() + } + + async get(address) { + let identityAddress = await this.identityAddress(address) + if (identityAddress) { + let userData = await this.getClaims(identityAddress) + return new UserObject(userData) + } + return new UserObject() + } + + async identityAddress(address) { + let account = await this.contractService.currentAccount() + let userRegistry = await this.contractService.deployed(this.contractService.userRegistryContract) + address = address || account + let result = await userRegistry.methods.users(address).call() + if (String(result) === zeroAddress) { + return false + } else { + return result + } + } + + 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 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.deployed(this.contractService.userRegistryContract) + let identityAddress = await this.identityAddress() + if (attestations.length) { + // format params for solidity methods to batch add claims + 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 data.substr(2) + }).join("") + let dataOffsets = attestations.map(() => 32) // all data hashes will be 32 bytes + + if (identityAddress) { + // batch add claims to existing identity + let claimHolder = await this.contractService.deployed( + this.contractService.claimHolderRegisteredContract, + identityAddress + ) + return await claimHolder.methods.addClaims( + claimTypes, + issuers, + sigs, + data + ).send({ from: account, gas: 4000000 }) + } else { + // create identity with presigned claims + return await this.contractService.deploy( + this.contractService.claimHolderPresignedContract, + [ + userRegistry.options.address, + claimTypes, + issuers, + sigs, + data, + dataOffsets + ], + { from: account, gas: 4000000 } + ) + } + } else if (!identityAddress) { + // create identity + return await this.contractService.claimHolderRegisteredContract.new( + userRegistry.address, + { from: account, gas: 4000000 } + ) + } + } + + async getClaims(identityAddress) { + 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 = mapped.filter(({ claimType }) => claimType === selfAttestationClaimType ) + let nonProfileClaims = mapped.filter(({ claimType }) => claimType !== selfAttestationClaimType ) + let profile = {} + if (profileClaims.length) { + let bytes32 = profileClaims[profileClaims.length - 1].data + let ipfsHash = this.contractService.getIpfsHashFromBytes32(bytes32) + profile = await this.ipfsService.getFile(ipfsHash) + } + let validAttestations = await this.validAttestations(identityAddress, nonProfileClaims) + let attestations = validAttestations.map(att => new AttestationObject(att)) + return { profile, attestations } + } + + async isValidAttestation({ claimType, data, signature }, identityAddress) { + let originIdentity = await this.contractService.deployed(this.contractService.originIdentityContract) + 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) + } +} + +module.exports = Users 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) + }) + }) +}) 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") + }) + }) +})