diff --git a/craun/README.md b/craun/README.md new file mode 100644 index 0000000..af5a1b0 --- /dev/null +++ b/craun/README.md @@ -0,0 +1,34 @@ +# Sport goals tracker CRAUN. + +_Приложение для отслеживания спортивных целей с системой штрафов._ + +Blockchain часть приложения состоит из 2 dApps: +* craunDapp - следит за выполнением целей и управляет балансом пользователя +* goalDapp - хранит конфигурацию цели, текущий статус, количество проваленных попыток и записи фактов исполнения цели + +Как это работает? +1. Создается цель + 1. Пользователь настраивает цель (Например бег 2км. каждые пн., ср., пт., 5 попыток и стоимость пропуска и т.д.) + 2. Создается новый аккаунт цели и затем скриптуется. + 3. Записать пользовательские данные в аккаунт цели через функцию «setup». +2. Отслеживание выполнения цели + 1. Так как одной из задач craunDapp является управление балансом пользователя, то после создания цели, нужно заморозить баланс пользователя на сумму цели (как вычисляется сумма цели см. ниже). + 2. В craunDapp, раз в день проверять статус выполнения цели и принимать решение о снятии средств из замороженного баланса. + +Методы goalDapp: +* func setup(userAddress, title, distance, etc.) - записать данные цели в goalDapp +* func record(recordDate: String, recordDistance: Int) - валидирует выполнение цели и меняет статус цели. + +Методы craunDapp: +* func deposit() - позволяет пополнить баланс пользователя +* func addGoal(goalAccountAddressStr: String) - замораживает средства пользователя исходя из стоимости цели (колличесто попыток * стоимость 1 провала + стоимость удаления). После этого, для аккаунта пользователя создается 3 баланса для хранения замороженных, активных и потерянных средств. +* func checkUserGoal(goalAccountAddressStr, userAccountAddressStr, recordDate) - проверяет исполнение цели на определенную дату, проверяет статус цели и принимает штрафовать ли пользователя. + +_P.S._ +_В будущем можно создать мобильное приложение, которое в атоматическом режиме может фиксировать факты испольнения различных целей с использованием GPS, CV и тд:_ +* бег +* подтягивания на турнике +* отжимания +* приседания +* скручивания на пресс +* и др. \ No newline at end of file diff --git a/craun/craunApp.ride b/craun/craunApp.ride new file mode 100644 index 0000000..8d13664 --- /dev/null +++ b/craun/craunApp.ride @@ -0,0 +1,179 @@ +{-# STDLIB_VERSION 3 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +func isSubstrExist(str: String, substr: String) = match indexOf(str, substr) { + case index: Int => true + case index: Unit => false +} + +func getIntegerFromAccount(accountAddress: Address | Alias, key: String) = match getInteger(accountAddress, key) { + case a: Int => a + case _ => 0 +} + +func getUserBalanceKey(userAccountAddressStr: String, balanceType: String) = { + userAccountAddressStr + "_" + balanceType + "-balance" +} + +@Callable(i) +func deposit() = { + let payment = extract(i.payment) + + if (isDefined(payment.assetId)) then { + throw("you can deposit only waves") + } else { + let userAccountAddress = toBase58String(i.caller.bytes) + + let userAvailableBalanceKey = getUserBalanceKey(userAccountAddress, "available") + + let userBalance = getIntegerFromAccount(this, userAvailableBalanceKey) + + let newUserActiveBalance = userBalance + payment.amount + + WriteSet([ + DataEntry(userAvailableBalanceKey, newUserActiveBalance) + ]) + } +} + +@Callable(i) +func withdraw(amount: Int) = { + let userAccountAddressStr = toBase58String(i.caller.bytes) + let userAccountAddress = addressFromStringValue(userAccountAddressStr) + + let userAvailableBalanceKey = getUserBalanceKey(userAccountAddressStr, "available") + + let userActiveBalance = getIntegerFromAccount(this, userAvailableBalanceKey) + + let newUserActiveBalance = userActiveBalance - amount + + if (amount < 0) then { + throw("Can't withdraw negative amount") + } else if (newUserActiveBalance < 0) then { + throw("Not enough balance") + } else { + ScriptResult( + WriteSet([DataEntry(userAvailableBalanceKey, newUserActiveBalance)]), + TransferSet([ScriptTransfer(userAccountAddress, amount, unit)]) + ) + } +} + +@Callable(i) +func addGoal( + goalAccountAddressStr: String +) = { + let userAccountAddress = toBase58String(i.caller.bytes) + + let goalAccountAddress = addressFromStringValue(goalAccountAddressStr); + + let userAvailableBalanceKey = getUserBalanceKey(userAccountAddress, "available") + let userFrozenBalanceKey = getUserBalanceKey(userAccountAddress, "frozen") + let userLostBalanceKey = getUserBalanceKey(userAccountAddress, "lost") + + let userActiveBalance = getIntegerFromAccount(this, userAvailableBalanceKey) + let userFrozenBalance = getIntegerFromAccount(this, userFrozenBalanceKey) + + let failPenalty = getIntegerFromAccount(goalAccountAddress, "failPenalty") + let archievePenalty = getIntegerFromAccount(goalAccountAddress, "archievePenalty") + let attemptsCount = getIntegerFromAccount(goalAccountAddress, "attemptsCount") + + let frozeAmount = failPenalty * attemptsCount + archievePenalty + + if (frozeAmount > userActiveBalance) then { + throw("You can't create goal, not enough balance on your account to froze") + } + else { + let newUserActiveBalance = userActiveBalance - frozeAmount; + let newUserFrozenBalance = userFrozenBalance + frozeAmount; + + WriteSet([ + DataEntry(userAvailableBalanceKey, newUserActiveBalance), + DataEntry(userFrozenBalanceKey, newUserFrozenBalance) + ]) + } +} + +@Callable(i) +func checkUserGoal( + goalAccountAddressStr: String, + userAccountAddressStr: String, + recordDate: String +) = { + let goalAccountAddress = addressFromStringValue(goalAccountAddressStr); + + let userInactiveGoalsKey = userAccountAddressStr + "_" + "innactive-goals" + + let userInactiveGoals = match getString(this, userInactiveGoalsKey) { + case x: Unit => "" + case x: String => x + } + + let isGoalInactive = isSubstrExist(userInactiveGoals, goalAccountAddressStr); + + if (isGoalInactive) then { + throw("Can't not check user goal execution. The goal is not active.") + } else { + let userAvailableBalanceKey = getUserBalanceKey(userAccountAddressStr, "available") + let userFrozenBalanceKey = getUserBalanceKey(userAccountAddressStr, "frozen") + let userLostBalanceKey = getUserBalanceKey(userAccountAddressStr, "lost") + + let userActiveBalance = getIntegerFromAccount(this, userAvailableBalanceKey) + let userFrozenBalance = getIntegerFromAccount(this, userFrozenBalanceKey) + let userLostBalance = getIntegerFromAccount(this, userLostBalanceKey) + + let failPenalty = getIntegerFromAccount(goalAccountAddress, "failPenalty") + let archievePenalty = getIntegerFromAccount(goalAccountAddress, "archievePenalty") + + let goalStatus = getStringValue(goalAccountAddress, "status") + + if (goalStatus == "archived") then { + WriteSet([ + DataEntry(userAvailableBalanceKey, userActiveBalance + (userFrozenBalance - archievePenalty)), + DataEntry(userFrozenBalanceKey, 0), + DataEntry(userLostBalanceKey, userLostBalance + archievePenalty), + DataEntry(userInactiveGoalsKey, userInactiveGoals + "," + goalAccountAddressStr) + ]) + } else if (goalStatus == "failed") then { + WriteSet([ + DataEntry(userAvailableBalanceKey, userActiveBalance + archievePenalty), + DataEntry(userFrozenBalanceKey, 0), + DataEntry(userInactiveGoalsKey, userInactiveGoals + "," + goalAccountAddressStr) + ]) + } else if (goalStatus == "completed") then { + WriteSet([ + DataEntry(userAvailableBalanceKey, userActiveBalance + userFrozenBalance), + DataEntry(userFrozenBalanceKey, 0), + DataEntry(userInactiveGoalsKey, userInactiveGoals + "," + goalAccountAddressStr) + ]) + } else { + let goalRecordKey = "record" + "_" + recordDate + + let goalRecordValue = match getBoolean(goalAccountAddress, goalRecordKey) { + case a:Boolean => a + case _ => false + } + + if (goalRecordValue == false) then { + let updatedUserFrozenBalance = userFrozenBalance - failPenalty; + let updatedUserLostBalance = userLostBalance + failPenalty; + + WriteSet([ + DataEntry(userFrozenBalanceKey, updatedUserFrozenBalance), + DataEntry(userLostBalanceKey, updatedUserLostBalance) + ]) + } else { + WriteSet([ + DataEntry(userFrozenBalanceKey, userFrozenBalance), + DataEntry(userLostBalanceKey, userLostBalance) + ]) + } + } + } +} + +@Verifier(tx) +func verify() = { + true +} \ No newline at end of file diff --git a/craun/craunGoal.ride b/craun/craunGoal.ride new file mode 100644 index 0000000..4fa689e --- /dev/null +++ b/craun/craunGoal.ride @@ -0,0 +1,89 @@ +{-# STDLIB_VERSION 3 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +func getIntegerFromAccount(accountAddress: Address | Alias, key: String) = match getInteger(accountAddress, key) { + case a: Int => a + case _ => 0 +} + +@Callable(i) +func setup( + userAddress: String, + title: String, + distance: Int, + recordWeekDays: String, + archievePenalty: Int, + failPenalty: Int, + attemptsCount: Int, + startDate: String, + endDate: String +) = { + WriteSet([ + DataEntry("userAddress", userAddress), + DataEntry("status", "running"), #running | archived | failed | completed + DataEntry("title", title), + DataEntry("distance", distance), + DataEntry("recordWeekDays", recordWeekDays), + DataEntry("archievePenalty", archievePenalty), + DataEntry("failPenalty", failPenalty), + DataEntry("attemptsCount", attemptsCount), + DataEntry("failedAttemptsCount", 0), + DataEntry("startDate", startDate), + DataEntry("endDate", endDate) + ]) +} +@Callable(i) +func archieveGoal() = { + WriteSet([ + DataEntry("status", "archived") + ]) +} + +@Callable(i) +func addRecord(recordDate: String, recordDistance: Int) = { + let recordKey = "record" + "_" + recordDate + + let distance = getIntegerFromAccount(this, "distance") + let failPenalty = getIntegerFromAccount(this, "failPenalty") + let archievePenalty = getIntegerFromAccount(this, "archievePenalty") + let recordWeekDays = getIntegerFromAccount(this, "recordWeekDays") + let attemptsCount = getIntegerFromAccount(this, "attemptsCount") + let failedAttemptsCount = getIntegerFromAccount(this, "failedAttemptsCount") + + let startDate = getStringValue(this, "startDate") + let endDate = getStringValue(this, "endDate") + + if (recordDistance >= distance) then { + if (endDate == recordDate) then { + WriteSet([ + DataEntry(recordKey, true), + DataEntry("status", "completed") + ]) + } else { + WriteSet([ + DataEntry(recordKey, true) + ]) + } + } else { + let newFailedAttemptsCount = failedAttemptsCount + 1; + + if (newFailedAttemptsCount == attemptsCount) then { + WriteSet([ + DataEntry(recordKey, false), + DataEntry("failedAttemptsCount", newFailedAttemptsCount), + DataEntry("status", "failed") + ]) + } else { + WriteSet([ + DataEntry(recordKey, false), + DataEntry("failedAttemptsCount", newFailedAttemptsCount) + ]) + } + } +} + +@Verifier(tx) +func verify() = { + true +} \ No newline at end of file diff --git a/craun/test.js b/craun/test.js new file mode 100644 index 0000000..13d2bba --- /dev/null +++ b/craun/test.js @@ -0,0 +1,237 @@ +const craunAppSeed = env.accounts[0]; +const craunAppAddress = address(craunAppSeed); +const craunAppPubKey= publicKey(craunAppSeed); +const compiledCraunAppDApp = compile(file('craunApp')); + +const craunUserSeed = env.accounts[1]; +const craunUserAddress = address(craunUserSeed); + +const craunGoalSeed = env.accounts[2]; +const craunGoalAddress = address(craunGoalSeed); +const craunGoalPubKey= publicKey(craunGoalSeed); +const compiledCraunGoalDApp = compile(file('craunGoal')); + +const goal = { + title: 'Начать бегать', + distance: 200, + recordWeekDays: 'mon_tue_fri', + archievePenalty: 200000000, + failPenalty: 100000000, + attemptsCount: 3, + startDate: '12.05.2019', + endDate: '19.05.2019', +} + +describe('Create craunApp', () => { + it('Server: create new craunApp account in Blockchain, setSript it with craunAppDApp', async function() { + this.timeout(0); + + const setScriptTxParams = { + script: compiledCraunAppDApp, + fee: 1400000, + }; + + const setScriptTx = setScript(setScriptTxParams, craunAppSeed); + + const tx = await broadcast(setScriptTx); + + const minedTx = await waitForTx(tx.id) + + console.log(minedTx); + }) +}) + +describe('User money operations on craunApp account ', () => { + it('Deposit User money to craunApp', async function() { + this.timeout(0); + + const amount = 1000000000; // 10 waves + + const invokeScriptTxParams = { + dApp: craunAppAddress, + call: { function: 'deposit', args:[] }, + payment: [{ amount: amount, asset: null }] + }; + + const invokeScriptTx = invokeScript(invokeScriptTxParams, craunUserSeed); + + const tx = await broadcast(invokeScriptTx); + + const minedTx = await waitForTx(tx.id) + + console.log(minedTx); + }) + + it('Withdraw User money from craunApp', async function() { + this.timeout(0); + + const invokeScriptTxParams = { + dApp: craunAppAddress, + call: { function: 'withdraw', args:[] }, + call:{ + function: 'withdraw', + args: [{ + type: "integer", + value: 500000000 + }] + } + }; + + const invokeScriptTx = invokeScript(invokeScriptTxParams, craunUserSeed); + + const tx = await broadcast(invokeScriptTx); + + const minedTx = await waitForTx(tx.id) + + console.log(minedTx); + }) +}) + +describe('Create new goal for user', () => { + it('Generate new goal account for goal in blockchain', function() {}) + + it('SetSript goal account with craunGoalDApp', async function() { + this.timeout(0); + + const setScriptTxParams = { + script: compiledCraunGoalDApp, + senderPublicKey: craunGoalPubKey, + fee: 1400000 + }; + + const setScriptTx = setScript(setScriptTxParams, craunGoalSeed); + + const tx = await broadcast(setScriptTx); + + const minedTx = await waitForTx(tx.id) + + console.log(minedTx); + }) + + it('Setup goal configuraton by invoking setup method in craunGoalDApp', async function() { + this.timeout(0); + + const invokeScriptSetupTxParams = { + dApp: craunGoalAddress, + call:{ + function: 'setup', + args: [{ + type: "string", + value: craunUserAddress + }, { + type: "string", + value: goal.title + }, { + type: "integer", + value: goal.distance + }, { + type: 'string', + value: goal.recordWeekDays + },{ + type: "integer", + value: goal.archievePenalty + }, { + type: "integer", + value: goal.failPenalty + }, { + type: "integer", + value: goal.attemptsCount + }, { + type: "string", + value: goal.startDate + },{ + type: "string", + value: goal.endDate + }] + } + }; + + const invokeScriptSetupTx = invokeScript(invokeScriptSetupTxParams, craunUserSeed); + + const tx = await broadcast(invokeScriptSetupTx); + + const minedTx = await waitForTx(tx.id); + + console.log(minedTx); + }) + + it('Register user goal in craunAppAccount by invoking addGoal method in craunAppDApp', function() { + const invokeScriptAddGoalParams = { + dApp: craunAppAddress, + call:{ + function: 'addGoal', + args: [{ + type: "string", + value: craunGoalAddress + }] + } + }; + + const invokeScriptFrozeTx = invokeScript(invokeScriptAddGoalParams, craunUserSeed); + + const tx = await broadcast(invokeScriptFrozeTx); + + const minedTx = await waitForTx(tx.id); + + console.log(minedTx); + }) +}) + +describe('Track goal execution record', () => { + it('Add record of goal execution by invoking addRecord method in craunGoalDApp', async function() { + this.timeout(0); + + const params = { + dApp: craunGoalAddress, + call: { + function: 'addRecord', + args: [{ + type: "string", + value: "15.05.2019" + }, { + type: "integer", + value: 100 + }] + }, + }; + + const invokeScriptTx = invokeScript(params); + + const tx = await broadcast(invokeScriptTx); + + const minedTx = await waitForTx(tx.id) + + console.log(tx); + }) +}) + +describe('Check goal execution record', () => { + it('Check goal execution by invoking checkUserGoal method on craunAppDApp)', async function() { + this.timeout(0); + + const params = { + dApp: craunAppAddress, + call: { + function: 'checkUserGoal', + args: [{ + type: "string", + value: craunGoalAddress + }, { + type: "string", + value: craunUserAddress + }, { + type: "string", + value: "13.05.2019" + }] + }, + }; + + const invokeScriptTx = invokeScript(params, craunUserSeed); + + const tx = await broadcast(invokeScriptTx); + + const minedTx = await waitForTx(tx.id) + + console.log(tx); + }) +})