agreement update, contract tested and fixed, readme init

This commit is contained in:
emochka2007 2024-05-24 12:19:19 +03:00
parent 7809a91416
commit 0f995d4617
46 changed files with 11033 additions and 135 deletions

1
.gitignore vendored
View File

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

8
.idea/.gitignore vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/block-accounting.iml" filepath="$PROJECT_DIR$/.idea/block-accounting.iml" />
</modules>
</component>
</project>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,3 +0,0 @@
{
"solidity.compileUsingRemoteVersion": "v0.8.25+commit.b61c2a91"
}

View File

@ -1,11 +1,14 @@
![LOGIN FLOW](./login-flow.png "Login") ### CHAIN-API
[//]: # (![Example architecture]&#40;./arch.png "Arch"&#41;) - ### Multi-Sig Deploy
![License](./license.png "Arch") ![Alt Text](./excalidraw/multisig.png)
![Salaries](./salaries.png "Arch")
# Registration Flow - ### Payroll Deploy
![Alt Text](./excalidraw/payroll-deploy.png)
- On First Login - Owner inputs his SEED_KEY (mnemonic), creates an organization, we save its seed hash for future login and signing internal txs. - ### Payroll
- When inviting an employee to organization- we generate an invitation link, then after clicking on this link - the user is asked for seed, if he's already registered or able to generate a seed for new account. ![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)

View File

@ -4,6 +4,8 @@ import { AppService } from './app.service';
import { ContractInteractModule } from './contract-interact/contract-interact.module'; import { ContractInteractModule } from './contract-interact/contract-interact.module';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { EthereumModule } from './ethereum/ethereum.module';
import { AgreementModule } from './contract-interact/agreement/agreement.module';
@Module({ @Module({
imports: [ imports: [
@ -11,6 +13,8 @@ import { ConfigModule } from '@nestjs/config';
isGlobal: true, isGlobal: true,
}), }),
ContractInteractModule, ContractInteractModule,
EthereumModule,
AgreementModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

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

View File

@ -18,11 +18,7 @@ export class ProviderService {
if (this.provider) { if (this.provider) {
return this.provider; return this.provider;
} }
const polygonProvider = new ethers.JsonRpcProvider( this.provider = new ethers.JsonRpcProvider(this.nodeUrl, this.networkId);
this.nodeUrl,
this.networkId,
);
this.provider = polygonProvider;
return this.provider; return this.provider;
} }

View File

@ -7,6 +7,7 @@ export const CHAINLINK = {
}, },
JOB_IDS: { JOB_IDS: {
UINT: 'a8356f48569c434eaa4ac5fcb4db5cc0', UINT: 'a8356f48569c434eaa4ac5fcb4db5cc0',
BOOL: '43309009a154495cb2ed794233e6ff56',
}, },
}, },
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { IsArray, IsNumber, IsString } from 'class-validator'; import { IsArray, IsNumber, IsString, IsUrl } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class GetLicenseInfoDto { export class GetLicenseInfoDto {
@ApiProperty() @ApiProperty()
@ -24,6 +24,9 @@ export class RequestLicenseDto extends GetLicenseInfoDto {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
multiSigWallet: string; multiSigWallet: string;
@ApiProperty()
@IsUrl()
url: string;
} }
export class GetLicenseResponseDto extends GetLicenseInfoDto {} export class GetLicenseResponseDto extends GetLicenseInfoDto {}

View File

@ -10,23 +10,25 @@ import {
RequestLicenseDto, RequestLicenseDto,
SetPayoutContractDto, SetPayoutContractDto,
} from './license.dto'; } from './license.dto';
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
import { ProviderService } from '../../base/provider/provider.service';
import { CHAINLINK } from '../../config/chainlink.config'; import { CHAINLINK } from '../../config/chainlink.config';
import { ProviderService } from '../../base/provider/provider.service';
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
@Injectable() @Injectable()
export class LicenseService extends BaseContractService { export class LicenseService extends BaseContractService {
constructor( constructor(
private readonly multiSigService: MultiSigWalletService,
public readonly providerService: ProviderService, public readonly providerService: ProviderService,
public readonly multiSigService: MultiSigWalletService,
) { ) {
super(providerService); super(providerService);
} }
async request(dto: RequestLicenseDto) { async request(dto: RequestLicenseDto) {
const { multiSigWallet, contractAddress } = dto; const { multiSigWallet, contractAddress, url } = dto;
const ISubmitMultiSig = new ethers.Interface(['function request()']); const ISubmitMultiSig = new ethers.Interface([
const data = ISubmitMultiSig.encodeFunctionData('request'); 'function request(string memory url)',
]);
const data = ISubmitMultiSig.encodeFunctionData('request', [url]);
return await this.multiSigService.submitTransaction({ return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet, contractAddress: multiSigWallet,
@ -46,7 +48,6 @@ export class LicenseService extends BaseContractService {
const contract = new ethers.Contract(contractAddress, abi, signer); const contract = new ethers.Contract(contractAddress, abi, signer);
const answer: bigint = await contract.totalPayoutInUSD(); const answer: bigint = await contract.totalPayoutInUSD();
console.log('=>(license.service.ts:45) answer', answer);
return answer.toString(); return answer.toString();
} }

View File

@ -7,17 +7,17 @@ import {
SetSalaryDto, SetSalaryDto,
} from './salaries.dto'; } from './salaries.dto';
import * as hre from 'hardhat'; import * as hre from 'hardhat';
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
import { BaseContractService } from '../../base/base-contract.service'; import { BaseContractService } from '../../base/base-contract.service';
import { ProviderService } from '../../base/provider/provider.service';
import { DepositContractDto } from '../multi-sig.dto'; import { DepositContractDto } from '../multi-sig.dto';
import { CHAINLINK } from '../../config/chainlink.config'; import { CHAINLINK } from '../../config/chainlink.config';
import { ProviderService } from '../../base/provider/provider.service';
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
@Injectable() @Injectable()
export class SalariesService extends BaseContractService { export class SalariesService extends BaseContractService {
constructor( constructor(
private readonly multiSigWalletService: MultiSigWalletService,
public readonly providerService: ProviderService, public readonly providerService: ProviderService,
public readonly multiSigService: MultiSigWalletService,
) { ) {
super(providerService); super(providerService);
} }
@ -57,7 +57,7 @@ export class SalariesService extends BaseContractService {
salary, salary,
]); ]);
return await this.multiSigWalletService.submitTransaction({ return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet, contractAddress: multiSigWallet,
destination: contractAddress, destination: contractAddress,
value: '0', value: '0',
@ -87,7 +87,7 @@ export class SalariesService extends BaseContractService {
employeeAddress, employeeAddress,
]); ]);
return await this.multiSigWalletService.submitTransaction({ return await this.multiSigService.submitTransaction({
contractAddress: multiSigWallet, contractAddress: multiSigWallet,
destination: contractAddress, destination: contractAddress,
value: '0', value: '0',

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
const responseBody = { const responseBody = {
statusCode: httpStatus, statusCode: httpStatus,
error: exception?.info?.error.message || exception.toString(), error: exception?.info?.error?.message || exception.toString(),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}; };

View File

@ -4,30 +4,20 @@ pragma solidity ^0.8.17;
import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
/** contract Agreement is ChainlinkClient, ConfirmedOwner {
* Request testnet LINK and ETH here: https://faucets.chain.link/
* Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
*/
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
*/
contract LinkWellBoolConsumerContractExample is ChainlinkClient, ConfirmedOwner {
using Chainlink for Chainlink.Request; using Chainlink for Chainlink.Request;
address private oracleAddress; address private oracleAddress;
bytes32 private jobId; bytes32 private jobId;
uint256 private fee; uint256 private fee;
address public multisigWallet;
constructor( constructor(
address _chainLinkToken, address _chainLinkToken,
address _oracleAddress, address _oracleAddress,
string memory _jobId, string memory _jobId,
uint _fee, uint _fee,
address _multiSigAddress, address _multiSigAddress
address[] memory _owners,
uint[] memory _shares
) ConfirmedOwner(_multiSigAddress) { ) ConfirmedOwner(_multiSigAddress) {
_setChainlinkToken(_chainLinkToken); _setChainlinkToken(_chainLinkToken);
@ -41,32 +31,18 @@ contract LinkWellBoolConsumerContractExample is ChainlinkClient, ConfirmedOwner
multisigWallet = _multiSigAddress; multisigWallet = _multiSigAddress;
} }
constructor() ConfirmedOwner(msg.sender) {
_setChainlinkToken(0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904);
setOracleAddress(0xd36c6B1777c7f3Db1B3201bDD87081A9045B7b46);
setJobId("43309009a154495cb2ed794233e6ff56");
setFeeInHundredthsOfLink(0); // 0 LINK
}
// Send a request to the Chainlink oracle // Send a request to the Chainlink oracle
function request() public { function request(string memory url) public {
Chainlink.Request memory req = _buildOperatorRequest(jobId, this.fulfill.selector); Chainlink.Request memory req = _buildOperatorRequest(jobId, this.fulfill.selector);
// DEFINE THE REQUEST PARAMETERS (example) req._add('method', 'GET');
req._add('method', 'POST'); req._add('url', url);
req._add('url', 'https://httpbin.org/post'); req._add('headers', '["content-type", "application/json"]');
req._add('headers', '["accept", "application/json", "set-cookie", "sid=14A52"]'); req._add('body', '');
req._add('body', '{"data":[{"coin":"BTC","isActive":false},{"coin":"ETH","isActive":false},{"coin":"LINK","isActive":true}]}'); req._add('contact', '');
req._add('contact', ''); // PLEASE ENTER YOUR CONTACT INFO. this allows us to notify you in the event of any emergencies related to your request (ie, bugs, downtime, etc.). example values: 'derek_linkwellnodes.io' (Discord handle) OR 'derek@linkwellnodes.io' OR '+1-617-545-4721' req._add('path', '');
// The following curl command simulates the above request parameters:
// curl 'https://httpbin.org/post' --request 'POST' --header 'content-type: application/json' --header 'set-cookie: sid=14A52' --data '{"data":[{"coin":"BTC","isActive":false},{"coin":"ETH","isActive":false},{"coin":"LINK","isActive":true}]}'
// PROCESS THE RESULT (example)
req._add('path', 'json,data,2,isActive');
// Send the request to the Chainlink oracle
_sendOperatorRequest(req, fee); _sendOperatorRequest(req, fee);
} }
@ -75,9 +51,8 @@ contract LinkWellBoolConsumerContractExample is ChainlinkClient, ConfirmedOwner
// Receive the result from the Chainlink oracle // Receive the result from the Chainlink oracle
event RequestFulfilled(bytes32 indexed requestId); event RequestFulfilled(bytes32 indexed requestId);
function fulfill(bytes32 requestId, bool data) public recordChainlinkFulfillment(requestId) { function fulfill(bytes32 requestId, bool data) public recordChainlinkFulfillment(requestId) {
// Process the oracle response emit RequestFulfilled(requestId);
// emit RequestFulfilled(requestId); // (optional) emits this event in the on-chain transaction logs, allowing Web3 applications to listen for this transaction response = data;
response = data; // example value: true
} }
// Update oracle address // Update oracle address

View File

@ -55,10 +55,6 @@ contract StreamingRightsManagement is ChainlinkClient, ConfirmedOwner {
owners.push(_owners[i]); owners.push(_owners[i]);
} }
} }
//get share
//update share
//change payout address
//
modifier hasValidPayoutContract() { modifier hasValidPayoutContract() {
require(address(payoutContract) != address(0), "payoutContract not initialized"); require(address(payoutContract) != address(0), "payoutContract not initialized");
_; _;
@ -73,26 +69,20 @@ contract StreamingRightsManagement is ChainlinkClient, ConfirmedOwner {
payoutContract = Payroll(_payoutAddress); payoutContract = Payroll(_payoutAddress);
} }
// Send a request to the Chainlink oracle // Send a request to the Chainlink oracle
function request() external onlyOwner{ function request(string memory url) external onlyOwner{
Chainlink.Request memory req = _buildOperatorRequest(jobId, this.fulfill.selector); Chainlink.Request memory req = _buildOperatorRequest(jobId, this.fulfill.selector);
// DEFINE THE REQUEST PARAMETERS (example)
req._add('method', 'GET'); req._add('method', 'GET');
req._add('url', 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=USD,EUR'); req._add('url', url);
req._add('headers', '["content-type", "application/json", "set-cookie", "sid=14A52"]');
req._add('body', '');
req._add('contact', ''); // PLEASE ENTER YOUR CONTACT INFO. this allows us to notify you in the event of any emergencies related to your request (ie, bugs, downtime, etc.). example values: 'derek_linkwellnodes.io' (Discord handle) OR 'derek@linkwellnodes.io' OR '+1-617-545-4721'
// The following curl command simulates the above request parameters: //if returns just int - then empty path
// curl 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=USD,EUR' --request 'GET' --header 'content-type: application/json' --header 'set-cookie: sid=14A52' req._add('path', '');
// PROCESS THE RESULT (example)
req._add('path', 'ETH,USD');
req._addInt('multiplier', 10 ** 18); req._addInt('multiplier', 10 ** 18);
// Send the request to the Chainlink oracle req._add('headers', '["content-type", "application/json"]');
req._add('body', '');
req._add('contact', '');
_sendOperatorRequest(req, fee); _sendOperatorRequest(req, fee);
} }
@ -102,9 +92,7 @@ contract StreamingRightsManagement is ChainlinkClient, ConfirmedOwner {
event RequestFulfilled(bytes32 indexed requestId); event RequestFulfilled(bytes32 indexed requestId);
function fulfill(bytes32 requestId, uint256 data) public recordChainlinkFulfillment(requestId) { function fulfill(bytes32 requestId, uint256 data) public recordChainlinkFulfillment(requestId) {
// Process the oracle response totalPayoutInUSD = data / 1e18;
// emit RequestFulfilled(requestId); // (optional) emits this event in the on-chain transaction logs, allowing Web3 applications to listen for this transaction
totalPayoutInUSD = data / 1e18 / 100; // example value: 1875870000000000000000 (1875.87 before "multiplier" is applied)
} }
function payout() external onlyOwner hasValidPayoutContract{ function payout() external onlyOwner hasValidPayoutContract{

View File

@ -1,4 +0,0 @@
# links
chainlink-feeds amoy
https://docs.chain.link/data-feeds/price-feeds/addresses?network=polygon&page=1

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 238 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because it is too large Load Diff

BIN
excalidraw/multisig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because it is too large Load Diff

BIN
excalidraw/payroll.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 282 KiB

After

Width:  |  Height:  |  Size: 282 KiB

File diff suppressed because it is too large Load Diff

BIN
excalidraw/set-salary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB