Merge pull request #3 from emo2007/polygon

Polygon
This commit is contained in:
Nick 2024-05-24 12:24:22 +03:00 committed by GitHub
commit d257227497
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 53202 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea
.env
.vscode

View File

@ -0,0 +1,14 @@
### CHAIN-API
- ### Multi-Sig Deploy
![Alt Text](./excalidraw/multisig.png)
- ### Payroll Deploy
![Alt Text](./excalidraw/payroll-deploy.png)
- ### Payroll
![set-salary.png](excalidraw%2Fset-salary.png)
![payroll.png](excalidraw%2Fpayroll.png)
- ### License
![license-deploy.png](excalidraw%2Flicense-deploy.png)
![data-request-license.png](excalidraw%2Fdata-request-license.png)
![license-payout-2of3steps.png](excalidraw%2Flicense-payout-2of3steps.png)![3step-license-payout.png](excalidraw%2F3step-license-payout.png)

25
chain-api/.eslintrc.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

19
chain-api/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
node_modules
.env
# Hardhat files
/cache
/artifacts
# TypeChain files
/typechain
/typechain-types
# solidity-coverage files
/coverage
/coverage.json
# Hardhat Ignition default folder for deployments against a local node
ignition/deployments/chain-31337
dist

4
chain-api/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

73
chain-api/README.md Normal file
View File

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

View File

@ -0,0 +1,27 @@
require('@nomicfoundation/hardhat-toolbox');
require('@nomicfoundation/hardhat-ethers');
const dotenv = require('dotenv');
dotenv.config();
const config = {
solidity: '0.8.24',
networks: {
amoy: {
url: `https://polygon-amoy.g.alchemy.com/v2/pEtFFy_Qr_NrM1vMnlzSXmYXkozVNzLy`,
accounts: [process.env.POLYGON_PK || ''],
},
},
typechain: {
outDir: 'typechain',
target: 'ethers-v6',
},
paths: {
sources: './src/hardhat/contracts',
tests: './src/hardhat/test',
ignition: './src/hardhat/ignition',
cache: './src/hardhat/cache',
artifacts: './src/hardhat/artifacts',
},
};
module.exports = config;

8
chain-api/nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

16720
chain-api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

82
chain-api/package.json Normal file
View File

@ -0,0 +1,82 @@
{
"name": "accounter-api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"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",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"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",
"@nestjs/mapped-types": "*",
"@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"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.5",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.12.11",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"ethers": "^6.12.1",
"hardhat": "^2.22.3",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ContractInteractModule } from './contract-interact/contract-interact.module';
import { ConfigModule } from '@nestjs/config';
import { EthereumModule } from './ethereum/ethereum.module';
import { AgreementModule } from './contract-interact/agreement/agreement.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
ContractInteractModule,
EthereumModule,
AgreementModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

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

View File

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

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,34 @@
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;
}
this.provider = new ethers.JsonRpcProvider(this.nodeUrl, this.networkId);
return this.provider;
}
async getSigner() {
if (!this.provider) {
await this.getProvider();
}
return new ethers.Wallet(
this.configService.getOrThrow('POLYGON_PK'),
this.provider,
);
}
}

View File

@ -0,0 +1,13 @@
export const CHAINLINK = {
AMOY: {
CHAINLINK_TOKEN: '0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904',
ORACLE_ADDRESS: '0xd36c6B1777c7f3Db1B3201bDD87081A9045B7b46',
AGGREGATOR_ADDRESS: {
USDT_ETH: '0xF0d50568e3A7e8259E16663972b11910F89BD8e7',
},
JOB_IDS: {
UINT: 'a8356f48569c434eaa4ac5fcb4db5cc0',
BOOL: '43309009a154495cb2ed794233e6ff56',
},
},
};

View File

@ -0,0 +1,29 @@
import { Body, Controller, Post, Get, Param } from '@nestjs/common';
import { AgreementService } from './agreement.service';
import {
DeployAgreementDto,
GetAgreementInfoDto,
RequestAgreementDto,
} from './agreement.dto';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('Agreement')
@Controller('agreements')
export class AgreementController {
constructor(private readonly agreementService: AgreementService) {}
@Post('deploy')
async deployAgreement(@Body() deployDto: DeployAgreementDto) {
return await this.agreementService.deploy(deployDto);
}
@Get(':contractAddress')
async getAgreementResponse(
@Param('contractAddress') contractAddress: string,
) {
return await this.agreementService.getResponse({ contractAddress });
}
@Post('request')
async requestAgreement(@Body() requestDto: RequestAgreementDto) {
return await this.agreementService.request(requestDto);
}
}

View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsUrl } from 'class-validator';
export class DeployAgreementDto {
@ApiProperty()
@IsString()
multiSigWallet: string;
}
export class GetAgreementInfoDto {
@ApiProperty()
@IsString()
contractAddress: string;
}
export class RequestAgreementDto extends GetAgreementInfoDto {
@ApiProperty()
@IsString()
multiSigWallet: string;
@ApiProperty()
@IsUrl()
url: string;
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { AgreementController } from './agreement.controller';
import { AgreementService } from './agreement.service';
import { BaseModule } from '../../base/base.module';
import { MultiSigModule } from '../multi-sig/multi-sig.module';
@Module({
imports: [BaseModule, MultiSigModule],
controllers: [AgreementController],
providers: [AgreementService],
exports: [],
})
export class AgreementModule {}

View File

@ -0,0 +1,75 @@
import { Injectable } from '@nestjs/common';
import { BaseContractService } from '../../base/base-contract.service';
import * as hre from 'hardhat';
import { ethers } from 'ethers';
import { CHAINLINK } from '../../config/chainlink.config';
import {
DeployAgreementDto,
GetAgreementInfoDto,
RequestAgreementDto,
} from './agreement.dto';
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
import { ProviderService } from '../../base/provider/provider.service';
@Injectable()
export class AgreementService extends BaseContractService {
constructor(
public readonly providerService: ProviderService,
public readonly multiSigService: MultiSigWalletService,
) {
super(providerService);
}
async deploy(dto: DeployAgreementDto): Promise<any> {
const { multiSigWallet } = dto;
const { bytecode } = await hre.artifacts.readArtifact('Agreement');
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
const abiEncodedConstructorArguments = abiCoder.encode(
['address', 'address', 'string', 'uint', 'address'],
[
CHAINLINK.AMOY.CHAINLINK_TOKEN,
CHAINLINK.AMOY.ORACLE_ADDRESS,
CHAINLINK.AMOY.JOB_IDS.BOOL,
0,
multiSigWallet,
],
);
const fullBytecode = bytecode + abiEncodedConstructorArguments.substring(2);
const submitData = await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet,
destination: null,
value: '0',
data: fullBytecode,
});
delete submitData.data;
return submitData;
}
async getResponse(dto: GetAgreementInfoDto) {
const { contractAddress } = dto;
const { abi } = await hre.artifacts.readArtifact('Agreement');
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
const answer = await contract.response();
return answer.toString();
}
async request(dto: RequestAgreementDto) {
const { multiSigWallet, contractAddress, url } = dto;
const ISubmitMultiSig = new ethers.Interface([
'function request(string memory url)',
]);
const data = ISubmitMultiSig.encodeFunctionData('request', [url]);
return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet,
destination: contractAddress,
value: '0',
data,
});
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { SalariesModule } from './salaries/salaries.module';
import { MultiSigModule } from './multi-sig/multi-sig.module';
import { LicenseModule } from './license/license.module';
@Module({
imports: [SalariesModule, MultiSigModule, LicenseModule],
controllers: [],
providers: [],
})
export class ContractInteractModule {}

View File

@ -0,0 +1,55 @@
import { Body, Controller, Get, Post } from '@nestjs/common';
import { LicenseService } from './license.service';
import { ApiTags } from '@nestjs/swagger';
import {
DeployLicenseDto,
GetLicenseInfoDto,
GetShareLicense,
LicensePayoutDto,
RequestLicenseDto,
SetPayoutContractDto,
} from './license.dto';
@ApiTags('license')
@Controller('license')
export class LicenseController {
constructor(private readonly licenseService: LicenseService) {}
@Post('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: GetLicenseInfoDto) {
return this.licenseService.getTotalPayoutInUSD(dto);
}
@Get('shares')
async getShares(@Body() dto: GetShareLicense) {
return this.licenseService.getShares(dto);
}
@Get('owners')
async getOwners(@Body() dto: GetLicenseInfoDto) {
return this.licenseService.getOwners(dto);
}
@Get('payout-contract')
async getPayoutContract(@Body() dto: GetLicenseInfoDto) {
return this.licenseService.getPayoutContract(dto);
}
@Post('payout')
async payout(@Body() dto: LicensePayoutDto) {
return this.licenseService.payout(dto);
}
@Post('set-payout-contract')
async setPayoutContract(@Body() dto: SetPayoutContractDto) {
return this.licenseService.setPayoutContract(dto);
}
}

View File

@ -0,0 +1,45 @@
import { IsArray, IsNumber, IsString, IsUrl } 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[];
}
export class RequestLicenseDto extends GetLicenseInfoDto {
@ApiProperty()
@IsString()
multiSigWallet: string;
@ApiProperty()
@IsUrl()
url: string;
}
export class GetLicenseResponseDto extends GetLicenseInfoDto {}
export class GetShareLicense extends GetLicenseInfoDto {
@IsString()
@ApiProperty()
ownerAddress: string;
}
export class LicensePayoutDto extends RequestLicenseDto {}
export class SetPayoutContractDto extends RequestLicenseDto {
@IsString()
@ApiProperty()
payoutContract: string;
}

View File

@ -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 {}

View File

@ -0,0 +1,178 @@
import { Injectable } from '@nestjs/common';
import * as hre from 'hardhat';
import { ethers } from 'ethers';
import { BaseContractService } from '../../base/base-contract.service';
import {
DeployLicenseDto,
GetLicenseInfoDto,
GetShareLicense,
LicensePayoutDto,
RequestLicenseDto,
SetPayoutContractDto,
} from './license.dto';
import { CHAINLINK } from '../../config/chainlink.config';
import { ProviderService } from '../../base/provider/provider.service';
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
@Injectable()
export class LicenseService extends BaseContractService {
constructor(
public readonly providerService: ProviderService,
public readonly multiSigService: MultiSigWalletService,
) {
super(providerService);
}
async request(dto: RequestLicenseDto) {
const { multiSigWallet, contractAddress, url } = dto;
const ISubmitMultiSig = new ethers.Interface([
'function request(string memory url)',
]);
const data = ISubmitMultiSig.encodeFunctionData('request', [url]);
return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet,
destination: contractAddress,
value: '0',
data,
});
}
async getTotalPayoutInUSD(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: bigint = await contract.totalPayoutInUSD();
return answer.toString();
}
async deploy(dto: DeployLicenseDto) {
const { multiSigWallet, shares, owners } = dto;
const { bytecode } = await hre.artifacts.readArtifact(
'StreamingRightsManagement',
);
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
const abiEncodedConstructorArguments = abiCoder.encode(
[
'address',
'address',
'string',
'uint',
'address',
'address[]',
'uint[]',
],
[
CHAINLINK.AMOY.CHAINLINK_TOKEN,
CHAINLINK.AMOY.ORACLE_ADDRESS,
CHAINLINK.AMOY.JOB_IDS.UINT,
0,
multiSigWallet,
owners,
shares,
],
);
const fullBytecode = bytecode + abiEncodedConstructorArguments.substring(2);
const submitData = await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet,
destination: null,
value: '0',
data: fullBytecode,
});
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 owners: string[] = [];
for (let i = 0; i < 10; i++) {
try {
const owner = await contract.owners(i);
owners.push(owner);
} catch (e) {
// this.logger.error(e);
console.log('OWNERS LIMIT');
break;
}
}
return owners;
}
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);
console.log('=>(license.service.ts:135) answer', answer);
return answer;
}
async payout(dto: LicensePayoutDto) {
const { multiSigWallet, contractAddress } = dto;
const ISubmitMultiSig = new ethers.Interface(['function payout()']);
const data = ISubmitMultiSig.encodeFunctionData('payout');
return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet,
destination: contractAddress,
value: '0',
data,
});
}
async setPayoutContract(dto: SetPayoutContractDto) {
const { multiSigWallet, contractAddress, payoutContract } = dto;
const ISubmitMultiSig = new ethers.Interface([
'function setPayoutContract(address payable)',
]);
const data = ISubmitMultiSig.encodeFunctionData('setPayoutContract', [
payoutContract,
]);
return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet,
destination: contractAddress,
value: '0',
data,
});
}
}

View File

@ -0,0 +1,53 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsNumber, 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;
}
export class ConfirmTransactionDto {
@IsString()
@ApiProperty()
contractAddress: string;
@ApiProperty()
@IsNumber()
index: number;
}
export class ExecuteTransactionDto extends ConfirmTransactionDto {
@IsOptional()
@IsBoolean()
@ApiProperty()
isDeploy: boolean;
}
export class RevokeConfirmationDto extends ConfirmTransactionDto {}
export class GetTransactionCount {}
export class GetTransactionDto extends ConfirmTransactionDto {}
export class DepositContractDto {
@IsString()
@ApiProperty()
contractAddress: string;
@IsString()
@ApiProperty()
value: string;
}
export class DeployMultiSigResponseDto {
@IsString()
address: string;
}

View File

@ -0,0 +1,75 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { MultiSigWalletDto } from './multi-sig.dto';
import { MultiSigWalletService } from './multi-sig.service';
import {
ConfirmTransactionDto,
DeployMultiSigResponseDto,
DepositContractDto,
ExecuteTransactionDto,
GetTransactionDto,
RevokeConfirmationDto,
SubmitTransactionDto,
} from '../multi-sig.dto';
@ApiTags('multi-sig')
@Controller('multi-sig')
export class MultiSigInteractController {
constructor(private readonly multiSigWalletService: MultiSigWalletService) {}
@ApiOkResponse({
type: DeployMultiSigResponseDto,
})
@Post('deploy')
async deploy(
@Body() dto: MultiSigWalletDto,
): Promise<DeployMultiSigResponseDto> {
const addr = await this.multiSigWalletService.deploy(dto);
return {
address: addr,
};
}
@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);
}
@ApiOkResponse()
@Post('confirm-transaction')
async confirmTransaction(@Body() dto: ConfirmTransactionDto) {
return this.multiSigWalletService.confirmTransaction(dto);
}
@ApiOkResponse()
@Post('execute-transaction')
async executeTransaction(@Body() dto: ExecuteTransactionDto) {
return this.multiSigWalletService.executeTransaction(dto);
}
@ApiOkResponse()
@Post('revoke-confirmation')
async revokeConfirmation(@Body() dto: RevokeConfirmationDto) {
return this.multiSigWalletService.revokeConfirmation(dto);
}
@Get('transaction-count/:contractAddress')
async getTransactionCount(@Param('contractAddress') contractAddress: string) {
return this.multiSigWalletService.getTransactionCount(contractAddress);
}
@Get('transaction')
async getTransaction(@Body() dto: GetTransactionDto) {
return this.multiSigWalletService.getTransaction(dto);
}
@Post('deposit')
async deposit(@Body() dto: DepositContractDto) {
return this.multiSigWalletService.deposit(dto);
}
}

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,13 @@
import { Module } from '@nestjs/common';
import { MultiSigWalletService } from './multi-sig.service';
import { MultiSigInteractController } from './multi-sig-interact.controller';
import { BaseModule } from '../../base/base.module';
@Module({
imports: [BaseModule],
controllers: [MultiSigInteractController],
providers: [MultiSigWalletService],
exports: [MultiSigWalletService],
})
export class MultiSigModule {}

View File

@ -0,0 +1,196 @@
import { ethers, parseEther, TransactionReceipt } from 'ethers';
import * as hre from 'hardhat';
import { MultiSigWalletDto } from './multi-sig.dto';
import {
ConfirmTransactionDto,
DepositContractDto,
ExecuteTransactionDto,
GetTransactionDto,
RevokeConfirmationDto,
SubmitTransactionDto,
} 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) {
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();
return myContract.getAddress();
}
async getOwners(address: string) {
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(address, abi, signer);
return await contract.getOwners();
}
async submitTransaction(dto: SubmitTransactionDto) {
const { destination, value, data, contractAddress } = dto;
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
const tx = await contract.submitTransaction(
destination || '0x0000000000000000000000000000000000000000',
value,
data,
);
const txResponse: TransactionReceipt = await tx.wait();
const eventParse = parseLogs(txResponse, contract, 'SubmitTransaction');
return {
txHash: txResponse.hash,
sender: eventParse.args[0].toString(),
txIndex: eventParse.args[1].toString(),
to: eventParse.args[2].toString(),
value: eventParse.args[3].toString(),
data: eventParse.args[4].toString(),
};
}
async confirmTransaction(dto: ConfirmTransactionDto) {
const { contractAddress, index } = dto;
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
const tx = await contract.confirmTransaction(index);
const txResponse: TransactionReceipt = await tx.wait();
const eventParse = parseLogs(txResponse, contract, 'ConfirmTransaction');
return {
txHash: txResponse.hash,
sender: eventParse.args[0].toString(),
txIndex: eventParse.args[1].toString(),
};
}
async executeTransaction(dto: ExecuteTransactionDto) {
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 input = dto.index + new Date().getTime().toString();
const hashed = ethers.keccak256(ethers.toUtf8Bytes(input));
const salt = BigInt(hashed.substring(0, 10));
if (isDeploy) {
const tx = await contract.executeDeployTransaction(index, salt);
const txResponse: TransactionReceipt = await tx.wait();
const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction');
const deployedParse = parseLogs(txResponse, contract, 'ContractDeployed');
return {
txHash: txResponse.hash,
sender: eventParse.args[0].toString(),
txIndex: eventParse.args[1].toString(),
deployedAddress: deployedParse.args[0].toString(),
};
} else {
const tx = await contract.executeTransaction(index);
const txResponse: TransactionReceipt = await tx.wait();
const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction');
return {
txHash: txResponse.hash,
sender: eventParse.args[0].toString(),
txIndex: eventParse.args[1].toString(),
};
}
}
async calculateFutureAddress(contractAddress: string) {
const provider = await this.providerService.getProvider();
const nonce = await provider.getTransactionCount(contractAddress);
return getContractAddress({
from: contractAddress,
nonce: nonce,
});
}
async revokeConfirmation(dto: RevokeConfirmationDto) {
const { index, contractAddress } = dto;
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
const tx = await contract.revokeConfirmation(index);
return tx;
}
async getTransactionCount(contractAddress: string) {
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
const txCount = await contract.getTransactionCount();
return txCount;
}
async getTransaction(dto: GetTransactionDto) {
const { index, contractAddress } = dto;
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
const tx = await contract.getTransaction(index);
return tx;
}
async deposit(dto: DepositContractDto) {
const { contractAddress, value } = dto;
const convertValue = parseEther(value);
const signer = await this.providerService.getSigner();
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
const contract = new ethers.Contract(contractAddress, abi, signer);
const tx = await signer.sendTransaction({
to: contractAddress,
value: convertValue,
});
const txResponse: TransactionReceipt = await tx.wait();
const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction');
return {
txHash: txResponse.hash,
sender: eventParse.args[0].toString(),
value: eventParse.args[1].toString(),
contractBalance: eventParse.args[2].toString(),
};
}
}

View File

@ -0,0 +1,55 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { SalariesService } from './salaries.service';
import {
CreatePayoutDto,
DeployContractResponseDto,
GetEmployeeSalariesDto,
SalariesDeployDto,
SetSalaryDto,
} from './salaries.dto';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { DepositContractDto } from '../multi-sig.dto';
@ApiTags('salaries')
@Controller('salaries')
export class SalariesController {
constructor(private readonly salariesService: SalariesService) {}
@ApiOkResponse({
type: DeployContractResponseDto,
})
@Post('deploy')
async deploy(
@Body() dto: SalariesDeployDto,
): Promise<DeployContractResponseDto> {
const address = await this.salariesService.deploy(dto);
return {
address,
};
}
@Get('usdt-price/:contractAddress')
async getUsdtPrice(@Param('contractAddress') contractAddress: string) {
return this.salariesService.getLatestUSDTPrice(contractAddress);
}
@Post('set-salary')
async setSalary(@Body() dto: SetSalaryDto) {
return this.salariesService.setSalary(dto);
}
@Get('salary')
async getSalary(@Body() dto: GetEmployeeSalariesDto) {
return this.salariesService.getSalary(dto);
}
@Post('payout')
async createPayout(@Body() dto: CreatePayoutDto) {
return this.salariesService.createPayout(dto);
}
@Post('deposit')
async deposit(@Body() dto: DepositContractDto) {
return this.salariesService.deposit(dto);
}
}

View File

@ -0,0 +1,42 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
export class SalariesDeployDto {
@ApiProperty()
@IsString()
authorizedWallet: string;
}
export class SetSalaryDto {
@ApiProperty()
@IsString()
multiSigWallet: string;
@ApiProperty()
@IsString()
contractAddress: string;
@ApiProperty()
@IsString()
employeeAddress: string;
@ApiProperty()
@IsNumber()
salary: number;
}
export class GeneralEmpoyeeSalaryDto {
@ApiProperty()
@IsString()
contractAddress: string;
@ApiProperty()
@IsString()
employeeAddress: string;
}
export class GetEmployeeSalariesDto extends GeneralEmpoyeeSalaryDto {}
export class CreatePayoutDto extends GeneralEmpoyeeSalaryDto {
@IsString()
multiSigWallet: string;
}
export class DeployContractResponseDto {
@IsString()
address: string;
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { SalariesService } from './salaries.service';
import { SalariesController } from './salaries-interact.controller';
import { MultiSigModule } from '../multi-sig/multi-sig.module';
import { BaseModule } from '../../base/base.module';
@Module({
imports: [BaseModule, MultiSigModule],
controllers: [SalariesController],
providers: [SalariesService],
exports: [SalariesService],
})
export class SalariesModule {}

View File

@ -0,0 +1,113 @@
import { Injectable } from '@nestjs/common';
import { ethers, parseEther, TransactionReceipt } from 'ethers';
import {
CreatePayoutDto,
GetEmployeeSalariesDto,
SalariesDeployDto,
SetSalaryDto,
} from './salaries.dto';
import * as hre from 'hardhat';
import { BaseContractService } from '../../base/base-contract.service';
import { DepositContractDto } from '../multi-sig.dto';
import { CHAINLINK } from '../../config/chainlink.config';
import { ProviderService } from '../../base/provider/provider.service';
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
@Injectable()
export class SalariesService extends BaseContractService {
constructor(
public readonly providerService: ProviderService,
public readonly multiSigService: MultiSigWalletService,
) {
super(providerService);
}
async deploy(dto: SalariesDeployDto) {
const { abi, bytecode } = await hre.artifacts.readArtifact('Payroll');
const signer = await this.providerService.getSigner();
const salaryContract = new ethers.ContractFactory(abi, bytecode, signer);
const myContract = await salaryContract.deploy(
dto.authorizedWallet,
CHAINLINK.AMOY.AGGREGATOR_ADDRESS.USDT_ETH,
);
await myContract.waitForDeployment();
return await myContract.getAddress();
}
async getLatestUSDTPrice(contractAddress: string) {
const { abi } = await hre.artifacts.readArtifact('Payroll');
const signer = await this.providerService.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
const answer: string = await contract.getLatestUSDTPriceInETH();
return parseInt(answer) / 1e8;
}
async setSalary(dto: SetSalaryDto) {
const { employeeAddress, salary, contractAddress, multiSigWallet } = dto;
const ISubmitMultiSig = new ethers.Interface([
'function setSalary(address employee, uint salaryInUSDT)',
]);
const data = ISubmitMultiSig.encodeFunctionData('setSalary', [
employeeAddress,
salary,
]);
return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet,
destination: contractAddress,
value: '0',
data,
});
}
async getSalary(dto: GetEmployeeSalariesDto) {
const { employeeAddress, contractAddress } = dto;
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);
return {
salaryInUsd: answer.toString(),
};
}
async createPayout(dto: CreatePayoutDto) {
const { employeeAddress, contractAddress, multiSigWallet } = dto;
const ISubmitMultiSig = new ethers.Interface([
'function payoutInETH(address employee)',
]);
const data = ISubmitMultiSig.encodeFunctionData('payoutInETH', [
employeeAddress,
]);
return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet,
destination: contractAddress,
value: '0',
data,
});
}
async deposit(dto: DepositContractDto) {
const { contractAddress, value } = dto;
const signer = await this.providerService.getSigner();
const convertValue = parseEther(value);
const tx = await signer.sendTransaction({
to: contractAddress,
value: convertValue,
});
const txResponse: TransactionReceipt = await tx.wait();
return txResponse;
}
}

View File

@ -0,0 +1,12 @@
import { Controller, Get, Param } from '@nestjs/common';
import { EthereumService } from './ethereum.service';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('Ethereum')
@Controller()
export class EthereumController {
constructor(private readonly ethereumService: EthereumService) {}
@Get('/address/:privateKey')
async getAddressFromPrivateKey(@Param('privateKey') privateKey: string) {
return this.ethereumService.getAddressFromPrivateKey(privateKey);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { EthereumController } from './ethereum.controller';
import { EthereumService } from './ethereum.service';
@Module({
imports: [],
controllers: [EthereumController],
providers: [EthereumService],
exports: [],
})
export class EthereumModule {}

View File

@ -0,0 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
@Injectable()
export class EthereumService {
async getAddressFromPrivateKey(privateKey: string) {
const wallet = new ethers.Wallet(privateKey);
return wallet.address;
}
}

View File

@ -0,0 +1,60 @@
import { ethers } from 'ethers';
// Define a TypeScript type for the EventLog based on the provided structure
export type TransactionLogs = {
provider: ethers.JsonRpcApiProvider;
transactionHash: string;
blockHash: string;
blockNumber: number;
removed: boolean | undefined;
address: string;
data: string;
topics: string[];
index: number;
transactionIndex: number;
interface: Interface;
fragment: EventFragment;
};
type Interface = {
fragments: Fragment[];
deploy: ConstructorFragment[];
fallback: any | null;
receive: boolean;
};
type Fragment = {};
type ConstructorFragment = {};
type EventFragment = {
type: string;
inputs: any[];
name: string;
anonymous: boolean;
};
type SubmitArgs = {
args: [
owner: string,
txIndex: bigint,
to: string,
value: bigint,
data: string,
];
};
type ConfirmArgs = {
args: [owner: string, txIndex: bigint];
};
type ExecuteArgs = {
args: [owner: string, txIndex: bigint];
};
type DepositArgs = {
args: [owner: string, value: bigint, address: string];
};
export type SubmitTransactionLogs = TransactionLogs & SubmitArgs;
export type ConfirmTransactionLogs = TransactionLogs & ConfirmArgs;
export type ExecuteTransactionLogs = TransactionLogs & ExecuteArgs;
export type DepositLogs = TransactionLogs & DepositArgs;

View File

@ -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);
};

View File

@ -0,0 +1,32 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { error } from 'console';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
// constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: any, host: ArgumentsHost): void {
console.log('🚀 ~ AllExceptionsFilter ~ exception:', exception);
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const responseBody = {
statusCode: httpStatus,
error: exception?.info?.error?.message || exception.toString(),
timestamp: new Date().toISOString(),
};
response.status(500).json(responseBody);
}
}

18
chain-api/src/hardhat/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
node_modules
.env
# Hardhat files
/cache
/artifacts
# TypeChain files
/typechain
/typechain-types
# solidity-coverage files
/coverage
/coverage.json
# Hardhat Ignition default folder for deployments against a local node
ignition/deployments/chain-31337
ignition/deployments/chain-80002

View File

@ -0,0 +1,93 @@
//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";
contract Agreement is ChainlinkClient, ConfirmedOwner {
using Chainlink for Chainlink.Request;
address private oracleAddress;
bytes32 private jobId;
uint256 private fee;
address public multisigWallet;
constructor(
address _chainLinkToken,
address _oracleAddress,
string memory _jobId,
uint _fee,
address _multiSigAddress
) ConfirmedOwner(_multiSigAddress) {
_setChainlinkToken(_chainLinkToken);
setOracleAddress(_oracleAddress);
setJobId(_jobId);
setFeeInHundredthsOfLink(_fee);
multisigWallet = _multiSigAddress;
}
// Send a request to the Chainlink oracle
function request(string memory url) public {
Chainlink.Request memory req = _buildOperatorRequest(jobId, this.fulfill.selector);
req._add('method', 'GET');
req._add('url', url);
req._add('headers', '["content-type", "application/json"]');
req._add('body', '');
req._add('contact', '');
req._add('path', '');
_sendOperatorRequest(req, fee);
}
bool public response;
// Receive the result from the Chainlink oracle
event RequestFulfilled(bytes32 indexed requestId);
function fulfill(bytes32 requestId, bool data) public recordChainlinkFulfillment(requestId) {
emit RequestFulfilled(requestId);
response = data;
}
// 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

@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import {AggregatorV3Interface} from '@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol';
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);
constructor(address _multisigWallet, address _priceFeedAddress) {
multisigWallet = _multisigWallet;
dataFeed = AggregatorV3Interface(_priceFeedAddress);
}
modifier onlyMultisig() {
require(msg.sender == multisigWallet, '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;
}
function oneTimePayout(address payable employee) external onlyMultisig {
}
function setSalary(
address employee,
uint salaryInUSDT
) external onlyMultisig {
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 onlyMultisig {
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 {}
}

View File

@ -0,0 +1,230 @@
// SPDX-License-Identifier: MIT
// 0x74f11486DB0FCAA2dCDE0aEB477e1F37fCAa510A
pragma solidity ^0.8.19;
// The wallet owners can
// submit a transaction
// approve and revoke approval of pending transactions
// anyone can execute a transaction after enough owners has approved it.
contract MultiSigWallet {
event Deposit(address indexed sender, uint amount, uint balance);
event SubmitTransaction(
address indexed owner,
uint indexed txIndex,
address indexed to,
uint value,
bytes data
);
event ConfirmTransaction(address indexed owner, uint indexed txIndex);
event RevokeConfirmation(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;
mapping(address => bool) public isOwner;
uint public numConfirmationsRequired;
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint numConfirmations;
}
mapping(uint => mapping(address => bool)) public isConfirmed;
Transaction[] public transactions;
modifier onlyOwner() {
require(isOwner[msg.sender], 'not owner');
_;
}
modifier txExists(uint _txIndex) {
require(_txIndex < transactions.length, 'tx does not exist');
_;
}
modifier notConfirmed(uint _txIndex) {
require(!isConfirmed[_txIndex][msg.sender], 'tx already confirmed');
_;
}
modifier notExecuted(uint _txIndex) {
require(!transactions[_txIndex].executed, 'tx already confirmed');
_;
}
constructor(address[] memory _owners, uint _numConfirmationsRequired) {
require(_owners.length > 0, 'owners required');
require(
_numConfirmationsRequired > 0 &&
_numConfirmationsRequired <= _owners.length,
'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');
isOwner[owner] = true;
owners.push(owner);
}
numConfirmationsRequired = _numConfirmationsRequired;
}
receive() external payable {
emit Deposit(msg.sender, msg.value, address(this).balance);
}
function submitTransaction(
address _to,
uint _value,
bytes memory _data
) public onlyOwner {
uint txIndex = transactions.length;
transactions.push(
Transaction({
to: _to,
value: _value,
data: _data,
executed: false,
numConfirmations: 0
})
);
emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);
}
function confirmTransaction(
uint _txIndex
)
public
onlyOwner
txExists(_txIndex)
notExecuted(_txIndex)
notConfirmed(_txIndex)
{
Transaction storage transaction = transactions[_txIndex];
transaction.numConfirmations += 1;
isConfirmed[_txIndex][msg.sender] = true;
emit ConfirmTransaction(msg.sender, _txIndex);
}
function executeTransaction(uint _txIndex)
public
onlyOwner
txExists(_txIndex)
notExecuted(_txIndex)
{
Transaction storage transaction = transactions[_txIndex];
require(
transaction.numConfirmations >= numConfirmationsRequired,
"cannot execute tx"
);
(bool success, bytes memory returnData) = transaction.to.call{value: transaction.value}(transaction.data);
if (success) {
transaction.executed = true;
emit ExecuteTransaction(msg.sender, _txIndex, transaction.to);
removeTransaction(_txIndex);
} else {
// Get the revert reason and emit it
if (returnData.length > 0) {
// The call reverted with a message
assembly {
let returndata_size := mload(returnData)
revert(add(32, returnData), returndata_size)
}
} else {
// The call reverted without a message
emit ExecuteTransactionFailed(msg.sender, _txIndex, "Transaction failed without a reason");
}
}
}
function executeDeployTransaction(uint _txIndex, uint256 _salt) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
Transaction storage transaction = transactions[_txIndex];
require(
transaction.numConfirmations >= numConfirmationsRequired,
"cannot execute tx"
);
address deployedAddress;
bytes memory bytecode = transaction.data;
// Assembly to deploy contract using CREATE2
assembly {
deployedAddress :=
create2(
callvalue(), // wei sent with current call
// Actual code starts after skipping the first 32 bytes
add(bytecode, 0x20),
mload(bytecode), // Load the size of code contained in the first 32 bytes
_salt // Salt from function arguments
)
if iszero(extcodesize(deployedAddress)) { revert(0, 0) }
}
require(deployedAddress != address(0), "Failed to deploy contract");
transaction.executed = true;
emit ExecuteTransaction(msg.sender, _txIndex, deployedAddress);
emit ContractDeployed(deployedAddress);
removeTransaction(_txIndex);
}
function revokeConfirmation(
uint _txIndex
) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
Transaction storage transaction = transactions[_txIndex];
require(isConfirmed[_txIndex][msg.sender], 'tx not confirmed');
transaction.numConfirmations -= 1;
isConfirmed[_txIndex][msg.sender] = false;
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;
}
function getTransactionCount() public view returns (uint) {
return transactions.length;
}
function getTransaction(
uint _txIndex
)
public
view
returns (
address to,
uint value,
bytes memory data,
bool executed,
uint numConfirmations
)
{
Transaction storage transaction = transactions[_txIndex];
return (
transaction.to,
transaction.value,
transaction.data,
transaction.executed,
transaction.numConfirmations
);
}
}

View File

@ -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 {}
}

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

@ -0,0 +1,148 @@
//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";
import "./Payroll.sol";
contract StreamingRightsManagement is ChainlinkClient, ConfirmedOwner {
using Chainlink for Chainlink.Request;
address public oracleAddress;
bytes32 private jobId;
uint256 private fee;
address public multisigWallet;
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
) ConfirmedOwner(_multiSigAddress) {
_setChainlinkToken(_chainLinkToken);
setOracleAddress(_oracleAddress);
setJobId(_jobId);
setFeeInHundredthsOfLink(_fee);
multisigWallet = _multiSigAddress;
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]);
}
}
modifier hasValidPayoutContract() {
require(address(payoutContract) != address(0), "payoutContract not initialized");
_;
}
function getShare(address owner) public view returns(uint){
return ownerShare[owner];
}
function setPayoutContract(address payable _payoutAddress) public onlyOwner {
require(_payoutAddress != address(0), "Invalid address: zero address not allowed");
payoutContract = Payroll(_payoutAddress);
}
// Send a request to the Chainlink oracle
function request(string memory url) external onlyOwner{
Chainlink.Request memory req = _buildOperatorRequest(jobId, this.fulfill.selector);
req._add('method', 'GET');
req._add('url', url);
//if returns just int - then empty path
req._add('path', '');
req._addInt('multiplier', 10 ** 18);
req._add('headers', '["content-type", "application/json"]');
req._add('body', '');
req._add('contact', '');
_sendOperatorRequest(req, fee);
}
uint256 public totalPayoutInUSD;
// Receive the result from the Chainlink oracle
event RequestFulfilled(bytes32 indexed requestId);
function fulfill(bytes32 requestId, uint256 data) public recordChainlinkFulfillment(requestId) {
totalPayoutInUSD = data / 1e18;
}
function payout() external onlyOwner hasValidPayoutContract{
// using arrays to reduce gas
uint[] memory shares = new uint[](owners.length);
for(uint i=0; i< owners.length; i++){
shares[i] = ownerShare[owners[i]] * totalPayoutInUSD / 100;
}
payoutContract.oneTimePayout(owners, shares);
}
// Update oracle address
function setOracleAddress(address _oracleAddress) public onlyOwner {
oracleAddress = _oracleAddress;
_setChainlinkOracle(_oracleAddress);
}
function getOracleAddress() public view 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

@ -0,0 +1,57 @@
import { StreamingRightsManagement } from '../../../typechain';
import { CHAINLINK } from '../../config/chainlink.config';
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe('StreamingRightsManagement', function () {
let streamingRightsManagement: StreamingRightsManagement,
payContract,
owner,
addr1,
addr2;
const shares = [25, 25, 50];
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const Payroll = await ethers.getContractFactory('Payroll');
payContract = await Payroll.deploy(owner.address, owner.address); // assume an oracle price feed address
const StreamingRightsManagement = await ethers.getContractFactory(
'StreamingRightsManagement',
);
streamingRightsManagement = await StreamingRightsManagement.deploy(
CHAINLINK.AMOY.CHAINLINK_TOKEN, // Chainlink Token address
CHAINLINK.AMOY.ORACLE_ADDRESS, // Oracle address
CHAINLINK.AMOY.JOB_IDS.UINT,
0,
owner.address,
[owner.address, addr1.address, addr2.address],
shares,
);
});
describe('Initialization', function () {
it('should set owners and shares correctly', async function () {
expect(await streamingRightsManagement.getShare(owner.address)).to.equal(
25,
);
expect(await streamingRightsManagement.getShare(addr1.address)).to.equal(
25,
);
expect(await streamingRightsManagement.getShare(addr2.address)).to.equal(
50,
);
});
});
describe('Payout Functionality', function () {
it('should successfully call payout', async function () {
await streamingRightsManagement.setPayoutContract(payContract.address);
await expect(streamingRightsManagement.payout()).to.not.be.reverted;
});
});
// More tests as needed for other functions
});

View File

@ -0,0 +1,71 @@
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: Payroll;
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('Payroll');
salaries = (await SalariesFactory.deploy(
multisigWallet.address,
await priceFeedMock.getAddress(),
)) as Payroll;
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',
);
});
});

22
chain-api/src/main.ts Normal file
View File

@ -0,0 +1,22 @@
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { AllExceptionsFilter } from './filters/http.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Chain')
.setDescription('The chain API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
console.log('Swagger avaliable at http://localhost:3000/api');
}
bootstrap();

View File

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

23
chain-api/tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"resolveJsonModule": true
},
"include": ["src/**/*", "src/**/*.json"]
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
excalidraw/arch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

20877
excalidraw/front.excalidraw Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

File diff suppressed because it is too large Load Diff

BIN
excalidraw/license.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
excalidraw/login-flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because it is too large Load Diff

BIN
excalidraw/multisig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because it is too large Load Diff

BIN
excalidraw/payroll.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because it is too large Load Diff

BIN
excalidraw/salaries.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

File diff suppressed because it is too large Load Diff

BIN
excalidraw/set-salary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

58
whitepaper.md Normal file
View File

@ -0,0 +1,58 @@
### Chainlink Multisig Transaction Contract
- Overview:
- This contract is used to manage transactions within an organization that requires multiple confirmations. It leverages a multisig mechanism to ensure that actions within the contract are approved by multiple parties before they are executed.
### Key Features:
- Owners Array: Initial deployment includes an array of addresses (owners[]), representing individuals linked to the organization. And the number of needed confirmations (uint <= owners.length);
- Confirmation Tracking: The contract tracks the number of confirmations required and received for each transaction.
- Transaction States: Each transaction can be in one of three states:
- Submitted: The initial state when a transaction is proposed.
- Confirmed: After the transaction receives the required number of confirmations.
- Executed: The final state after the transaction is executed.
- Deployment: Deployed using a main wallet which is considered the primary interface for administrative interactions.
### Workflow:
- Submission: A user proposes a transaction, which is recorded in the 'Submitted' state.
- Confirmation: As required confirmations are collected, the transaction transitions to the 'Confirmed' state.
- Execution: Once confirmed, the transaction can be executed by anyone, transitioning to the 'Executed' state.
### Chainlink Payroll Contract
### Overview:
This contract manages the payroll system, allowing salaries to be set in USD and then paid out in ETH based on the current exchange rate provided via Chainlink oracles.
### Key Features:
- Authorized Wallet: Only a specified wallet can execute payouts, set during contract deployment.
- Salary Management: Salaries are set in USD for each employee, needing confirmation through the multisig mechanism before execution.
- Currency Conversion: Utilizes Chainlink to fetch real-time ETH/USD prices to calculate the payout amount in ETH.
### Workflow:
- Set Salary: Propose salaries in USD which are confirmed and executed via multisig.
- Payout: On payroll day, the contract calculates the equivalent ETH for each employee's USD salary and transfers it. You have to deposit funds before calling this function.
### Chainlink Licensing Contract
### Overview:
- This contract manages licensing agreements by distributing funds based on predefined shares, after fetching and storing relevant data from Chainlink data feeds.
### Key Features:
- Data Request: Requests data, like total payout amounts, from a Chainlink data feed.
- Share and Owner Management: Stores shares and owner addresses, setting how distributions are handled.
- Multi-layer Confirmation: Deployment and critical functions require multisig confirmation. CREATE2 OPCODE is used on the multisig side.
### Workflow:
- Data Fetching: Requests data from Chainlink Custom Data Feed and stores it.
- Payout Setup: Before executing payouts, set the payroll contract address.
- Distribution: Distributes funds according to shares among the owners.
### Chainlink Agreement Contract
### Overview:
- Similar in functionality to the licensing contract, this contract fetches and evaluates boolean data points to determine outcomes of agreements.
### Key Features:
- Boolean Data Handling: Manages agreement validations based on true/false responses from Chainlink data feeds.
### Workflow:
- Data Request: Fetches a boolean value determining the agreement's state.
- Outcome Execution: Executes actions based on the true/false outcome, similar to licensing contract operations.
These contracts collectively form a robust framework using Chainlink oracles and Ethereum blockchain technology to ensure secure, transparent, and decentralized transaction management within an organization. Each contract's deployment and operations are safeguarded by multisig processes to maintain organizational control and integrity.
During development, we utilized the Polygon Amoy network because it simplifies handling multi calls and offers lower costs and faster transactions. This made Amoy an optimal choice for implementing these contracts.