before coordinators distribution change

This commit is contained in:
Alex Ant
2025-08-12 11:51:20 +05:00
parent e551b6c202
commit a89bc35c6f
60 changed files with 1813 additions and 853 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ export default antfu(
},
{
rules: {
'no-unused-vars': 'warn',
'no-unused-vars': 'off',
'unused-imports/no-unused-vars': 'off',
'no-console': 'off',
'unicorn/prefer-number-properties': 'off',
+1 -1
View File
@@ -330,7 +330,7 @@ export async function startInfra() {
await blockchain.transfer({
from: 'eosio',
to: config.provider,
quantity: `100.0000 ${config.token.symbol}`,
quantity: `1000.0000 ${config.token.symbol}`,
memo: '',
})
+6 -23
View File
@@ -6,20 +6,8 @@ import type { Account, Contract, Keys } from '../types'
import config, { GOVERN_SYMBOL, SYMBOL } from '../configs'
import Blockchain from '../blockchain'
import { sendPostToCoopbackWithSecret, sleep } from '../utils'
import { fakeDocument } from '../tests/shared/fakeDocument'
// const test_hash
// = '157192b276da23cc84ab078fc8755c051c5f0430bf4802e55718221e6b76c777'
// const test_sign
// = 'SIG_K1_KmKWPBC8dZGGDGhbKEoZEzPr3h5crRrR2uLdGRF5DJbeibY1MY1bZ9sPwHsgmPfiGFv9psfoCVsXFh9TekcLuvaeuxRKA8'
// const test_pkey = 'EOS5JhMfxbsNebajHcTEK8yC9uNN9Dit9hEmzE8ri8yMhhzxrLg3J'
// const test_meta = JSON.stringify({})
// const document = {
// hash: test_hash,
// signature: test_sign,
// public_key: test_pkey,
// meta: test_meta,
// }
export class CooperativeClass {
public blockchain: Blockchain
@@ -28,7 +16,7 @@ export class CooperativeClass {
this.blockchain = blockchain
}
async addUser(username: string, keys?: Keys) {
async addUser(username: string, keys?: Keys, referer?: string) {
const account = await this.blockchain.generateKeypair(
username,
keys,
@@ -38,7 +26,7 @@ export class CooperativeClass {
const data: RegistratorContract.Actions.AddUser.IAddUser = {
coopname: config.provider,
referer: '',
referer: referer || '',
username: account.username,
type: 'individual',
created_at: '2025-02-05T09:34:27',
@@ -92,12 +80,7 @@ export class CooperativeClass {
administrator: config.provider,
username: username!,
agreement_type: 'wallet',
document: { // отправляем произвольный документ с валидной подписью
hash: '157192B276DA23CC84AB078FC8755C051C5F0430BF4802E55718221E6B76C777',
public_key: 'PUB_K1_5JhMfxbsNebajHcTEK8yC9uNN9Dit9hEmzE8ri8yMhhzzEtUA4',
signature: 'SIG_K1_KmKWPBC8dZGGDGhbKEoZEzPr3h5crRrR2uLdGRF5DJbeibY1MY1bZ9sPwHsgmPfiGFv9psfoCVsXFh9TekcLuvaeuxRKA8',
meta: '{}',
},
document: fakeDocument,
})
console.log('создаём кошелёк')
@@ -114,7 +97,7 @@ export class CooperativeClass {
}
}
export async function addUser(username: string) {
export async function addUser(username: string, referer?: string) {
// инициализируем инстанс с ключами
const blockchain = new Blockchain(config.network, config.private_keys)
const cooperative = new CooperativeClass(blockchain)
@@ -124,5 +107,5 @@ export async function addUser(username: string) {
privateKey: process.env.EOSIO_PRV_KEY!,
// eslint-disable-next-line node/prefer-global/process
publicKey: process.env.EOSIO_PUB_KEY!,
})
}, referer)
}
+647 -268
View File
@@ -1,13 +1,27 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
// eslint-disable-next-line ts/consistent-type-imports
import { CapitalContract, SovietContract } from 'cooptypes'
import Blockchain from '../blockchain'
import config from '../configs'
import { getTotalRamUsage, globalRamStats } from '../utils/getTotalRamUsage'
import { addUser } from '../init/participant'
import { generateRandomUsername } from '../utils/randomUsername'
import { generateRandomSHA256 } from '../utils/randomHash'
import { sleep } from '../utils'
import { getCoopProgramWallet } from './wallet/walletUtils'
import { registerContributor } from './capital/registerContributor'
import { signAppendix } from './capital/signAppendix'
import { capitalProgramId, sourceProgramId } from './capital/consts'
import { makeCombinedChecksum256NameIndexKey } from './shared/combinedKeys'
import { commitToResult } from './capital/commitToResult'
import { processApprove } from './capital/processApprove'
import { refreshSegment } from './capital/refreshSegment'
import { signWalletAgreement } from './wallet/signWalletAgreement'
import { fakeDocument } from './shared/fakeDocument'
import { depositToWallet } from './wallet/depositToWallet'
import { signCapitalAgreement } from './capital/signCapitalAgreement'
import { addAuthor } from './capital/addAuthor'
import { getSegment } from './capital/getSegment'
import { investInProject } from './capital/investInProject'
// const CLI_PATH = 'src/index.ts'
@@ -19,25 +33,23 @@ let _project3: CapitalContract.Actions.CreateProject.ICreateProject
let tester1: string
let tester2: string
let tester3: string
let tester4: string
let tester5: string
let investor1: string
let investor2: string
let investor3: string
// let result1: CapitalContract.Actions.CreateResult.ICreateResult
// let result2: CapitalContract.Actions.CreateResult.ICreateResult
// let result3: CapitalContract.Actions.CreateResult.ICreateResult
const commits: string[] = []
const tester1CommitHours = 10
const tester2CommitHours = 10
const investAmount = '100000.0000 RUB'
const fakeDocument = {
hash: '157192B276DA23CC84AB078FC8755C051C5F0430BF4802E55718221E6B76C777',
public_key: 'PUB_K1_5JhMfxbsNebajHcTEK8yC9uNN9Dit9hEmzE8ri8yMhhzzEtUA4',
signature: 'SIG_K1_KmKWPBC8dZGGDGhbKEoZEzPr3h5crRrR2uLdGRF5DJbeibY1MY1bZ9sPwHsgmPfiGFv9psfoCVsXFh9TekcLuvaeuxRKA8',
meta: '{}',
}
const tester2CommitHours = 20
const tester3CommitHours = 100
const investAmount1 = 100000
const investAmount2 = 25000
const investAmount3 = 25000
beforeAll(async () => {
await blockchain.update_pass_instance()
@@ -57,6 +69,22 @@ beforeAll(async () => {
console.log('tester3: ', tester3)
await addUser(tester3)
tester4 = generateRandomUsername()
console.log('tester4: ', tester4)
await addUser(tester4)
tester5 = generateRandomUsername()
console.log('tester5: ', tester5)
await addUser(tester5)
investor2 = generateRandomUsername()
console.log('investor2: ', investor2)
await addUser(investor2, tester4)
investor3 = generateRandomUsername()
console.log('investor3: ', investor3)
await addUser(investor3, tester5)
// const { stdout } = await execa('esno', [CLI_PATH, 'boot'], { stdio: 'inherit' })
// expect(stdout).toContain('Boot process completed')
}, 500_000)
@@ -72,6 +100,8 @@ afterAll(() => {
}
console.log(`\n💾 **TOTAL RAM USED IN TESTS**: ${(totalGlobalRam / 1024).toFixed(2)} kb\n`)
console.log(`tester1: ${tester1}\ntester2: ${tester2}\ntester3: ${tester3}\ntester4: ${tester4}\ntester5: ${tester5}\ninvestor1: ${investor1}\ninvestor2: ${investor2}\ninvestor3: ${investor3}`)
})
describe('тест контракта CAPITAL', () => {
@@ -166,294 +196,643 @@ describe('тест контракта CAPITAL', () => {
}
})
// it('инициализируем контракт CAPITAL', async () => {
// const data: CapitalContract.Actions.Init.IInit = {
// coopname: 'voskhod',
// initiator: 'voskhod',
// }
it('инициализируем контракт CAPITAL и сверяем конфиг', async () => {
const data: CapitalContract.Actions.SetConfig.ISetConfig = {
coopname: 'voskhod',
config: {
coordinator_bonus_percent: 4,
expense_pool_percent: 100,
coordinator_invite_validity_days: 30,
voting_period_in_days: 7,
authors_voting_percent: 38.2,
creators_voting_percent: 38.2,
},
}
// const state = (await blockchain.getTableRows(
// CapitalContract.contractName.production,
// CapitalContract.contractName.production,
// 'state',
// 1,
// 'voskhod',
// 'voskhod',
// ))[0]
const result = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.SetConfig.actionName,
authorization: [
{
actor: 'voskhod',
permission: 'active',
},
],
data,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(result)
expect(result.transaction_id).toBeDefined()
// if (state) {
// expect(state.coopname).toBe('voskhod')
// }
// else {
// const result = await blockchain.api.transact(
// {
// actions: [
// {
// account: CapitalContract.contractName.production,
// name: CapitalContract.Actions.Init.actionName,
// authorization: [
// {
// actor: 'voskhod',
// permission: 'active',
// },
// ],
// data,
// },
// ],
// },
// {
// blocksBehind: 3,
// expireSeconds: 30,
// },
// )
// getTotalRamUsage(result)
const state = (await blockchain.getTableRows(
CapitalContract.contractName.production,
CapitalContract.contractName.production,
'state',
1,
'voskhod',
'voskhod',
))[0]
console.log('state: ', state)
expect(state).toBeDefined()
expect(state.coopname).toBe('voskhod')
expect(parseFloat(state.config.coordinator_bonus_percent)).toBe(data.config.coordinator_bonus_percent)
expect(parseFloat(state.config.expense_pool_percent)).toBe(data.config.expense_pool_percent)
expect(parseFloat(state.config.coordinator_invite_validity_days)).toBe(data.config.coordinator_invite_validity_days)
expect(parseFloat(state.config.voting_period_in_days)).toBe(data.config.voting_period_in_days)
expect(parseFloat(state.config.authors_voting_percent)).toBe(data.config.authors_voting_percent)
expect(parseFloat(state.config.creators_voting_percent)).toBe(data.config.creators_voting_percent)
})
// expect(result.transaction_id).toBeDefined()
// }
// })
it('создаём проект', async () => {
const hash = generateRandomSHA256()
const parentHash = '0000000000000000000000000000000000000000000000000000000000000000'
// it('создаём проект', async () => {
// const hash = generateRandomSHA256()
// const parentHash = '0000000000000000000000000000000000000000000000000000000000000000'
const data: CapitalContract.Actions.CreateProject.ICreateProject = {
coopname: 'voskhod',
project_hash: hash,
parent_hash: parentHash,
title: `Идея ${hash.slice(0, 10)}`,
description: `Описание ${hash.slice(0, 10)}`,
meta: '',
}
// const data: CapitalContract.Actions.CreateProject.ICreateProject = {
// coopname: 'voskhod',
// application: 'voskhod',
// title: `Идея ${hash.slice(0, 10)}`,
// description: `Описание ${hash.slice(0, 10)}`,
// project_hash: hash,
// parent_project_hash: parentHash,
// parent_distribution_ratio: 0,
// terms: '',
// subject: '',
// }
project1 = data
// project1 = data
const result = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.CreateProject.actionName,
authorization: [
{
actor: 'voskhod',
permission: 'active',
},
],
data,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
// const result = await blockchain.api.transact(
// {
// actions: [
// {
// account: CapitalContract.contractName.production,
// name: CapitalContract.Actions.CreateProject.actionName,
// authorization: [
// {
// actor: 'voskhod',
// permission: 'active',
// },
// ],
// data,
// },
// ],
// },
// {
// blocksBehind: 3,
// expireSeconds: 30,
// },
// )
expect(result.transaction_id).toBeDefined()
// expect(result.transaction_id).toBeDefined()
const project = (await blockchain.getTableRows(
CapitalContract.contractName.production,
'voskhod',
'projects',
1,
project1.project_hash,
project1.project_hash,
3,
'sha256',
))[0]
// const project = (await blockchain.getTableRows(
// CapitalContract.contractName.production,
// 'voskhod',
// 'projects',
// 1,
// project1.project_hash,
// project1.project_hash,
// 3,
// 'sha256',
// ))[0]
expect(project).toBeDefined()
console.log('project: ', project)
expect(project.coopname).toBe('voskhod')
expect(project.status).toBe('pending')
// План/факт и счетчики по умолчанию
expect(project.plan.target_expense_pool).toBe('0.0000 RUB')
expect(project.fact.target_expense_pool).toBe('0.0000 RUB')
expect(project.fact.accumulated_expense_pool).toBe('0.0000 RUB')
expect(project.fact.used_expense_pool).toBe('0.0000 RUB')
expect(project.fact.invest_pool).toBe('0.0000 RUB')
expect(project.counts.total_authors).toBe(0)
expect(project.counts.total_creators).toBe(0)
expect(project.counts.total_investors).toBe(0)
expect(project.counts.total_contributors).toBe(0)
expect(project.counts.total_commits).toBe(0)
expect(project.project_hash).toBe(data.project_hash)
expect(project.title).toBe(data.title)
expect(project.description).toBe(data.description)
getTotalRamUsage(result)
})
// expect(project).toBeDefined()
it('заключаем договор УХД и приложения к договору со множеством участников', async () => {
const testerNames = [tester1, tester2, tester3, tester4, tester5, investor1, investor2, investor3]
for (const tester of testerNames) {
const appendixHash = generateRandomSHA256()
const contributorHash = generateRandomSHA256()
await registerContributor(blockchain, 'voskhod', tester, contributorHash, '1000.0000 RUB')
await signAppendix(blockchain, 'voskhod', tester, project1.project_hash, appendixHash)
const contributor = (await blockchain.getTableRows(
CapitalContract.contractName.production,
'voskhod',
'contributors',
1,
tester,
tester,
2,
'i64',
))[0]
expect(contributor).toBeDefined()
expect(contributor.appendixes).toContain(project1.project_hash)
expect(contributor.status).toBe('active')
}
})
// expect(project.status).toBe('created')
// expect(project.target).toBe('0.0000 RUB')
// expect(project.invested).toBe('0.0000 RUB')
// expect(project.authors_count).toBe(0)
// expect(project.authors_shares).toBe(0)
// expect(project.project_hash).toBe(data.project_hash)
// expect(project.title).toBe(data.title)
// expect(project.description).toBe(data.description)
// getTotalRamUsage(result)
// })
it(`подписываем соглашения о ЦПП "Цифровой Кошелек" и "Капитализация"`, async () => {
const testerNames = [tester1, tester2, tester3, tester4, tester5, investor1, investor2, investor3] // Можно передавать любое количество пользователей
// it('заключаем договор УХД с автором по проекту', async () => {
// const testerNames = [tester1, tester2, investor1] // Можно передавать любое количество пользователей
for (const tester of testerNames) {
await signWalletAgreement(blockchain, 'voskhod', tester, fakeDocument)
await signCapitalAgreement(blockchain, 'voskhod', tester, fakeDocument)
}
})
// for (const tester of testerNames) {
// await registerAndApproveUHDContract(blockchain, tester, project1.project_hash)
it(`пополняем баланс кошелька investor1`, async () => {
const { depositId, program, userWallet } = await depositToWallet(blockchain, 'voskhod', investor1, investAmount1)
})
// // Получаем все решения и утверждаем последнее
// const decisions = await blockchain.getTableRows(
// SovietContract.contractName.production,
// 'voskhod',
// 'decisions',
// 1000,
// )
// const lastDecision = decisions[decisions.length - 1]
it(`пополняем баланс кошелька investor2`, async () => {
const { depositId, program, userWallet } = await depositToWallet(blockchain, 'voskhod', investor2, investAmount2 * 2)
})
// console.log(`Последнее решение: `, lastDecision)
// await processDecision(blockchain, lastDecision.id)
it(`пополняем баланс кошелька investor3`, async () => {
const { depositId, program, userWallet } = await depositToWallet(blockchain, 'voskhod', investor3, investAmount3)
})
// // Проверка финального статуса контрибьютора
// const contributor = (await blockchain.getTableRows(
// CapitalContract.contractName.production,
// 'voskhod',
// 'contributors',
// 1,
// tester,
// tester,
// 2,
// 'i64',
// ))[0]
it('добавляем мастера к проекту', async () => {
const data: CapitalContract.Actions.SetMaster.ISetMaster = {
coopname: 'voskhod',
project_hash: project1.project_hash,
master: tester1,
}
// console.log(`Контрибьютор ${tester} после авторизации: `, contributor)
// expect(contributor).toBeDefined()
// expect(contributor.status).toBe('authorized')
// }
// })
const result = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.SetMaster.actionName,
authorization: [
{
actor: 'voskhod',
permission: 'active',
},
],
data,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(result)
expect(result.transaction_id).toBeDefined()
// it('добавляем автора к идее', async () => {
// const data: CapitalContract.Actions.AddAuthor.IAddAuthor = {
// coopname: 'voskhod',
// application: 'voskhod',
// project_hash: project1.project_hash,
// author: tester1,
// shares: '100',
// }
const project = (await blockchain.getTableRows(
CapitalContract.contractName.production,
'voskhod',
'projects',
1,
project1.project_hash,
project1.project_hash,
3,
'sha256',
))[0]
// const result = await blockchain.api.transact(
// {
// actions: [
// {
// account: CapitalContract.contractName.production,
// name: CapitalContract.Actions.AddAuthor.actionName,
// authorization: [
// {
// actor: 'voskhod',
// permission: 'active',
// },
// ],
// data,
// },
// ],
// },
// {
// blocksBehind: 3,
// expireSeconds: 30,
// },
// )
// getTotalRamUsage(result)
// expect(result.transaction_id).toBeDefined()
expect(project).toBeDefined()
console.log('project: ', project)
expect(project.master).toBe(tester1)
})
// const author = (await blockchain.getTableRows(
// CapitalContract.contractName.production,
// 'voskhod',
// 'authors',
// 1,
// project1.project_hash,
// project1.project_hash,
// 3,
// 'sha256',
// ))[0]
// console.log('author: ', author)
// expect(author).toBeDefined()
// expect(author.username).toBe(tester1)
// expect(author.shares).toBe(100)
// expect(author.project_hash).toBe(project1.project_hash)
it('добавляем автора к идее', async () => {
const { project, segment } = await addAuthor(blockchain, 'voskhod', project1.project_hash, tester1)
// const project = (await blockchain.getTableRows(
// CapitalContract.contractName.production,
// 'voskhod',
// 'projects',
// 1,
// project1.project_hash,
// project1.project_hash,
// 3,
// 'sha256',
// ))[0]
console.log('segment: ', segment)
expect(segment).toBeDefined()
expect(segment.username).toBe(tester1)
expect(segment.project_hash).toBe(project1.project_hash)
expect(segment.is_author).toBe(1)
expect(segment.is_creator).toBe(0)
expect(segment.is_coordinator).toBe(0)
expect(segment.is_investor).toBe(0)
expect(segment.is_contributor).toBe(0)
expect(segment.is_coordinator).toBe(0)
expect(segment.is_investor).toBe(0)
expect(segment.is_contributor).toBe(0)
expect(project).toBeDefined()
expect(project.counts.total_authors).toBe(1)
})
// expect(project).toBeDefined()
// expect(project.authors_count).toBe(1)
// expect(project.authors_shares).toBe(100)
// })
it('устанавливаем план проекта', async () => {
const data: CapitalContract.Actions.SetPlan.ISetPlan = {
coopname: 'voskhod',
master: tester1,
project_hash: project1.project_hash,
plan_creators_hours: 100,
plan_expenses: '10000.0000 RUB',
plan_hour_cost: '1000.0000 RUB',
}
// it('создать результат', async () => {
// const data: CapitalContract.Actions.CreateResult.ICreateResult = {
// coopname: 'voskhod',
// application: 'voskhod',
// project_hash: project1.project_hash,
// result_hash: generateRandomSHA256(),
// assignee: 'ant',
// assignment: 'Задание #1: тут описание',
// }
const result = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.SetPlan.actionName,
authorization: [
{
actor: 'voskhod',
permission: 'active',
},
],
data,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(result)
expect(result.transaction_id).toBeDefined()
// result1 = data
const project = (await blockchain.getTableRows(
CapitalContract.contractName.production,
'voskhod',
'projects',
1,
project1.project_hash,
project1.project_hash,
3,
'sha256',
))[0]
// const result = await blockchain.api.transact(
// {
// actions: [
// {
// account: CapitalContract.contractName.production,
// name: CapitalContract.Actions.CreateResult.actionName,
// authorization: [
// {
// actor: 'voskhod',
// permission: 'active',
// },
// ],
// data,
// },
// ],
// },
// {
// blocksBehind: 3,
// expireSeconds: 30,
// },
// )
expect(project).toBeDefined()
console.log('plan: ', project.plan)
// Проверяем плановые показатели согласно формулам из generation.cpp
expect(project.plan.hour_cost).toBe('1000.0000 RUB')
expect(parseInt(project.plan.creators_hours)).toBe(100)
expect(project.plan.target_expense_pool).toBe('10000.0000 RUB')
// getTotalRamUsage(result)
// expect(result.transaction_id).toBeDefined()
// Себестоимости и премии
expect(project.plan.creators_base_pool).toBe('100000.0000 RUB')
expect(project.plan.authors_base_pool).toBe('61800.0000 RUB')
expect(project.plan.coordinators_base_pool).toBe('6872.0000 RUB')
expect(project.plan.creators_bonus_pool).toBe('100000.0000 RUB')
expect(project.plan.authors_bonus_pool).toBe('61800.0000 RUB')
// const blockchainResult = (await blockchain.getTableRows(
// CapitalContract.contractName.production,
// 'voskhod',
// 'results',
// 1,
// result1.result_hash,
// result1.result_hash,
// 2,
// 'sha256',
// ))[0]
// Инвестиции и коэффициент возврата
expect(project.plan.invest_pool).toBe('168672.0000 RUB')
expect(project.plan.total_received_investments).toBe('178672.0000 RUB')
expect(project.plan.coordinators_investment_pool).toBe('178672.0000 RUB')
expect(parseFloat(project.plan.return_base_percent)).toBeCloseTo(100, 10)
expect(parseFloat(project.plan.use_invest_percent)).toBeCloseTo(100, 10)
// expect(blockchainResult).toBeDefined()
// expect(blockchainResult.result_hash).toBe(result1.result_hash)
// expect(blockchainResult.project_hash).toBe(project1.project_hash)
// expect(blockchainResult.coopname).toBe('voskhod')
// })
// Итого генерация, бонусы вкладчиков и общая сумма с расходами
expect(project.plan.total_generation_pool).toBe('330472.0000 RUB')
expect(project.plan.contributors_bonus_pool).toBe('534703.6960 RUB')
expect(project.plan.total).toBe('875175.6960 RUB')
})
// it(`подписать соглашение о ЦПП "Цифровой Кошелек" и пополнить баланс кошелька investor1`, async () => {
// {
// const { wallet, program } = await signWalletAgreement(blockchain, 'voskhod', investor1, fakeDocument)
// console.log('wallet investor1: ', wallet)
// }
// {
// const { depositId, program, userWallet } = await depositToWallet(blockchain, 'voskhod', investor1, 100000)
// }
// })
it('стартовать проект на приём коммитов', async () => {
const data: CapitalContract.Actions.StartProject.IStartProject = {
coopname: 'voskhod',
project_hash: project1.project_hash,
}
const tx = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.StartProject.actionName,
authorization: [{ actor: 'voskhod', permission: 'active' }],
data,
},
],
},
{ blocksBehind: 3, expireSeconds: 30 },
)
getTotalRamUsage(tx)
expect(tx.transaction_id).toBeDefined()
// it('подписать соглашение о ЦПП "Капитализация"', async () => {
// const testerNames = [tester1, tester2, investor1] // Можно передавать любое количество пользователей
const project = (await blockchain.getTableRows(
CapitalContract.contractName.production,
'voskhod',
'projects',
1,
project1.project_hash,
project1.project_hash,
3,
'sha256',
))[0]
expect(project).toBeDefined()
expect(project.status).toBe('active')
})
// for (const tester of testerNames) {
// await signCapitalAgreement(blockchain, 'voskhod', tester, fakeDocument)
// }
// })
it('добавить коммит создателя tester1 на 10 часов по 1000 RUB', async () => {
const { finalProject, commitHash, commit } = await commitToResult(blockchain, 'voskhod', project1.project_hash, tester1, tester1CommitHours)
commits.push(commitHash)
// it('инвестировать в проект аккаунтом investor1', async () => {
// await investInProject(blockchain, 'voskhod', investor1, project1.project_hash, investAmount, fakeDocument)
// })
console.log('commit: ', commit)
console.log('project fact: ', finalProject.fact)
expect(finalProject.fact.creators_hours).toBe(10)
expect(finalProject.fact.creators_base_pool).toBe('10000.0000 RUB')
expect(finalProject.fact.creators_bonus_pool).toBe('10000.0000 RUB')
expect(finalProject.fact.authors_bonus_pool).toBe('6180.0000 RUB')
expect(finalProject.fact.total_generation_pool).toBe('32360.0000 RUB')
expect(finalProject.fact.contributors_bonus_pool).toBe('52358.4800 RUB')
expect(finalProject.fact.total).toBe('84718.4800 RUB')
})
it('обновить CRPS и проверить распределение авторских наград после первого коммита', async () => {
// Обновляем сегмент tester1 через CRPS
const { prevSegment, updatedSegment } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester1)
console.log('Сегмент до обновления:', prevSegment)
console.log('Сегмент после обновления:', updatedSegment)
// Проверяем, что весь пул авторских вознаграждений распределился одному автору
// В проекте authors_bonus_pool = '6180.0000 RUB' должен полностью перейти в author_bonus
expect(updatedSegment.author_bonus).toBe('6180.0000 RUB')
// Базовая награда автора также должна быть распределена
// Из формулы генерации: base_bonus_coefficient * authors_bonus_pool / total_authors = 1.0 * 6180 / 1 = 6180
expect(updatedSegment.author_base).toBe('6180.0000 RUB')
// Проверяем, что is_author = true
expect(updatedSegment.is_author).toBe(1)
console.log(`✅ Авторские награды успешно распределены: base=${updatedSegment.author_base}, bonus=${updatedSegment.author_bonus}`)
})
it('добавляем второго автора tester2 к идее', async () => {
const { project, segment } = await addAuthor(blockchain, 'voskhod', project1.project_hash, tester2)
console.log('segment tester2: ', segment)
expect(segment).toBeDefined()
expect(segment.username).toBe(tester2)
expect(segment.project_hash).toBe(project1.project_hash)
expect(segment.is_author).toBe(1)
expect(segment.is_creator).toBe(0)
expect(segment.is_coordinator).toBe(0)
expect(segment.is_investor).toBe(0)
expect(segment.is_contributor).toBe(0)
expect(project).toBeDefined()
expect(project.counts.total_authors).toBe(2) // Теперь у нас 2 автора
})
it('добавить коммит создателя tester2 на 20 часов по 1000 RUB', async () => {
const { finalProject, commitHash, commit } = await commitToResult(blockchain, 'voskhod', project1.project_hash, tester2, tester2CommitHours)
commits.push(commitHash)
console.log('commit tester2: ', commit)
console.log('project fact after tester2 commit: ', finalProject.fact)
expect(commit.amounts.creators_base_pool).toBe('20000.0000 RUB')
expect(commit.amounts.creators_bonus_pool).toBe('20000.0000 RUB')
expect(commit.amounts.authors_bonus_pool).toBe('12360.0000 RUB')
expect(commit.amounts.authors_base_pool).toBe('12360.0000 RUB')
expect(commit.amounts.total_generation_pool).toBe('64720.0000 RUB')
expect(commit.amounts.contributors_bonus_pool).toBe('104716.9600 RUB')
expect(commit.amounts.total_contribution).toBe('169436.9600 RUB')
// Проверяем накопительные значения после двух коммитов (10 + 20 = 30 часов)
expect(finalProject.fact.creators_hours).toBe(30)
expect(finalProject.fact.creators_base_pool).toBe('30000.0000 RUB')
expect(finalProject.fact.creators_bonus_pool).toBe('30000.0000 RUB')
expect(finalProject.fact.authors_bonus_pool).toBe('18540.0000 RUB')
expect(finalProject.fact.total_generation_pool).toBe('97080.0000 RUB')
expect(finalProject.fact.contributors_bonus_pool).toBe('157075.4400 RUB')
expect(finalProject.fact.total).toBe('254155.4400 RUB')
})
it('обновить CRPS для обоих авторов и проверить распределение авторских наград', async () => {
// Обновляем сегмент tester1 через CRPS
await sleep(1000) // т.к. ранее мы делали обновление и будет дублирование - надо подождать
const { prevSegment: prevTester1, updatedSegment: updatedTester1 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester1)
// Обновляем сегмент tester2 через CRPS
const { prevSegment: prevTester2, updatedSegment: updatedTester2 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester2)
// console.log('Tester1 - сегмент до обновления:', prevTester1)
// console.log('Tester1 - сегмент после обновления:', updatedTester1)
// console.log('Tester2 - сегмент до обновления:', prevTester2)
// console.log('Tester2 - сегмент после обновления:', updatedTester2)
// Получаем проект для проверки текущего authors_bonus_pool
const project = (await blockchain.getTableRows(
CapitalContract.contractName.production,
'voskhod',
'projects',
1,
project1.project_hash,
project1.project_hash,
3,
'sha256',
))[0]
console.log('Current project authors_bonus_pool:', project.fact.authors_bonus_pool)
// После второго коммита авторский пул составляет 18540 RUB, прирост составил 12360 RUB
// Он должен распределиться поровну между двумя авторами: 12360 / 2 = 6180 RUB каждому
// Проверяем tester1 (у него была награда от первого коммита + доля от второго)
// tester1 должен получить: 6180 (от первого коммита) + 6180 (доля от второго) = 12360
// Но поскольку распределение происходит от текущего пула, каждый получает половину от 12360 = 6180
expect(updatedTester1.author_bonus).toBe('12360.0000 RUB')
expect(updatedTester1.author_base).toBe('12360.0000 RUB')
// Проверяем что сумма к ссуде равна 0 т.к. инвестиций нет
expect(updatedTester1.provisional_amount).toBe('0.0000 RUB')
// Проверяем tester2 (получает свою долю от текущего пула)
expect(updatedTester2.author_bonus).toBe('6180.0000 RUB')
expect(updatedTester2.author_base).toBe('6180.0000 RUB')
// Проверяем что сумма к ссуде равна 0 т.к. инвестиций нет
expect(updatedTester2.provisional_amount).toBe('0.0000 RUB')
// Проверяем, что оба являются авторами
expect(updatedTester1.is_author).toBe(1)
expect(updatedTester2.is_author).toBe(1)
console.log(`✅ Авторские награды успешно распределены между двумя авторами:`)
console.log(` tester1: base=${updatedTester1.author_base}, bonus=${updatedTester1.author_bonus}`)
console.log(` tester2: base=${updatedTester2.author_base}, bonus=${updatedTester2.author_bonus}`)
})
it('проверяем, что оба пайщика являются авторами и создателями', async () => {
const segment1 = await getSegment(blockchain, 'voskhod', project1.project_hash, tester1)
const segment2 = await getSegment(blockchain, 'voskhod', project1.project_hash, tester2)
expect(segment1.is_author).toBe(1)
expect(segment2.is_author).toBe(1)
expect(segment1.is_creator).toBe(1)
expect(segment2.is_creator).toBe(1)
})
it('открыть приём инвестиций', async () => {
const data: CapitalContract.Actions.OpenProject.IOpenProject = {
coopname: 'voskhod',
project_hash: project1.project_hash,
}
const result = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.OpenProject.actionName,
authorization: [{ actor: 'voskhod', permission: 'active' }],
data,
},
],
},
{ blocksBehind: 3, expireSeconds: 30 },
)
const project = (await blockchain.getTableRows(
CapitalContract.contractName.production,
'voskhod',
'projects',
1,
project1.project_hash,
project1.project_hash,
3,
'sha256',
))[0]
console.log('project: ', project)
expect(project).toBeDefined()
expect(project.is_opened).toBe(1)
expect(project.fact.target_expense_pool).toBe('10000.0000 RUB')
expect(project.fact.accumulated_expense_pool).toBe('0.0000 RUB')
expect(project.fact.used_expense_pool).toBe('0.0000 RUB')
})
it('инвестировать в проект аккаунтом investor1 на 100000 RUB', async () => {
fakeDocument.signatures[0].signer = investor1
const { project, transactionId } = await investInProject(blockchain, 'voskhod', investor1, project1.project_hash, `${investAmount1}.0000 RUB`, fakeDocument)
expect(project).toBeDefined()
expect(transactionId).toBeDefined()
expect(project.fact.total_received_investments).toBe('100000.0000 RUB')
expect(project.fact.invest_pool).toBe('90000.0000 RUB')
expect(project.fact.accumulated_expense_pool).toBe('10000.0000 RUB')
expect(parseFloat(project.fact.use_invest_percent)).toBeCloseTo(58.54, 1)
expect(parseFloat(project.fact.return_base_percent)).toBeCloseTo(100, 1)
})
it('обновляем сегменты tester1, tester2 и investor1 и проверяем обеспечение инвестициями для ссуд', async () => {
await sleep(1000)
const { prevSegment: prevTester1, updatedSegment: updatedTester1 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester1)
const { prevSegment: prevTester2, updatedSegment: updatedTester2 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester2)
const { prevSegment: prevInvestor1, updatedSegment: updatedInvestor1 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, investor1)
console.log('updatedTester1: ', updatedTester1)
console.log('updatedTester2: ', updatedTester2)
console.log('updatedInvestor1: ', updatedInvestor1)
expect(updatedTester1.provisional_amount).toBe('22360.0000 RUB')
expect(updatedTester2.provisional_amount).toBe('26180.0000 RUB')
expect(updatedInvestor1.provisional_amount).toBe('0.0000 RUB')
// Проверяем что сумма инвестиций равна 100000 RUB
expect(updatedInvestor1.investor_amount).toBe('100000.0000 RUB')
// Проверяем что используемая сумма инвестиций равна 58540 RUB
expect(updatedInvestor1.investor_base).toBe('58540.0000 RUB')
})
it('tester3 вносит 100000 RUB коммитом и делает сумму инвестиций недостаточной для полного обеспечения', async () => {
const { finalProject, commitHash, commit } = await commitToResult(blockchain, 'voskhod', project1.project_hash, tester3, tester3CommitHours)
commits.push(commitHash)
console.log('commit tester3: ', commit)
console.log('project fact after tester3 commit: ', finalProject.fact)
expect(finalProject.fact.creators_hours).toBe(130)
expect(finalProject.fact.creators_base_pool).toBe('130000.0000 RUB')
expect(finalProject.fact.creators_bonus_pool).toBe('130000.0000 RUB')
expect(finalProject.fact.authors_base_pool).toBe('80340.0000 RUB')
expect(finalProject.fact.authors_bonus_pool).toBe('80340.0000 RUB')
expect(finalProject.fact.total_generation_pool).toBe('420680.0000 RUB')
expect(finalProject.fact.contributors_bonus_pool).toBe('680660.2400 RUB')
expect(finalProject.fact.total).toBe('1101340.2400 RUB')
expect(parseFloat(finalProject.fact.use_invest_percent)).toBeCloseTo(100, 1)
expect(parseFloat(finalProject.fact.return_base_percent)).toBeCloseTo(42.78, 1)
await sleep(1000)
// Проверяем сегменты
const { prevSegment: prevTester1, updatedSegment: updatedTester1 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester1)
const { prevSegment: prevTester2, updatedSegment: updatedTester2 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester2)
const { prevSegment: prevInvestor1, updatedSegment: updatedInvestor1 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, investor1)
// console.log('prevTester2: ', prevTester2)
// console.log('updatedTester2: ', updatedTester2)
// console.log('updatedInvestor1: ', updatedInvestor1)
expect(updatedTester1.provisional_amount).toBe('22788.8181 RUB')
expect(updatedTester2.provisional_amount).toBe('24423.3146 RUB')
expect(updatedInvestor1.provisional_amount).toBe('0.0000 RUB')
expect(updatedInvestor1.investor_base).toBe('100000.0000 RUB')
expect(updatedInvestor1.investor_amount).toBe('100000.0000 RUB')
})
it('инвестировать в проект аккаунтом investor2 на 25000 RUB с координатором tester4', async () => {
fakeDocument.signatures[0].signer = investor2
const { project, prevProject, transactionId } = await investInProject(blockchain, 'voskhod', investor2, project1.project_hash, `${investAmount2}.0000 RUB`, fakeDocument)
expect(project).toBeDefined()
expect(transactionId).toBeDefined()
// console.log('prevProject: ', prevProject)
// console.log('project: ', project)
expect(project.counts.total_coordinators).toBe(1)
expect(project.fact.coordinators_investment_pool).toBe('25000.0000 RUB')
expect(project.fact.coordinators_base_pool).toBe('961.5384 RUB')
expect(project.fact.total_received_investments).toBe('125000.0000 RUB')
expect(parseFloat(project.fact.return_base_percent)).toBeCloseTo(54.42, 1)
expect(parseFloat(project.fact.use_invest_percent)).toBeCloseTo(100, 1)
// Проверяем сегменты
const { prevSegment: prevTester4, updatedSegment: updatedTester4 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester4)
expect(updatedTester4.provisional_amount).toBe('523.3133 RUB')
expect(updatedTester4.coordinator_base).toBe('961.5384 RUB')
expect(updatedTester4.is_coordinator).toBe(1)
expect(updatedTester4.coordinator_investments).toBe('25000.0000 RUB')
console.log('updatedTester4: ', updatedTester4)
const { prevSegment: prevInvestor2, updatedSegment: updatedInvestor2 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, investor2)
console.log('updatedInvestor2: ', updatedInvestor2)
expect(updatedInvestor2.provisional_amount).toBe('0.0000 RUB')
expect(updatedInvestor2.investor_base).toBe('25000.0000 RUB')
expect(updatedInvestor2.investor_amount).toBe('25000.0000 RUB')
})
it('инвестировать в проект аккаунтом investor3 еще на 25000 RUB с координатором tester5', async () => {
fakeDocument.signatures[0].signer = investor3
const { project, prevProject, transactionId } = await investInProject(blockchain, 'voskhod', investor3, project1.project_hash, `${investAmount3}.0000 RUB`, fakeDocument)
expect(project).toBeDefined()
expect(transactionId).toBeDefined()
console.log('prevProject: ', prevProject)
console.log('project: ', project)
expect(project.counts.total_coordinators).toBe(2)
expect(project.fact.coordinators_investment_pool).toBe('50000.0000 RUB')
expect(project.fact.coordinators_base_pool).toBe('1923.0768 RUB')
expect(project.fact.total_received_investments).toBe('150000.0000 RUB')
expect(parseFloat(project.fact.return_base_percent)).toBeCloseTo(65.9558, 1)
expect(parseFloat(project.fact.use_invest_percent)).toBeCloseTo(100, 1)
const { prevSegment: prevTester5, updatedSegment: updatedTester5 } = await refreshSegment(blockchain, 'voskhod', project1.project_hash, tester5)
console.log('updatedTester5: ', updatedTester5)
// expect(updatedTester5.provisional_amount).toBe('0.0000 RUB')
// expect(updatedTester5.investor_base).toBe('25000.0000 RUB')
// expect(updatedTester5.investor_amount).toBe('25000.0000 RUB')
})
// it('финансировать результат проекта на 10000 RUB', async () => {
// await allocateFundsToResult(blockchain, 'voskhod', project1.project_hash, result1.result_hash, '10000.0000 RUB')
@@ -0,0 +1,76 @@
import { CapitalContract } from 'cooptypes'
import { expect } from 'vitest'
import type Blockchain from '../../blockchain'
import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
import { makeCombinedChecksum256NameIndexKey } from '../shared/combinedKeys'
export async function addAuthor(blockchain: Blockchain, coopname: string, project_hash: string, author: string) {
const data: CapitalContract.Actions.AddAuthor.IAddAuthor = {
coopname,
application: coopname,
project_hash,
author,
}
const result = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.AddAuthor.actionName,
authorization: [
{
actor: coopname,
permission: 'active',
},
],
data,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(result)
expect(result.transaction_id).toBeDefined()
const key = makeCombinedChecksum256NameIndexKey(project_hash, author)
const rows = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
CapitalContract.Tables.Segments.tableName,
1000,
key,
key,
3,
'i128',
))
const segment = rows[0]
console.log('segment: ', segment)
expect(segment).toBeDefined()
expect(segment.username).toBe(author)
expect(segment.project_hash).toBe(project_hash)
expect(segment.is_author).toBe(1)
const project = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
CapitalContract.Tables.Projects.tableName,
1000,
project_hash,
project_hash,
3,
'sha256',
))[0]
expect(project).toBeDefined()
return {
project,
segment,
}
}
@@ -2,34 +2,20 @@ import { expect } from 'vitest'
import { CapitalContract, SovietContract } from 'cooptypes'
import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
import { generateRandomSHA256 } from '../../utils/randomHash'
import { fakeDocument } from '../soviet/fakeDocument'
import { processDecision } from '../soviet/processDecision'
import { ratePerHour } from './consts'
import { fakeDocument } from '../shared/fakeDocument'
import { processApprove } from './processApprove'
export async function commitToResult(
blockchain: any,
coopname: string,
resultHash: string,
projectHash: string,
creator: string,
spendHours: number,
) {
const commitHash = generateRandomSHA256()
const totalSpended = `${(spendHours * parseFloat(ratePerHour)).toFixed(4)} RUB`
console.log(`\n🔹 Начало коммита: ${commitHash}, часы: ${spendHours}, сумма: ${totalSpended}`)
// Получение текущих данных перед коммитом
const prevResult = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'results',
1,
resultHash,
resultHash,
2,
'sha256',
))[0] || { spended: '0.0000 RUB', commits_count: 0 }
console.log(`\n🔹 Начало коммита: ${commitHash}, проект: ${projectHash}, часы: ${spendHours}`)
// Получение текущих данных проекта перед коммитом
const prevProject = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
@@ -53,21 +39,33 @@ export async function commitToResult(
))[0] || { contributed_hours: 0, available: '0.0000 RUB' }
console.log('📊 Балансы до коммита:')
console.log('▶ Результат:', prevResult)
console.log('▶ Проект:', prevProject)
console.log('▶ Контрибьютор:', prevContributor)
// Обновим состояние проекта после возможной смены статуса
const projectBefore = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'projects',
1,
projectHash,
projectHash,
3,
'sha256',
))[0]
const prevTotalCommits = projectBefore?.counts?.total_commits ?? 0
const prevCreatorsHours = projectBefore?.fact?.creators_hours ?? 0
// Создание коммита
const commitData: CapitalContract.Actions.CreateCommit.ICommit = {
coopname,
application: coopname,
result_hash: resultHash,
commit_hash: commitHash,
contributed_hours: spendHours,
username: creator,
project_hash: projectHash,
commit_hash: commitHash,
creator_hours: spendHours,
}
console.log(`\n🚀 Отправка транзакции CreateCommit для результата ${resultHash}`)
const createCommitResult = await blockchain.api.transact(
{
actions: [
@@ -89,7 +87,7 @@ export async function commitToResult(
expect(createCommitResult.transaction_id).toBeDefined()
// Проверка созданного коммита
let blockchainCommit = (await blockchain.getTableRows(
const blockchainCommit = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'commits',
@@ -103,39 +101,15 @@ export async function commitToResult(
console.log('🔍 Коммит после создания:', blockchainCommit)
expect(blockchainCommit).toBeDefined()
expect(blockchainCommit.commit_hash).toBe(commitHash)
expect(blockchainCommit.spended).toBe(totalSpended)
expect(blockchainCommit.project_hash).toBe(projectHash)
expect(blockchainCommit.status).toBe('created')
// Утверждение коммита
const approveCommitData: SovietContract.Actions.Approves.ConfirmApprove.IConfirmApprove = {
coopname,
approval_hash: commitHash,
approved_document: fakeDocument,
}
console.log(`\n✅ Подтверждение коммита ${commitHash}`)
const approveCommitResult = await blockchain.api.transact(
{
actions: [
{
account: SovietContract.contractName.production,
name: SovietContract.Actions.Approves.ConfirmApprove.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data: approveCommitData,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(approveCommitResult)
expect(approveCommitResult.transaction_id).toBeDefined()
const approveCommitResult = await processApprove(blockchain, coopname, commitHash)
// Проверка утвержденного коммита
blockchainCommit = (await blockchain.getTableRows(
const blockchainEmptyCommit = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'commits',
@@ -147,96 +121,9 @@ export async function commitToResult(
))[0]
// Коммит удаляем после утверждения
expect(blockchainCommit).toBeUndefined()
// // Получение всех решений и выполнение последнего
// const decisions = await blockchain.getTableRows(
// SovietContract.contractName.production,
// coopname,
// 'decisions',
// 1000,
// )
// const lastDecision = decisions[decisions.length - 1]
// console.log(`\n📜 Выполнение последнего решения: ${lastDecision.id}`)
// await processDecision(blockchain, lastDecision.id)
// {
// // Утверждение коммита
// const act1Data: CapitalContract.Actions.SetAct1.ISetAct1 = {
// coopname,
// application: coopname,
// username: creator,
// commit_hash: commitHash,
// act: fakeDocument,
// }
// console.log(`\n✅ Установка акта1 по коммиту ${commitHash}`)
// const act1Result = await blockchain.api.transact(
// {
// actions: [
// {
// account: CapitalContract.contractName.production,
// name: CapitalContract.Actions.SetAct1.actionName,
// authorization: [{ actor: coopname, permission: 'active' }],
// data: act1Data,
// },
// ],
// },
// {
// blocksBehind: 3,
// expireSeconds: 30,
// },
// )
// getTotalRamUsage(act1Result)
// expect(act1Result.transaction_id).toBeDefined()
// }
// {
// // Утверждение коммита
// const act2Data: CapitalContract.Actions.SetAct2.ISetAct2 = {
// coopname,
// application: coopname,
// username: creator,
// commit_hash: commitHash,
// act: fakeDocument,
// }
// console.log(`\n✅ Установка акта2 по коммиту ${commitHash}`)
// const act2Result = await blockchain.api.transact(
// {
// actions: [
// {
// account: CapitalContract.contractName.production,
// name: CapitalContract.Actions.SetAct2.actionName,
// authorization: [{ actor: coopname, permission: 'active' }],
// data: act2Data,
// },
// ],
// },
// {
// blocksBehind: 3,
// expireSeconds: 30,
// },
// )
// getTotalRamUsage(act2Result)
// expect(act2Result.transaction_id).toBeDefined()
// }
// Проверка результата после утверждения коммита
const finalResult = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'results',
1,
resultHash,
resultHash,
2,
'sha256',
))[0]
expect(blockchainEmptyCommit).toBeUndefined()
// Проверка проекта после утверждения коммита
const finalProject = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
@@ -260,23 +147,18 @@ export async function commitToResult(
))[0]
console.log('\n📊 Балансы после утверждения коммита:')
console.log('▶ Результат:', finalResult)
console.log('▶ Проект:', finalProject)
console.log('▶ Контрибьютор:', finalContributor)
// expect(parseFloat(finalResult.spended)).toBe(parseFloat(prevResult.spended) + parseFloat(totalSpended))
// expect(parseFloat(finalProject.spended)).toBe(parseFloat(prevProject.spended))
// Проверка, что у контрибьютора увеличились contributed_hours и available
// expect(parseFloat(finalContributor.contributed_hours)).toBe(parseFloat(prevContributor.contributed_hours) + spendHours)
// expect(parseFloat(finalContributor.available)).toBe(parseFloat(prevContributor.available) + parseFloat(totalSpended))
// Проверки по проекту
expect(finalProject.counts.total_commits).toBe(prevTotalCommits + 1)
expect(parseInt(finalProject.fact.creators_hours)).toBe(prevCreatorsHours + spendHours)
console.log(`\n✅ Коммит ${commitHash} завершен успешно!`)
return {
commitHash,
commit: blockchainCommit,
transactionId: approveCommitResult.transaction_id,
finalResult,
finalProject,
finalContributor,
}
@@ -4,3 +4,4 @@ export const walletProgramId = 1
export const ratePerHour = '1000.0000 RUB'
export const sourceProgramName = 'source'
export const capitalProgramName = 'capital'
export const circulationAccountId = 80
@@ -0,0 +1,19 @@
import { CapitalContract } from 'cooptypes'
import type Blockchain from '../../blockchain'
import { makeCombinedChecksum256NameIndexKey } from '../shared/combinedKeys'
export async function getSegment(blockchain: Blockchain, coopname: string, project_hash: string, author: string): Promise<CapitalContract.Tables.Segments.ISegment> {
const key = makeCombinedChecksum256NameIndexKey(project_hash, author)
const rows = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
CapitalContract.Tables.Segments.tableName,
1000,
key,
key,
3,
'i128',
))
return rows[0]
}
@@ -4,7 +4,9 @@ import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
import { generateRandomSHA256 } from '../../utils/randomHash'
import { getCoopProgramWallet, getUserProgramWallet } from '../wallet/walletUtils'
import { processDecision } from '../soviet/processDecision'
import { capitalProgramId, sourceProgramId } from './consts'
import { processApprove } from './processApprove'
import { getSegment } from './getSegment'
import { sourceProgramId } from './consts'
export async function investInProject(
blockchain: any,
@@ -17,18 +19,6 @@ export async function investInProject(
const investHash = generateRandomSHA256()
console.log(`\n🔹 Начало инвестиции: ${investHash}`)
// Получение текущих данных перед инвестицией
const prevContributor = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'contributors',
1,
investor,
investor,
2,
'i64',
))[0] || { invested: '0.0000 RUB' }
const prevProject = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
@@ -40,11 +30,10 @@ export async function investInProject(
'sha256',
))[0] || { invested: '0.0000 RUB', available: '0.0000 RUB' }
const prevUserWallet = await getUserProgramWallet(blockchain, coopname, investor, capitalProgramId) || { blocked: '0.0000 RUB' }
const prevProgramWallet = await getCoopProgramWallet(blockchain, coopname, capitalProgramId) || { blocked: '0.0000 RUB', share_contributions: '0.0000 RUB' }
const prevUserWallet = await getUserProgramWallet(blockchain, coopname, investor, sourceProgramId) || { blocked: '0.0000 RUB' }
const prevProgramWallet = await getCoopProgramWallet(blockchain, coopname, sourceProgramId) || { blocked: '0.0000 RUB', share_contributions: '0.0000 RUB' }
console.log('📊 Балансы до инвестиции:')
console.log('▶ Контрибьютор:', prevContributor)
console.log('▶ Проект:', prevProject)
console.log('▶ Кошелек пользователя:', prevUserWallet)
console.log('▶ Кошелек программы:', prevProgramWallet)
@@ -81,7 +70,7 @@ export async function investInProject(
getTotalRamUsage(createInvestResult)
expect(createInvestResult.transaction_id).toBeDefined()
let blockchainInvest = (await blockchain.getTableRows(
const blockchainInvest = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'invests',
@@ -97,36 +86,11 @@ export async function investInProject(
expect(blockchainInvest.status).toBe('created')
// Утверждение инвестиции
const approveInvestData: CapitalContract.Actions.ApproveInvest.IApproveInvest = {
coopname,
application: coopname,
approver: 'ant',
invest_hash: investHash,
approved_statement: fakeDocument,
}
console.log(`\n✅ Подтверждение инвестиции ${investHash}`)
const approveInvestResult = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.ApproveInvest.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data: approveInvestData,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
const approveInvestResult = await processApprove(blockchain, coopname, investHash)
getTotalRamUsage(approveInvestResult)
expect(approveInvestResult.transaction_id).toBeDefined()
blockchainInvest = (await blockchain.getTableRows(
// Проверка утвержденной инвестиции
const blockchainEmptyInvest = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'invests',
@@ -137,49 +101,10 @@ export async function investInProject(
'sha256',
))[0]
console.log('🔍 Инвестиция после утверждения:', blockchainInvest)
expect(blockchainInvest).toBeDefined()
expect(blockchainInvest.status).toBe('approved')
// Инвестиция удаляется после утверждения
expect(blockchainEmptyInvest).toBeUndefined()
// Получение всех решений и выполнение последнего
const decisions = await blockchain.getTableRows(
SovietContract.contractName.production,
coopname,
'decisions',
1000,
)
const lastDecision = decisions[decisions.length - 1]
console.log(`\n📜 Выполнение последнего решения: ${lastDecision.id}`)
await processDecision(blockchain, lastDecision.id)
blockchainInvest = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'invests',
1,
investHash,
investHash,
2,
'sha256',
))[0]
console.log('📌 Инвестиция после завершения:', blockchainInvest)
expect(blockchainInvest).toBeUndefined()
// Получение новых данных после инвестиции
const contributor = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'contributors',
1,
investor,
investor,
2,
'i64',
))[0]
const project = (await blockchain.getTableRows(
const finalProject = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'projects',
@@ -190,24 +115,30 @@ export async function investInProject(
'sha256',
))[0]
const userWallet = await getUserProgramWallet(blockchain, coopname, investor, capitalProgramId)
const programWallet = await getCoopProgramWallet(blockchain, coopname, capitalProgramId)
const finalUserWallet = await getUserProgramWallet(blockchain, coopname, investor, sourceProgramId)
const finalProgramWallet = await getCoopProgramWallet(blockchain, coopname, sourceProgramId)
// Получение сегмента инвестора для данного проекта
const segment = await getSegment(blockchain, coopname, projectHash, investor)
console.log('\n📊 Балансы после инвестиции:')
console.log('▶ Контрибьютор:', contributor)
console.log('▶ Проект:', project)
console.log('▶ Кошелек пользователя:', userWallet)
console.log('▶ Кошелек программы:', programWallet)
console.log('▶ Проект:', finalProject)
console.log('▶ Сегмент:', segment)
console.log('▶ Кошелек пользователя:', finalUserWallet)
console.log('▶ Кошелек программы:', finalProgramWallet)
// Проверка изменения балансов
expect(parseFloat(contributor.invested)).toBe(parseFloat(prevContributor.invested) + parseFloat(investAmount))
expect(parseFloat(project.invested)).toBe(parseFloat(prevProject.invested) + parseFloat(investAmount))
expect(parseFloat(project.available)).toBe(parseFloat(prevProject.available) + parseFloat(investAmount))
expect(parseFloat(userWallet.blocked)).toBe(parseFloat(prevUserWallet.blocked) + parseFloat(investAmount))
expect(parseFloat(programWallet.blocked)).toBe(parseFloat(prevProgramWallet.blocked) + parseFloat(investAmount))
expect(parseFloat(programWallet.share_contributions)).toBe(parseFloat(prevProgramWallet.share_contributions) + parseFloat(investAmount))
expect(parseFloat(finalUserWallet.blocked)).toBe(parseFloat(prevUserWallet.blocked) + parseFloat(investAmount))
expect(parseFloat(finalProgramWallet.blocked)).toBe(parseFloat(prevProgramWallet.blocked) + parseFloat(investAmount))
console.log(`\n✅ Инвестирование на ${investAmount} завершено успешно!`)
return { investHash, transactionId: approveInvestResult.transaction_id }
return {
investHash,
invest: blockchainInvest,
transactionId: approveInvestResult.transaction_id,
prevProject,
project: finalProject,
segment,
}
}
@@ -0,0 +1,34 @@
import { expect } from 'vitest'
import { SovietContract } from 'cooptypes'
import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
import { fakeDocument } from '../shared/fakeDocument'
export async function processApprove(blockchain: any, coopname: string, approvalHash: string) {
const data: SovietContract.Actions.Approves.ConfirmApprove.IConfirmApprove = {
coopname,
approval_hash: approvalHash,
approved_document: fakeDocument,
}
const result = await blockchain.api.transact(
{
actions: [
{
account: SovietContract.contractName.production,
name: SovietContract.Actions.Approves.ConfirmApprove.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(result)
expect(result.transaction_id).toBeDefined()
return result
}
@@ -0,0 +1,67 @@
import { expect } from 'vitest'
import { CapitalContract } from 'cooptypes'
import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
import type Blockchain from '../../blockchain'
export async function refreshSegment(
blockchain: Blockchain,
coopname: string,
projectHash: string,
username: string,
) {
console.log(`\n🔄 Обновляем сегмент CRPS для ${username} в проекте ${projectHash}`)
// Получение данных сегмента перед обновлением
const prevSegment = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
CapitalContract.Tables.Segments.tableName,
1000,
)).filter((s: any) => s.project_hash === projectHash && s.username === username)[0]
// console.log('📊 Сегмент до обновления:', prevSegment)
// Обновление сегмента через rfrshsegment
const refreshSegmentData: CapitalContract.Actions.RefreshSegment.IRefreshSegment = {
coopname,
project_hash: projectHash,
username,
}
const refreshResult = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.RefreshSegment.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data: refreshSegmentData,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(refreshResult)
expect(refreshResult.transaction_id).toBeDefined()
// Получение обновленного сегмента
const updatedSegment = (await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
CapitalContract.Tables.Segments.tableName,
1000,
)).filter((s: any) => s.project_hash === projectHash && s.username === username)[0]
// console.log('📊 Сегмент после обновления:', updatedSegment)
console.log(`✅ Сегмент для ${username} обновлен успешно!`)
return {
transactionId: refreshResult.transaction_id,
prevSegment,
updatedSegment,
}
}
@@ -1,3 +1,64 @@
export function registerContributor() {
import { expect } from 'vitest'
import { CapitalContract } from 'cooptypes'
import { fakeDocument } from '../shared/fakeDocument'
import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
import { processApprove } from './processApprove'
export async function registerContributor(
blockchain: any,
coopname: string,
username: string,
contributorHash: string,
ratePerHour: string,
) {
const contract = fakeDocument
contract.signatures[0].signer = username
const data: CapitalContract.Actions.RegisterContributor.IRegisterContributor = {
coopname,
username,
contributor_hash: contributorHash,
rate_per_hour: ratePerHour,
is_external_contract: false,
contract,
}
const result = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.RegisterContributor.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(result)
expect(result.transaction_id).toBeDefined()
// Аппрув председателя (через совет) по хэшу договора УХД
await processApprove(blockchain, coopname, contributorHash)
// Проверяем, что контрибьютор активирован и договор сохранён
const contributor = (
await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'contributors',
1,
username,
username,
2,
'i64',
)
)[0]
expect(contributor).toBeDefined()
expect(contributor.status).toBe('active')
expect(contributor.contract).toBeDefined()
}
@@ -0,0 +1,62 @@
import { expect } from 'vitest'
import { CapitalContract } from 'cooptypes'
import { fakeDocument } from '../shared/fakeDocument'
import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
import { processApprove } from './processApprove'
export async function signAppendix(
blockchain: any,
coopname: string,
username: string,
projectHash: string,
appendixHash: string,
) {
// Подписание приложения пайщиком (через кооператив, как и в контракте)
const data: CapitalContract.Actions.SignAppendix.ISignAppendix = {
coopname,
username,
project_hash: projectHash,
appendix_hash: appendixHash,
document: fakeDocument,
}
const result = await blockchain.api.transact(
{
actions: [
{
account: CapitalContract.contractName.production,
name: CapitalContract.Actions.SignAppendix.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
getTotalRamUsage(result)
expect(result.transaction_id).toBeDefined()
// Аппрув председателя (через совет) по хэшу приложения
await processApprove(blockchain, coopname, appendixHash)
// Проверяем, что приложение применилось к контрибьютору, а запись удалена
const contributor = (
await blockchain.getTableRows(
CapitalContract.contractName.production,
coopname,
'contributors',
1,
username,
username,
2,
'i64',
)
)[0]
expect(contributor).toBeDefined()
expect(contributor.appendixes).toContain(projectHash)
}
@@ -0,0 +1,57 @@
// Комбинирование индекса (project_hash + username) как в C++ combine_checksum_ids
// Возвращает десятичную строку uint128 для использования с key_type 'i128'
function charToSymbol(c: string): number {
if (c === '.') return 0
if (c >= '1' && c <= '5') return c.charCodeAt(0) - '1'.charCodeAt(0) + 1
if (c >= 'a' && c <= 'z') return c.charCodeAt(0) - 'a'.charCodeAt(0) + 6
return 0
}
function nameToUint64(name: string): bigint {
let value = 0n
let i = 0
for (; i < 12 && i < name.length; i++) {
value <<= 5n
value |= BigInt(charToSymbol(name[i]))
}
value <<= 4n + 5n * BigInt(12 - i)
if (i < name.length) {
value |= BigInt(charToSymbol(name[i])) & 0x0fn
}
return value
}
function hexToBytes(hex: string): Uint8Array {
const clean = hex.startsWith('0x') ? hex.slice(2) : hex
if (clean.length !== 64) throw new Error('Invalid sha256 hex')
const out = new Uint8Array(32)
for (let i = 0; i < 32; i++) {
out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16)
}
return out
}
function readBigUInt64LE(bytes: Uint8Array, offset = 0): bigint {
let result = 0n
for (let i = 0; i < 8; i++) {
result |= BigInt(bytes[offset + i]) << (8n * BigInt(i))
}
return result
}
export function makeCombinedChecksum256NameIndexKey(projectHashHex: string, username: string): string {
const bytes = hexToBytes(projectHashHex)
const truncatedHashLE = readBigUInt64LE(bytes, 0)
const nameValue = nameToUint64(username)
const key128 = (truncatedHashLE << 64n) | nameValue
return key128.toString()
}
export function makeCombinedChecksum256Uint64IndexKey(projectHashHex: string, id: number): string {
const bytes = hexToBytes(projectHashHex)
const truncatedHashLE = readBigUInt64LE(bytes, 0)
const idValue = BigInt(id)
const key128 = (truncatedHashLE << 64n) | idValue
return key128.toString()
}
@@ -0,0 +1,16 @@
export const fakeDocument = {
version: '1.0.0',
hash: '157192b276da23cc84ab078fc8755c051c5f0430bf4802e55718221e6b76c777',
doc_hash: '157192b276da23cc84ab078fc8755c051c5f0430bf4802e55718221e6b76c777',
meta_hash: '157192b276da23cc84ab078fc8755c051c5f0430bf4802e55718221e6b76c777',
meta: '{}',
signatures: [{
id: 1,
signed_hash: '157192b276da23cc84ab078fc8755c051c5f0430bf4802e55718221e6b76c777',
signer: 'cooperative1',
public_key: 'EOS5JhMfxbsNebajHcTEK8yC9uNN9Dit9hEmzE8ri8yMhhzxrLg3J',
signature: 'SIG_K1_KmKWPBC8dZGGDGhbKEoZEzPr3h5crRrR2uLdGRF5DJbeibY1MY1bZ9sPwHsgmPfiGFv9psfoCVsXFh9TekcLuvaeuxRKA8',
signed_at: '2025-05-14T12:22:26',
meta: '{}',
}],
}
@@ -3,7 +3,7 @@ import { GatewayContract, WalletContract } from 'cooptypes'
import { expect } from 'vitest'
import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
import { generateRandomSHA256 } from '../../utils/randomHash'
import { compareTokenAmounts, getCoopProgramWallet, getCoopWallet, getDeposit, getUserProgramWallet } from './walletUtils'
import { compareTokenAmounts, getCirculationAccount, getCoopProgramWallet, getDeposit, getUserProgramWallet } from './walletUtils'
export async function depositToWallet(blockchain: any, coopname: string, username: string, amount: number) {
const depositId = randomInt(100000)
@@ -48,8 +48,7 @@ export async function depositToWallet(blockchain: any, coopname: string, usernam
const prevUserWallet = await getUserProgramWallet(blockchain, coopname, username, 1)
const prevUserWalletAvailable = prevUserWallet?.available || '0.0000 RUB'
const prevCoopWallet = await getCoopWallet(blockchain, coopname)
const prevCoopWalletAvailable = prevCoopWallet?.circulating_account?.available || '0.0000 RUB'
const prevCoopWalletAvailable = (await getCirculationAccount(blockchain, coopname)).available
const data2: GatewayContract.Actions.CompleteIncome.ICompleteIncome = {
coopname,
@@ -85,7 +84,7 @@ export async function depositToWallet(blockchain: any, coopname: string, usernam
// Проверяем изменение балансов
compareTokenAmounts(prevUserWalletAvailable, userWallet.available, amount)
compareTokenAmounts(prevCoopWalletAvailable, (await getCoopWallet(blockchain, coopname)).circulating_account.available, amount)
compareTokenAmounts(prevCoopWalletAvailable, (await getCirculationAccount(blockchain, coopname)).available, amount)
return { depositId, program, userWallet }
}
@@ -1,6 +1,7 @@
import { FundContract, SovietContract, WalletContract } from 'cooptypes'
import { FundContract, LedgerContract, SovietContract, WalletContract } from 'cooptypes'
import { expect } from 'vitest'
import type Blockchain from '../../blockchain'
import { circulationAccountId } from '../capital/consts'
export async function getUserProgramWallet(blockchain: any, coopname: string, username: string, program_id: number) {
const wallets = await blockchain.getTableRows(
@@ -29,6 +30,20 @@ export async function getCoopProgramWallet(blockchain: any, coopname: string, pr
return program[0]
}
export async function getLedgerAccounts(blockchain: Blockchain, coopname: string) {
return (await blockchain.getTableRows(
LedgerContract.contractName.production,
coopname,
LedgerContract.Tables.Laccount.tableName,
100,
))
}
export async function getCirculationAccount(blockchain: Blockchain, coopname: string) {
const accounts = await getLedgerAccounts(blockchain, coopname)
return accounts.find((account: any) => account.id === circulationAccountId) || { available: '0.0000 RUB' }
}
export async function getCoopWallet(blockchain: any, coopname: string) {
return (await blockchain.getTableRows(
FundContract.contractName.production,
@@ -7,4 +7,12 @@ alwaysApply: true
- entity/ - слой сущностей домена. Содержит выделенные сущности, структуры, индексы их таблиц, и inline реализации простейших бизнес-функций, которые по своим названиям помогают явно определить что происходит с сущностью.
- core/ - слой корневой бизнес-логики, которая объединяет несколько сущностей. Если бизнес-функция сущности требует для своего исполнения нескольких операций с сущностями или она явно относится к доменному уровню приложения за счет своего применения, то такая функция должна быть вынесена в ядро.
Мы допускаем последовательное-повторное обновление сущностей по ходу исполнения действия контракта, если это необходимо в пользу ясности. Длинна типа name не должна превышать 12 символов и не должна содержать специальных знаков.
Мы допускаем последовательное-повторное обновление сущностей по ходу исполнения действия контракта, если это необходимо в пользу ясности. Длинна типа name не должна превышать 12 символов и не должна содержать специальных знаков.
При документировании контрактов используй следующие группы для doxygen:
\ingroup public_contracts - для контрактов
\ingroup public_actions - для действий
\ingroup public_consts - для действий
\ingroup public_tables - для действий
И используй следующий тег для указания требуемой аудентификации:
* @note Авторизация требуется от аккаунта: @p coopname
+1 -1
View File
@@ -874,7 +874,7 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.
INPUT = README.md cpp/common/ cpp/registrator/ cpp/marketplace/ cpp/soviet/ cpp/system/ cpp/gateway/ cpp/fund/ cpp/draft cpp/contributor
INPUT = README.md cpp/
# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
@@ -17,7 +17,8 @@ void capital::createcmmt(eosio::name coopname, eosio::name application, eosio::n
"Пайщик не подписывал приложение к договору УХД для данного проекта");
// Проверяем, что действие с указанным хэшем не существует
Capital::Commits::get_commit_or_fail(coopname, commit_hash);
auto commit = Capital::Commits::get_commit(coopname, commit_hash);
eosio::check(!commit.has_value(), "Коммит с указанным хэшем уже существует");
// Проверяем, что сумма часов создателя больше 0
eosio::check(creator_hours > 0, "Только положительная сумма часов создателя");
@@ -1,5 +1,5 @@
void capital::rfrshsegment(eosio::name coopname, checksum256 project_hash, eosio::name username) {
require_auth(username);
require_auth(coopname);
// Проверяем существование проекта
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
@@ -11,6 +11,8 @@ void capital::createinvest(name coopname, name application, name username, check
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
eosio::check(project.is_opened == true, "Проект закрыт для инвестиций");
std::string memo = Capital::Memo::get_invest_memo(contributor -> id);
// блокируем средства в программе кошелька
@@ -10,6 +10,7 @@ void capital::returnunused(name coopname, name application, checksum256 project_
eosio::check(segment.is_investor, "Пользователь не является инвестором проекта");
// Рассчитываем неиспользованную сумму
eosio::check(segment.investor_base <= segment.investor_amount, "Ошибка: использованная сумма больше инвестированной");
eosio::asset unused_amount = segment.investor_amount - segment.investor_base;
eosio::check(unused_amount.amount > 0, "Нет неиспользованных средств для возврата");
@@ -22,6 +23,14 @@ void capital::returnunused(name coopname, name application, checksum256 project_
Wallet::sub_blocked_funds(_capital, coopname, username, unused_amount, _source_program, memo);
Wallet::add_available_funds(_capital, coopname, username, unused_amount, _wallet_program, memo);
// Обновляем проект - увеличиваем сумму возвращенных инвестиций
Capital::project_index projects(_capital, coopname.value);
auto project_itr = projects.find(project.id);
projects.modify(project_itr, application, [&](auto &p) {
p.fact.total_returned_investments += unused_amount;
});
// Обновляем сегмент - отмечаем что неиспользованные средства возвращены
Capital::Segments::segments_index segments(_capital, coopname.value);
auto segment_itr = segments.find(segment.id);
@@ -1,42 +0,0 @@
# Приложения к договорам УХД
## Бизнес-процесс
### Общая схема
1. **Основной договор УХД** - пайщик подписывает общий договор участия в хозяйственной деятельности
2. **Приложения к проектам** - для участия в конкретном проекте пайщик должен подписать отдельное приложение
### Процесс создания приложения
1. **Создание заявки** (`createappndx`)
- Приложение создает заявку на подписание приложения к договору УХД
- Проверяется наличие основного договора УХД у пайщика
- Проверяется отсутствие уже подписанного приложения для данного проекта
- Создается временная запись в таблице `appendixes`
- Заявка отправляется на рассмотрение председателю
2. **Рассмотрение председателем**
- Председатель рассматривает заявку через контракт совета
- Принимает решение: одобрить или отклонить
3. **Обработка решения**
**При одобрении** (`apprvappndx`):
- `project_hash` добавляется в вектор `contributor.appendixes`
- Временная запись удаляется из таблицы `appendixes`
- Пайщик получает право участвовать в проекте
**При отклонении** (`dclineappndx`):
- Временная запись удаляется из таблицы `appendixes`
- Пайщик не получает право участвовать в проекте
### Проверка прав участия
При выполнении операций (создание коммитов, инвестиций, выводов) система проверяет:
1. Наличие основного договора УХД (`contributor.status == "authorized"`)
2. Наличие подписанного приложения для проекта (`project_hash` в `contributor.appendixes`)
### Структуры данных
- **`contributor.appendixes`** - вектор хэшей проектов с одобренными приложениями
- **`appendixes`** - временная таблица для хранения заявок на рассмотрении
@@ -2,25 +2,17 @@ void capital::apprvappndx(eosio::name coopname, checksum256 appendix_hash, docum
require_auth(_soviet);
// Находим приложение
auto appendix = Capital::get_appendix(coopname, appendix_hash);
auto appendix = Capital::Appendix::get_appendix(coopname, appendix_hash);
eosio::check(appendix.has_value(), "Приложение не найдено");
eosio::check(appendix -> status == Capital::Appendix::Status::CREATED, "Приложение уже обработано");
Capital::appendix_index appendixes(_capital, coopname.value);
auto itr = appendixes.find(appendix -> id);
// Находим контрибьютора с основным договором УХД
auto contributor = Capital::Contributors::get_contributor(coopname, appendix -> username);
eosio::check(contributor.has_value(), "Контрибьютор с основным договором УХД не найден");
// Добавляем project_hash в вектор appendixes у контрибьютора
Capital::contributor_index contributors(_capital, coopname.value);
auto contributor_itr = contributors.find(contributor -> id);
// Добавляем проект в вектор appendixes у контрибьютора
Capital::Contributors::push_appendix_to_contributor(coopname, appendix -> username, appendix -> project_hash);
contributors.modify(contributor_itr, _capital, [&](auto &c) {
c.appendixes.push_back(appendix -> project_hash);
});
// Удаляем запись из таблицы appendixes
appendixes.erase(itr);
// Удаляем приложение
Capital::Appendix::delete_appendix(coopname, appendix -> id);
}
@@ -2,13 +2,10 @@ void capital::dclineappndx(eosio::name coopname, checksum256 appendix_hash, std:
require_auth(_soviet);
// Находим приложение
auto appendix = Capital::get_appendix(coopname, appendix_hash);
eosio::check(appendix.has_value(), "Приложение не найдено");
eosio::check(appendix -> status == Capital::Appendix::Status::CREATED, "Приложение уже обработано");
auto appendix = Capital::Appendix::get_appendix(coopname, appendix_hash);
Capital::appendix_index appendixes(_capital, coopname.value);
auto itr = appendixes.find(appendix -> id);
// Удаляем запись из таблицы appendixes - приложение отклонено
appendixes.erase(itr);
// Удаляем приложение
if (appendix.has_value()) {
Capital::Appendix::delete_appendix(coopname, appendix -> id);
}
}
@@ -1,32 +1,21 @@
void capital::signappndx(eosio::name coopname, eosio::name application, eosio::name username, checksum256 project_hash, checksum256 appendix_hash, document2 document) {
void capital::signappndx(eosio::name coopname, eosio::name username, checksum256 project_hash, checksum256 appendix_hash, document2 document) {
require_auth(coopname);
// Проверяем что пользователь подписал общий договор УХД
auto contributor = Capital::Contributors::get_contributor(coopname, username);
eosio::check(contributor.has_value(), "Пайщик не подписывал основной договор УХД");
eosio::check(contributor -> status == Capital::Contributors::Status::PENDING, "Основной договор УХД не активен");
eosio::check(contributor -> status == Capital::Contributors::Status::ACTIVE, "Основной договор УХД не активен");
// Проверяем что приложение с таким хэшем не существует
auto exist_appendix = Capital::get_appendix(coopname, appendix_hash);
auto exist_appendix = Capital::Appendix::get_appendix(coopname, appendix_hash);
eosio::check(!exist_appendix.has_value(), "Приложение с указанным хэшем уже существует");
// Проверяем что у пользователя нет уже приложения для этого проекта
eosio::check(!Capital::Contributors::is_contributor_has_appendix_in_project(coopname, project_hash, username),
"У пайщика уже есть приложение для данного проекта");
Capital::appendix_index appendixes(_capital, coopname.value);
auto appendix_id = get_global_id_in_scope(_capital, coopname, "appendixes"_n);
appendixes.emplace(coopname, [&](auto &a) {
a.id = appendix_id;
a.coopname = coopname;
a.username = username;
a.project_hash = project_hash;
a.appendix_hash = appendix_hash;
a.status = Capital::Appendix::Status::CREATED;
a.created_at = current_time_point();
a.appendix = document;
});
// Создаём приложение к договору УХД
Capital::Appendix::create_appendix(coopname, username, project_hash, appendix_hash, document);
// Отправляем на аппрув председателю
::Soviet::create_approval(
@@ -1,3 +1,22 @@
/**
* Создаёт проект.
*
* Выполняет проверки отсутствия проекта с указанным хэшем и валидирует
* родительский хэш согласно правилам: допускается либо пустой `parent_hash`,
* либо ссылка на проект первого уровня без собственного родителя.
* После проверок создаёт запись в таблице `projects` со статусом `created`.
*
* \ingroup public_actions
*
* @param coopname Имя кооператива (scope таблицы).
* @param project_hash Уникальный хэш проекта.
* @param parent_hash Хэш родителя или пустой хэш, если проект корневой.
* @param title Название проекта.
* @param description Описание проекта.
* @param meta Дополнительные метаданные проекта.
*
* @note Авторизация требуется от аккаунта: @p coopname
*/
void capital::createproj (
eosio::name coopname,
checksum256 project_hash,
@@ -5,7 +5,7 @@ void capital::openproject(name coopname, checksum256 project_hash) {
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
// Проверяем, что проект в статусе "created"
eosio::check(project.status == Capital::Projects::Status::CREATED, "Проект должен быть в статусе 'created'");
eosio::check(project.is_opened == false, "Проект уже открыт");
// Открываем проект через доменную логику
Capital::Projects::open_project(coopname, project_hash);
@@ -1,17 +1,37 @@
void capital::setconfig(eosio::name coopname, eosio::name initiator, Capital::config cfg) {
check_auth_or_fail(_capital, coopname, initiator, "setconfig"_n);
/**
* Устанавливает конфигурацию кооператива.
*
* Обновляет глобальное состояние контракта для указанного кооператива, создавая запись,
* если она отсутствует, либо модифицируя существующую.
*
* \ingroup public_actions
*
* @param coopname Имя кооператива.
* @param config Новая конфигурация кооператива.
*
* @note Авторизация требуется от аккаунта: @p coopname
*/
void capital::setconfig(eosio::name coopname, Capital::config config) {
require_auth(coopname);
eosio::check(config.coordinator_bonus_percent >= 0.0 && config.coordinator_bonus_percent <= 10, "Координатор не может получить больше 0.1 (10%) от инвестиций");
eosio::check(config.expense_pool_percent >= 0.0 && config.expense_pool_percent <= 100, "Процент инвестиций в пул расходов должен быть между 0 и 1 (100%)");
eosio::check(config.coordinator_invite_validity_days >= 0, "Срок действия приглашения координатора должен быть не отрицательным числом дней");
eosio::check(config.voting_period_in_days >= 1 && config.voting_period_in_days <= 30, "Период голосования должен быть между 1 и 30 днями");
eosio::check(config.authors_voting_percent >= 0.0 && config.authors_voting_percent <= 100, "Процент премий авторов на голосовании должен быть между 0 и 100");
eosio::check(config.creators_voting_percent >= 0.0 && config.creators_voting_percent <= 100, "Процент премий создателей на голосовании должен быть между 0 и 100");
Capital::global_state_table global_state_inst(_self, _self.value);
auto itr = global_state_inst.find(coopname.value);
if (itr == global_state_inst.end()) {
global_state_inst.emplace(initiator, [&](auto& s) {
global_state_inst.emplace(coopname, [&](auto& s) {
s.coopname = coopname;
s.config = cfg;
s.config = config;
});
} else {
global_state_inst.modify(itr, initiator, [&](auto& s) {
s.config = cfg;
global_state_inst.modify(itr, coopname, [&](auto& s) {
s.config = config;
});
}
}
@@ -4,8 +4,8 @@ void capital::startproject(name coopname, checksum256 project_hash) {
// Проверяем существование проекта и получаем его
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
// Проверяем, что проект в статусе "opened"
eosio::check(project.status == Capital::Projects::Status::OPENED, "Проект должен быть в статусе 'opened'");
// Проверяем, что проект в статусе "pending"
eosio::check(project.status == Capital::Projects::Status::PENDING, "Проект должен быть в статусе 'pending'");
// Обновляем статус проекта на "active"
Capital::Projects::update_status(coopname, project_hash, Capital::Projects::Status::ACTIVE);
@@ -1,4 +1,4 @@
void capital::cmpltproject(name coopname, checksum256 project_hash) {
void capital::startvoting(name coopname, checksum256 project_hash) {
require_auth(coopname);
// Проверяем существование проекта и получаем его
@@ -15,10 +15,5 @@ void capital::setmaster(name coopname, checksum256 project_hash, name master) {
"Мастер должен быть участником проекта");
// Назначаем мастера проекта
Capital::project_index projects(_capital, coopname.value);
auto project_itr = projects.find(project->id);
projects.modify(project_itr, coopname, [&](auto &p) {
p.master = master;
});
Capital::Projects::set_master(coopname, project -> id, master);
}
@@ -1,17 +1,32 @@
void capital::setplan(name coopname, checksum256 project_hash, uint64_t plan_creators_hours, asset plan_expenses, asset plan_hour_cost){
/**
* @brief Устанавливает плановые показатели проекта.
*
* Действие рассчитывает плановые пулы (себестоимости, премии, инвестиции, расходы)
* на основании входных параметров и текущего конфига кооператива, и сохраняет их в поле `plan` проекта.
*
* @ingroup public_actions
* @param coopname Имя кооператива (scope таблицы).
* @param project_hash Хэш проекта, для которого устанавливается план.
* @param plan_creators_hours Планируемое время создателей в часах (> 0).
* @param plan_expenses Планируемый целевой размер пула расходов (`asset`).
* @param plan_hour_cost Планируемая стоимость часа создателя (`asset`).
* @note Авторизация требуется от аккаунта: @p coopname
*/
void capital::setplan(name coopname, name master, checksum256 project_hash, uint64_t plan_creators_hours, asset plan_expenses, asset plan_hour_cost){
require_auth(coopname);
// Получаем проект
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
// Проверяем, что проект в статусе "created"
eosio::check(project.status == Capital::Projects::Status::CREATED, "Проект должен быть в статусе 'created'");
eosio::check(project.master == master, "Мастер проекта не совпадает с мастером, который устанавливает план");
// Проверяем, что проект в статусе "pending"
eosio::check(project.status == Capital::Projects::Status::PENDING, "Проект должен быть в статусе 'pending'");
Wallet::validate_asset(plan_expenses);
Wallet::validate_asset(plan_hour_cost);
eosio::check(plan_creators_hours > 0, "План времени должен быть положительным");
eosio::check(plan_hour_cost.amount > 0, "Стоимость нормо-часа должна быть положительной");
// Вычисляем плановые показатели через ядро
auto calculated_plan = Capital::Core::Generation::calculate_plan_generation_amounts(coopname, plan_hour_cost, plan_creators_hours, plan_expenses);
@@ -9,14 +9,14 @@ void capital::diallocate(eosio::name coopname, eosio::name application, checksum
eosio::check(project.fact.program_invest_pool.amount > 0, "Нет программных средств для возврата");
// Неиспользованная сумма программных средств
// При коэффициенте > 1 часть программных средств не используется
// При коэффициенте > 100% часть программных средств не используется
eosio::asset unused_program_amount = asset(0, _root_govern_symbol);
if (project.fact.return_cost_coefficient > 1.0) {
if (project.fact.use_invest_percent > 100.0) {
// Рассчитываем фактически использованную сумму программных средств
eosio::asset used_program_amount = Capital::Core::Generation::calculate_investor_used_amount(
project.fact.program_invest_pool,
project.fact.return_cost_coefficient
project.fact.use_invest_percent
);
unused_program_amount = project.fact.program_invest_pool - used_program_amount;
@@ -1,8 +1,8 @@
/**
* @brief Регистрация пайщика в контракте и получение договора УХД от него.
*/
void capital::regcontrib(eosio::name coopname, eosio::name application, eosio::name username, checksum256 contributor_hash, eosio::asset rate_per_hour, bool is_external_contract, document2 contract) {
check_auth_or_fail(_capital, coopname, application, "regcontrib"_n);
void capital::regcontrib(eosio::name coopname, eosio::name username, checksum256 contributor_hash, eosio::asset rate_per_hour, bool is_external_contract, document2 contract) {
require_auth(coopname);
// если договор не внешний, то проверяем его на корректность
if (!is_external_contract) {
@@ -10,25 +10,15 @@ void capital::regcontrib(eosio::name coopname, eosio::name application, eosio::n
verify_document_or_fail(contract, {username});
}
Wallet::validate_asset(rate_per_hour);
auto exist_by_username = Capital::Contributors::get_contributor(coopname, username);
eosio::check(!exist_by_username.has_value(), "Пайщик уже обладает подписанным договором УХД");
auto exist_by_hash = Capital::Contributors::get_contributor_by_hash(coopname, contributor_hash);
eosio::check(!exist_by_hash.has_value(), "Контрибьютор с данным хэшем уже зарегистрирован");
Capital::contributor_index contributors(_capital, coopname.value);
contributors.emplace(coopname, [&](auto &c) {
c.id = get_global_id_in_scope(_capital, coopname, "contributors"_n);
c.coopname = coopname;
c.username = username;
c.contributor_hash = contributor_hash;
c.status = Capital::Contributors::Status::PENDING;
c.is_external_contract = is_external_contract;
c.contract = contract;
c.created_at = eosio::current_time_point();
c.rate_per_hour = rate_per_hour;
});
Capital::Contributors::create_contributor(coopname, username, contributor_hash, is_external_contract, contract, rate_per_hour);
std::string memo = "";
@@ -56,4 +56,7 @@ void capital::pushrslt(name coopname, name application, checksum256 project_hash
// для избежания двойного обновления одной записи
Capital::Segments::update_segment_after_result_contribution_with_shares(coopname, project_hash, application, application,
available_base_after_pay_debt, debt_amount);
// Обновляем накопительные показатели контрибьютора на основе его ролей в сегменте
Capital::Contributors::update_contributor_ratings_from_segment(coopname, segment);
}
+1 -1
View File
@@ -23,7 +23,7 @@
// Запустить проект на приём коммитов
#include "app/managment/startproject.cpp"
// Завершение проекта и начало голосования
#include "app/managment/cmpltproject.cpp"
#include "app/managment/startvoting.cpp"
// Завершение голосования
#include "app/managment/cmpltvoting.cpp"
// Закрытие проекта
+5 -5
View File
@@ -34,7 +34,7 @@ public:
* \ingroup public_actions
*/
[[eosio::action]]
void setconfig(name coopname, name initiator, Capital::config cfg);
void setconfig(name coopname, Capital::config config);
// Создать проект
[[eosio::action]]
@@ -57,7 +57,7 @@ public:
// Завершить проект и начать голосование
[[eosio::action]]
void cmpltproject(name coopname, checksum256 project_hash);
void startvoting(name coopname, checksum256 project_hash);
// Завершить голосование
[[eosio::action]]
@@ -172,7 +172,7 @@ public:
// Регистрация
[[eosio::action]]
void regcontrib(eosio::name coopname, eosio::name application, eosio::name username, checksum256 contributor_hash, eosio::asset rate_per_hour, bool is_external_contract, document2 contract);
void regcontrib(eosio::name coopname, eosio::name username, checksum256 contributor_hash, eosio::asset rate_per_hour, bool is_external_contract, document2 contract);
[[eosio::action]]
void approvereg(eosio::name coopname, checksum256 contributor_hash, document2 contract);
[[eosio::action]]
@@ -180,7 +180,7 @@ public:
// Приложения к договору УХД
[[eosio::action]]
void signappndx(eosio::name coopname, eosio::name application, eosio::name username, checksum256 project_hash, checksum256 appendix_hash, document2 document);
void signappndx(eosio::name coopname, eosio::name username, checksum256 project_hash, checksum256 appendix_hash, document2 document);
[[eosio::action]]
void apprvappndx(eosio::name coopname, checksum256 appendix_hash, document2 approved_document);
[[eosio::action]]
@@ -241,7 +241,7 @@ public:
// Планирование
[[eosio::action]] void setmaster(name coopname, checksum256 project_hash, name master);
[[eosio::action]] void setplan(name coopname, checksum256 project_hash, uint64_t plan_creators_hours, asset plan_expenses, asset plan_hour_cost);
[[eosio::action]] void setplan(name coopname, name master, checksum256 project_hash, uint64_t plan_creators_hours, asset plan_expenses, asset plan_hour_cost);
[[eosio::action]] void expandexpnss(name coopname, checksum256 project_hash, asset additional_expenses);
// CRPS
@@ -19,6 +19,7 @@ namespace Capital::Core {
if (!exist_segment.has_value()) {
segments.emplace(_capital, [&](auto &g){
g.id = segments.available_primary_key();
g.coopname = coopname;
g.project_hash = project_hash;
g.username = username;
g.is_author = true;
@@ -41,6 +41,7 @@ void upsert_contributor_segment(eosio::name coopname, const checksum256 &project
if (!exist_segment.has_value()) {
segments.emplace(_capital, [&](auto &g){
g.id = segments.available_primary_key();
g.coopname = coopname;
g.project_hash = project_hash;
g.username = username;
g.is_contributor = true; // Устанавливаем флаг вкладчика
@@ -3,19 +3,58 @@
namespace Capital::Core {
/**
* @brief Обновляет CRPS поля в проекте для координаторов при добавлении наград
* @brief Рассчитывает пропорциональную долю координатора в общем пуле премий
* @param coordinator_investments Сумма инвестиций, привлеченная координатором
* @param total_coordinator_investments Общая сумма инвестиций, привлеченная всеми координаторами
* @param plan_total_investments Плановая общая сумма инвестиций проекта
* @param coordinator_base_pool Общий пул премий координаторов
* @param all_coordinators_scores Сумма всех промежуточных баллов R всех координаторов
* @return Доля координатора в пуле премий
*/
inline void update_coordinator_crps(eosio::name coopname, const checksum256 &project_hash, const eosio::asset &reward_amount) {
Capital::project_index projects(_capital, coopname.value);
auto project = projects.find(Capital::Projects::get_project_or_fail(coopname, project_hash).id);
projects.modify(project, _capital, [&](auto &p) {
if (p.counts.total_coordinators > 0) {
// Рассчитываем дельту reward per share
int64_t delta = reward_amount.amount / p.counts.total_coordinators;
p.crps.coordinator_cumulative_reward_per_share += delta;
}
});
inline eosio::asset calculate_coordinator_proportional_share(
const eosio::asset &coordinator_investments,
const eosio::asset &total_coordinator_investments,
const eosio::asset &plan_total_investments,
const eosio::asset &coordinator_base_pool,
double all_coordinators_scores
) {
if (coordinator_investments.amount == 0 ||
total_coordinator_investments.amount == 0 ||
plan_total_investments.amount == 0 ||
all_coordinators_scores <= 0.0) {
return eosio::asset(0, _root_govern_symbol);
}
// Rn = (Yn / invest_plan) * (Yn / YN_fact_invest)
double coordinator_score =
(static_cast<double>(coordinator_investments.amount) / static_cast<double>(plan_total_investments.amount)) *
(static_cast<double>(coordinator_investments.amount) / static_cast<double>(total_coordinator_investments.amount));
// Zn = Rn / RN * coordinator_base_fact
double share_ratio = coordinator_score / all_coordinators_scores;
int64_t coordinator_share = static_cast<int64_t>(
static_cast<double>(coordinator_base_pool.amount) * share_ratio
);
return eosio::asset(coordinator_share, _root_govern_symbol);
}
/**
* @brief Рассчитывает промежуточный балл R для координатора
*/
inline double calculate_coordinator_score(
const eosio::asset &coordinator_investments,
const eosio::asset &total_coordinator_investments,
const eosio::asset &plan_total_investments
) {
if (coordinator_investments.amount == 0 ||
total_coordinator_investments.amount == 0 ||
plan_total_investments.amount == 0) {
return 0.0;
}
return (static_cast<double>(coordinator_investments.amount) / static_cast<double>(plan_total_investments.amount)) *
(static_cast<double>(coordinator_investments.amount) / static_cast<double>(total_coordinator_investments.amount));
}
@@ -30,16 +69,16 @@ void upsert_coordinator_segment(eosio::name coopname, const checksum256 &project
eosio::name coordinator_username, const eosio::asset &rised_amount) {
Segments::segments_index segments(_capital, coopname.value);
auto exist_segment = Segments::get_segment(coopname, project_hash, coordinator_username);
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
if (!exist_segment.has_value()) {
segments.emplace(_capital, [&](auto &g){
g.id = segments.available_primary_key();
g.coopname = coopname;
g.project_hash = project_hash;
g.username = coordinator_username;
g.coordinator_investments = rised_amount;
g.is_coordinator = true;
g.last_coordinator_reward_per_share = project.crps.coordinator_cumulative_reward_per_share;
// Не устанавливаем CRPS поля, т.к. используем пропорциональное распределение
});
Capital::Projects::increment_total_coordinators(coopname, project_hash);
@@ -57,32 +96,55 @@ void upsert_coordinator_segment(eosio::name coopname, const checksum256 &project
}
/**
* @brief Обновляет награды координатора в сегменте
* @brief Обновляет награды координатора в сегменте на основе пропорционального распределения
*/
void refresh_coordinator_segment(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
Segments::segments_index segments(_capital, coopname.value);
auto segment_opt = Segments::get_segment(coopname, project_hash, username);
if (!segment_opt.has_value()) {
return; // Сегмент не найден
}
auto segment_it = segments.find(segment_opt->id);
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
segments.modify(segment_it, _capital, [&](auto &s) {
if (s.is_coordinator) {
int64_t pending_coordinator_reward = 1 *
(project.crps.coordinator_cumulative_reward_per_share - s.last_coordinator_reward_per_share);
if (pending_coordinator_reward > 0) {
eosio::asset coordinator_reward_asset = eosio::asset(pending_coordinator_reward, _root_govern_symbol);
s.coordinator_base += coordinator_reward_asset;
s.last_coordinator_reward_per_share = project.crps.coordinator_cumulative_reward_per_share;
}
}
});
// Если нет координаторов или премий к распределению, ничего не делаем
if (project.counts.total_coordinators == 0 || project.fact.coordinators_base_pool.amount == 0) {
return;
}
// Получаем всех координаторов проекта
auto coordinators = Capital::Segments::get_project_coordinators(coopname, project_hash);
if (coordinators.empty()) {
return;
}
// Рассчитываем общую сумму промежуточных баллов R всех координаторов
double total_coordinators_score = 0.0;
for (const auto& coord : coordinators) {
total_coordinators_score += calculate_coordinator_score(
coord.coordinator_investments,
project.fact.coordinators_investment_pool,
project.plan.total_received_investments
);
}
if (total_coordinators_score <= 0.0) {
return; // Нет баллов для распределения
}
// Обновляем координаторские доли для всех координаторов проекта
Segments::segments_index segments(_capital, coopname.value);
for (const auto& coord : coordinators) {
auto segment_it = segments.find(coord.id);
eosio::asset new_coordinator_base = calculate_coordinator_proportional_share(
coord.coordinator_investments,
project.fact.coordinators_investment_pool,
project.plan.total_received_investments,
project.fact.coordinators_base_pool,
total_coordinators_score
);
segments.modify(segment_it, _capital, [&](auto &s) {
s.coordinator_base = new_coordinator_base;
});
}
}
}// namespace Capital::Core
@@ -0,0 +1,150 @@
#include "crps.hpp"
namespace Capital::Core {
/**
* @brief Рассчитывает пропорциональную долю координатора в общем пуле премий
* @param coordinator_investments Сумма инвестиций, привлеченная координатором
* @param total_coordinator_investments Общая сумма инвестиций, привлеченная всеми координаторами
* @param plan_total_investments Плановая общая сумма инвестиций проекта
* @param coordinator_base_pool Общий пул премий координаторов
* @param all_coordinators_scores Сумма всех промежуточных баллов R всех координаторов
* @return Доля координатора в пуле премий
*/
inline eosio::asset calculate_coordinator_proportional_share(
const eosio::asset &coordinator_investments,
const eosio::asset &total_coordinator_investments,
const eosio::asset &plan_total_investments,
const eosio::asset &coordinator_base_pool,
double all_coordinators_scores
) {
if (coordinator_investments.amount == 0 ||
total_coordinator_investments.amount == 0 ||
plan_total_investments.amount == 0 ||
all_coordinators_scores <= 0.0) {
return eosio::asset(0, _root_govern_symbol);
}
// Rn = (Yn / invest_plan) * (Yn / YN_fact_invest)
double coordinator_score =
(static_cast<double>(coordinator_investments.amount) / static_cast<double>(plan_total_investments.amount)) *
(static_cast<double>(coordinator_investments.amount) / static_cast<double>(total_coordinator_investments.amount));
// Zn = Rn / RN * coordinator_base_fact
double share_ratio = coordinator_score / all_coordinators_scores;
int64_t coordinator_share = static_cast<int64_t>(
static_cast<double>(coordinator_base_pool.amount) * share_ratio
);
return eosio::asset(coordinator_share, _root_govern_symbol);
}
/**
* @brief Рассчитывает промежуточный балл R для координатора
*/
inline double calculate_coordinator_score(
const eosio::asset &coordinator_investments,
const eosio::asset &total_coordinator_investments,
const eosio::asset &plan_total_investments
) {
if (coordinator_investments.amount == 0 ||
total_coordinator_investments.amount == 0 ||
plan_total_investments.amount == 0) {
return 0.0;
}
return (static_cast<double>(coordinator_investments.amount) / static_cast<double>(plan_total_investments.amount)) *
(static_cast<double>(coordinator_investments.amount) / static_cast<double>(total_coordinator_investments.amount));
}
/**
* @brief Создает или обновляет запись координатора в таблице segments.
* @param coopname Имя кооператива (scope таблицы).
* @param project_hash Хэш проекта.
* @param coordinator_username Имя пользователя координатора.
* @param rised_amount Сумма привлеченных средств.
*/
void upsert_coordinator_segment(eosio::name coopname, const checksum256 &project_hash,
eosio::name coordinator_username, const eosio::asset &rised_amount) {
Segments::segments_index segments(_capital, coopname.value);
auto exist_segment = Segments::get_segment(coopname, project_hash, coordinator_username);
if (!exist_segment.has_value()) {
segments.emplace(_capital, [&](auto &g){
g.id = segments.available_primary_key();
g.coopname = coopname;
g.project_hash = project_hash;
g.username = coordinator_username;
g.coordinator_investments = rised_amount;
g.is_coordinator = true;
// Не устанавливаем CRPS поля, т.к. используем пропорциональное распределение
});
Capital::Projects::increment_total_coordinators(coopname, project_hash);
} else {
auto segment = segments.find(exist_segment->id);
segments.modify(segment, _capital, [&](auto &g) {
if (!g.is_coordinator) {
g.is_coordinator = true;
Capital::Projects::increment_total_coordinators(coopname, project_hash);
}
g.coordinator_investments += rised_amount;
});
}
}
/**
* @brief Обновляет награды координатора в сегменте на основе пропорционального распределения
*/
void refresh_coordinator_segment(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
// Если нет координаторов или премий к распределению, ничего не делаем
if (project.counts.total_coordinators == 0 || project.fact.coordinators_base_pool.amount == 0) {
return;
}
// Получаем всех координаторов проекта
auto coordinators = Capital::Segments::get_project_coordinators(coopname, project_hash);
if (coordinators.empty()) {
return;
}
// Рассчитываем общую сумму промежуточных баллов R всех координаторов
double total_coordinators_score = 0.0;
for (const auto& coord : coordinators) {
total_coordinators_score += calculate_coordinator_score(
coord.coordinator_investments,
project.fact.coordinators_investment_pool,
project.plan.total_received_investments
);
}
if (total_coordinators_score <= 0.0) {
return; // Нет баллов для распределения
}
// Обновляем координаторские доли для всех координаторов проекта
Segments::segments_index segments(_capital, coopname.value);
for (const auto& coord : coordinators) {
auto segment_it = segments.find(coord.id);
eosio::asset new_coordinator_base = calculate_coordinator_proportional_share(
coord.coordinator_investments,
project.fact.coordinators_investment_pool,
project.plan.total_received_investments,
project.fact.coordinators_base_pool,
total_coordinators_score
);
segments.modify(segment_it, _capital, [&](auto &s) {
s.coordinator_base = new_coordinator_base;
});
}
}
}// namespace Capital::Core
@@ -17,6 +17,7 @@ namespace Capital::Core {
if (!exist_segment.has_value()) {
segments.emplace(_capital, [&](auto &g){
g.id = segments.available_primary_key();
g.coopname = coopname;
g.project_hash = project_hash;
g.username = username;
g.creator_base = delta_amounts.creators_base_pool;
@@ -6,7 +6,7 @@ using std::string;
// Подключение всех CRPS реализаций
#include "author_crps.cpp"
#include "contributor_crps.cpp"
#include "coordinator_crps.cpp"
#include "coordinator_share.cpp" // Пропорциональное распределение для координаторов
#include "creator_crps.cpp"
#include "program_crps.cpp"
@@ -40,7 +40,7 @@ namespace Capital::Core {
}
/**
* @brief Пересчитывает доступную сумму к компенсации на основе инвестиций с учетом return_cost_coefficient
* @brief Пересчитывает доступную сумму к компенсации на основе инвестиций с учетом return_base_percent
*/
void refresh_provisional_amount(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
auto segment_opt = Segments::get_segment(coopname, project_hash, username);
@@ -62,47 +62,28 @@ namespace Capital::Core {
segments.modify(segment_it, _capital, [&](auto &s) {
s.provisional_amount = eosio::asset(0, _root_govern_symbol);
s.last_known_invest_pool = project.fact.invest_pool; // Все равно синхронизируем инвестиции
s.last_known_creators_base_pool = project.fact.creators_base_pool; // Синхронизируем с актуальной суммой базового пула создателей
});
return;
}
// Рассчитываем provisional_amount с учетом return_cost_coefficient
double return_coefficient = project.fact.return_cost_coefficient;
// Рассчитываем provisional_amount с учетом return_base_percent (в процентах)
double return_coefficient_percent = project.fact.return_base_percent;
// Ограничиваем коэффициент максимум 1.0 (100% компенсации)
if (return_coefficient > 1.0) {
return_coefficient = 1.0;
}
// provisional_amount = себестоимость_труда * return_cost_coefficient
// provisional_amount = себестоимость_труда * (return_base_percent / 100)
int64_t provisional_amount = static_cast<int64_t>(
static_cast<double>(user_base_amount) * return_coefficient
static_cast<double>(user_base_amount) * (return_coefficient_percent / 100.0)
);
// Общая сумма базовых полей всех ролей в проекте
int64_t total_base_amount = project.fact.creators_base_pool.amount +
project.fact.authors_base_pool.amount +
project.fact.coordinators_base_pool.amount;
eosio::check(provisional_amount <= project.fact.invest_pool.amount, "Cумма доступной ссуды не может превышать сумму инвестиций проекта");
// Если есть инвестиции, проверяем что provisional_amount не превышает долю в инвестициях
if (project.fact.invest_pool.amount > 0 && total_base_amount > 0) {
int64_t user_investment_share = static_cast<int64_t>(
(static_cast<double>(user_base_amount) * static_cast<double>(project.fact.invest_pool.amount)) /
static_cast<double>(total_base_amount)
);
// provisional_amount не может превышать долю в инвестициях
if (provisional_amount > user_investment_share) {
provisional_amount = user_investment_share;
}
}
// Обновляем provisional_amount и last_known_invest_pool в сегменте
// Обновляем provisional_amount и синхронизируем известные пулы в сегменте
Segments::segments_index segments(_capital, coopname.value);
auto segment_it = segments.find(segment_opt->id);
segments.modify(segment_it, _capital, [&](auto &s) {
s.provisional_amount = eosio::asset(provisional_amount, _root_govern_symbol);
s.last_known_invest_pool = project.fact.invest_pool; // Синхронизируем с актуальной суммой инвестиций
s.last_known_creators_base_pool = project.fact.creators_base_pool; // Синхронизируем с актуальной суммой базового пула создателей
});
}
@@ -119,6 +100,7 @@ namespace Capital::Core {
if (!exist_segment.has_value()) {
segments.emplace(_capital, [&](auto &g){
g.id = segments.available_primary_key();
g.coopname = coopname;
g.project_hash = project_hash;
g.username = username;
g.investor_amount = investor_amount;
@@ -196,11 +178,14 @@ namespace Capital::Core {
auto segment = segments.find(segment_opt->id);
segments.modify(segment, _capital, [&](auto &s) {
// Рассчитываем фактически используемую сумму инвестора
// Рассчитываем фактически используемую сумму инвестора (используем коэффициент возврата инвестиций)
s.investor_base = Capital::Core::Generation::calculate_investor_used_amount(
s.investor_amount,
project.fact.return_cost_coefficient
project.fact.use_invest_percent
);
// Синхронизируем известные пулы для корректного определения необходимости обновления сегмента
s.last_known_invest_pool = project.fact.invest_pool;
s.last_known_creators_base_pool = project.fact.creators_base_pool;
});
}
@@ -20,48 +20,81 @@ namespace Capital::Core::Generation {
/**
* @brief Функция расчета коэффициента возврата себестоимости (для фактических показателей)
*/
double calculate_return_cost_coefficient(const fact_pool& current_pools) {
// invest_pool теперь содержит все инвестиции (включая программные)
if (current_pools.invest_pool.amount == 0) {
double calculate_return_base_percent(eosio::asset creators_base_pool, eosio::asset authors_base_pool, eosio::asset coordinators_base_pool, eosio::asset invest_pool) {
// Только трудозатраты (без расходов)
double work_costs = static_cast<double>(
creators_base_pool.amount +
authors_base_pool.amount +
coordinators_base_pool.amount
);
// Если нет трудозатрат, коэффициент = 0%
if (work_costs == 0.0) {
return 0.0;
}
double total_costs = static_cast<double>(
current_pools.creators_base_pool.amount +
current_pools.authors_base_pool.amount +
current_pools.coordinators_base_pool.amount +
current_pools.target_expense_pool.amount
);
double available_investments = static_cast<double>(invest_pool.amount);
double total_investments = static_cast<double>(current_pools.invest_pool.amount);
return total_costs / total_investments;
// Коэффициент в процентах показывает какую долю себестоимости можно компенсировать
// Не может быть больше 100.0% компенсации
double coefficient_percent = (available_investments / work_costs) * 100.0;
return coefficient_percent > 100.0 ? 100.0 : coefficient_percent;
}
/**
* @brief Функция расчета коэффициента возврата себестоимости (для плановых показателей)
*/
double calculate_return_cost_coefficient(const plan_pool& current_pools) {
// invest_pool теперь содержит все инвестиции (включая программные)
if (current_pools.invest_pool.amount == 0) {
* @brief Функция расчета коэффициента возврата инвестиций для плановых показателей
*/
double calculate_use_invest_percent_planned(eosio::asset creators_base_pool, eosio::asset authors_base_pool, eosio::asset coordinators_base_pool, eosio::asset target_expense_pool, eosio::asset total_received_investments) {
// Используем общую сумму инвестиций
if (total_received_investments.amount == 0) {
return 0.0;
}
// Все затраты проекта (трудозатраты + расходы)
double total_costs = static_cast<double>(
current_pools.creators_base_pool.amount +
current_pools.authors_base_pool.amount +
current_pools.coordinators_base_pool.amount +
current_pools.target_expense_pool.amount
creators_base_pool.amount +
authors_base_pool.amount +
coordinators_base_pool.amount +
target_expense_pool.amount
);
double total_investments = static_cast<double>(current_pools.invest_pool.amount);
double total_investments = static_cast<double>(total_received_investments.amount);
return total_costs / total_investments;
// Коэффициент в процентах показывает долю инвестиций, которая используется
// Физически не может быть больше 100.0% использования
double coefficient_percent = (total_costs / total_investments) * 100.0;
return coefficient_percent > 100.0 ? 100.0 : coefficient_percent;
}
/**
* @brief Функция расчета коэффициента возврата инвестиций для фактических показателей
*/
double calculate_use_invest_percent(eosio::asset creators_base_pool, eosio::asset authors_base_pool, eosio::asset coordinators_base_pool, eosio::asset accumulated_expense_pool, eosio::asset used_expense_pool, eosio::asset total_received_investments) {
// Используем общую сумму инвестиций
if (total_received_investments.amount == 0) {
return 0.0;
}
// Все затраты проекта (трудозатраты + фактические расходы)
double total_costs = static_cast<double>(
creators_base_pool.amount +
authors_base_pool.amount +
coordinators_base_pool.amount +
accumulated_expense_pool.amount +
used_expense_pool.amount
);
double total_investments = static_cast<double>(total_received_investments.amount);
// Коэффициент в процентах показывает долю инвестиций, которая используется
// Физически не может быть больше 100.0% использования
double coefficient_percent = (total_costs / total_investments) * 100.0;
return coefficient_percent > 100.0 ? 100.0 : coefficient_percent;
}
/**
* @brief Функция расчета плановых показателей проекта
*/
* @brief Функция расчета плановых показателей проекта
*/
plan_pool calculate_plan_generation_amounts(
eosio::name coopname,
const eosio::asset& plan_hour_cost,
@@ -94,7 +127,7 @@ namespace Capital::Core::Generation {
// расчет планируемых премий координаторов
plan.coordinators_base_pool = (plan.creators_base_pool + plan.authors_base_pool + plan.target_expense_pool) * st.config.coordinator_bonus_percent;
plan.coordinators_base_pool = (plan.creators_base_pool + plan.authors_base_pool + plan.target_expense_pool) * st.config.coordinator_bonus_percent / 100;
// расчет премий создателя
double creators_bonus_double = static_cast<double>(plan.creators_base_pool.amount * CREATORS_BONUS_COEFFICIENT);
@@ -104,15 +137,19 @@ namespace Capital::Core::Generation {
double authors_bonus_double = static_cast<double>(plan.authors_base_pool.amount * AUTHOR_BONUS_COEFFICIENT);
plan.authors_bonus_pool = eosio::asset(int64_t(authors_bonus_double), _root_govern_symbol);
// расчет планируемой суммы инвестиций
plan.invest_pool = plan.creators_base_pool + plan.authors_base_pool + plan.coordinators_base_pool + plan.target_expense_pool;
// расчет общей планируемой суммы инвестиций (все затраты)
plan.total_received_investments = plan.creators_base_pool + plan.authors_base_pool + plan.coordinators_base_pool + plan.target_expense_pool;
// расчет планируемой суммы инвестиций (только для трудозатрат, после вычета расходов)
plan.invest_pool = plan.creators_base_pool + plan.authors_base_pool + plan.coordinators_base_pool;
// расчет суммы, которую координаторы привлекут в проект
// считаем, что координаторы будут привлекать всю сумму инвестиций
plan.coordinators_investment_pool = plan.invest_pool;
// считаем, что координаторы будут привлекать всю общую сумму инвестиций
plan.coordinators_investment_pool = plan.total_received_investments;
// коэффициент возврата себестоимости для плана будет равен 1, т.к. мы планируем привлечение инвестиций в полном размере себестоимости
plan.return_cost_coefficient = calculate_return_cost_coefficient(plan);
// коэффициенты возврата
plan.return_base_percent = calculate_return_base_percent(plan.creators_base_pool, plan.authors_base_pool, plan.coordinators_base_pool, plan.invest_pool);
plan.use_invest_percent = calculate_use_invest_percent_planned(plan.creators_base_pool, plan.authors_base_pool, plan.coordinators_base_pool, plan.target_expense_pool, plan.total_received_investments);
plan.total_generation_pool = plan.creators_base_pool + plan.authors_base_pool + plan.coordinators_base_pool + plan.creators_bonus_pool + plan.authors_bonus_pool;
@@ -173,7 +210,7 @@ namespace Capital::Core::Generation {
eosio::symbol sym = investment_amount.symbol;
// Рассчитываем премию координатора от инвестиций
double k = st.config.coordinator_bonus_percent;
double k = st.config.coordinator_bonus_percent / 100;
return eosio::asset(int64_t(amount * k / (1 + k)), sym);
}
@@ -204,22 +241,16 @@ namespace Capital::Core::Generation {
};
/**
* @brief Рассчитывает фактически используемую сумму инвестора с учетом коэффициента возврата
* @param investor_base Базовая сумма инвестора
* @param return_cost_coefficient Коэффициент возврата себестоимости проекта
* @brief Рассчитывает фактически используемую сумму инвестора с учетом коэффициента использования
* @param investor_amount Общая сумма инвестора
* @param use_invest_percent_percent Коэффициент используемых инвестиций в процентах (от 0.0 до 100.0)
* @return Фактически используемая сумма инвестора
*/
eosio::asset calculate_investor_used_amount(const eosio::asset& investor_base, double return_cost_coefficient) {
if (return_cost_coefficient <= 1.0) {
// Если коэффициент <= 1, то все средства инвестора используются
return investor_base;
}
eosio::asset calculate_investor_used_amount(const eosio::asset& investor_amount, double use_invest_percent_percent) {
// Используемая сумма = общая_сумма * (коэффициент_процентов / 100)
double used_amount = static_cast<double>(investor_amount.amount) * (use_invest_percent_percent / 100.0);
// Если коэффициент > 1, то используется только часть средств инвестора
// Используемая сумма = базовая_сумма / коэффициент
double used_amount = static_cast<double>(investor_base.amount) / return_cost_coefficient;
return eosio::asset(static_cast<int64_t>(used_amount), investor_base.symbol);
return eosio::asset(static_cast<int64_t>(used_amount), investor_amount.symbol);
}
/**
@@ -245,7 +276,8 @@ namespace Capital::Core::Generation {
p.fact.coordinators_base_pool += coordinator_base;
// Пересчитываем коэффициент возврата себестоимости
p.fact.return_cost_coefficient = calculate_return_cost_coefficient(p.fact);
p.fact.return_base_percent = calculate_return_base_percent(p.fact.creators_base_pool, p.fact.authors_base_pool, p.fact.coordinators_base_pool, p.fact.invest_pool);
p.fact.use_invest_percent = calculate_use_invest_percent(p.fact.creators_base_pool, p.fact.authors_base_pool, p.fact.coordinators_base_pool, p.fact.accumulated_expense_pool, p.fact.used_expense_pool, p.fact.total_received_investments);
// Пересчитываем премии вкладчиков, т.к. премии координаторов влияют на премии вкладчиков
p.fact.contributors_bonus_pool = calculate_contributors_bonus_pool(p.fact.total_generation_pool);
@@ -257,8 +289,8 @@ namespace Capital::Core::Generation {
p.fact.total = p.fact.total_contribution + p.fact.used_expense_pool;
});
// Распределяем награды координаторов через CRPS систему
Capital::Core::update_coordinator_crps(coopname, project_hash, coordinator_base);
// Примечание: Координаторские награды будут распределены пропорционально при обновлении сегментов
// через refresh_coordinator_segment на основе привлеченных каждым координатором средств
}
} // namespace Capital::Core
@@ -43,12 +43,17 @@ namespace Capital::Core::Generation {
/**
* @brief Функция расчета коэффициента возврата себестоимости (для фактических показателей)
*/
double calculate_return_cost_coefficient(const fact_pool& current_pools);
double calculate_return_base_percent(eosio::asset creators_base_pool, eosio::asset authors_base_pool, eosio::asset coordinators_base_pool, eosio::asset invest_pool);
/**
* @brief Функция расчета коэффициента возврата себестоимости (для плановых показателей)
* @brief Функция расчета коэффициента используемых инвестиций для плановых показателей
*/
double calculate_return_cost_coefficient(const plan_pool& current_pools);
double calculate_use_invest_percent_planned(eosio::asset creators_base_pool, eosio::asset authors_base_pool, eosio::asset coordinators_base_pool, eosio::asset target_expense_pool, eosio::asset total_received_investments);
/**
* @brief Функция расчета коэффициента используемых инвестиций для фактических показателей
*/
double calculate_use_invest_percent(eosio::asset creators_base_pool, eosio::asset authors_base_pool, eosio::asset coordinators_base_pool, eosio::asset accumulated_expense_pool, eosio::asset used_expense_pool, eosio::asset total_received_investments);
/**
* @brief Распределение паевых средств проекта
@@ -56,9 +61,9 @@ namespace Capital::Core::Generation {
void distribute_project_membership_funds(eosio::name coopname, uint64_t project_id, asset amount, uint8_t level);
/**
* @brief Рассчитывает фактически используемую сумму инвестора с учетом коэффициента возврата
* @brief Рассчитывает фактически используемую сумму инвестора с учетом коэффициента использования
*/
eosio::asset calculate_investor_used_amount(const eosio::asset& investor_base, double return_cost_coefficient);
eosio::asset calculate_investor_used_amount(const eosio::asset& investor_amount, double use_invest_percent);
/**
* @brief Добавляет координаторские средства к проекту
@@ -43,7 +43,7 @@ namespace Capital::Core {
if (expense_gap.amount > 0) {
// Рассчитываем процент от инвестиций для пула расходов
auto st = Capital::get_global_state(coopname);
eosio::asset potential_to_expense = amount * st.config.expense_pool_percent;
eosio::asset potential_to_expense = amount * st.config.expense_pool_percent / 100;
// Но не больше, чем нужно для достижения цели
to_expense_pool = (potential_to_expense.amount <= expense_gap.amount) ? potential_to_expense : expense_gap;
@@ -58,9 +58,11 @@ namespace Capital::Core {
// Также фиксируем в program_invest_pool для отслеживания программных средств
p.fact.program_invest_pool += to_invest_pool;
p.fact.accumulated_expense_pool += to_expense_pool;
p.fact.total_received_investments += amount; // Увеличиваем общую сумму полученных инвестиций
// Пересчитываем коэффициент возврата себестоимости
p.fact.return_cost_coefficient = Capital::Core::Generation::calculate_return_cost_coefficient(p.fact);
// Пересчитываем коэффициенты возврата
p.fact.return_base_percent = Capital::Core::Generation::calculate_return_base_percent(p.fact.creators_base_pool, p.fact.authors_base_pool, p.fact.coordinators_base_pool, p.fact.invest_pool);
p.fact.use_invest_percent = Capital::Core::Generation::calculate_use_invest_percent(p.fact.creators_base_pool, p.fact.authors_base_pool, p.fact.coordinators_base_pool, p.fact.accumulated_expense_pool, p.fact.used_expense_pool, p.fact.total_received_investments);
});
// Списываем средства из глобального пула
@@ -37,20 +37,49 @@ typedef eosio::multi_index<
indexed_by<"byprojuser"_n, const_mem_fun<appendix, uint128_t, &appendix::by_project_user>>
> appendix_index;
}// namespace Capital
/**
* @brief Получает приложение по хэшу
*/
inline std::optional<appendix> get_appendix(eosio::name coopname, const checksum256 &appendix_hash) {
appendix_index appendixes(_capital, coopname.value);
auto by_hash = appendixes.get_index<"byhash"_n>();
auto itr = by_hash.find(appendix_hash);
namespace Capital::Appendix {
inline void create_appendix(eosio::name coopname, eosio::name username, checksum256 project_hash, checksum256 appendix_hash, document2 document){
Capital::appendix_index appendixes(_capital, coopname.value);
auto appendix_id = get_global_id_in_scope(_capital, coopname, "appendixes"_n);
if (itr == by_hash.end()) {
return std::nullopt;
}
return *itr;
appendixes.emplace(coopname, [&](auto &a) {
a.id = appendix_id;
a.coopname = coopname;
a.username = username;
a.project_hash = project_hash;
a.appendix_hash = appendix_hash;
a.status = Capital::Appendix::Status::CREATED;
a.created_at = current_time_point();
a.appendix = document;
});
}
/**
* @brief Получает приложение по хэшу
*/
inline std::optional<appendix> get_appendix(eosio::name coopname, const checksum256 &appendix_hash) {
appendix_index appendixes(_capital, coopname.value);
auto by_hash = appendixes.get_index<"byhash"_n>();
auto itr = by_hash.find(appendix_hash);
if (itr == by_hash.end()) {
return std::nullopt;
}
return *itr;
}
}// namespace Capital
/**
* @brief Удаляет приложение из таблицы appendixes
*/
inline void delete_appendix(eosio::name coopname, uint64_t appendix_id){
// Удаляем запись из таблицы appendixes
Capital::appendix_index appendixes(_capital, coopname.value);
auto itr = appendixes.find(appendix_id);
appendixes.erase(itr);
}
}// namespace Capital::Appendix
@@ -26,17 +26,34 @@ namespace Capital {
eosio::asset capital_available = asset(0, _root_govern_symbol); ///< Накопленные членские взносы по программе капитализации
int64_t reward_per_share_last = 0; ///< Последний зафиксированный cumulative_reward_per_share по программе капитализации
eosio::asset contributed_as_investor = asset(0, _root_govern_symbol); ///< Сумма, вложенная в проект как инвестор
eosio::asset contributed_as_creator = asset(0, _root_govern_symbol); ///< Сумма, вложенная в проект как создатель
eosio::asset contributed_as_author = asset(0, _root_govern_symbol); ///< Сумма, вложенная в проект как автор
eosio::asset contributed_as_coordinator = asset(0, _root_govern_symbol); ///< Сумма, вложенная в проект как координатор
eosio::asset contributed_as_contributor = asset(0, _root_govern_symbol); ///< Сумма, вложенная в проект как контрибьютор
uint64_t primary_key() const { return id; }
uint64_t by_username() const { return username.value; }
checksum256 by_hash() const { return contributor_hash; }
uint64_t by_investor_rating() const { return contributed_as_investor.amount; }
uint64_t by_creator_rating() const { return contributed_as_creator.amount; }
uint64_t by_author_rating() const { return contributed_as_author.amount; }
uint64_t by_coordinator_rating() const { return contributed_as_coordinator.amount; }
uint64_t by_contributor_rating() const { return contributed_as_contributor.amount; }
uint64_t by_rating() const { return contributed_as_investor.amount + contributed_as_creator.amount + contributed_as_author.amount + contributed_as_coordinator.amount + contributed_as_contributor.amount; }
};
typedef eosio::multi_index<
"contributors"_n, contributor,
indexed_by<"byusername"_n, const_mem_fun<contributor, uint64_t, &contributor::by_username>>,
indexed_by<"byhash"_n, const_mem_fun<contributor, checksum256, &contributor::by_hash>>
indexed_by<"byhash"_n, const_mem_fun<contributor, checksum256, &contributor::by_hash>>,
indexed_by<"byinvrate"_n, const_mem_fun<contributor, uint64_t, &contributor::by_investor_rating>>,
indexed_by<"bycreatorate"_n, const_mem_fun<contributor, uint64_t, &contributor::by_creator_rating>>,
indexed_by<"byauthorate"_n, const_mem_fun<contributor, uint64_t, &contributor::by_author_rating>>,
indexed_by<"bycoordrate"_n, const_mem_fun<contributor, uint64_t, &contributor::by_coordinator_rating>>,
indexed_by<"bycontrate"_n, const_mem_fun<contributor, uint64_t, &contributor::by_contributor_rating>>,
indexed_by<"byrating"_n, const_mem_fun<contributor, uint64_t, &contributor::by_rating>>
> contributor_index;
}// namespace Capital
@@ -49,6 +66,36 @@ namespace Capital::Contributors {
const eosio::name PENDING = "pending"_n; ///< Ожидает подтверждения
const eosio::name ACTIVE = "active"_n; ///< Авторизован/активен
}
inline void create_contributor(eosio::name coopname, eosio::name username, checksum256 contributor_hash, bool is_external_contract, document2 contract, eosio::asset rate_per_hour){
Capital::contributor_index contributors(_capital, coopname.value);
contributors.emplace(coopname, [&](auto &c) {
c.id = get_global_id_in_scope(_capital, coopname, "contributors"_n);
c.coopname = coopname;
c.username = username;
c.contributor_hash = contributor_hash;
c.status = Capital::Contributors::Status::PENDING;
c.is_external_contract = is_external_contract;
c.contract = contract;
c.created_at = eosio::current_time_point();
c.rate_per_hour = rate_per_hour;
});
}
/**
* @brief Добавляет project_hash в вектор appendixes у контрибьютора
*/
inline void push_appendix_to_contributor(eosio::name coopname, eosio::name username, checksum256 project_hash){
contributor_index contributors(_capital, coopname.value);
auto username_index = contributors.get_index<"byusername"_n>();
auto itr = username_index.find(username.value);
username_index.modify(*itr, _capital, [&](auto &c) {
c.appendixes.push_back(project_hash);
});
}
/**
* @brief Получает участника по имени аккаунта.
@@ -125,4 +172,36 @@ inline std::optional<contributor> get_active_contributor_with_appendix_or_fail(e
return contributor;
}
/**
* @brief Обновляет накопительные показатели контрибьютора на основе его ролей и вкладов в сегменте
*/
inline void update_contributor_ratings_from_segment(eosio::name coopname, const Capital::Segments::segment& segment) {
Capital::contributor_index contributors(_capital, coopname.value);
auto contributor_itr = contributors.find(segment.username.value);
eosio::check(contributor_itr != contributors.end(), "Контрибьютор не найден");
contributors.modify(contributor_itr, _capital, [&](auto &c) {
if (segment.is_investor) {
c.contributed_as_investor += segment.investor_base;
}
if (segment.is_author) {
c.contributed_as_author += (segment.author_base + segment.author_bonus);
}
if (segment.is_creator) {
c.contributed_as_creator += (segment.creator_base + segment.creator_bonus);
}
if (segment.is_coordinator) {
c.contributed_as_coordinator += segment.coordinator_base;
}
if (segment.is_contributor) {
c.contributed_as_contributor += segment.contributor_bonus;
}
});
}
}// namespace Capital::Contributors
@@ -15,8 +15,9 @@ struct fact_pool {
eosio::asset hour_cost = asset(0, _root_govern_symbol); ///< Фактическая стоимость часа
uint64_t creators_hours = 0; ///< Фактическое время в часах
// Коэффициент возврата себестоимости
double return_cost_coefficient = 0.0;
// Коэффициенты возврата (в процентах)
double return_base_percent = 0.0; ///< Коэффициент возврата себестоимости для создателей/авторов/координаторов (0-100%)
double use_invest_percent = 0.0; ///< Коэффициент используемых инвестиций для инвесторов (0-100%)
// Пулы себестоимостей
eosio::asset creators_base_pool = asset(0, _root_govern_symbol); ///< Фактическая себестоимость создателей
@@ -34,9 +35,11 @@ struct fact_pool {
eosio::asset used_expense_pool = asset(0, _root_govern_symbol); ///< Фактически израсходованные средства
// Пулы инвестиций
eosio::asset invest_pool = asset(0, _root_govern_symbol); ///< Фактические инвестиции от инвесторов
eosio::asset invest_pool = asset(0, _root_govern_symbol); ///< Фактические инвестиции от инвесторов (после вычета расходов)
eosio::asset coordinators_investment_pool = asset(0, _root_govern_symbol); ///< Фактические инвестиции, привлеченные координаторами
eosio::asset program_invest_pool = asset(0, _root_govern_symbol); ///< Фактические программные инвестиции, аллоцированные в проект
eosio::asset total_received_investments = asset(0, _root_govern_symbol); ///< Общая сумма всех полученных инвестиций (до распределения)
eosio::asset total_returned_investments = asset(0, _root_govern_symbol); ///< Общая сумма возвращенных неиспользованных инвестиций
// Общая сумма генерации (без вкладчиков)
eosio::asset total_generation_pool = asset(0, _root_govern_symbol); ///< Общая фактическая сумма генерации
@@ -35,9 +35,6 @@ struct generation_amounts {
// Общая сумма вкладов всех пайщиков
eosio::asset total_contribution = asset(0, _root_govern_symbol); ///< Общая сумма вкладов всех пайщиков
// Общая сумма вкладов с расходами
eosio::asset total = asset(0, _root_govern_symbol); ///< Общая сумма вкладов с расходами
};
} // namespace Capital
@@ -9,8 +9,8 @@ namespace Capital {
* @brief Конфигурация контракта, управляемая пользователем.
*/
struct config {
double coordinator_bonus_percent = 0.04; ///< Процент премий координатора от инвестиций (по умолчанию 4%)
double expense_pool_percent = 1.0; ///< Процент инвестиций в пул расходов (по умолчанию 1.0)
double coordinator_bonus_percent = 4; ///< Процент премий координатора от инвестиций (по умолчанию 4%)
double expense_pool_percent = 100; ///< Процент инвестиций в пул расходов (по умолчанию 1.0)
uint32_t coordinator_invite_validity_days = 30; ///< Срок действия приглашения координатора (по умолчанию 30 дней)
uint32_t voting_period_in_days = 7; ///< Период голосования в днях (по умолчанию 7)
double authors_voting_percent = 38.2; ///< Процент премий авторов для голосования (по умолчанию)
@@ -15,8 +15,9 @@ struct plan_pool {
eosio::asset hour_cost = asset(0, _root_govern_symbol); ///< Планируемая стоимость часа
uint64_t creators_hours = 0; ///< Планируемое время в часах
// Коэффициент возврата себестоимости
double return_cost_coefficient = 0.0;
// Коэффициенты возврата (в процентах)
double return_base_percent = 0.0; ///< Коэффициент возврата себестоимости для создателей/авторов/координаторов (0-100%)
double use_invest_percent = 0.0; ///< Коэффициент используемых инвестиций для инвесторов (0-100%)
// Пулы себестоимостей
eosio::asset creators_base_pool = asset(0, _root_govern_symbol); ///< Планируемая себестоимость создателей
@@ -32,9 +33,10 @@ struct plan_pool {
eosio::asset target_expense_pool = asset(0, _root_govern_symbol); ///< Планируемый размер расходов
// Пулы инвестиций
eosio::asset invest_pool = asset(0, _root_govern_symbol); ///< Планируемые инвестиции
eosio::asset invest_pool = asset(0, _root_govern_symbol); ///< Планируемые инвестиции (после вычета расходов)
eosio::asset coordinators_investment_pool = asset(0, _root_govern_symbol); ///< Планируемые инвестиции от координаторов
eosio::asset program_invest_pool = asset(0, _root_govern_symbol); ///< Планируемые программные инвестиции
eosio::asset total_received_investments = asset(0, _root_govern_symbol); ///< Планируемая общая сумма инвестиций (до распределения)
// Общая сумма генерации (без вкладчиков)
eosio::asset total_generation_pool = asset(0, _root_govern_symbol); ///< Общая планируемая сумма генерации
@@ -17,8 +17,7 @@ namespace Capital::Projects::Status {
/**
* @brief Константы статусов проекта
*/
const eosio::name CREATED = "created"_n; ///< Проект создан
const eosio::name OPENED = "opened"_n; ///< Проект открыт для инвестиций
const eosio::name PENDING = "pending"_n; ///< Проект создан
const eosio::name ACTIVE = "active"_n; ///< Проект активен для коммитов
const eosio::name VOTING = "voting"_n; ///< Проект на голосовании
const eosio::name COMPLETED = "completed"_n; ///< Проект завершен
@@ -38,7 +37,9 @@ struct [[eosio::table, eosio::contract(CAPITAL)]] project {
checksum256 parent_hash; // Хэш родительского проекта (если есть)
eosio::name status; // Статус проекта
bool is_opened; // Открыт ли проект для инвестиций
// Мастер проекта
name master; // Мастер проекта
@@ -140,7 +141,7 @@ namespace Capital::Projects {
projects.emplace(coopname, [&](auto& row) {
row.id = get_global_id_in_scope(_capital, coopname, "projects"_n);
row.status = Capital::Projects::Status::CREATED;
row.status = Capital::Projects::Status::PENDING;
row.project_hash = project_hash;
row.parent_hash = parent_hash;
row.coopname = coopname;
@@ -184,9 +185,30 @@ namespace Capital::Projects {
p.fact.total_generation_pool += delta.total_generation_pool;
p.fact.contributors_bonus_pool += delta.contributors_bonus_pool;
p.fact.total_contribution += delta.total_contribution;
p.fact.total += p.fact.total_contribution + p.fact.used_expense_pool;
p.fact.total = p.fact.total_contribution + p.fact.used_expense_pool;
// Пересчитываем коэффициенты
p.fact.return_base_percent = Capital::Core::Generation::calculate_return_base_percent(p.fact.creators_base_pool, p.fact.authors_base_pool, p.fact.coordinators_base_pool, p.fact.invest_pool);
p.fact.use_invest_percent = Capital::Core::Generation::calculate_use_invest_percent(p.fact.creators_base_pool, p.fact.authors_base_pool, p.fact.coordinators_base_pool, p.fact.accumulated_expense_pool, p.fact.used_expense_pool, p.fact.total_received_investments);
});
}
/**
* @brief Назначает мастера проекта
*
* @param coopname Имя кооператива
* @param project_id ID проекта
* @param master Имя мастера
*/
inline void set_master(eosio::name coopname, uint64_t project_id, eosio::name master) {
Capital::project_index projects(_capital, coopname.value);
auto project_itr = projects.find(project_id);
projects.modify(project_itr, coopname, [&](auto &p) {
p.master = master;
});
}
/**
* @brief Обновляет статус проекта.
@@ -244,7 +266,7 @@ namespace Capital::Projects {
if (expense_gap.amount > 0) {
// Рассчитываем процент от инвестиций для пула расходов
auto st = Capital::get_global_state(coopname);
eosio::asset potential_to_expense = amount * st.config.expense_pool_percent;
eosio::asset potential_to_expense = amount * st.config.expense_pool_percent / 100;
// Но не больше, чем нужно для достижения цели
to_expense_pool = (potential_to_expense.amount <= expense_gap.amount) ? potential_to_expense : expense_gap;
@@ -256,9 +278,11 @@ namespace Capital::Projects {
// Обновляем пулы
p.fact.invest_pool += to_invest_pool;
p.fact.accumulated_expense_pool += to_expense_pool;
p.fact.total_received_investments += amount; // Увеличиваем общую сумму полученных инвестиций
// Пересчитываем коэффициент возврата себестоимости
p.fact.return_cost_coefficient = Capital::Core::Generation::calculate_return_cost_coefficient(p.fact);
// Пересчитываем коэффициенты возврата
p.fact.return_base_percent = Capital::Core::Generation::calculate_return_base_percent(p.fact.creators_base_pool, p.fact.authors_base_pool, p.fact.coordinators_base_pool, p.fact.invest_pool);
p.fact.use_invest_percent = Capital::Core::Generation::calculate_use_invest_percent(p.fact.creators_base_pool, p.fact.authors_base_pool, p.fact.coordinators_base_pool, p.fact.accumulated_expense_pool, p.fact.used_expense_pool, p.fact.total_received_investments);
});
}
@@ -300,7 +324,7 @@ namespace Capital::Projects {
// так как фактических поступлений и трат еще не было
// Меняем статус на "opened"
p.status = Capital::Projects::Status::OPENED;
p.is_opened = true;
});
}
@@ -23,6 +23,7 @@ namespace Capital::Segments {
struct [[eosio::table, eosio::contract(CAPITAL)]] segment {
uint64_t id; // Идентификатор сегмента
checksum256 project_hash; // С каким проектом связан сегмент
eosio::name coopname; // Имя кооператива
eosio::name username; // Имя участника
// Статус результата сегмента
@@ -69,6 +70,9 @@ namespace Capital::Segments {
// Последняя известная сумма инвестиций в проекте для расчета provisional_amount
eosio::asset last_known_invest_pool = asset(0, _root_govern_symbol);
// Последняя известная сумма базового пула создателей для расчета использования инвестиций
eosio::asset last_known_creators_base_pool = asset(0, _root_govern_symbol);
// Финансовые данные для ссуд
eosio::asset provisional_amount = asset(0, _root_govern_symbol); //доступная сумма для залога при получении ссуды
eosio::asset debt_amount = asset(0, _root_govern_symbol); //сумма, которая уже выдана в ссуду
@@ -299,16 +303,22 @@ inline bool is_segment_updated(eosio::name coopname, const checksum256 &project_
(segment_opt->last_author_base_reward_per_share == project.crps.author_base_cumulative_reward_per_share &&
segment_opt->last_author_bonus_reward_per_share == project.crps.author_bonus_cumulative_reward_per_share);
// Координаторы используют пропорциональное распределение, проверяем что есть изменения в пулах
bool coordinator_updated = (!segment_opt->is_coordinator) ||
(segment_opt->last_coordinator_reward_per_share == project.crps.coordinator_cumulative_reward_per_share);
(project.fact.coordinators_base_pool.amount == 0 ||
project.fact.coordinators_investment_pool.amount == 0);
bool contributor_updated = (!segment_opt->is_contributor) ||
(segment_opt->last_contributor_reward_per_share == project.crps.contributor_cumulative_reward_per_share);
// Проверяем актуальность инвестиционного пула для расчета provisional_amount
// Проверяем актуальность инвестиционного пула для расчета provisional_amount (нужно всем ролям)
bool invest_pool_updated = (segment_opt->last_known_invest_pool == project.fact.invest_pool);
return author_updated && coordinator_updated && contributor_updated && invest_pool_updated;
// Проверяем актуальность базового пула создателей для корректного расчета использования инвестиций (нужно только инвесторам)
bool creators_base_pool_updated = (!segment_opt->is_investor) ||
(segment_opt->last_known_creators_base_pool == project.fact.creators_base_pool);
return author_updated && coordinator_updated && contributor_updated && invest_pool_updated && creators_base_pool_updated;
}
/**
@@ -25,16 +25,15 @@ namespace Capital {
*
*/
struct voting_data {
uint32_t total_voters = 0; ///< Общее количество участников
uint32_t votes_received = 0; ///< Количество полученных голосов
uint32_t total_voters; ///< Общее количество участников
uint32_t votes_received; ///< Количество полученных голосов
double authors_voting_percent = 38.2; ///< Процент премий авторов для голосования (по умолчанию; уточняется из config при инициализации голосования)
double creators_voting_percent = 38.2; ///< Процент премий создателей для голосования (по умолчанию; уточняется из config при инициализации голосования)
double authors_voting_percent; ///< Процент премий авторов для голосования (по умолчанию; уточняется из config при инициализации голосования)
double creators_voting_percent; ///< Процент премий создателей для голосования (по умолчанию; уточняется из config при инициализации голосования)
voting_amounts amounts;
voting_amounts amounts; ///< Суммы на голосовании
time_point_sec created_at = current_time_point();
time_point_sec voting_deadline = time_point_sec(current_time_point().sec_since_epoch() + 7 * 86400); ///< Дедлайн голосования (по умолчанию 7 дней; уточняется из config при старте голосования)
time_point_sec voting_deadline; ///< Дедлайн голосования
};
@@ -7,7 +7,7 @@ export * as SetConfig from './setConfig'
export * as CreateProject from './createProject'
export * as OpenProject from './openProject'
export * as StartProject from './startProject'
export * as CompleteProject from './completeProject'
export * as StartVoting from './startVoting'
export * as CompleteVoting from './completeVoting'
export * as FinalVoting from './finalVoting'
export * as SubmitVote from './submitVote'
@@ -7,9 +7,9 @@ export const authorizations = [{ permissions: [Permissions.active], actor: Actor
/**
* Имя действия
*/
export const actionName = 'cmpltproject'
export const actionName = 'startvoting'
/**
* @interface
*/
export type ICompleteProject = Capital.ICmpltproject
export type IStartVoting = Capital.IStartvoting
@@ -158,7 +158,7 @@ export interface ICloseproject {
project_hash: IChecksum256
}
export interface ICmpltproject {
export interface IStartvoting {
coopname: IName
project_hash: IChecksum256
}
@@ -642,7 +642,6 @@ export interface IRefreshproj {
export interface IRegcontrib {
coopname: IName
application: IName
username: IName
contributor_hash: IChecksum256
rate_per_hour: IAsset
@@ -729,8 +728,7 @@ export interface ISegment {
export interface ISetconfig {
coopname: IName
initiator: IName
cfg: IConfig
config: IConfig
}
export interface ISetmaster {
@@ -741,6 +739,7 @@ export interface ISetmaster {
export interface ISetplan {
coopname: IName
master: IName
project_hash: IChecksum256
plan_creators_hours: IUint64
plan_expenses: IAsset
@@ -767,7 +766,6 @@ export interface ISignact2 {
export interface ISignappndx {
coopname: IName
application: IName
username: IName
project_hash: IChecksum256
appendix_hash: IChecksum256