diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c5a937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.env \ No newline at end of file diff --git a/chain-api/package-lock.json b/chain-api/package-lock.json index c39aca9..de5d6b6 100644 --- a/chain-api/package-lock.json +++ b/chain-api/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "@chainlink/contracts": "^1.1.0", + "@ethersproject/address": "^5.7.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", diff --git a/chain-api/package.json b/chain-api/package.json index 56aa3ff..6782b90 100644 --- a/chain-api/package.json +++ b/chain-api/package.json @@ -10,6 +10,7 @@ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", + "compile": "npx hardhat compile", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", @@ -21,6 +22,7 @@ }, "dependencies": { "@chainlink/contracts": "^1.1.0", + "@ethersproject/address": "^5.7.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", diff --git a/chain-api/src/hardhat/modules/base-contract.service.ts b/chain-api/src/base/base-contract.service.ts similarity index 65% rename from chain-api/src/hardhat/modules/base-contract.service.ts rename to chain-api/src/base/base-contract.service.ts index 293b490..dca2b7d 100644 --- a/chain-api/src/hardhat/modules/base-contract.service.ts +++ b/chain-api/src/base/base-contract.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ProviderService } from 'src/provider/provider.service'; +import { ProviderService } from './provider/provider.service'; @Injectable() export abstract class BaseContractService { diff --git a/chain-api/src/base/base.module.ts b/chain-api/src/base/base.module.ts new file mode 100644 index 0000000..8e86d7f --- /dev/null +++ b/chain-api/src/base/base.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ProviderModule } from './provider/provider.module'; + +@Module({ + imports: [ProviderModule], + controllers: [], + providers: [ProviderModule], + exports: [ProviderModule], +}) +export class BaseModule {} diff --git a/chain-api/src/provider/provider.module.ts b/chain-api/src/base/provider/provider.module.ts similarity index 100% rename from chain-api/src/provider/provider.module.ts rename to chain-api/src/base/provider/provider.module.ts diff --git a/chain-api/src/provider/provider.service.ts b/chain-api/src/base/provider/provider.service.ts similarity index 100% rename from chain-api/src/provider/provider.service.ts rename to chain-api/src/base/provider/provider.service.ts diff --git a/chain-api/src/config/chainlink.config.ts b/chain-api/src/config/chainlink.config.ts new file mode 100644 index 0000000..6bc3ed9 --- /dev/null +++ b/chain-api/src/config/chainlink.config.ts @@ -0,0 +1,12 @@ +export const CHAINLINK = { + AMOY: { + CHAINLINK_TOKEN: '0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904', + ORACLE_ADDRESS: '0xd36c6B1777c7f3Db1B3201bDD87081A9045B7b46', + AGGREGATOR_ADDRESS: { + USDT_ETH: '0xF0d50568e3A7e8259E16663972b11910F89BD8e7', + }, + JOB_IDS: { + UINT: 'a8356f48569c434eaa4ac5fcb4db5cc0', + }, + }, +}; diff --git a/chain-api/src/contract-interact/contract-interact.module.ts b/chain-api/src/contract-interact/contract-interact.module.ts index 1e55000..48a9229 100644 --- a/chain-api/src/contract-interact/contract-interact.module.ts +++ b/chain-api/src/contract-interact/contract-interact.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; -import { HardhatModule } from 'src/hardhat/modules/hardhat.module'; +import { SalariesModule } from './salaries/salaries.module'; +import { MultiSigModule } from './multi-sig/multi-sig.module'; +import { LicenseModule } from './license/license.module'; @Module({ - imports: [HardhatModule], + imports: [SalariesModule, MultiSigModule, LicenseModule], controllers: [], providers: [], }) diff --git a/chain-api/src/contract-interact/dto/create-contract-interact.dto.ts b/chain-api/src/contract-interact/dto/create-contract-interact.dto.ts deleted file mode 100644 index 55510a9..0000000 --- a/chain-api/src/contract-interact/dto/create-contract-interact.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class CreateContractInteractDto { - @ApiProperty() - contractAddress: string; - @ApiProperty() - sender: string; -} diff --git a/chain-api/src/contract-interact/dto/update-contract-interact.dto.ts b/chain-api/src/contract-interact/dto/update-contract-interact.dto.ts deleted file mode 100644 index ec7c282..0000000 --- a/chain-api/src/contract-interact/dto/update-contract-interact.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateContractInteractDto } from './create-contract-interact.dto'; - -export class UpdateContractInteractDto extends PartialType(CreateContractInteractDto) {} diff --git a/chain-api/src/contract-interact/ethers.helpers.ts b/chain-api/src/contract-interact/ethers.helpers.ts deleted file mode 100644 index d3b5148..0000000 --- a/chain-api/src/contract-interact/ethers.helpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TransactionReceipt, ethers } from 'ethers'; - -export const parseLogs = ( - txReceipt: TransactionReceipt, - contract: ethers.Contract, - eventName: string, -) => { - return txReceipt.logs - .map((log) => contract.interface.parseLog(log)) - .find((log) => !!log && log.fragment.name === eventName); -}; diff --git a/chain-api/src/contract-interact/license/license.controller.ts b/chain-api/src/contract-interact/license/license.controller.ts new file mode 100644 index 0000000..502c70a --- /dev/null +++ b/chain-api/src/contract-interact/license/license.controller.ts @@ -0,0 +1,42 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { LicenseService } from './license.service'; +import { ApiTags } from '@nestjs/swagger'; +import { + DeployLicenseDto, + GetShareLicense, + RequestLicenseDto, +} from './license.dto'; +@ApiTags('license') +@Controller('license') +export class LicenseController { + constructor(private readonly licenseService: LicenseService) {} + @Get('request') + async getLicenseRequest(@Body() dto: RequestLicenseDto) { + return this.licenseService.request(dto); + } + + @Post('deploy') + async deploy(@Body() dto: DeployLicenseDto) { + return this.licenseService.deploy(dto); + } + + @Get('total-payout') + async getLicenseResponse(@Body() dto: RequestLicenseDto) { + return this.licenseService.getTotalPayoutInUSD(dto); + } + + @Get('shares') + async getShares(@Body() dto: GetShareLicense) { + return this.licenseService.getShares(dto); + } + + @Get('owners') + async getOwners(@Body() dto: GetShareLicense) { + return this.licenseService.getOwners(dto); + } + + @Get('payout-contract') + async getPayoutContract(@Body() dto: GetShareLicense) { + return this.licenseService.getPayoutContract(dto); + } +} diff --git a/chain-api/src/contract-interact/license/license.dto.ts b/chain-api/src/contract-interact/license/license.dto.ts new file mode 100644 index 0000000..f3422cf --- /dev/null +++ b/chain-api/src/contract-interact/license/license.dto.ts @@ -0,0 +1,37 @@ +import { IsArray, IsNumber, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +export class GetLicenseInfoDto { + @ApiProperty() + @IsString() + contractAddress: string; +} +export class DeployLicenseDto { + @ApiProperty() + @IsString() + multiSigWallet: string; + @ApiProperty() + @IsArray() + owners: string[]; + @ApiProperty({ + isArray: true, + type: Number, + }) + @IsNumber({}, { each: true }) + shares: number[]; + @ApiProperty() + @IsString() + payrollAddress: string; +} + +export class RequestLicenseDto extends GetLicenseInfoDto { + @ApiProperty() + @IsString() + multiSigWallet: string; +} + +export class GetLicenseResponseDto extends GetLicenseInfoDto {} + +export class GetShareLicense extends GetLicenseInfoDto { + @IsString() + ownerAddress: string; +} diff --git a/chain-api/src/contract-interact/license/license.module.ts b/chain-api/src/contract-interact/license/license.module.ts new file mode 100644 index 0000000..94fe76c --- /dev/null +++ b/chain-api/src/contract-interact/license/license.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { LicenseController } from './license.controller'; +import { LicenseService } from './license.service'; +import { BaseModule } from '../../base/base.module'; +import { MultiSigModule } from '../multi-sig/multi-sig.module'; + +@Module({ + imports: [BaseModule, MultiSigModule], + controllers: [LicenseController], + providers: [LicenseService], + exports: [LicenseService], +}) +export class LicenseModule {} diff --git a/chain-api/src/contract-interact/license/license.service.ts b/chain-api/src/contract-interact/license/license.service.ts new file mode 100644 index 0000000..9cb8484 --- /dev/null +++ b/chain-api/src/contract-interact/license/license.service.ts @@ -0,0 +1,137 @@ +import { Injectable } from '@nestjs/common'; +import * as hre from 'hardhat'; +import { ethers } from 'ethers'; +import { BaseContractService } from '../../base/base-contract.service'; +import { + DeployLicenseDto, + GetLicenseInfoDto, + GetLicenseResponseDto, + GetShareLicense, + RequestLicenseDto, +} from './license.dto'; +import { MultiSigWalletService } from '../multi-sig/multi-sig.service'; +import { ProviderService } from '../../base/provider/provider.service'; +import { CHAINLINK } from '../../config/chainlink.config'; + +@Injectable() +export class LicenseService extends BaseContractService { + constructor( + private readonly multiSigService: MultiSigWalletService, + public readonly providerService: ProviderService, + ) { + super(providerService); + } + async request(dto: RequestLicenseDto) { + const { multiSigWallet, contractAddress } = dto; + + const ISubmitMultiSig = new ethers.Interface(['function request()']); + const data = ISubmitMultiSig.encodeFunctionData('request'); + + return await this.multiSigService.submitTransaction({ + contractAddress: multiSigWallet, + destination: contractAddress, + value: '0', + data, + }); + } + + async getTotalPayoutInUSD(dto: GetLicenseResponseDto) { + const { contractAddress } = dto; + const { abi } = await hre.artifacts.readArtifact( + 'StreamingRightsManagement', + ); + const signer = await this.providerService.getSigner(); + + const contract = new ethers.Contract(contractAddress, abi, signer); + + const answer: bigint = await contract.totalPayoutInUSD(); + console.log('=>(license.service.ts:45) answer', answer); + return answer.toString(); + } + + async deploy(dto: DeployLicenseDto) { + console.log('=>(license.service.ts:53) dto', dto); + const { multiSigWallet, shares, owners, payrollAddress } = dto; + const { abi, bytecode } = await hre.artifacts.readArtifact( + 'StreamingRightsManagement', + ); + const signer = await this.providerService.getSigner(); + + const licenseContract = new ethers.ContractFactory(abi, bytecode, signer); + + const myContract = await licenseContract.getDeployTransaction( + CHAINLINK.AMOY.CHAINLINK_TOKEN, + CHAINLINK.AMOY.ORACLE_ADDRESS, + CHAINLINK.AMOY.JOB_IDS.UINT, + 0, + multiSigWallet, + owners, + shares, + payrollAddress, + ); + const submitData = await this.multiSigService.submitTransaction({ + contractAddress: multiSigWallet, + destination: null, + value: '0', + data: myContract.data, + }); + delete submitData.data; + return submitData; + } + + async getPayoutContract(dto: GetLicenseInfoDto) { + const { contractAddress } = dto; + const { abi } = await hre.artifacts.readArtifact( + 'StreamingRightsManagement', + ); + const signer = await this.providerService.getSigner(); + + const contract = new ethers.Contract(contractAddress, abi, signer); + + const answer: string = await contract.payoutContract(); + + return answer; + } + + async getOwners(dto: GetLicenseInfoDto) { + const { contractAddress } = dto; + const { abi } = await hre.artifacts.readArtifact( + 'StreamingRightsManagement', + ); + const signer = await this.providerService.getSigner(); + + const contract = new ethers.Contract(contractAddress, abi, signer); + + const answer: string[] = await contract.owners(); + + return answer; + } + + async getShares(dto: GetShareLicense) { + const { contractAddress, ownerAddress } = dto; + const { abi } = await hre.artifacts.readArtifact( + 'StreamingRightsManagement', + ); + const signer = await this.providerService.getSigner(); + + const contract = new ethers.Contract(contractAddress, abi, signer); + + const answer: number = await contract.getShare(ownerAddress); + + return answer; + } + + async getTotalPayout(dto: GetLicenseInfoDto) { + const { contractAddress } = dto; + const { abi } = await hre.artifacts.readArtifact( + 'StreamingRightsManagement', + ); + const signer = await this.providerService.getSigner(); + + const contract = new ethers.Contract(contractAddress, abi, signer); + + const answer: number = await contract.totalPayoutInUSD(); + + return answer; + } +} diff --git a/chain-api/src/contract-interact/dto/multi-sig.dto.ts b/chain-api/src/contract-interact/multi-sig.dto.ts similarity index 84% rename from chain-api/src/contract-interact/dto/multi-sig.dto.ts rename to chain-api/src/contract-interact/multi-sig.dto.ts index 2803201..61d19b1 100644 --- a/chain-api/src/contract-interact/dto/multi-sig.dto.ts +++ b/chain-api/src/contract-interact/multi-sig.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator'; export class SubmitTransactionDto { @IsString() @@ -13,7 +13,7 @@ export class SubmitTransactionDto { value: string; @IsOptional() @IsString() - // @ApiProperty() + @ApiProperty() data: string; } @@ -26,7 +26,12 @@ export class ConfirmTransactionDto { index: number; } -export class ExecuteTransactionDto extends ConfirmTransactionDto {} +export class ExecuteTransactionDto extends ConfirmTransactionDto { + @IsOptional() + @IsBoolean() + @ApiProperty() + isDeploy: boolean; +} export class RevokeConfirmationDto extends ConfirmTransactionDto {} diff --git a/chain-api/src/hardhat/modules/multi-sig/multi-sig-interact.controller.ts b/chain-api/src/contract-interact/multi-sig/multi-sig-interact.controller.ts similarity index 93% rename from chain-api/src/hardhat/modules/multi-sig/multi-sig-interact.controller.ts rename to chain-api/src/contract-interact/multi-sig/multi-sig-interact.controller.ts index 07d0d49..97796e0 100644 --- a/chain-api/src/hardhat/modules/multi-sig/multi-sig-interact.controller.ts +++ b/chain-api/src/contract-interact/multi-sig/multi-sig-interact.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { MultiSigWalletService } from 'src/hardhat/modules/multi-sig/multi-sig.service'; + +import { MultiSigWalletDto } from './multi-sig.dto'; +import { MultiSigWalletService } from './multi-sig.service'; import { ConfirmTransactionDto, DeployMultiSigResponseDto, @@ -9,8 +11,7 @@ import { GetTransactionDto, RevokeConfirmationDto, SubmitTransactionDto, -} from '../../../contract-interact/dto/multi-sig.dto'; -import { MultiSigWalletDto } from './multi-sig.dto'; +} from '../multi-sig.dto'; @ApiTags('multi-sig') @Controller('multi-sig') export class MultiSigInteractController { diff --git a/chain-api/src/hardhat/modules/multi-sig/multi-sig.dto.ts b/chain-api/src/contract-interact/multi-sig/multi-sig.dto.ts similarity index 100% rename from chain-api/src/hardhat/modules/multi-sig/multi-sig.dto.ts rename to chain-api/src/contract-interact/multi-sig/multi-sig.dto.ts diff --git a/chain-api/src/hardhat/modules/multi-sig/multi-sig.module.ts b/chain-api/src/contract-interact/multi-sig/multi-sig.module.ts similarity index 60% rename from chain-api/src/hardhat/modules/multi-sig/multi-sig.module.ts rename to chain-api/src/contract-interact/multi-sig/multi-sig.module.ts index c2d621d..f7f16fa 100644 --- a/chain-api/src/hardhat/modules/multi-sig/multi-sig.module.ts +++ b/chain-api/src/contract-interact/multi-sig/multi-sig.module.ts @@ -1,14 +1,11 @@ import { Module } from '@nestjs/common'; -import { ProviderModule } from 'src/provider/provider.module'; - -import { BaseContractService } from '../base-contract.service'; -import { ProviderService } from 'src/provider/provider.service'; import { MultiSigWalletService } from './multi-sig.service'; import { MultiSigInteractController } from './multi-sig-interact.controller'; +import { BaseModule } from '../../base/base.module'; @Module({ - imports: [ProviderModule], + imports: [BaseModule], controllers: [MultiSigInteractController], providers: [MultiSigWalletService], exports: [MultiSigWalletService], diff --git a/chain-api/src/hardhat/modules/multi-sig/multi-sig.service.ts b/chain-api/src/contract-interact/multi-sig/multi-sig.service.ts similarity index 83% rename from chain-api/src/hardhat/modules/multi-sig/multi-sig.service.ts rename to chain-api/src/contract-interact/multi-sig/multi-sig.service.ts index f913f65..2545073 100644 --- a/chain-api/src/hardhat/modules/multi-sig/multi-sig.service.ts +++ b/chain-api/src/contract-interact/multi-sig/multi-sig.service.ts @@ -1,6 +1,5 @@ import { ethers, parseEther, TransactionReceipt } from 'ethers'; import * as hre from 'hardhat'; -import { BaseContractService } from '../base-contract.service'; import { MultiSigWalletDto } from './multi-sig.dto'; import { ConfirmTransactionDto, @@ -9,8 +8,10 @@ import { GetTransactionDto, RevokeConfirmationDto, SubmitTransactionDto, -} from 'src/contract-interact/dto/multi-sig.dto'; -import { parseLogs } from 'src/contract-interact/ethers.helpers'; +} from 'src/contract-interact/multi-sig.dto'; +import { parseLogs } from 'src/ethers-custom/ethers.helpers'; +import { BaseContractService } from '../../base/base-contract.service'; +import { getContractAddress } from '@ethersproject/address'; export class MultiSigWalletService extends BaseContractService { async deploy(dto: MultiSigWalletDto) { @@ -31,7 +32,6 @@ export class MultiSigWalletService extends BaseContractService { async getOwners(address: string) { const { abi } = await hre.artifacts.readArtifact('MultiSigWallet'); - const multiSigContract = new ethers.Contract(address, abi); const signer = await this.providerService.getSigner(); @@ -47,7 +47,11 @@ export class MultiSigWalletService extends BaseContractService { const contract = new ethers.Contract(contractAddress, abi, signer); - const tx = await contract.submitTransaction(destination, value, data); + const tx = await contract.submitTransaction( + destination || '0x0000000000000000000000000000000000000000', + value, + data, + ); const txResponse: TransactionReceipt = await tx.wait(); const eventParse = parseLogs(txResponse, contract, 'SubmitTransaction'); @@ -83,21 +87,40 @@ export class MultiSigWalletService extends BaseContractService { } async executeTransaction(dto: ExecuteTransactionDto) { - const { index, contractAddress } = dto; + const { index, contractAddress, isDeploy } = dto; const { abi } = await hre.artifacts.readArtifact('MultiSigWallet'); const signer = await this.providerService.getSigner(); const contract = new ethers.Contract(contractAddress, abi, signer); + const deployedAddress = await this.calculateFutureAddress(contractAddress); const tx = await contract.executeTransaction(index); const txResponse: TransactionReceipt = await tx.wait(); + console.log('=>(multi-sig.service.ts:101) txResponse', txResponse.logs); + const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction'); - return { + const data = { txHash: txResponse.hash, sender: eventParse.args[0].toString(), txIndex: eventParse.args[1].toString(), }; + if (isDeploy) { + return { ...data, deployedAddress }; + } else { + return data; + } + } + + async calculateFutureAddress(contractAddress: string) { + const provider = await this.providerService.getProvider(); + + const nonce = await provider.getTransactionCount(contractAddress); + + return getContractAddress({ + from: contractAddress, + nonce: nonce + 1, + }); } async revokeConfirmation(dto: RevokeConfirmationDto) { diff --git a/chain-api/src/hardhat/modules/salaries/salaries-interact.controller.ts b/chain-api/src/contract-interact/salaries/salaries-interact.controller.ts similarity index 86% rename from chain-api/src/hardhat/modules/salaries/salaries-interact.controller.ts rename to chain-api/src/contract-interact/salaries/salaries-interact.controller.ts index bb7d100..ee529c3 100644 --- a/chain-api/src/hardhat/modules/salaries/salaries-interact.controller.ts +++ b/chain-api/src/contract-interact/salaries/salaries-interact.controller.ts @@ -8,10 +8,8 @@ import { SetSalaryDto, } from './salaries.dto'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { - DeployMultiSigResponseDto, - DepositContractDto, -} from '../../../contract-interact/dto/multi-sig.dto'; +import { DepositContractDto } from '../multi-sig.dto'; + @ApiTags('salaries') @Controller('salaries') export class SalariesController { @@ -54,9 +52,4 @@ export class SalariesController { async deposit(@Body() dto: DepositContractDto) { return this.salariesService.deposit(dto); } - - @Get('get-license-request') - async getLicenseRequest() { - return this.salariesService.getLicenseRequest(); - } } diff --git a/chain-api/src/hardhat/modules/salaries/salaries.dto.ts b/chain-api/src/contract-interact/salaries/salaries.dto.ts similarity index 100% rename from chain-api/src/hardhat/modules/salaries/salaries.dto.ts rename to chain-api/src/contract-interact/salaries/salaries.dto.ts diff --git a/chain-api/src/hardhat/modules/salaries/salaries.module.ts b/chain-api/src/contract-interact/salaries/salaries.module.ts similarity index 77% rename from chain-api/src/hardhat/modules/salaries/salaries.module.ts rename to chain-api/src/contract-interact/salaries/salaries.module.ts index bb8d5fc..441c9cb 100644 --- a/chain-api/src/hardhat/modules/salaries/salaries.module.ts +++ b/chain-api/src/contract-interact/salaries/salaries.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { SalariesService } from './salaries.service'; -import { ProviderModule } from 'src/provider/provider.module'; import { SalariesController } from './salaries-interact.controller'; import { MultiSigModule } from '../multi-sig/multi-sig.module'; +import { BaseModule } from '../../base/base.module'; @Module({ - imports: [ProviderModule, MultiSigModule], + imports: [BaseModule, MultiSigModule], controllers: [SalariesController], providers: [SalariesService], exports: [SalariesService], diff --git a/chain-api/src/hardhat/modules/salaries/salaries.service.ts b/chain-api/src/contract-interact/salaries/salaries.service.ts similarity index 66% rename from chain-api/src/hardhat/modules/salaries/salaries.service.ts rename to chain-api/src/contract-interact/salaries/salaries.service.ts index f37adc2..1e1d459 100644 --- a/chain-api/src/hardhat/modules/salaries/salaries.service.ts +++ b/chain-api/src/contract-interact/salaries/salaries.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { BaseContractService } from '../base-contract.service'; import { ethers, parseEther, TransactionReceipt } from 'ethers'; import { CreatePayoutDto, @@ -9,8 +8,10 @@ import { } from './salaries.dto'; import * as hre from 'hardhat'; import { MultiSigWalletService } from '../multi-sig/multi-sig.service'; -import { ProviderService } from '../../../provider/provider.service'; -import { DepositContractDto } from '../../../contract-interact/dto/multi-sig.dto'; +import { BaseContractService } from '../../base/base-contract.service'; +import { ProviderService } from '../../base/provider/provider.service'; +import { DepositContractDto } from '../multi-sig.dto'; +import { CHAINLINK } from '../../config/chainlink.config'; @Injectable() export class SalariesService extends BaseContractService { @@ -21,7 +22,7 @@ export class SalariesService extends BaseContractService { super(providerService); } async deploy(dto: SalariesDeployDto) { - const { abi, bytecode } = await hre.artifacts.readArtifact('Salaries'); + const { abi, bytecode } = await hre.artifacts.readArtifact('Payroll'); const signer = await this.providerService.getSigner(); @@ -29,50 +30,14 @@ export class SalariesService extends BaseContractService { const myContract = await salaryContract.deploy( dto.multiSigWallet, - '0xF0d50568e3A7e8259E16663972b11910F89BD8e7', + CHAINLINK.AMOY.AGGREGATOR_ADDRESS.USDT_ETH, ); await myContract.waitForDeployment(); return await myContract.getAddress(); } - async getLicenseRequest() { - const { abi } = await hre.artifacts.readArtifact( - 'LinkWellStringBytesConsumerContractExample', - ); - const signer = await this.providerService.getSigner(); - - const contract = new ethers.Contract( - '0xbc3c4fed4C3A977b8868b589662270F1aEA6A777', - abi, - signer, - ); - - const answer: string = await contract.request(); - console.log('=>(salaries.service.ts:45) answer', answer); - const licenseres = await this.getLicenseResponse(); - console.log('=>(salaries.service.ts:53) licenseres', licenseres); - return answer; - } - - async getLicenseResponse() { - const { abi } = await hre.artifacts.readArtifact( - 'LinkWellStringBytesConsumerContractExample', - ); - const signer = await this.providerService.getSigner(); - - const contract = new ethers.Contract( - '0xbc3c4fed4C3A977b8868b589662270F1aEA6A777', - abi, - signer, - ); - - const answer: string = await contract.responseBytes(); - console.log('=>(salaries.service.ts:45) answer', answer); - return answer; - } - async getLatestUSDTPrice(contractAddress: string) { - const { abi } = await hre.artifacts.readArtifact('Salaries'); + const { abi } = await hre.artifacts.readArtifact('Payroll'); const signer = await this.providerService.getSigner(); const contract = new ethers.Contract(contractAddress, abi, signer); @@ -102,12 +67,12 @@ export class SalariesService extends BaseContractService { async getSalary(dto: GetEmployeeSalariesDto) { const { employeeAddress, contractAddress } = dto; - const { abi } = await hre.artifacts.readArtifact('Salaries'); + const { abi } = await hre.artifacts.readArtifact('Payroll'); const signer = await this.providerService.getSigner(); const contract = new ethers.Contract(contractAddress, abi, signer); - const answer: BigInt = await contract.getUsdtSalary(employeeAddress); + const answer: bigint = await contract.getUsdtSalary(employeeAddress); return { salaryInUsd: answer.toString(), }; diff --git a/chain-api/src/hardhat/modules/dto/ethers.dto.ts b/chain-api/src/ethers-custom/dto/ethers.dto.ts similarity index 100% rename from chain-api/src/hardhat/modules/dto/ethers.dto.ts rename to chain-api/src/ethers-custom/dto/ethers.dto.ts diff --git a/chain-api/src/ethers-custom/ethers.helpers.ts b/chain-api/src/ethers-custom/ethers.helpers.ts new file mode 100644 index 0000000..a62c43c --- /dev/null +++ b/chain-api/src/ethers-custom/ethers.helpers.ts @@ -0,0 +1,13 @@ +import { TransactionReceipt, ethers } from 'ethers'; + +export const parseLogs = ( + txReceipt: TransactionReceipt, + contract: ethers.Contract, + eventName: string, +) => { + const parsedLogs = txReceipt.logs.map((log) => + contract.interface.parseLog(log), + ); + console.log('=>(ethers.helpers.ts:10) parsedLogs', parsedLogs); + return parsedLogs.find((log) => !!log && log.fragment.name === eventName); +}; diff --git a/chain-api/src/hardhat/contracts/License.sol b/chain-api/src/hardhat/contracts/License.sol index 7e11e62..82a94c4 100644 --- a/chain-api/src/hardhat/contracts/License.sol +++ b/chain-api/src/hardhat/contracts/License.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.17; import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; - +import "./Payroll.sol"; /** * Request testnet LINK and ETH here: https://faucets.chain.link/ * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/ @@ -13,19 +13,65 @@ import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. */ -contract LinkWellStringBytesConsumerContractExample is ChainlinkClient, ConfirmedOwner { +contract StreamingRightsManagement is ChainlinkClient, ConfirmedOwner { using Chainlink for Chainlink.Request; - address private oracleAddress; + address public oracleAddress; bytes32 private jobId; uint256 private fee; + address public multisigWallet; - constructor() ConfirmedOwner(msg.sender) { - _setChainlinkToken(0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904); - setOracleAddress(0xd36c6B1777c7f3Db1B3201bDD87081A9045B7b46); - setJobId("8ced832954544a3c98543c94a51d6a8d"); - setFeeInHundredthsOfLink(0); // 0 LINK + mapping(address => uint) public ownerShare; + address[] public owners; + + Payroll public payoutContract; + + constructor( + address _chainLinkToken, + address _oracleAddress, + string memory _jobId, + uint _fee, + address _multiSigAddress, + address[] memory _owners, + uint[] memory _shares, + address payable _payoutAddress + ) ConfirmedOwner(_multiSigAddress) { + + _setChainlinkToken(_chainLinkToken); + + setOracleAddress(_oracleAddress); + + setJobId(_jobId); + + setFeeInHundredthsOfLink(_fee); + + multisigWallet = _multiSigAddress; + + payoutContract = Payroll(_payoutAddress); + + require(_owners.length == _shares.length, "Owners and shares length mismatch"); + + uint sumShare = 0; + + for(uint i=0; i<_shares.length;i++){ + sumShare += _shares[i]; + } + + require(sumShare ==100, 'Invalid share percentage'); + for (uint i = 0; i < _owners.length; i++) { + require(_shares[i] > 0, 'Share cannot be less than 0'); + ownerShare[_owners[i]] = _shares[i]; + owners.push(_owners[i]); + } } + //get share + //update share + //change payout address + // + function getShare(address owner) public returns(uint){ + return ownerShare[owner]; + } + // Send a request to the Chainlink oracle function request() public { @@ -33,35 +79,40 @@ contract LinkWellStringBytesConsumerContractExample is ChainlinkClient, Confirme Chainlink.Request memory req = _buildOperatorRequest(jobId, this.fulfill.selector); // DEFINE THE REQUEST PARAMETERS (example) - req._add('method', 'POST'); - req._add('url', 'https://httpbin.org/post'); - req._add('headers', '["accept", "application/json", "set-cookie", "sid=14A52"]'); - req._add('body', '{"data":[{"id":1,"name":"Bitcoin","price":20194.52},{"id":2,"name":"Ethereum","price":1850.46},{"id":3,"name":"Chainlink","price":18.36}]}'); + req._add('method', 'GET'); + req._add('url', 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=USD,EUR'); + req._add('headers', '["content-type", "application/json", "set-cookie", "sid=14A52"]'); + req._add('body', ''); req._add('contact', ''); // PLEASE ENTER YOUR CONTACT INFO. this allows us to notify you in the event of any emergencies related to your request (ie, bugs, downtime, etc.). example values: 'derek_linkwellnodes.io' (Discord handle) OR 'derek@linkwellnodes.io' OR '+1-617-545-4721' // The following curl command simulates the above request parameters: - // curl 'https://httpbin.org/post' --request 'POST' --header 'content-type: application/json' --header 'set-cookie: sid=14A52' --data '{"data":[{"id":1,"name":"Bitcoin","price":20194.52},{"id":2,"name":"Ethereum","price":1850.46},{"id":3,"name":"Chainlink","price":18.36}]}' + // curl 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=USD,EUR' --request 'GET' --header 'content-type: application/json' --header 'set-cookie: sid=14A52' // PROCESS THE RESULT (example) - req._add('path', 'json,data,0,name'); - + req._add('path', 'ETH,USD'); // Send the request to the Chainlink oracle _sendOperatorRequest(req, fee); } - bytes public responseBytes; + uint256 public totalPayoutInUSD; // Receive the result from the Chainlink oracle event RequestFulfilled(bytes32 indexed requestId); - function fulfill(bytes32 requestId, bytes memory bytesData) public recordChainlinkFulfillment(requestId) { + + function fulfill(bytes32 requestId, uint256 data) public recordChainlinkFulfillment(requestId) { // Process the oracle response // emit RequestFulfilled(requestId); // (optional) emits this event in the on-chain transaction logs, allowing Web3 applications to listen for this transaction - responseBytes = bytesData; // example value: 0x426974636f696e + totalPayoutInUSD = data / 100; // example value: 1875870000000000000000 (1875.87 before "multiplier" is applied) } - // Retrieve the response data as a string - function getResponseString() public view onlyOwner returns (string memory) { - return string(responseBytes); // example value: Bitcoin + function payout() external onlyOwner { + // using arrays to reduce gas + uint[] memory shares; + + for(uint i=0; i< owners.length; i++){ + shares[i] = ownerShare[owners[i]]; + } + payoutContract.oneTimePayout(owners, shares); } // Update oracle address @@ -69,7 +120,8 @@ contract LinkWellStringBytesConsumerContractExample is ChainlinkClient, Confirme oracleAddress = _oracleAddress; _setChainlinkOracle(_oracleAddress); } - function getOracleAddress() public view onlyOwner returns (address) { + + function getOracleAddress() public view returns (address) { return oracleAddress; } @@ -77,6 +129,7 @@ contract LinkWellStringBytesConsumerContractExample is ChainlinkClient, Confirme function setJobId(string memory _jobId) public onlyOwner { jobId = bytes32(bytes(_jobId)); } + function getJobId() public view onlyOwner returns (string memory) { return string(abi.encodePacked(jobId)); } @@ -85,9 +138,11 @@ contract LinkWellStringBytesConsumerContractExample is ChainlinkClient, Confirme function setFeeInJuels(uint256 _feeInJuels) public onlyOwner { fee = _feeInJuels; } + function setFeeInHundredthsOfLink(uint256 _feeInHundredthsOfLink) public onlyOwner { setFeeInJuels((_feeInHundredthsOfLink * LINK_DIVISIBILITY) / 100); } + function getFeeInHundredthsOfLink() public view onlyOwner returns (uint256) { return (fee * 100) / LINK_DIVISIBILITY; } diff --git a/chain-api/src/hardhat/contracts/Salaries.sol b/chain-api/src/hardhat/contracts/LicensePayout.sol similarity index 90% rename from chain-api/src/hardhat/contracts/Salaries.sol rename to chain-api/src/hardhat/contracts/LicensePayout.sol index 1f6a132..ead5bc4 100644 --- a/chain-api/src/hardhat/contracts/Salaries.sol +++ b/chain-api/src/hardhat/contracts/LicensePayout.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: MIT -// 0x2F9442900d067a3D37A1C2aE99462E055e32c741 pragma solidity ^0.8.7; import {AggregatorV3Interface} from '@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol'; -contract Salaries { +contract LicensePayout { AggregatorV3Interface internal dataFeed; address public multisigWallet; mapping(address => uint) public salaries; event Payout(address indexed employee, uint salaryInETH); event PayoutFailed(address indexed employee, uint salaryInETH, string reason); - //0xF0d50568e3A7e8259E16663972b11910F89BD8e7 constructor(address _multisigWallet, address _priceFeedAddress) { multisigWallet = _multisigWallet; dataFeed = AggregatorV3Interface(_priceFeedAddress); @@ -28,7 +26,7 @@ contract Salaries { function getLatestUSDTPriceInETH() public view returns (int) { ( , - /* uint80 roundID */ int answer /* uint startedAt */ /* uint timeStamp */ /* uint80 answeredInRound */, + /* uint80 roundID */ int answer /* uint startedAt */ /* uint timeStamp */ /* uint80 answeredInRound */, , , @@ -36,6 +34,10 @@ contract Salaries { return answer; } + function oneTimePayout(address payable employee) external onlyMultisig { + + } + function setSalary( address employee, uint salaryInUSDT diff --git a/chain-api/src/hardhat/contracts/MultiSigWallet.sol b/chain-api/src/hardhat/contracts/MultiSigWallet.sol index 83dd3b2..036d2a1 100644 --- a/chain-api/src/hardhat/contracts/MultiSigWallet.sol +++ b/chain-api/src/hardhat/contracts/MultiSigWallet.sol @@ -18,8 +18,10 @@ contract MultiSigWallet { event ConfirmTransaction(address indexed owner, uint indexed txIndex); event RevokeConfirmation(address indexed owner, uint indexed txIndex); - event ExecuteTransaction(address indexed owner, uint indexed txIndex); + event ExecuteTransaction(address indexed owner, uint indexed txIndex, address indexed to); event ExecuteTransactionFailed(address indexed owner, uint indexed txIndex, string reason); + event ContractDeployed(address indexed contractAddress); + address[] public owners; @@ -129,7 +131,16 @@ contract MultiSigWallet { (bool success, bytes memory returnData) = transaction.to.call{value: transaction.value}(transaction.data); if (success) { transaction.executed = true; - emit ExecuteTransaction(msg.sender, _txIndex); + emit ExecuteTransaction(msg.sender, _txIndex, transaction.to); + if (returnData.length > 0) { + address deployedContractAddress; + assembly { + deployedContractAddress := mload(add(returnData, 20)) + } + // You can emit an event with the address of the deployed contract + emit ContractDeployed(deployedContractAddress); + } + removeTransaction(_txIndex); } else { // Get the revert reason and emit it if (returnData.length > 0) { @@ -156,6 +167,11 @@ contract MultiSigWallet { emit RevokeConfirmation(msg.sender, _txIndex); } + function removeTransaction(uint _txIndex) public onlyOwner { + require(_txIndex < transactions.length, "tx does not exist"); + delete transactions[_txIndex]; + } + function getOwners() public view returns (address[] memory) { return owners; } diff --git a/chain-api/src/hardhat/contracts/Payroll.sol b/chain-api/src/hardhat/contracts/Payroll.sol new file mode 100644 index 0000000..bba2f06 --- /dev/null +++ b/chain-api/src/hardhat/contracts/Payroll.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import {AggregatorV3Interface} from '@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol'; + +contract Payroll { + AggregatorV3Interface internal dataFeed; + address public authorizedWallet; + mapping(address => uint) public salaries; + + event Payout(address indexed employee, uint salaryInETH); + event PayoutFailed(address indexed employee, uint salaryInETH, string reason); + + constructor(address _authorizedWallet, address _priceFeedAddress) { + authorizedWallet = _authorizedWallet; + dataFeed = AggregatorV3Interface(_priceFeedAddress); + } + + modifier onlyAuthorized() { + require(msg.sender == authorizedWallet, 'Unauthorized'); + _; + } + + function getUsdtSalary(address employee) public view returns (uint) { + return salaries[employee]; + } + + function getLatestUSDTPriceInETH() public view returns (int) { + ( + , + /* uint80 roundID */ int answer /* uint startedAt */ /* uint timeStamp */ /* uint80 answeredInRound */, + , + , + + ) = dataFeed.latestRoundData(); + return answer; + } + + // using arrays to reduce gas + function oneTimePayout(address[] memory employees, uint[] memory usdAmounts) external onlyAuthorized { + require(employees.length == usdAmounts.length, "Mismatched input lengths"); + int ethToUSDT = getLatestUSDTPriceInETH(); + require(ethToUSDT > 0, 'Invalid price data'); + for (uint i = 0; i < employees.length; i++) { + uint salaryInUSDT = usdAmounts[i]; + require(salaryInUSDT > 0, 'No salary set'); + uint salaryInETH = uint(salaryInUSDT * 1e18) / uint(ethToUSDT); + salaryInETH = salaryInETH * 1e8; + // Check sufficient balance + require( + address(this).balance >= salaryInETH, + 'Insufficient contract balance' + ); + + (bool success,) = employees[i].call{value: salaryInETH}(""); + if (success) { + emit Payout(employees[i], salaryInETH); + } else { + emit PayoutFailed(employees[i], salaryInETH, "Transfer failed"); + } + } + + } + + function setSalary( + address employee, + uint salaryInUSDT + ) external onlyAuthorized { + salaries[employee] = salaryInUSDT; + } + + function getEmployeeSalaryInEth(address employee) public view returns (uint){ + uint salaryInUSDT = salaries[employee]; + require(salaryInUSDT > 0, 'No salary set'); + + int ethToUSDT = getLatestUSDTPriceInETH(); + require(ethToUSDT > 0, 'Invalid price data'); + uint salaryInETH = uint(salaryInUSDT * 1e18) / uint(ethToUSDT); + return salaryInETH * 1e8; + } + + function payoutInETH(address payable employee) external onlyAuthorized { + uint salaryInETH = getEmployeeSalaryInEth(employee); + // Check sufficient balance + require( + address(this).balance >= salaryInETH, + 'Insufficient contract balance' + ); + + (bool success,) = employee.call{value: salaryInETH}(""); + if (success) { + emit Payout(employee, salaryInETH); + } else { + emit PayoutFailed(employee, salaryInETH, "Transfer failed"); + } + } + + // Fallback to receive ETH + receive() external payable {} +} diff --git a/chain-api/src/hardhat/modules/hardhat.module.ts b/chain-api/src/hardhat/modules/hardhat.module.ts deleted file mode 100644 index 577d8c7..0000000 --- a/chain-api/src/hardhat/modules/hardhat.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HardhatService } from './hardhat.service'; -import { ProviderModule } from 'src/provider/provider.module'; -import { MultiSigModule } from './multi-sig/multi-sig.module'; -import { SalariesModule } from './salaries/salaries.module'; - -@Module({ - imports: [ProviderModule, MultiSigModule, SalariesModule], - controllers: [], - providers: [HardhatService], - exports: [HardhatService, MultiSigModule, SalariesModule], -}) -export class HardhatModule {} diff --git a/chain-api/src/hardhat/modules/hardhat.service.ts b/chain-api/src/hardhat/modules/hardhat.service.ts deleted file mode 100644 index 55c2582..0000000 --- a/chain-api/src/hardhat/modules/hardhat.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class HardhatService {} diff --git a/chain-api/src/hardhat/test/Salaries.test.ts b/chain-api/src/hardhat/test/Salaries.test.ts index fc14681..cfd041c 100644 --- a/chain-api/src/hardhat/test/Salaries.test.ts +++ b/chain-api/src/hardhat/test/Salaries.test.ts @@ -1,11 +1,13 @@ -import { PriceFeedMock, Salaries } from '../../../typechain'; +import { PriceFeedMock, Payroll } from '../../../typechain'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { ethers } = require('hardhat'); +// eslint-disable-next-line @typescript-eslint/no-var-requires const { expect } = require('chai'); import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; describe('Salaries', function () { - let salaries: Salaries; + let salaries: Payroll; let owner: SignerWithAddress; let multisigWallet: SignerWithAddress; let addr1: SignerWithAddress; @@ -19,11 +21,11 @@ describe('Salaries', function () { priceFeedMock = await PriceFeedMockFactory.deploy(); await priceFeedMock.getDeployedCode(); // Deploy the Salaries contract - const SalariesFactory = await ethers.getContractFactory('Salaries'); + const SalariesFactory = await ethers.getContractFactory('Payroll'); salaries = (await SalariesFactory.deploy( multisigWallet.address, await priceFeedMock.getAddress(), - )) as Salaries; + )) as Payroll; await salaries.getDeployedCode(); });