diff --git a/src/contract_core/qpi_spectrum_impl.h b/src/contract_core/qpi_spectrum_impl.h index 93434bbf..ce90b124 100644 --- a/src/contract_core/qpi_spectrum_impl.h +++ b/src/contract_core/qpi_spectrum_impl.h @@ -182,6 +182,16 @@ long long QPI::QpiContextProcedureCall::transfer(const m256i& destination, long return __transfer(destination, amount, TransferType::qpiTransfer); } +sint64 QPI::QpiContextProcedureCall::returnInvocatorReward() const +{ + if (invocationReward() > 0) + { + return transfer(invocator(), invocationReward()); + } + + return 0; +} + m256i QPI::QpiContextFunctionCall::nextId(const m256i& currentId) const { int index = spectrumIndex(currentId); diff --git a/src/contracts/Pulse.h b/src/contracts/Pulse.h index f1ba76f5..a9078f0c 100644 --- a/src/contracts/Pulse.h +++ b/src/contracts/Pulse.h @@ -222,12 +222,20 @@ struct PULSE : public ContractBase EState currentState; }; + /** + * @brief Input for ValidateDigits. + */ struct ValidateDigits_input { + // Candidate ticket digits; only the first PULSE_PLAYER_DIGITS entries are validated. Array digits; }; + /** + * @brief Output for ValidateDigits. + */ struct ValidateDigits_output { + // True when every checked digit is in the supported range [0..9]. bit isValid; }; struct ValidateDigits_locals @@ -236,13 +244,21 @@ struct PULSE : public ContractBase uint8 value; }; + /** + * @brief Input for BuyTicket. + */ struct BuyTicket_input { + // Ticket digits chosen by the invocator; only the first PULSE_PLAYER_DIGITS entries are used. Array digits; }; + /** + * @brief Output for BuyTicket. + */ struct BuyTicket_output { + // EReturnCode value describing whether the ticket was accepted and paid. uint8 returnCode; }; @@ -334,13 +350,21 @@ struct PULSE : public ContractBase } randomData; }; + /** + * @brief Input for BuyRandomTickets. + */ struct BuyRandomTickets_input { + // Requested number of random tickets to buy for the invocator. uint16 count; }; + /** + * @brief Output for BuyRandomTickets. + */ struct BuyRandomTickets_output { + // EReturnCode value describing validation, payment, or allocation result. uint8 returnCode; }; @@ -368,14 +392,24 @@ struct PULSE : public ContractBase sint64 elementIndex; }; + /** + * @brief Input for GetAutoParticipation. + */ struct GetAutoParticipation_input { + // Account whose auto-participation entry should be read. id player; }; + /** + * @brief Output for GetAutoParticipation. + */ struct GetAutoParticipation_output { + // QHeart amount reserved for future automatic purchases. uint64 deposit; + // Number of tickets requested per automatic purchase round. uint16 desiredTickets; + // EReturnCode::SUCCESS when an entry exists, otherwise EReturnCode::INVALID_VALUE. uint8 returnCode; }; struct GetAutoParticipation_locals @@ -383,15 +417,27 @@ struct PULSE : public ContractBase AutoParticipant entry; }; + /** + * @brief Input for GetAutoStats. + * @note This function has no parameters. + */ struct GetAutoStats_input { }; + /** + * @brief Output for GetAutoStats. + */ struct GetAutoStats_output { + // Snapshot of registered auto-participants; unused trailing slots are zeroed. Array participants; + // Maximum number of auto-participants supported by storage. uint16 maxAutoParticipants; + // Current per-user automatic ticket cap; 0 means unlimited. uint16 maxAutoTicketsPerUser; + // Remaining ticket capacity in the current round. uint16 roundSlotsLeft; + // EReturnCode value for the query. uint8 returnCode; }; struct GetAutoStats_locals @@ -399,14 +445,24 @@ struct PULSE : public ContractBase HashMapConverter converter; }; + /** + * @brief Input for DepositAutoParticipation. + */ struct DepositAutoParticipation_input { + // QHeart amount to transfer into the auto-participation deposit. sint64 amount; + // Desired number of tickets to buy automatically per draw. sint16 desiredTickets; + // When true, attempts an immediate random-ticket purchase before storing the remaining deposit. bit buyNow; }; + /** + * @brief Output for DepositAutoParticipation. + */ struct DepositAutoParticipation_output { + // EReturnCode value describing validation, transfer, or purchase result. uint8 returnCode; }; struct DepositAutoParticipation_locals @@ -432,12 +488,20 @@ struct PULSE : public ContractBase BuyRandomTickets_output buyRandomTicketsOutput; }; + /** + * @brief Input for WithdrawAutoParticipation. + */ struct WithdrawAutoParticipation_input { + // QHeart amount to withdraw; values <= 0 request the full stored deposit. sint64 amount; }; + /** + * @brief Output for WithdrawAutoParticipation. + */ struct WithdrawAutoParticipation_output { + // EReturnCode value describing whether the withdrawal succeeded. uint8 returnCode; }; struct WithdrawAutoParticipation_locals @@ -448,12 +512,20 @@ struct PULSE : public ContractBase sint64 withdrawAmount; }; + /** + * @brief Input for SetAutoConfig. + */ struct SetAutoConfig_input { + // New desired ticket count; -1 keeps the current value. sint16 desiredTickets; }; + /** + * @brief Output for SetAutoConfig. + */ struct SetAutoConfig_output { + // EReturnCode value describing whether the configuration update was accepted. uint8 returnCode; }; struct SetAutoConfig_locals @@ -465,12 +537,20 @@ struct PULSE : public ContractBase FindAutoParticipant_output findOutput; }; + /** + * @brief Input for SetAutoLimits. + */ struct SetAutoLimits_input { + // Maximum automatic tickets per participant; 0 disables the limit. uint16 maxTicketsPerUser; }; + /** + * @brief Output for SetAutoLimits. + */ struct SetAutoLimits_output { + // EReturnCode value describing whether the owner-only update succeeded. uint8 returnCode; }; struct SetAutoLimits_locals @@ -479,12 +559,20 @@ struct PULSE : public ContractBase sint64 index; }; + /** + * @brief Input for DepositManagedQHeart. + */ struct DepositManagedQHeart_input { + // QHeart amount to move from the invocator into the Pulse contract wallet. sint64 amount; }; + /** + * @brief Output for DepositManagedQHeart. + */ struct DepositManagedQHeart_output { + // EReturnCode value describing validation or transfer result. uint8 returnCode; }; struct DepositManagedQHeart_locals @@ -493,15 +581,25 @@ struct PULSE : public ContractBase sint64 userBalance; }; - struct TransferTokenToQx_input + /** + * @brief Input for TransferShareManagementRights. + */ + struct TransferShareManagementRights_input { + // Number of managed QHeart shares to release. sint64 numberOfShares; + // Destination contract index that should acquire management rights. + uint16 newManagingContractIndex; }; - struct TransferTokenToQx_output + /** + * @brief Output for TransferShareManagementRights. + */ + struct TransferShareManagementRights_output { + // EReturnCode value describing validation or share-release result. uint8 returnCode; }; - struct TransferTokenToQx_locals + struct TransferShareManagementRights_locals { Asset asset; sint64 releaseResult; @@ -509,85 +607,166 @@ struct PULSE : public ContractBase QX::Fees_output feesOutput; }; + /** + * @brief Input for GetTicketPrice. + * @note This function has no parameters. + */ struct GetTicketPrice_input { }; + /** + * @brief Output for GetTicketPrice. + */ struct GetTicketPrice_output { + // Current ticket price in QHeart units. uint64 ticketPrice; }; + /** + * @brief Input for GetPlayerBalance. + */ struct GetPlayerBalance_input { + // Account whose QHeart balance should be queried. id player; }; + /** + * @brief Output for GetPlayerBalance. + */ struct GetPlayerBalance_output { + // Player QHeart balance managed by the Pulse contract context. uint64 balance; + // EReturnCode value for the query. uint8 returnCode; }; + /** + * @brief Input for GetFees. + * @note This function has no parameters. + */ struct GetFees_input { }; + /** + * @brief Output for GetFees. + */ struct GetFees_output { + // Percent of round revenue allocated to the dev wallet. uint8 devPercent; + // Percent of round revenue burned. uint8 burnPercent; + // Percent of round revenue distributed to Pulse shareholders. uint8 shareholdersPercent; + // Percent of round revenue distributed to RandomLottery shareholders. uint8 rlShareholdersPercent; + // EReturnCode value for the query. uint8 returnCode; }; + /** + * @brief Input for GetQHeartHoldLimit. + * @note This function has no parameters. + */ struct GetQHeartHoldLimit_input { }; + /** + * @brief Output for GetQHeartHoldLimit. + */ struct GetQHeartHoldLimit_output { + // Maximum QHeart balance retained by the Pulse wallet after settlement. uint64 qheartHoldLimit; }; + /** + * @brief Input for GetQHeartWallet. + * @note This function has no parameters. + */ struct GetQHeartWallet_input { }; + /** + * @brief Output for GetQHeartWallet. + */ struct GetQHeartWallet_output { + // Current QHeart issuer wallet configured for Pulse. id wallet; }; + /** + * @brief Input for GetWinningDigits. + * @note This function has no parameters. + */ struct GetWinningDigits_input { }; + /** + * @brief Output for GetWinningDigits. + */ struct GetWinningDigits_output { + // Winning digits from the last settled draw. Array digits; }; + /** + * @brief Input for GetBalance. + * @note This function has no parameters. + */ struct GetBalance_input { }; + /** + * @brief Output for GetBalance. + */ struct GetBalance_output { + // QHeart balance currently held by the Pulse contract wallet. uint64 balance; }; + /** + * @brief Input for GetPlayers. + * @note This function has no parameters. + */ struct GetPlayers_input { }; + /** + * @brief Output for GetPlayers. + */ struct GetPlayers_output { + // Snapshot of current-round tickets; unused trailing slots are zeroed. Array players; + // EReturnCode value for the query. uint8 returnCode; }; + /** + * @brief Input for GetPrizeTable. + * @note This function has no parameters. + */ struct GetPrizeTable_input { }; + /** + * @brief Output for GetPrizeTable. + */ struct GetPrizeTable_output { + // Reward table indexed by left-aligned match count. Array leftAlignedRewards; + // Reward table indexed by any-position match count. Array anyPositionRewards; + // Ticket price used to derive the reward tables. uint64 ticketPrice; + // EReturnCode value for the query. uint8 returnCode; }; struct GetPrizeTable_locals @@ -595,20 +774,37 @@ struct PULSE : public ContractBase uint8 matches; }; + /** + * @brief Input for GetRoundState. + * @note This function has no parameters. + */ struct GetRoundState_input { }; + /** + * @brief Output for GetRoundState. + */ struct GetRoundState_output { + // Current Qubic epoch. uint32 epoch; + // Compact date stamp for the most recent draw. uint32 lastDrawDateStamp; + // Number of tickets allocated in the current round. uint16 ticketCounter; + // Maximum ticket capacity for one round. uint16 maxPlayers; + // Remaining ticket slots for the current round. uint16 slotsLeft; + // Runtime state flags encoded from EState. uint8 currentState; + // UTC hour when scheduled draws become eligible. uint8 drawHour; + // Weekday bitmask for scheduled draws. uint8 schedule; + // True when ticket sales are currently open. bit sellingOpen; + // EReturnCode value for the query. uint8 returnCode; }; struct GetRoundState_locals @@ -630,61 +826,114 @@ struct PULSE : public ContractBase uint64 insertIdx; }; + /** + * @brief Input for GetWinners. + * @note This function has no parameters. + */ struct GetWinners_input { }; + /** + * @brief Output for GetWinners. + */ struct GetWinners_output { + // Winner history ring buffer snapshot. Array winners; + // Monotonic insertion counter for interpreting ring-buffer order. uint64 winnersCounter; + // EReturnCode value for the query. uint8 returnCode; }; + /** + * @brief Input for SetPrice. + */ struct SetPrice_input { + // Ticket price to apply at the next epoch. uint64 newPrice; }; + /** + * @brief Output for SetPrice. + */ struct SetPrice_output { + // EReturnCode value describing whether the owner-only update was scheduled. uint8 returnCode; }; + /** + * @brief Input for SetSchedule. + */ struct SetSchedule_input { + // Weekday bitmask to apply at the next epoch. uint8 newSchedule; }; + /** + * @brief Output for SetSchedule. + */ struct SetSchedule_output { + // EReturnCode value describing whether the owner-only update was scheduled. uint8 returnCode; }; + /** + * @brief Input for SetDrawHour. + */ struct SetDrawHour_input { + // UTC draw hour to apply at the next epoch; valid range is 0..23. uint8 newDrawHour; }; + /** + * @brief Output for SetDrawHour. + */ struct SetDrawHour_output { + // EReturnCode value describing whether the owner-only update was scheduled. uint8 returnCode; }; + /** + * @brief Input for SetFees. + */ struct SetFees_input { + // Dev fee percent to apply at the next epoch. uint8 devPercent; + // Burn percent to apply at the next epoch. uint8 burnPercent; + // Pulse shareholder distribution percent to apply at the next epoch. uint8 shareholdersPercent; + // RandomLottery shareholder distribution percent to apply at the next epoch. uint8 rlShareholdersPercent; }; + /** + * @brief Output for SetFees. + */ struct SetFees_output { + // EReturnCode value describing whether the owner-only update was scheduled. uint8 returnCode; }; + /** + * @brief Input for SetQHeartHoldLimit. + */ struct SetQHeartHoldLimit_input { + // QHeart balance cap to apply at the next epoch. uint64 newQHeartHoldLimit; }; + /** + * @brief Output for SetQHeartHoldLimit. + */ struct SetQHeartHoldLimit_output { + // EReturnCode value describing whether the owner-only update was scheduled. uint8 returnCode; }; @@ -827,7 +1076,7 @@ struct PULSE : public ContractBase REGISTER_USER_PROCEDURE(WithdrawAutoParticipation, 9); REGISTER_USER_PROCEDURE(SetAutoConfig, 10); REGISTER_USER_PROCEDURE(SetAutoLimits, 11); - REGISTER_USER_PROCEDURE(TransferTokenToQx, 12); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 12); REGISTER_USER_PROCEDURE(DepositManagedQHeart, 13); } @@ -1094,10 +1343,7 @@ struct PULSE : public ContractBase /** Schedules a new ticket price for the next epoch (owner-only). */ PUBLIC_PROCEDURE(SetPrice) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (qpi.invocator() != state.get().qheartIssuer) { @@ -1119,10 +1365,7 @@ struct PULSE : public ContractBase /** Schedules a new draw schedule bitmask for the next epoch (owner-only). */ PUBLIC_PROCEDURE(SetSchedule) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (qpi.invocator() != state.get().qheartIssuer) { @@ -1144,10 +1387,7 @@ struct PULSE : public ContractBase /** Schedules a new draw hour in UTC for the next epoch (owner-only). */ PUBLIC_PROCEDURE(SetDrawHour) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (qpi.invocator() != state.get().qheartIssuer) { @@ -1169,10 +1409,7 @@ struct PULSE : public ContractBase /** Schedules new fee splits for the next epoch (owner-only). */ PUBLIC_PROCEDURE(SetFees) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (qpi.invocator() != state.get().qheartIssuer) { @@ -1198,10 +1435,7 @@ struct PULSE : public ContractBase /** Schedules a new QHeart hold limit for the next epoch (owner-only). */ PUBLIC_PROCEDURE(SetQHeartHoldLimit) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (qpi.invocator() != state.get().qheartIssuer) { @@ -1223,10 +1457,7 @@ struct PULSE : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(DepositAutoParticipation) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (state.get().autoParticipants.population() >= state.get().autoParticipants.capacity()) { @@ -1305,10 +1536,7 @@ struct PULSE : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(WithdrawAutoParticipation) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (!state.get().autoParticipants.contains(qpi.invocator())) { @@ -1360,10 +1588,7 @@ struct PULSE : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(SetAutoConfig) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (!state.get().autoParticipants.contains(qpi.invocator())) { @@ -1401,10 +1626,7 @@ struct PULSE : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(SetAutoLimits) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (qpi.invocator() != state.get().qheartIssuer) { @@ -1441,10 +1663,7 @@ struct PULSE : public ContractBase /** Buys a single ticket and transfers the ticket price from the invocator. */ PUBLIC_PROCEDURE_WITH_LOCALS(BuyTicket) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (!isSellingOpen(state)) { @@ -1494,10 +1713,7 @@ struct PULSE : public ContractBase /** Buys multiple random tickets and transfers the total price from the invocator. */ PUBLIC_PROCEDURE_WITH_LOCALS(BuyRandomTickets) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); locals.prepareInput.count = input.count; CALL(PrepareRandomTickets, locals.prepareInput, locals.prepareOutput); @@ -1531,10 +1747,7 @@ struct PULSE : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(DepositManagedQHeart) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + qpi.returnInvocatorReward(); if (input.amount <= 0) { @@ -1562,43 +1775,46 @@ struct PULSE : public ContractBase } /** - * @brief Releases PULSE share management rights back to QX for the invocator. - * @param input Number of PULSE shares to transfer under QX management. - * @param output Number of shares transferred and a status code. - * @note The current QX transfer fee is paid from the Pulse contract balance; any invocation reward is refunded. + * @brief Releases managed QHeart token rights to another contract for the invocator. + * @param input Number of QHeart tokens and the contract index that should acquire management rights. + * @param output Status code describing validation or rights-release result. + * @note The destination contract fee is paid from the invocation reward; any unused reward is refunded. */ - PUBLIC_PROCEDURE_WITH_LOCALS(TransferTokenToQx) + PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (input.numberOfShares <= 0) + if (input.numberOfShares <= 0 || input.newManagingContractIndex == 0 || input.newManagingContractIndex >= MAX_NUMBER_OF_CONTRACTS || + input.newManagingContractIndex == SELF_INDEX) { + qpi.returnInvocatorReward(); output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); return; } - if (qpi.numberOfPossessedShares(PULSE_QHEART_ASSET_NAME, state.get().qheartIssuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, - SELF_INDEX) < input.numberOfShares) + if (qpi.numberOfPossessedShares(PULSE_QHEART_ASSET_NAME, state.get().qheartIssuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < + input.numberOfShares) { + qpi.returnInvocatorReward(); output.returnCode = toReturnCode(EReturnCode::TRANSFER_FROM_PULSE_FAILED); return; } - CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); - locals.asset.issuer = state.get().qheartIssuer; locals.asset.assetName = PULSE_QHEART_ASSET_NAME; - locals.releaseResult = qpi.releaseShares(locals.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, QX_CONTRACT_INDEX, - QX_CONTRACT_INDEX, locals.feesOutput.transferFee); + + locals.releaseResult = qpi.releaseShares(locals.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagingContractIndex, + input.newManagingContractIndex, qpi.invocationReward()); if (locals.releaseResult < 0) { + qpi.returnInvocatorReward(); output.returnCode = toReturnCode(EReturnCode::TRANSFER_FROM_PULSE_FAILED); return; } + if (qpi.invocationReward() > locals.releaseResult) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.releaseResult); + } + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index d3b735af..8c7d2fdc 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -2797,6 +2797,15 @@ namespace QPI const id& newOwnerAndPossessor // New owner and possessor. Pass NULL_ID to burn shares (not allowed for contract shares). ) const; // Returns remaining number of possessed shares satisfying all the conditions; if the value is less than 0, the attempt has failed, in this case the absolute value equals to the insufficient number, INVALID_AMOUNT indicates another error + /** + * @brief Return the full invocation reward to the current invocator. + * @return Remaining energy amount of the current contract on success; a negative value means the transfer failed, + * in which case the absolute value equals the missing amount. INVALID_AMOUNT indicates another error. + * @note Equivalent to calling transfer(invocator(), invocationReward()) and therefore sends 0 when no invocation + * reward is attached. + */ + inline sint64 returnInvocatorReward() const; + /// Unsubscribe oracle based on subscription ID (returning false if oracleSubscriptionId is invalid). inline bool unsubscribeOracle( sint32 oracleSubscriptionId diff --git a/test/qpi.cpp b/test/qpi.cpp index 09a25f5f..6b1c8e9d 100644 --- a/test/qpi.cpp +++ b/test/qpi.cpp @@ -457,6 +457,29 @@ struct ContractExecInitDeinitGuard } }; +TEST(TestCoreQPI, ReturnInvocatorReward) +{ + ContractTesting test; + test.initEmptySpectrum(); + + const QPI::id invocator(101, 202, 303, 404); + const QPI::id contractId(QX_CONTRACT_INDEX, 0, 0, 0); + constexpr sint64 invocationReward = 123; + constexpr sint64 extraContractBalance = 77; + + increaseEnergy(contractId, invocationReward + extraContractBalance); + + const sint64 contractBalanceBefore = getBalance(contractId); + const sint64 invocatorBalanceBefore = getBalance(invocator); + + QpiContextUserProcedureCall qpi(QX_CONTRACT_INDEX, invocator, invocationReward); + const sint64 remainingContractBalance = qpi.returnInvocatorReward(); + + EXPECT_EQ(remainingContractBalance, extraContractBalance); + EXPECT_EQ(getBalance(contractId), contractBalanceBefore - invocationReward); + EXPECT_EQ(getBalance(invocator), invocatorBalanceBefore + invocationReward); +} + TEST(TestCoreQPI, ProposalAndVotingByComputors) { ContractExecInitDeinitGuard initDeinitGuard; @@ -1843,8 +1866,8 @@ TEST(TestCoreQPI, ProposalVotingV1proposalByAnyoneWithoutScalarVoteSupport) testProposalVotingComputorsV1(); } -// TODO: ProposalVoting YesNo - +// TODO: ProposalVoting YesNo + template void testProposalVotingShareholdersV1() {