salaries ready, started data feed chainlink

This commit is contained in:
emochka2007 2024-05-18 02:01:27 +03:00
parent 958bf670ae
commit a2b4823fcf
17 changed files with 254 additions and 101 deletions

View File

@ -1,5 +1,6 @@
![LOGIN FLOW](./login-flow.png "Login") ![LOGIN FLOW](./login-flow.png "Login")
![Example architecture](./arch.png "Arch")
[//]: # (![Example architecture](./arch.png "Arch"))
![License](./license.png "Arch") ![License](./license.png "Arch")
![Salaries](./salaries.png "Arch") ![Salaries](./salaries.png "Arch")

View File

@ -11,9 +11,13 @@ const config = {
accounts: [process.env.POLYGON_PK || ''], accounts: [process.env.POLYGON_PK || ''],
}, },
}, },
typechain: {
outDir: 'typechain',
target: 'ethers-v6',
},
paths: { paths: {
sources: './src/hardhat/contracts', sources: './src/hardhat/contracts',
// tests: './src/hardhat/test', tests: './src/hardhat/test',
ignition: './src/hardhat/ignition', ignition: './src/hardhat/ignition',
cache: './src/hardhat/cache', cache: './src/hardhat/cache',
artifacts: './src/hardhat/artifacts', artifacts: './src/hardhat/artifacts',

View File

@ -6600,15 +6600,15 @@
} }
}, },
"node_modules/chai-as-promised": { "node_modules/chai-as-promised": {
"version": "7.1.1", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz",
"integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"check-error": "^1.0.2" "check-error": "^1.0.2"
}, },
"peerDependencies": { "peerDependencies": {
"chai": ">= 2.1.2 < 5" "chai": ">= 2.1.2 < 6"
} }
}, },
"node_modules/chalk": { "node_modules/chalk": {

View File

@ -42,3 +42,7 @@ export class DepositContractDto {
@ApiProperty() @ApiProperty()
value: string; value: string;
} }
export class DeployMultiSigResponseDto {
@IsString()
address: string;
}

View File

@ -3,8 +3,9 @@ import { TransactionReceipt, ethers } from 'ethers';
export const parseLogs = ( export const parseLogs = (
txReceipt: TransactionReceipt, txReceipt: TransactionReceipt,
contract: ethers.Contract, contract: ethers.Contract,
eventName: string,
) => { ) => {
return txReceipt.logs return txReceipt.logs
.map((log) => contract.interface.parseLog(log)) .map((log) => contract.interface.parseLog(log))
.find((log) => !!log); .find((log) => !!log && log.fragment.name === eventName);
}; };

View File

@ -0,0 +1,101 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.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/
*/
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
*/
contract LinkWellStringBytesConsumerContractExample is ChainlinkClient, ConfirmedOwner {
using Chainlink for Chainlink.Request;
address private oracleAddress;
bytes32 private jobId;
uint256 private fee;
constructor() ConfirmedOwner(msg.sender) {
_setChainlinkToken(0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904);
setOracleAddress(0xd36c6B1777c7f3Db1B3201bDD87081A9045B7b46);
setJobId("8ced832954544a3c98543c94a51d6a8d");
setFeeInHundredthsOfLink(0); // 0 LINK
}
// Send a request to the Chainlink oracle
function request() public {
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('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}]}'
// PROCESS THE RESULT (example)
req._add('path', 'json,data,0,name');
// Send the request to the Chainlink oracle
_sendOperatorRequest(req, fee);
}
bytes public responseBytes;
// Receive the result from the Chainlink oracle
event RequestFulfilled(bytes32 indexed requestId);
function fulfill(bytes32 requestId, bytes memory bytesData) 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
}
// Retrieve the response data as a string
function getResponseString() public view onlyOwner returns (string memory) {
return string(responseBytes); // example value: Bitcoin
}
// Update oracle address
function setOracleAddress(address _oracleAddress) public onlyOwner {
oracleAddress = _oracleAddress;
_setChainlinkOracle(_oracleAddress);
}
function getOracleAddress() public view onlyOwner returns (address) {
return oracleAddress;
}
// Update jobId
function setJobId(string memory _jobId) public onlyOwner {
jobId = bytes32(bytes(_jobId));
}
function getJobId() public view onlyOwner returns (string memory) {
return string(abi.encodePacked(jobId));
}
// Update fees
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;
}
function withdrawLink() public onlyOwner {
LinkTokenInterface link = LinkTokenInterface(_chainlinkTokenAddress());
require(
link.transfer(msg.sender, link.balanceOf(address(this))),
"Unable to transfer"
);
}
}

View File

@ -1,34 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
// Uncomment this line to use console.log
// import "hardhat/console.sol";
contract Lock {
uint public unlockTime;
address payable public owner;
event Withdrawal(uint amount, uint when);
constructor(uint _unlockTime) payable {
require(
block.timestamp < _unlockTime,
"Unlock time should be in the future"
);
unlockTime = _unlockTime;
owner = payable(msg.sender);
}
function withdraw() public {
// Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal
// console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);
require(block.timestamp >= unlockTime, "You can't withdraw yet");
require(msg.sender == owner, "You aren't the owner");
emit Withdrawal(address(this).balance, block.timestamp);
owner.transfer(address(this).balance);
}
}

View File

@ -20,8 +20,6 @@ contract MultiSigWallet {
event RevokeConfirmation(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);
event ExecuteTransactionFailed(address indexed owner, uint indexed txIndex, string reason); event ExecuteTransactionFailed(address indexed owner, uint indexed txIndex, string reason);
event Payout(address indexed employee, uint salaryInETH);
event PayoutFailed(address indexed employee, uint salaryInETH, string reason);
address[] public owners; address[] public owners;
@ -132,9 +130,6 @@ contract MultiSigWallet {
if (success) { if (success) {
transaction.executed = true; transaction.executed = true;
emit ExecuteTransaction(msg.sender, _txIndex); emit ExecuteTransaction(msg.sender, _txIndex);
if (returnData.length > 0) {
emitEventFromReturnData(returnData);
}
} else { } else {
// Get the revert reason and emit it // Get the revert reason and emit it
if (returnData.length > 0) { if (returnData.length > 0) {
@ -150,30 +145,6 @@ contract MultiSigWallet {
} }
} }
function emitEventFromReturnData(bytes memory returnData) internal {
// Decode the selector from returnData
bytes4 selector;
assembly {
selector := mload(add(returnData, 32))
}
// Match the selector to the known events
if (selector == Payout.selector) {
(address employee, uint salaryInETH) = abi.decode(slice(returnData, 4, returnData.length), (address, uint));
emit Payout(employee, salaryInETH);
} else if (selector == PayoutFailed.selector) {
(address employee, uint salaryInETH, string memory reason) = abi.decode(slice(returnData, 4, returnData.length), (address, uint, string));
emit PayoutFailed(employee, salaryInETH, reason);
}
}
function slice(bytes memory data, uint start, uint length) internal pure returns (bytes memory) {
bytes memory result = new bytes(length);
for (uint i = 0; i < length; i++) {
result[i] = data[start + i];
}
return result;
}
function revokeConfirmation( function revokeConfirmation(
uint _txIndex uint _txIndex
) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) { ) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract PriceFeedMock {
function latestRoundData()
external
pure
returns (
uint80 roundId,
int answer,
uint startedAt,
uint updatedAt,
uint80 answeredInRound
)
{
return (0, 3087, 0, 0, 0); // Mock data, 1 ETH = 3087 USDT
}
}

View File

@ -21,7 +21,7 @@ contract Salaries {
_; _;
} }
function getSalary(address employee) public view returns(uint) { function getUsdtSalary(address employee) public view returns(uint) {
return salaries[employee]; return salaries[employee];
} }
@ -43,16 +43,18 @@ contract Salaries {
salaries[employee] = salaryInUSDT; salaries[employee] = salaryInUSDT;
} }
function payoutInETH(address payable employee) external onlyMultisig { function getEmployeeSalaryInEth(address employee) public view returns(uint){
uint salaryInUSDT = salaries[employee]; uint salaryInUSDT = salaries[employee];
require(salaryInUSDT > 0, 'No salary set'); require(salaryInUSDT > 0, 'No salary set');
int ethToUSDT = getLatestUSDTPriceInETH(); int ethToUSDT = getLatestUSDTPriceInETH();
require(ethToUSDT > 0, 'Invalid price data'); require(ethToUSDT > 0, 'Invalid price data');
// Convert salary from USDT to ETH based on the latest price
uint salaryInETH = uint(salaryInUSDT * 1e18) / uint(ethToUSDT); uint salaryInETH = uint(salaryInUSDT * 1e18) / uint(ethToUSDT);
return salaryInETH * 1e8;
}
function payoutInETH(address payable employee) external onlyMultisig {
uint salaryInETH = getEmployeeSalaryInEth(employee);
// Check sufficient balance // Check sufficient balance
require( require(
address(this).balance >= salaryInETH, address(this).balance >= salaryInETH,
@ -67,10 +69,6 @@ contract Salaries {
} }
} }
function dummy() public pure returns (uint){
return 1337;
}
// Fallback to receive ETH // Fallback to receive ETH
receive() external payable {} receive() external payable {}
} }

View File

@ -3,6 +3,7 @@ import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { MultiSigWalletService } from 'src/hardhat/modules/multi-sig/multi-sig.service'; import { MultiSigWalletService } from 'src/hardhat/modules/multi-sig/multi-sig.service';
import { import {
ConfirmTransactionDto, ConfirmTransactionDto,
DeployMultiSigResponseDto,
DepositContractDto, DepositContractDto,
ExecuteTransactionDto, ExecuteTransactionDto,
GetTransactionDto, GetTransactionDto,
@ -15,9 +16,17 @@ import { MultiSigWalletDto } from './multi-sig.dto';
export class MultiSigInteractController { export class MultiSigInteractController {
constructor(private readonly multiSigWalletService: MultiSigWalletService) {} constructor(private readonly multiSigWalletService: MultiSigWalletService) {}
@ApiOkResponse({
type: DeployMultiSigResponseDto,
})
@Post('deploy') @Post('deploy')
async deploy(@Body() dto: MultiSigWalletDto) { async deploy(
return this.multiSigWalletService.deploy(dto); @Body() dto: MultiSigWalletDto,
): Promise<DeployMultiSigResponseDto> {
const addr = await this.multiSigWalletService.deploy(dto);
return {
address: addr,
};
} }
@Get('owners/:address') @Get('owners/:address')
async getOwners(@Param('address') address: string) { async getOwners(@Param('address') address: string) {

View File

@ -1,5 +1,4 @@
import { TransactionReceipt, ethers, parseEther } from 'ethers'; import { ethers, parseEther, TransactionReceipt } from 'ethers';
import { ConfigService } from '@nestjs/config';
import * as hre from 'hardhat'; import * as hre from 'hardhat';
import { BaseContractService } from '../base-contract.service'; import { BaseContractService } from '../base-contract.service';
import { MultiSigWalletDto } from './multi-sig.dto'; import { MultiSigWalletDto } from './multi-sig.dto';
@ -27,8 +26,7 @@ export class MultiSigWalletService extends BaseContractService {
dto.confirmations, dto.confirmations,
); );
await myContract.waitForDeployment(); await myContract.waitForDeployment();
const address = myContract.getAddress(); return myContract.getAddress();
return address;
} }
async getOwners(address: string) { async getOwners(address: string) {
@ -39,9 +37,7 @@ export class MultiSigWalletService extends BaseContractService {
const contract = new ethers.Contract(address, abi, signer); const contract = new ethers.Contract(address, abi, signer);
const owners = await contract.getOwners(); return await contract.getOwners();
return owners;
} }
async submitTransaction(dto: SubmitTransactionDto) { async submitTransaction(dto: SubmitTransactionDto) {
@ -54,7 +50,7 @@ export class MultiSigWalletService extends BaseContractService {
const tx = await contract.submitTransaction(destination, value, data); const tx = await contract.submitTransaction(destination, value, data);
const txResponse: TransactionReceipt = await tx.wait(); const txResponse: TransactionReceipt = await tx.wait();
const eventParse = parseLogs(txResponse, contract); const eventParse = parseLogs(txResponse, contract, 'SubmitTransaction');
return { return {
txHash: txResponse.hash, txHash: txResponse.hash,
@ -77,7 +73,7 @@ export class MultiSigWalletService extends BaseContractService {
const txResponse: TransactionReceipt = await tx.wait(); const txResponse: TransactionReceipt = await tx.wait();
const eventParse = parseLogs(txResponse, contract); const eventParse = parseLogs(txResponse, contract, 'ConfirmTransaction');
return { return {
txHash: txResponse.hash, txHash: txResponse.hash,
@ -96,8 +92,7 @@ export class MultiSigWalletService extends BaseContractService {
const tx = await contract.executeTransaction(index); const tx = await contract.executeTransaction(index);
const txResponse: TransactionReceipt = await tx.wait(); const txResponse: TransactionReceipt = await tx.wait();
console.log('=>(multi-sig.service.ts:99) txResponse', txResponse.logs); const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction');
const eventParse = parseLogs(txResponse, contract);
return { return {
txHash: txResponse.hash, txHash: txResponse.hash,
sender: eventParse.args[0].toString(), sender: eventParse.args[0].toString(),
@ -155,7 +150,7 @@ export class MultiSigWalletService extends BaseContractService {
const txResponse: TransactionReceipt = await tx.wait(); const txResponse: TransactionReceipt = await tx.wait();
const eventParse = parseLogs(txResponse, contract); const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction');
return { return {
txHash: txResponse.hash, txHash: txResponse.hash,

View File

@ -2,20 +2,32 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { SalariesService } from './salaries.service'; import { SalariesService } from './salaries.service';
import { import {
CreatePayoutDto, CreatePayoutDto,
DeployContractResponseDto,
GetEmployeeSalariesDto, GetEmployeeSalariesDto,
SalariesDeployDto, SalariesDeployDto,
SetSalaryDto, SetSalaryDto,
} from './salaries.dto'; } from './salaries.dto';
import { ApiTags } from '@nestjs/swagger'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { DepositContractDto } from '../../../contract-interact/dto/multi-sig.dto'; import {
DeployMultiSigResponseDto,
DepositContractDto,
} from '../../../contract-interact/dto/multi-sig.dto';
@ApiTags('salaries') @ApiTags('salaries')
@Controller('salaries') @Controller('salaries')
export class SalariesController { export class SalariesController {
constructor(private readonly salariesService: SalariesService) {} constructor(private readonly salariesService: SalariesService) {}
@ApiOkResponse({
type: DeployContractResponseDto,
})
@Post('deploy') @Post('deploy')
async deploy(@Body() dto: SalariesDeployDto) { async deploy(
return this.salariesService.deploy(dto); @Body() dto: SalariesDeployDto,
): Promise<DeployContractResponseDto> {
const address = await this.salariesService.deploy(dto);
return {
address,
};
} }
@Get('usdt-price/:contractAddress') @Get('usdt-price/:contractAddress')

View File

@ -35,3 +35,8 @@ export class CreatePayoutDto extends GeneralEmpoyeeSalaryDto {
@IsString() @IsString()
multiSigWallet: string; multiSigWallet: string;
} }
export class DeployContractResponseDto {
@IsString()
address: string;
}

View File

@ -20,7 +20,7 @@ export class SalariesService extends BaseContractService {
) { ) {
super(providerService); super(providerService);
} }
async deploy(dto: SalariesDeployDto): Promise<any> { async deploy(dto: SalariesDeployDto) {
const { abi, bytecode } = await hre.artifacts.readArtifact('Salaries'); const { abi, bytecode } = await hre.artifacts.readArtifact('Salaries');
const signer = await this.providerService.getSigner(); const signer = await this.providerService.getSigner();
@ -71,7 +71,7 @@ export class SalariesService extends BaseContractService {
const contract = new ethers.Contract(contractAddress, abi, signer); const contract = new ethers.Contract(contractAddress, abi, signer);
const answer: BigInt = await contract.getSalary(employeeAddress); const answer: BigInt = await contract.getUsdtSalary(employeeAddress);
return { return {
salaryInUsd: answer.toString(), salaryInUsd: answer.toString(),
}; };
@ -79,7 +79,6 @@ export class SalariesService extends BaseContractService {
async createPayout(dto: CreatePayoutDto) { async createPayout(dto: CreatePayoutDto) {
const { employeeAddress, contractAddress, multiSigWallet } = dto; const { employeeAddress, contractAddress, multiSigWallet } = dto;
console.log('=>(salaries.service.ts:82) employeeAddress', employeeAddress);
const ISubmitMultiSig = new ethers.Interface([ const ISubmitMultiSig = new ethers.Interface([
'function payoutInETH(address employee)', 'function payoutInETH(address employee)',
]); ]);

View File

@ -0,0 +1,69 @@
import { PriceFeedMock, Salaries } from '../../../typechain';
const { ethers } = require('hardhat');
const { expect } = require('chai');
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
describe('Salaries', function () {
let salaries: Salaries;
let owner: SignerWithAddress;
let multisigWallet: SignerWithAddress;
let addr1: SignerWithAddress;
let priceFeedMock: PriceFeedMock;
beforeEach(async function () {
[owner, multisigWallet, addr1] = await ethers.getSigners();
const PriceFeedMockFactory =
await ethers.getContractFactory('PriceFeedMock');
priceFeedMock = await PriceFeedMockFactory.deploy();
await priceFeedMock.getDeployedCode();
// Deploy the Salaries contract
const SalariesFactory = await ethers.getContractFactory('Salaries');
salaries = (await SalariesFactory.deploy(
multisigWallet.address,
await priceFeedMock.getAddress(),
)) as Salaries;
await salaries.getDeployedCode();
});
it('Should set and get salary correctly', async function () {
await salaries.connect(multisigWallet).setSalary(addr1.address, 1000);
expect(await salaries.getUsdtSalary(addr1.address)).to.equal(1000);
});
it('Should payout in ETH correctly', async function () {
// Set the salary in USDT
await salaries.connect(multisigWallet).setSalary(addr1.address, 100);
expect(await salaries.getUsdtSalary(addr1.address)).to.equal(100);
// Fund the contract with ETH
await owner.sendTransaction({
to: await salaries.getAddress(),
value: ethers.parseEther('1'), // 1 ETH
});
await expect(() =>
salaries.connect(multisigWallet).payoutInETH(addr1.address),
).to.changeEtherBalances(
[salaries, addr1],
['-32393909944930353', '32393909944930353'],
);
// Check events
expect(salaries.connect(multisigWallet).payoutInETH(addr1.address));
});
});
describe('PriceFeedMock', function () {
it('Should return the mocked price', async function () {
const PriceFeedMockFactory =
await ethers.getContractFactory('PriceFeedMock');
const priceFeedMock = await PriceFeedMockFactory.deploy();
await priceFeedMock.getDeployedCode();
expect((await priceFeedMock.latestRoundData())[1].toString()).to.equal(
'3087',
);
});
});

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class ProviderService { export class ProviderService {
public provider: ethers.JsonRpcProvider; public provider: ethers.JsonRpcProvider;
@ -29,10 +30,9 @@ export class ProviderService {
if (!this.provider) { if (!this.provider) {
await this.getProvider(); await this.getProvider();
} }
const signer = new ethers.Wallet( return new ethers.Wallet(
this.configService.getOrThrow('POLYGON_PK'), this.configService.getOrThrow('POLYGON_PK'),
this.provider, this.provider,
); );
return signer;
} }
} }