3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.idea
|
||||||
|
.env
|
||||||
|
.vscode
|
14
README.md
@ -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
@ -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
@ -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
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
73
chain-api/README.md
Normal 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).
|
27
chain-api/hardhat.config.ts
Normal 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
@ -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
82
chain-api/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
22
chain-api/src/app.controller.spec.ts
Normal 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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
12
chain-api/src/app.controller.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
22
chain-api/src/app.module.ts
Normal 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 {}
|
8
chain-api/src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
8
chain-api/src/base/base-contract.service.ts
Normal 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>;
|
||||||
|
}
|
10
chain-api/src/base/base.module.ts
Normal 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 {}
|
10
chain-api/src/base/provider/provider.module.ts
Normal 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 {}
|
34
chain-api/src/base/provider/provider.service.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
13
chain-api/src/config/chainlink.config.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
21
chain-api/src/contract-interact/agreement/agreement.dto.ts
Normal 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;
|
||||||
|
}
|
@ -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 {}
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
11
chain-api/src/contract-interact/contract-interact.module.ts
Normal 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 {}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
45
chain-api/src/contract-interact/license/license.dto.ts
Normal 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;
|
||||||
|
}
|
13
chain-api/src/contract-interact/license/license.module.ts
Normal 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 {}
|
178
chain-api/src/contract-interact/license/license.service.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
53
chain-api/src/contract-interact/multi-sig.dto.ts
Normal 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;
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
11
chain-api/src/contract-interact/multi-sig/multi-sig.dto.ts
Normal 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;
|
||||||
|
}
|
@ -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 {}
|
196
chain-api/src/contract-interact/multi-sig/multi-sig.service.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
42
chain-api/src/contract-interact/salaries/salaries.dto.ts
Normal 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;
|
||||||
|
}
|
13
chain-api/src/contract-interact/salaries/salaries.module.ts
Normal 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 {}
|
113
chain-api/src/contract-interact/salaries/salaries.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
12
chain-api/src/ethereum/ethereum.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
11
chain-api/src/ethereum/ethereum.module.ts
Normal 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 {}
|
10
chain-api/src/ethereum/ethereum.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
60
chain-api/src/ethers-custom/dto/ethers.dto.ts
Normal 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;
|
13
chain-api/src/ethers-custom/ethers.helpers.ts
Normal 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);
|
||||||
|
};
|
32
chain-api/src/filters/http.filter.ts
Normal 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
@ -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
|
93
chain-api/src/hardhat/contracts/Agreement.sol
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
76
chain-api/src/hardhat/contracts/LicensePayout.sol
Normal 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 {}
|
||||||
|
}
|
230
chain-api/src/hardhat/contracts/MultiSigWallet.sol
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
100
chain-api/src/hardhat/contracts/Payroll.sol
Normal 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 {}
|
||||||
|
}
|
18
chain-api/src/hardhat/contracts/PriceFeedMock.sol
Normal 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
|
||||||
|
}
|
||||||
|
}
|
148
chain-api/src/hardhat/contracts/StreamingRightsManagement.sol
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
57
chain-api/src/hardhat/test/License.test.ts
Normal 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
|
||||||
|
});
|
71
chain-api/src/hardhat/test/Payroll.test.ts
Normal 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
@ -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();
|
24
chain-api/test/app.e2e-spec.ts
Normal 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!');
|
||||||
|
});
|
||||||
|
});
|
9
chain-api/test/jest-e2e.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
4
chain-api/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
23
chain-api/tsconfig.json
Normal 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"]
|
||||||
|
}
|
1603
excalidraw/3step-license-payout.excalidraw
Normal file
BIN
excalidraw/3step-license-payout.png
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
excalidraw/arch.png
Normal file
After Width: | Height: | Size: 238 KiB |
1772
excalidraw/data-request-license.excalidraw
Normal file
BIN
excalidraw/data-request-license.png
Normal file
After Width: | Height: | Size: 130 KiB |
20877
excalidraw/front.excalidraw
Normal file
1038
excalidraw/license-deploy.excalidraw
Normal file
BIN
excalidraw/license-deploy.png
Normal file
After Width: | Height: | Size: 113 KiB |
2088
excalidraw/license-payout-2of3steps.excalidraw
Normal file
BIN
excalidraw/license-payout-2of3steps.png
Normal file
After Width: | Height: | Size: 138 KiB |
1077
excalidraw/license.excalidraw
Normal file
BIN
excalidraw/license.png
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
excalidraw/login-flow.png
Normal file
After Width: | Height: | Size: 36 KiB |
1004
excalidraw/multisig.excalidraw
Normal file
BIN
excalidraw/multisig.png
Normal file
After Width: | Height: | Size: 108 KiB |
1014
excalidraw/payroll-deploy.excalidraw
Normal file
BIN
excalidraw/payroll-deploy.png
Normal file
After Width: | Height: | Size: 42 KiB |
1250
excalidraw/payroll.excalidraw
Normal file
BIN
excalidraw/payroll.png
Normal file
After Width: | Height: | Size: 112 KiB |
1231
excalidraw/salaries.excalidraw
Normal file
BIN
excalidraw/salaries.png
Normal file
After Width: | Height: | Size: 282 KiB |
1033
excalidraw/set-salary.excalidraw
Normal file
BIN
excalidraw/set-salary.png
Normal file
After Width: | Height: | Size: 88 KiB |
58
whitepaper.md
Normal 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.
|
||||||
|
|