multisig ready, deploy and interact controller

This commit is contained in:
emochka2007 2024-05-10 00:17:03 +03:00
parent 01371286d0
commit ed2f6b9eca
23 changed files with 353 additions and 112 deletions

View File

@ -17,6 +17,8 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.1",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.5",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
@ -28,7 +30,7 @@
"@nomicfoundation/hardhat-ethers": "^3.0.5",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/node": "^20.12.11",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
@ -5138,9 +5140,9 @@
"peer": true
},
"node_modules/@types/node": {
"version": "20.12.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz",
"integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==",
"version": "20.12.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
"integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
"dependencies": {
"undici-types": "~5.26.4"
}
@ -5235,6 +5237,11 @@
"@types/superagent": "*"
}
},
"node_modules/@types/validator": {
"version": "13.11.9",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
"integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw=="
},
"node_modules/@types/yargs": {
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
@ -6714,6 +6721,21 @@
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
"dev": true
},
"node_modules/class-transformer": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
},
"node_modules/class-validator": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
"integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==",
"dependencies": {
"@types/validator": "^13.11.8",
"libphonenumber-js": "^1.10.53",
"validator": "^13.9.0"
}
},
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@ -11544,6 +11566,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/libphonenumber-js": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.1.tgz",
"integrity": "sha512-Wze1LPwcnzvcKGcRHFGFECTaLzxOtujwpf924difr5zniyYv1C2PiW0419qDR7m8lKDxsImu5mwxFuXhXpjmvw=="
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -16173,6 +16200,14 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -28,6 +28,8 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.1",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.5",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
@ -39,7 +41,7 @@
"@nomicfoundation/hardhat-ethers": "^3.0.5",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/node": "^20.12.11",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",

View File

@ -11,6 +11,7 @@ import { ContractFactoryService } from './contract-factory.service';
import { CreateContractFactoryDto } from './dto/create-contract-factory.dto';
import { UpdateContractFactoryDto } from './dto/update-contract-factory.dto';
import { ApiTags } from '@nestjs/swagger';
import { MultiSigWalletDto } from 'src/hardhat/modules/dto/multi-sig.dto';
@ApiTags('contract-factory')
@Controller('contract-factory')
export class ContractFactoryController {
@ -18,8 +19,8 @@ export class ContractFactoryController {
private readonly contractFactoryService: ContractFactoryService,
) {}
@Post('')
create(@Body() createContractFactoryDto: CreateContractFactoryDto) {
return this.contractFactoryService.create(createContractFactoryDto);
@Post('multi-sig')
create(@Body() createContractFactoryDto: MultiSigWalletDto) {
return this.contractFactoryService.createMultiSig(createContractFactoryDto);
}
}

View File

@ -1,4 +1,4 @@
import { HardhatModule } from '../hardhat/module/hardhat.module';
import { HardhatModule } from '../hardhat/modules/hardhat.module';
import { Module } from '@nestjs/common';
import { ContractFactoryService } from './contract-factory.service';
import { ContractFactoryController } from './contract-factory.controller';

View File

@ -1,11 +1,21 @@
import { HardhatService } from '../hardhat/module/hardhat.service';
import { HardhatService } from '../hardhat/modules/hardhat.service';
import { Injectable } from '@nestjs/common';
import { CreateContractFactoryDto } from './dto/create-contract-factory.dto';
import { SalariesService } from 'src/hardhat/modules/salary.service';
import { MultiSigWalletService } from 'src/hardhat/modules/multi-sig/multi-sig.service';
import { MultiSigWalletDto } from 'src/hardhat/modules/dto/multi-sig.dto';
@Injectable()
export class ContractFactoryService {
constructor(private readonly hhService: HardhatService) {}
async create(createContractFactoryDto: CreateContractFactoryDto) {
return await this.hhService.deploySalaryContract();
constructor(
private readonly salaryService: SalariesService,
private readonly multiSigService: MultiSigWalletService,
) {}
async createSalary(createContractFactoryDto: CreateContractFactoryDto) {
return await this.salaryService.deploy();
}
async createMultiSig(dto: MultiSigWalletDto) {
return await this.multiSigService.deploy(dto);
}
}

View File

@ -1,49 +0,0 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { ContractInteractService } from './contract-interact.service';
import { CreateContractInteractDto } from './dto/create-contract-interact.dto';
import { UpdateContractInteractDto } from './dto/update-contract-interact.dto';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('contract-interact')
@Controller('contract-interact')
export class ContractInteractController {
constructor(
private readonly contractInteractService: ContractInteractService,
) {}
@Post()
create(@Body() createContractInteractDto: CreateContractInteractDto) {
return this.contractInteractService.create(createContractInteractDto);
}
@Get()
findAll() {
return this.contractInteractService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.contractInteractService.findOne(+id);
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() updateContractInteractDto: UpdateContractInteractDto,
) {
return this.contractInteractService.update(+id, updateContractInteractDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.contractInteractService.remove(+id);
}
}

View File

@ -1,9 +1,12 @@
import { Module } from '@nestjs/common';
import { ContractInteractService } from './contract-interact.service';
import { ContractInteractController } from './contract-interact.controller';
import { HardhatModule } from 'src/hardhat/modules/hardhat.module';
import { MultiSigInteractController } from './multi-sig-interact.controller';
@Module({
controllers: [ContractInteractController],
imports: [HardhatModule],
controllers: [MultiSigInteractController],
providers: [ContractInteractService],
})
export class ContractInteractModule {}

View File

@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class SubmitTransactionDto {
@IsString()
@ApiProperty()
contractAddress: string;
@ApiProperty()
@IsString()
destination: string;
@IsString()
@ApiProperty()
value: string;
@IsOptional()
@IsString()
// @ApiProperty()
data: string;
}

View File

@ -0,0 +1,20 @@
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 { SubmitTransactionDto } from './dto/multi-sig.dto';
@ApiTags('multi-sig-interact')
@Controller()
export class MultiSigInteractController {
constructor(private readonly multiSigWalletService: MultiSigWalletService) {}
@Get('owners/:address')
async getOwners(@Param('address') address: string) {
return this.multiSigWalletService.getOwners(address);
}
@ApiOkResponse()
@Post('submit-transaction')
async submitTransaction(@Body() dto: SubmitTransactionDto) {
return this.multiSigWalletService.submitTransaction(dto);
}
}

View File

@ -8,7 +8,7 @@ pragma solidity ^0.8.19;
contract MultiSigWallet {
event Deposit(address indexed sender, uint amount, uint balance);
event SubmitTransaction(
address indexed owener,
address indexed owner,
uint indexed txIndex,
address indexed to,
uint value,
@ -38,36 +38,36 @@ contract MultiSigWallet {
Transaction[] public transactions;
modifier onlyOwner() {
require(isOwner[msg.sender], "not owner");
require(isOwner[msg.sender], 'not owner');
_;
}
modifier txExists(uint _txIndex) {
require(_txIndex < transactions.length, "tx does not exist");
require(_txIndex < transactions.length, 'tx does not exist');
_;
}
modifier notConfirmed(uint _txIndex) {
require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
require(!isConfirmed[_txIndex][msg.sender], 'tx already confirmed');
_;
}
modifier notExecuted(uint _txIndex) {
require(!transactions[_txIndex].executed, "tx already confirmed");
require(!transactions[_txIndex].executed, 'tx already confirmed');
_;
}
constructor(address[] memory _owners, uint _numConfirmationsRequired) {
require(_owners.length > 0, "owners required");
require(_owners.length > 0, 'owners required');
require(
_numConfirmationsRequired > 0 &&
_numConfirmationsRequired <= _owners.length,
"invalid number of required confirmations"
'invalid number of required confirmations'
);
for (uint i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "invalid owner");
require(!isOwner[owner], "owner not unique");
require(owner != address(0), 'invalid owner');
require(!isOwner[owner], 'owner not unique');
isOwner[owner] = true;
owners.push(owner);
}
@ -117,13 +117,13 @@ contract MultiSigWallet {
Transaction storage transaction = transactions[_txIndex];
require(
transaction.numConfirmations >= numConfirmationsRequired,
"cannot execute tx"
'cannot execute tx'
);
transaction.executed = true;
(bool success, ) = transaction.to.call{value: transaction.value}(
transaction.data
);
require(success, "tx failed");
require(success, 'tx failed');
emit ExecuteTransaction(msg.sender, _txIndex);
}
@ -131,7 +131,7 @@ contract MultiSigWallet {
uint _txIndex
) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
Transaction storage transaction = transactions[_txIndex];
require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");
require(isConfirmed[_txIndex][msg.sender], 'tx not confirmed');
transaction.numConfirmations -= 1;
isConfirmed[_txIndex][msg.sender] = false;

View File

@ -1,26 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import {AggregatorV3Interface} from '@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol';
contract Salaries {
AggregatorV3Interface internal dataFeed;
address public multisigWallet;
mapping(address => uint) public salaries;
constructor() {
dataFeed = AggregatorV3Interface(
0xF0d50568e3A7e8259E16663972b11910F89BD8e7
);
constructor(address _multisigWallet, address _priceFeedAddress) {
multisigWallet = _multisigWallet;
dataFeed = AggregatorV3Interface(_priceFeedAddress);
}
function getChainlinkDataFeedLatestAnswer() public view returns (int) {
// prettier-ignore
modifier onlyMultisig() {
require(msg.sender == multisigWallet, 'Unauthorized');
_;
}
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 */,
,
,
) = dataFeed.latestRoundData();
return answer;
}
function setSalary(
address employee,
uint salaryInUSDT
) external onlyMultisig {
salaries[employee] = salaryInUSDT;
}
function payoutInETH(address employee) external onlyMultisig {
uint salaryInUSDT = salaries[employee];
require(salaryInUSDT > 0, 'No salary set');
int ethToUSDT = getLatestUSDTPriceInETH();
require(ethToUSDT > 0, 'Invalid price data');
// Convert salary from USDT to ETH based on the latest price
uint salaryInETH = uint(salaryInUSDT * 1e18) / uint(ethToUSDT);
// Check sufficient balance
require(
address(this).balance >= salaryInETH,
'Insufficient contract balance'
);
salaries[employee] = 0; // Reset salary after payment
payable(employee).transfer(salaryInETH);
}
// Fallback to receive ETH
receive() external payable {}
}

View File

@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { HardhatService } from './hardhat.service';
@Module({
imports: [],
controllers: [],
providers: [HardhatService],
exports: [HardhatService],
})
export class HardhatModule {}

View File

@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ProviderService } from 'src/provider/provider.service';
@Injectable()
export abstract class BaseContractService {
constructor(
public readonly configService: ConfigService,
public readonly providerService: ProviderService,
) {}
abstract deploy(dto: object): Promise<any>;
}

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNumber } from 'class-validator';
export class MultiSigWalletDto {
@IsArray()
@ApiProperty()
owners: string[];
@IsNumber()
@ApiProperty()
confirmations: number;
}

View File

@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { HardhatService } from './hardhat.service';
import { ProviderModule } from 'src/provider/provider.module';
import { MultiSigWalletService } from './multi-sig/multi-sig.service';
import { SalariesService } from './salary.service';
import { BaseContractService } from './base-contract.service';
import { MultiSigModule } from './multi-sig/multi-sig.module';
@Module({
imports: [ProviderModule, MultiSigModule],
controllers: [],
providers: [HardhatService, SalariesService],
exports: [HardhatService, SalariesService, MultiSigModule],
})
export class HardhatModule {}

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class HardhatService {}

View File

@ -0,0 +1,15 @@
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';
@Module({
imports: [ProviderModule],
controllers: [],
providers: [MultiSigWalletService],
exports: [MultiSigWalletService],
})
export class MultiSigModule {}

View File

@ -0,0 +1,68 @@
import { MultiSigWallet } from '../../typechain-types/contracts/MultiSigWallet';
import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
import { ConfigService } from '@nestjs/config';
import * as hre from 'hardhat';
import { BaseContractService } from '../base-contract.service';
import { MultiSigWalletDto } from '../dto/multi-sig.dto';
import { SubmitTransactionDto } from 'src/contract-interact/dto/multi-sig.dto';
export class MultiSigWalletService extends BaseContractService {
async deploy(dto: MultiSigWalletDto) {
const { abi, bytecode } =
await hre.artifacts.readArtifact('MultiSigWallet');
const signer = await this.providerService.getSigner();
const salaryContract = new ethers.ContractFactory(abi, bytecode, signer);
const myContract = await salaryContract.deploy(
dto.owners,
dto.confirmations,
);
await myContract.waitForDeployment();
console.log(
'🚀 ~ HardhatService ~ deploySalaryContract ~ myContract:',
myContract,
);
const address = myContract.getAddress();
console.log('🚀 ~ SalariesService ~ deploy ~ address:', address);
}
async getOwners(address: string) {
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const multiSigContract = new ethers.Contract(address, abi);
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(address, abi, signer);
const owners = await contract.getOwners();
return owners;
}
async submitTransaction(dto: SubmitTransactionDto) {
const { destination, value, data, contractAddress } = dto;
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const multiSigContract = new ethers.Contract(contractAddress, abi);
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
console.log(
'🚀 ~ MultiSigWalletService ~ submitTransaction ~ contract:',
contract.interface,
);
const tx = await contract.submitTransaction(
destination,
value,
new TextEncoder().encode(data),
);
console.log('🚀 ~ MultiSigWalletService ~ submitTransaction ~ tx:', tx);
return tx;
}
}

View File

@ -1,25 +1,19 @@
// const hre = require('hardhat');
import * as hre from 'hardhat';
import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
import { ConfigService } from '@nestjs/config';
import * as hre from 'hardhat';
import { BaseContractService } from './base-contract.service';
@Injectable()
export class HardhatService {
constructor(private readonly configService: ConfigService) {}
async deploySalaryContract() {
const provider = new ethers.JsonRpcProvider(
'https://polygon-amoy.g.alchemy.com/v2/pEtFFy_Qr_NrM1vMnlzSXmYXkozVNzLy',
80002,
);
export class SalariesService extends BaseContractService {
getSalaries() {}
async deploy() {
const provider = await this.providerService.getProvider();
const salary = await hre.artifacts.readArtifact('Salaries');
const abi = salary.abi;
console.log('🚀 ~ HardhatService ~ deploySalaryContract ~ abi:', abi);
const bytecode = salary.deployedBytecode;
console.log(
'🚀 ~ HardhatService ~ deploySalaryContract ~ bytecode:',
bytecode,
);
const signer = new ethers.Wallet(
this.configService.getOrThrow('POLYGON_PK'),
provider,
@ -31,12 +25,17 @@ export class HardhatService {
signer,
);
const myContract = await salaryContract.deploy();
const myContract = await salaryContract.deploy(
'multisig address',
this.configService.getOrThrow('CHAINLINK_AGGREGATOR_V3'),
);
await myContract.waitForDeployment();
console.log(
'🚀 ~ HardhatService ~ deploySalaryContract ~ myContract:',
myContract,
);
const address = myContract.getAddress();
console.log('🚀 ~ SalariesService ~ deploy ~ address:', address);
}
}

View File

@ -1,6 +1,7 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
@ -11,7 +12,9 @@ async function bootstrap() {
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
console.log('Swagger avaliable at http://localhost:3000/api');
}
bootstrap();

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ProviderService } from './provider.service';
@Module({
imports: [],
controllers: [],
providers: [ProviderService],
exports: [ProviderService],
})
export class ProviderModule {}

View File

@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class ProviderService {
public provider: ethers.JsonRpcProvider;
public networkId: number;
private nodeUrl: string;
constructor(private readonly configService: ConfigService) {
this.networkId = parseInt(
this.configService.getOrThrow('POLYGON_NETWORK_ID'),
);
this.nodeUrl = this.configService.getOrThrow('POLYGON_NODE');
}
async getProvider() {
if (this.provider) {
return this.provider;
}
const polygonProvider = new ethers.JsonRpcProvider(
this.nodeUrl,
this.networkId,
);
this.provider = polygonProvider;
return this.provider;
}
async getSigner() {
if (!this.provider) {
await this.getProvider();
}
const signer = new ethers.Wallet(
this.configService.getOrThrow('POLYGON_PK'),
this.provider,
);
return signer;
}
}