From 1a72a937b84bb848b10f79471aa2bab5b5207f87 Mon Sep 17 00:00:00 2001 From: Joseph Bagaric Date: Wed, 16 Jun 2021 19:34:39 +0200 Subject: [PATCH] feat(issuer): add batch claiming facade --- .../traceability/issuer/contracts/Issuer.sol | 14 ++- .../issuer/contracts/PrivateIssuer.sol | 16 +-- .../issuer/contracts/Registry.sol | 55 ++++---- .../src/blockchain-facade/Certificate.ts | 12 +- .../CertificateBatchOperations.ts | 119 ++++++++++++++++++ .../src/blockchain-facade/CertificateUtils.ts | 68 ---------- .../blockchain-facade/CertificationRequest.ts | 10 +- packages/traceability/issuer/src/index.ts | 3 +- .../issuer/test/Certificate.test.ts | 47 ++++++- 9 files changed, 207 insertions(+), 137 deletions(-) create mode 100644 packages/traceability/issuer/src/blockchain-facade/CertificateBatchOperations.ts diff --git a/packages/traceability/issuer/contracts/Issuer.sol b/packages/traceability/issuer/contracts/Issuer.sol index 08bdfff8355..da1aedddef3 100644 --- a/packages/traceability/issuer/contracts/Issuer.sol +++ b/packages/traceability/issuer/contracts/Issuer.sol @@ -79,8 +79,8 @@ contract Issuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { function requestCertificationForBatch(bytes[] memory _data, address[] memory _owners) public returns (uint256[] memory) { uint256[] memory requestIds = new uint256[](_data.length); - for (uint i = 1; i <= _data.length; i++) { - uint256 id = i + _latestCertificationRequestId; + for (uint256 i = 0; i < _data.length; i++) { + uint256 id = i + _latestCertificationRequestId + 1; _certificationRequests[id] = CertificationRequest({ owner: _owners[i], @@ -95,6 +95,8 @@ contract Issuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { emit CertificationRequestedBatch(_owners, requestIds); + _latestCertificationRequestId = requestIds[requestIds.length - 1]; + return requestIds; } @@ -104,7 +106,7 @@ contract Issuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { function isRequestValid(uint256 _requestId) external view returns (bool) { CertificationRequest memory request = _certificationRequests[_requestId]; - uint certificateId = requestToCertificate[_requestId]; + uint256 certificateId = requestToCertificate[_requestId]; return _requestId <= _latestCertificationRequestId && request.approved @@ -162,7 +164,7 @@ contract Issuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { ) public returns (uint256[] memory) { require(_msgSender() == owner() || _msgSender() == privateIssuer, "Issuer::approveCertificationRequestBatch: caller is not the owner or private issuer contract"); - for (uint i = 0; i < _requestIds.length; i++) { + for (uint256 i = 0; i < _requestIds.length; i++) { require(_requestNotApprovedOrRevoked(_requestIds[i]), "Issuer::approveCertificationRequestBatch: request already approved or revoked"); } @@ -170,7 +172,7 @@ contract Issuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { bytes[] memory data = new bytes[](_requestIds.length); bytes[] memory validityData = new bytes[](_requestIds.length); - for (uint i = 0; i < _requestIds.length; i++) { + for (uint256 i = 0; i < _requestIds.length; i++) { CertificationRequest storage request = _certificationRequests[_requestIds[i]]; request.approved = true; @@ -187,7 +189,7 @@ contract Issuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { validityData ); - for (uint i = 0; i < _requestIds.length; i++) { + for (uint256 i = 0; i < _requestIds.length; i++) { requestToCertificate[_requestIds[i]] = certificateIds[i]; } diff --git a/packages/traceability/issuer/contracts/PrivateIssuer.sol b/packages/traceability/issuer/contracts/PrivateIssuer.sol index b9ce90423cb..522261cd892 100644 --- a/packages/traceability/issuer/contracts/PrivateIssuer.sol +++ b/packages/traceability/issuer/contracts/PrivateIssuer.sol @@ -59,7 +59,7 @@ contract PrivateIssuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { Certification requests */ - function getCertificateCommitment(uint certificateId) public view returns (bytes32) { + function getCertificateCommitment(uint256 certificateId) public view returns (bytes32) { return _commitments[certificateId]; } @@ -135,18 +135,18 @@ contract PrivateIssuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { return _requestMigrateToPublicFor(_certificateId, _ownerAddressLeafHash, _msgSender()); } - function getPrivateTransferRequest(uint _certificateId) external view onlyOwner returns (PrivateTransferRequest memory) { + function getPrivateTransferRequest(uint256 _certificateId) external view onlyOwner returns (PrivateTransferRequest memory) { return _requestPrivateTransferStorage[_certificateId]; } - function getMigrationRequest(uint _requestId) external view onlyOwner returns (RequestStateChange memory) { + function getMigrationRequest(uint256 _requestId) external view onlyOwner returns (RequestStateChange memory) { return _requestMigrateToPublicStorage[_requestId]; } - function getMigrationRequestId(uint _certificateId) external view onlyOwner returns (uint256 _migrationRequestId) { + function getMigrationRequestId(uint256 _certificateId) external view onlyOwner returns (uint256 _migrationRequestId) { bool found = false; - for (uint i = 1; i <= _requestMigrateToPublicNonce; i++) { + for (uint256 i = 1; i <= _requestMigrateToPublicNonce; i++) { if (_requestMigrateToPublicStorage[i].certificateId == _certificateId) { found = true; return i; @@ -185,7 +185,7 @@ contract PrivateIssuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { function validateOwnershipProof( address _ownerAddress, - uint _volume, + uint256 _volume, string memory _salt, bytes32 _rootHash, Proof[] memory _proof @@ -222,10 +222,10 @@ contract PrivateIssuer is Initializable, OwnableUpgradeable, UUPSUpgradeable { emit CommitmentUpdated(_msgSender(), _id, _commitment); } - function _migrationRequestExists(uint _certificateId) private view returns (bool) { + function _migrationRequestExists(uint256 _certificateId) private view returns (bool) { bool exists = false; - for (uint i = 1; i <= _requestMigrateToPublicNonce; i++) { + for (uint256 i = 1; i <= _requestMigrateToPublicNonce; i++) { if (_requestMigrateToPublicStorage[i].certificateId == _certificateId) { exists = true; return exists; diff --git a/packages/traceability/issuer/contracts/Registry.sol b/packages/traceability/issuer/contracts/Registry.sol index 1aebe6f6aab..75bea697199 100644 --- a/packages/traceability/issuer/contracts/Registry.sol +++ b/packages/traceability/issuer/contracts/Registry.sol @@ -32,18 +32,21 @@ contract Registry is ERC1155, ERC1888 { } function batchIssue(address _to, bytes memory _issuanceData, uint256 _topic, uint256[] memory _values, bytes[] memory _validityCalls) external override returns (uint256[] memory) { + require(_issuanceData.length == _values.length, "Registry::batchIssueMultiple: _issuanceData and _values arrays have to be the same length"); + require(_values.length == _validityCalls.length, "Registry::batchIssueMultiple: _values and _validityCalls arrays have to be the same length"); + uint256[] memory _ids = new uint256[](_values.length); address operator = _msgSender(); - for (uint256 i = 1; i <= _values.length; ++i) { - _ids[i] = i + _latestCertificateId; - _validate(operator, _validityCalls[_ids[i]]); + for (uint256 i = 0; i <= _values.length; i++) { + _ids[i] = i + _latestCertificateId + 1; + _validate(operator, _validityCalls[i]); } ERC1155._mintBatch(_to, _ids, _values, _issuanceData); - for (uint256 i = 0; i <= _ids.length; ++i) { + for (uint256 i = 0; i < _ids.length; i++) { certificateStorage[_ids[i]] = Certificate({ topic: _topic, issuer: operator, @@ -58,16 +61,20 @@ contract Registry is ERC1155, ERC1888 { } function batchIssueMultiple(address[] memory _to, bytes[] memory _issuanceData, uint256 _topic, uint256[] memory _values, bytes[] memory _validityCalls) external returns (uint256[] memory) { + require(_to.length == _issuanceData.length, "Registry::batchIssueMultiple: _to and _issuanceData arrays have to be the same length"); + require(_issuanceData.length == _values.length, "Registry::batchIssueMultiple: _issuanceData and _values arrays have to be the same length"); + require(_values.length == _validityCalls.length, "Registry::batchIssueMultiple: _values and _validityCalls arrays have to be the same length"); + uint256[] memory _ids = new uint256[](_values.length); address operator = _msgSender(); - for (uint256 i = 0; i < _values.length; ++i) { - _ids[i] = i + _latestCertificateId; - _validate(operator, _validityCalls[_ids[i]]); + for (uint256 i = 0; i < _values.length; i++) { + _ids[i] = i + _latestCertificateId + 1; + _validate(operator, _validityCalls[i]); } - for (uint256 i = 0; i <= _ids.length; ++i) { + for (uint256 i = 0; i < _ids.length; i++) { ERC1155._mint(_to[i], _ids[i], _values[i], _issuanceData[i]); certificateStorage[_ids[i]] = Certificate({ @@ -78,6 +85,8 @@ contract Registry is ERC1155, ERC1888 { }); } + _latestCertificateId = _ids[_ids.length - 1]; + emit IssuanceBatch(operator, _topic, _ids, _values); return _ids; @@ -125,7 +134,7 @@ contract Registry is ERC1155, ERC1888 { bytes calldata _data, bytes[] calldata _claimData ) external override { - uint numberOfClaims = _ids.length; + uint256 numberOfClaims = _ids.length; require(_to != address(0x0), "Registry::safeBatchTransferAndClaimFrom: _to address must be non-zero."); require(_ids.length == _values.length, "Registry::safeBatchTransferAndClaimFrom: _ids and _values array length must match."); @@ -139,7 +148,7 @@ contract Registry is ERC1155, ERC1888 { uint256[] memory topics = new uint256[](numberOfClaims); - for (uint256 i = 0; i < numberOfClaims; ++i) { + for (uint256 i = 0; i < numberOfClaims; i++) { Certificate memory cert = certificateStorage[_ids[i]]; _validate(cert.issuer, cert.validityData); topics[i] = cert.topic; @@ -149,7 +158,7 @@ contract Registry is ERC1155, ERC1888 { safeBatchTransferFrom(_from, _to, _ids, _values, _data); } - for (uint256 i = 0; i < numberOfClaims; ++i) { + for (uint256 i = 0; i < numberOfClaims; i++) { _burn(_to, _ids[i], _values[i]); } @@ -172,35 +181,13 @@ contract Registry is ERC1155, ERC1888 { uint256[] memory batchClaimBalances = new uint256[](_owners.length); - for (uint256 i = 0; i < _owners.length; ++i) { + for (uint256 i = 0; i < _owners.length; i++) { batchClaimBalances[i] = this.claimedBalanceOf(_owners[i], _ids[i]); } return batchClaimBalances; } - /** - * Modification to the OpenZeppelin ERC-1155 to support to[] addresses - */ - // function _mintBatch(address[] to, uint256[] memory ids, uint256[] memory amounts, bytes[] memory data, bytes[] memory validityData) internal override { - // require(to.length == ids.length, "ERC1155: to and ids length mismatch"); - // require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); - - // address operator = _msgSender(); - - // for (uint i = 0; i < ids.length; i++) { - // require(_to != address(0), "ERC1155: mint to the zero address"); - // _validate(operator, _validityData[i]); - // } - - // for (uint i = 0; i < ids.length; i++) { - // _balances[ids[i]][to[i]] += amounts[i]; - - // emit TransferSingle(operator, address(0), to[i], ids[i], amounts[i]); - // _doSafeTransferAcceptanceCheck(operator, address(0), to[i], ids[i], amounts[i], data); - // } - // } - function _burn(address _from, uint256 _id, uint256 _value) internal override { ERC1155._burn(_from, _id, _value); diff --git a/packages/traceability/issuer/src/blockchain-facade/Certificate.ts b/packages/traceability/issuer/src/blockchain-facade/Certificate.ts index c767fd76807..e3c1e1b7419 100644 --- a/packages/traceability/issuer/src/blockchain-facade/Certificate.ts +++ b/packages/traceability/issuer/src/blockchain-facade/Certificate.ts @@ -32,8 +32,8 @@ export interface IClaimData { export interface IData { deviceId: string; - generationStartTime: number; - generationEndTime: number; + generationStartTime: Timestamp; + generationEndTime: Timestamp; metadata: string; } @@ -50,7 +50,7 @@ export interface ICertificate extends IData { id: number; issuer: string; certificationRequestId: number; - creationTime: number; + creationTime: Timestamp; creationBlockHash: string; owners: IShareInCertificate; claimers: IShareInCertificate; @@ -59,13 +59,13 @@ export interface ICertificate extends IData { export class Certificate implements ICertificate { public deviceId: string; - public generationStartTime: number; + public generationStartTime: Timestamp; - public generationEndTime: number; + public generationEndTime: Timestamp; public issuer: string; - public creationTime: number; + public creationTime: Timestamp; public creationBlockHash: string; diff --git a/packages/traceability/issuer/src/blockchain-facade/CertificateBatchOperations.ts b/packages/traceability/issuer/src/blockchain-facade/CertificateBatchOperations.ts new file mode 100644 index 00000000000..fed2997255f --- /dev/null +++ b/packages/traceability/issuer/src/blockchain-facade/CertificateBatchOperations.ts @@ -0,0 +1,119 @@ +import { BigNumber, ContractTransaction, utils } from 'ethers'; + +import { IBlockchainProperties } from './BlockchainProperties'; +import { Certificate, IClaimData, IData } from './Certificate'; +import { encodeClaimData, encodeData } from './CertificateUtils'; + +interface CertificateInfoInBatch extends IData { + to: string; + amount: BigNumber; +} + +export async function issueCertificates( + certificateInfo: CertificateInfoInBatch[], + blockchainProperties: IBlockchainProperties +): Promise { + const { issuer, registry, activeUser } = blockchainProperties; + const issuerWithSigner = issuer.connect(activeUser); + + const data = certificateInfo.map((info) => + encodeData({ + generationStartTime: info.generationStartTime, + generationEndTime: info.generationEndTime, + deviceId: info.deviceId, + metadata: info.metadata + }) + ); + + const batchIssueTx = await issuerWithSigner.issueBatch( + certificateInfo.map((info) => info.to), + certificateInfo.map((info) => info.amount), + data + ); + + const { events } = await batchIssueTx.wait(); + + let issuanceEvent: utils.LogDescription; + + for (const event of events) { + try { + issuanceEvent = issuer.interface.parseLog(event); + } catch (e) { + issuanceEvent = registry.interface.parseLog(event); + } + + if (issuanceEvent.name === 'IssuanceBatch') { + break; + } + } + + return issuanceEvent.args[2].map((id: BigNumber) => id.toNumber()); +} + +export async function transferCertificates( + certificateIds: number[], + to: string, + blockchainProperties: IBlockchainProperties, + from?: string +): Promise { + const certificatesPromises = certificateIds.map((certId) => + new Certificate(certId, blockchainProperties).sync() + ); + + const { registry, activeUser } = blockchainProperties; + const registryWithSigner = registry.connect(activeUser); + + const activeUserAddress = await activeUser.getAddress(); + const fromAddress = from ?? activeUserAddress; + + const certificates = await Promise.all(certificatesPromises); + + const values = certificates.map((cert) => BigNumber.from(cert.owners[fromAddress] ?? 0)); + + const transferTx = await registryWithSigner.safeBatchTransferFrom( + fromAddress, + to, + certificateIds, + values, + utils.randomBytes(32) // TO-DO: replace with proper data + ); + + await transferTx.wait(); + + return transferTx; +} + +export async function claimCertificates( + certificateIds: number[], + claimData: IClaimData, + blockchainProperties: IBlockchainProperties, + forAddress?: string +): Promise { + const certificatesPromises = certificateIds.map((certId) => + new Certificate(certId, blockchainProperties).sync() + ); + const certificates = await Promise.all(certificatesPromises); + + const { activeUser, registry } = blockchainProperties; + const claimer = forAddress ?? (await activeUser.getAddress()); + + const values = certificates.map((cert) => BigNumber.from(cert.owners[claimer] ?? 0)); + + const encodedClaimData = encodeClaimData(claimData); + const data = utils.randomBytes(32); + + const registryWithSigner = registry.connect(activeUser); + + const claimTx = await registryWithSigner.safeBatchTransferAndClaimFrom( + claimer, + claimer, + certificateIds, + values, + data, + certificates.map(() => encodedClaimData) + ); + + await claimTx.wait(); + + return claimTx; +} diff --git a/packages/traceability/issuer/src/blockchain-facade/CertificateUtils.ts b/packages/traceability/issuer/src/blockchain-facade/CertificateUtils.ts index 60fbb3c6cf5..caa547293cb 100644 --- a/packages/traceability/issuer/src/blockchain-facade/CertificateUtils.ts +++ b/packages/traceability/issuer/src/blockchain-facade/CertificateUtils.ts @@ -72,74 +72,6 @@ export const decodeData = (encodedData: string): IData => { }; }; -export async function claimCertificates( - certificateIds: number[], - claimData: IClaimData, - blockchainProperties: IBlockchainProperties, - forAddress?: string -): Promise { - const certificatesPromises = certificateIds.map((certId) => - new Certificate(certId, blockchainProperties).sync() - ); - const certificates = await Promise.all(certificatesPromises); - - const { activeUser, registry } = blockchainProperties; - const claimer = forAddress ?? (await activeUser.getAddress()); - - const values = certificates.map((cert) => BigNumber.from(cert.owners[claimer] ?? 0)); - - const encodedClaimData = encodeClaimData(claimData); - const data = utils.randomBytes(32); - - const registryWithSigner = registry.connect(activeUser); - - const claimTx = await registryWithSigner.safeBatchTransferAndClaimFrom( - claimer, - claimer, - certificateIds, - values, - data, - certificates.map(() => encodedClaimData) - ); - - await claimTx.wait(); - - return claimTx; -} - -export async function transferCertificates( - certificateIds: number[], - to: string, - blockchainProperties: IBlockchainProperties, - from?: string -): Promise { - const certificatesPromises = certificateIds.map((certId) => - new Certificate(certId, blockchainProperties).sync() - ); - - const { registry, activeUser } = blockchainProperties; - const registryWithSigner = registry.connect(activeUser); - - const activeUserAddress = await activeUser.getAddress(); - const fromAddress = from ?? activeUserAddress; - - const certificates = await Promise.all(certificatesPromises); - - const values = certificates.map((cert) => BigNumber.from(cert.owners[fromAddress] ?? 0)); - - const transferTx = await registryWithSigner.safeBatchTransferFrom( - fromAddress, - to, - certificateIds, - values, - utils.randomBytes(32) // TO-DO: replace with proper data - ); - - await transferTx.wait(); - - return transferTx; -} - export async function getAllCertificates( blockchainProperties: IBlockchainProperties ): Promise { diff --git a/packages/traceability/issuer/src/blockchain-facade/CertificationRequest.ts b/packages/traceability/issuer/src/blockchain-facade/CertificationRequest.ts index f56f0516842..55e589b846c 100644 --- a/packages/traceability/issuer/src/blockchain-facade/CertificationRequest.ts +++ b/packages/traceability/issuer/src/blockchain-facade/CertificationRequest.ts @@ -139,17 +139,9 @@ export class CertificationRequest implements ICertificationRequestBlockchain { async approve(energy: BigNumber): Promise { const { issuer, activeUser } = this.blockchainProperties; - const validityData = issuer.interface.encodeFunctionData('isRequestValid', [ - this.id.toString() - ]); - const issuerWithSigner = issuer.connect(activeUser); - const approveTx = await issuerWithSigner.approveCertificationRequest( - this.id, - energy, - validityData - ); + const approveTx = await issuerWithSigner.approveCertificationRequest(this.id, energy); const { events } = await approveTx.wait(); diff --git a/packages/traceability/issuer/src/index.ts b/packages/traceability/issuer/src/index.ts index aef87a13e19..6ced4be3638 100644 --- a/packages/traceability/issuer/src/index.ts +++ b/packages/traceability/issuer/src/index.ts @@ -1,4 +1,5 @@ import * as CertificateUtils from './blockchain-facade/CertificateUtils'; +import * as CertificateBatchOperations from './blockchain-facade/CertificateBatchOperations'; import * as Contracts from './contracts'; export * from './const'; @@ -9,4 +10,4 @@ export * from './blockchain-facade/CertificationRequest'; export * from './blockchain-facade/Certificate'; export * from './blockchain-facade/BlockchainProperties'; -export { Contracts, CertificateUtils }; +export { Contracts, CertificateUtils, CertificateBatchOperations }; diff --git a/packages/traceability/issuer/test/Certificate.test.ts b/packages/traceability/issuer/test/Certificate.test.ts index 34f02b6d749..c19b15c26aa 100644 --- a/packages/traceability/issuer/test/Certificate.test.ts +++ b/packages/traceability/issuer/test/Certificate.test.ts @@ -7,7 +7,13 @@ import { Wallet, BigNumber } from 'ethers'; import { getProviderWithFallback } from '@energyweb/utils-general'; import { migrateIssuer, migrateRegistry } from '../src/migrate'; -import { Certificate, CertificateUtils, IClaimData, IBlockchainProperties } from '../src'; +import { + Certificate, + CertificateUtils, + IClaimData, + IBlockchainProperties, + CertificateBatchOperations +} from '../src'; describe('Certificate tests', () => { let blockchainProperties: IBlockchainProperties; @@ -76,7 +82,7 @@ describe('Certificate tests', () => { ); }; - it('migrates Registry', async () => { + it('migrates Registry and Issuer', async () => { const registry = await migrateRegistry(provider, issuerPK); const issuer = await migrateIssuer(provider, issuerPK, registry.address); @@ -245,6 +251,37 @@ describe('Certificate tests', () => { ); }); + it('batch issues certificates', async () => { + setActiveUser(issuerWallet); + + const certInfo1 = { + to: deviceOwnerWallet.address, + amount: BigNumber.from(100), + generationStartTime: timestamp, + generationEndTime: (timestamp += 30 * 24 * 3600), + deviceId: '1', + metadata: '' + }; + + const certInfo2 = { + to: traderWallet.address, + amount: BigNumber.from(200), + generationStartTime: timestamp, + generationEndTime: (timestamp += 30 * 24 * 3600), + deviceId: '2', + metadata: '' + }; + + const certificateIds = await CertificateBatchOperations.issueCertificates( + [certInfo1, certInfo2], + blockchainProperties + ); + + assert.lengthOf(certificateIds, 2); + + certificateIds.forEach((id) => assert.typeOf(id, 'number')); + }); + it('batch transfers certificates', async () => { let certificate = await issueCertificate(totalVolume, deviceOwnerWallet.address); let certificate2 = await issueCertificate(totalVolume, deviceOwnerWallet.address); @@ -256,7 +293,7 @@ describe('Certificate tests', () => { assert.equal(certificate.owners[deviceOwnerWallet.address], totalVolume.toString()); assert.equal(certificate2.owners[deviceOwnerWallet.address], totalVolume.toString()); - await CertificateUtils.transferCertificates( + await CertificateBatchOperations.transferCertificates( [certificate.id, certificate2.id], traderWallet.address, blockchainProperties @@ -284,7 +321,7 @@ describe('Certificate tests', () => { assert.equal(certificate.claimers[deviceOwnerWallet.address], undefined); assert.equal(certificate2.claimers[deviceOwnerWallet.address], undefined); - await CertificateUtils.claimCertificates( + await CertificateBatchOperations.claimCertificates( [certificate.id, certificate2.id], claimData, blockchainProperties @@ -350,7 +387,7 @@ describe('Certificate tests', () => { let failed = false; try { - await CertificateUtils.claimCertificates( + await CertificateBatchOperations.claimCertificates( [certificate.id, certificate2.id], claimData, blockchainProperties,