From c8aefe4e66f9e74d9ff2e80c3d2401f505297c29 Mon Sep 17 00:00:00 2001 From: Michal Iskierko Date: Mon, 13 Mar 2023 11:16:08 +0100 Subject: [PATCH] feat(@desktop/communities): Airdrop community tokens Issue #9783 --- .../main/communities/tokens/controller.nim | 6 ++ .../main/communities/tokens/io_interface.nim | 3 + .../main/communities/tokens/module.nim | 56 ++++++++++++++++--- .../modules/main/communities/tokens/view.nim | 3 + .../service/community_tokens/service.nim | 24 +++++++- src/backend/community_tokens.nim | 10 +++- .../CommunityAirdropsSettingsPanel.qml | 4 +- .../Chat/stores/CommunityTokensStore.qml | 8 ++- .../Chat/views/CommunitySettingsView.qml | 2 +- .../communities/CommunityNewAirdropView.qml | 9 ++- 10 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/app/modules/main/communities/tokens/controller.nim b/src/app/modules/main/communities/tokens/controller.nim index a12b3cf74b..7d87b6038e 100644 --- a/src/app/modules/main/communities/tokens/controller.nim +++ b/src/app/modules/main/communities/tokens/controller.nim @@ -42,6 +42,9 @@ proc init*(self: Controller) = proc deployCollectibles*(self: Controller, communityId: string, addressFrom: string, password: string, deploymentParams: DeploymentParameters, tokenMetadata: CommunityTokensMetadataDto, chainId: int) = self.communityTokensService.deployCollectibles(communityId, addressFrom, password, deploymentParams, tokenMetadata, chainId) +proc airdropCollectibles*(self: Controller, communityId: string, password: string, collectiblesAndAmounts: seq[CommunityTokenAndAmount], walletAddresses: seq[string]) = + self.communityTokensService.airdropCollectibles(communityId, password, collectiblesAndAmounts, walletAddresses) + proc authenticateUser*(self: Controller, keyUid = "") = let data = SharedKeycarModuleAuthenticationArgs(uniqueIdentifier: UNIQUE_DEPLOY_COLLECTIBLES_COMMUNITY_TOKENS_MODULE_IDENTIFIER, keyUid: keyUid) self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data) @@ -54,3 +57,6 @@ proc getSuggestedFees*(self: Controller, chainId: int): SuggestedFeesDto = proc getFiatValue*(self: Controller, cryptoBalance: string, cryptoSymbol: string): string = return self.communityTokensService.getFiatValue(cryptoBalance, cryptoSymbol) + +proc getCommunityTokenBySymbol*(self: Controller, communityId: string, symbol: string): CommunityTokenDto = + return self.communityTokensService.getCommunityTokenBySymbol(communityId, symbol) diff --git a/src/app/modules/main/communities/tokens/io_interface.nim b/src/app/modules/main/communities/tokens/io_interface.nim index c1d1767c05..0898b3d94e 100644 --- a/src/app/modules/main/communities/tokens/io_interface.nim +++ b/src/app/modules/main/communities/tokens/io_interface.nim @@ -9,6 +9,9 @@ method delete*(self: AccessInterface) {.base.} = method load*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") +method airdropCollectibles*(self: AccessInterface, communityId: string, collectiblesJsonString: string, walletsJsonString: string) {.base.} = + raise newException(ValueError, "No implementation available") + method deployCollectible*(self: AccessInterface, communityId: string, address: string, name: string, symbol: string, description: string, supply: int, infiniteSupply: bool, transferable: bool, selfDestruct: bool, chainId: int, image: string) {.base.} = raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/communities/tokens/module.nim b/src/app/modules/main/communities/tokens/module.nim index 43601c3780..bd74e59062 100644 --- a/src/app/modules/main/communities/tokens/module.nim +++ b/src/app/modules/main/communities/tokens/module.nim @@ -1,9 +1,10 @@ -import NimQml, json, stint, strformat, strutils +import NimQml, json, stint, strformat, strutils, chronicles import ../../../../../app_service/service/community_tokens/service as community_tokens_service import ../../../../../app_service/service/transaction/service as transaction_service import ../../../../../app_service/service/community/dto/community import ../../../../../app_service/common/conversion +import ../../../../../app_service/service/accounts/utils as utl import ../../../../core/eventemitter import ../../../../global/global_singleton import ../io_interface as parent_interface @@ -11,17 +12,27 @@ import ./io_interface, ./view , ./controller export io_interface +type + ContractAction {.pure.} = enum + Unknown = 0 + Deploy = 1 + Airdrop = 2 + type Module* = ref object of io_interface.AccessInterface parent: parent_interface.AccessInterface controller: Controller view: View viewVariant: QVariant + tempTokenAndAmountList: seq[CommunityTokenAndAmount] tempAddressFrom: string tempCommunityId: string tempChainId: int + tempContractAddress: string tempDeploymentParams: DeploymentParameters tempTokenMetadata: CommunityTokensMetadataDto + tempWalletAddresses: seq[string] + tempContractAction: ContractAction proc newCommunityTokensModule*( parent: parent_interface.AccessInterface, @@ -45,12 +56,40 @@ method resetTempValues(self:Module) = self.tempDeploymentParams = DeploymentParameters() self.tempTokenMetadata = CommunityTokensMetadataDto() self.tempChainId = 0 + self.tempContractAddress = "" + self.tempWalletAddresses = @[] + self.tempContractAction = ContractAction.Unknown + self.tempTokenAndAmountList = @[] method load*(self: Module) = singletonInstance.engine.setRootContextProperty("communityTokensModule", self.viewVariant) self.controller.init() self.view.load() +proc authenticate(self: Module) = + if singletonInstance.userProfile.getIsKeycardUser(): + let keyUid = singletonInstance.userProfile.getKeyUid() + self.controller.authenticateUser(keyUid) + else: + self.controller.authenticateUser() + +method airdropCollectibles*(self: Module, communityId: string, collectiblesJsonString: string, walletsJsonString: string) = + let collectiblesJson = collectiblesJsonString.parseJson + self.tempTokenAndAmountList = @[] + for collectible in collectiblesJson: + let symbol = collectible["key"].getStr + let amount = collectible["amount"].getInt + let tokenDto = self.controller.getCommunityTokenBySymbol(communityId, symbol) + if tokenDto.tokenType == TokenType.Unknown: + error "Can't find token for community", communityId=communityId, symbol=symbol + return + self.tempTokenAndAmountList.add(CommunityTokenAndAmount(communityToken: tokenDto, amount: amount)) + + self.tempWalletAddresses = walletsJsonString.parseJson.to(seq[string]) + self.tempCommunityId = communityId + self.tempContractAction = ContractAction.Airdrop + self.authenticate() + method deployCollectible*(self: Module, communityId: string, fromAddress: string, name: string, symbol: string, description: string, supply: int, infiniteSupply: bool, transferable: bool, selfDestruct: bool, chainId: int, image: string) = self.tempAddressFrom = fromAddress @@ -62,13 +101,11 @@ method deployCollectible*(self: Module, communityId: string, fromAddress: string self.tempDeploymentParams.infiniteSupply = infiniteSupply self.tempDeploymentParams.transferable = transferable self.tempDeploymentParams.remoteSelfDestruct = selfDestruct + self.tempDeploymentParams.tokenUri = utl.changeCommunityKeyCompression(communityId) & "/" self.tempTokenMetadata.image = singletonInstance.utils.formatImagePath(image) self.tempTokenMetadata.description = description - if singletonInstance.userProfile.getIsKeycardUser(): - let keyUid = singletonInstance.userProfile.getKeyUid() - self.controller.authenticateUser(keyUid) - else: - self.controller.authenticateUser() + self.tempContractAction = ContractAction.Deploy + self.authenticate() method onUserAuthenticated*(self: Module, password: string) = defer: self.resetTempValues() @@ -76,7 +113,10 @@ method onUserAuthenticated*(self: Module, password: string) = discard #TODO signalize somehow else: - self.controller.deployCollectibles(self.tempCommunityId, self.tempAddressFrom, password, self.tempDeploymentParams, self.tempTokenMetadata, self.tempChainId) + if self.tempContractAction == ContractAction.Deploy: + self.controller.deployCollectibles(self.tempCommunityId, self.tempAddressFrom, password, self.tempDeploymentParams, self.tempTokenMetadata, self.tempChainId) + elif self.tempContractAction == ContractAction.Airdrop: + self.controller.airdropCollectibles(self.tempCommunityId, password, self.tempTokenAndAmountList, self.tempWalletAddresses) method computeDeployFee*(self: Module, chainId: int): string = let suggestedFees = self.controller.getSuggestedFees(chainId) @@ -93,4 +133,4 @@ method computeDeployFee*(self: Module, chainId: int): string = let fiatValue = self.controller.getFiatValue(ethValueStr, "ETH") - return fmt"{ethValue:.4f}ETH (${fiatValue})" \ No newline at end of file + return fmt"{ethValue:.4f}ETH (${fiatValue})" diff --git a/src/app/modules/main/communities/tokens/view.nim b/src/app/modules/main/communities/tokens/view.nim index 1a0c2dd4dd..7a4c55121b 100644 --- a/src/app/modules/main/communities/tokens/view.nim +++ b/src/app/modules/main/communities/tokens/view.nim @@ -22,6 +22,9 @@ QtObject: proc deployCollectible*(self: View, communityId: string, fromAddress: string, name: string, symbol: string, description: string, supply: int, infiniteSupply: bool, transferable: bool, selfDestruct: bool, chainId: int, image: string) {.slot.} = self.communityTokensModule.deployCollectible(communityId, fromAddress, name, symbol, description, supply, infiniteSupply, transferable, selfDestruct, chainId, image) + proc airdropCollectibles*(self: View, communityId: string, collectiblesJsonString: string, walletsJsonString: string) {.slot.} = + self.communityTokensModule.airdropCollectibles(communityId, collectiblesJsonString, walletsJsonString) + proc deployFeeUpdated*(self: View) {.signal.} proc computeDeployFee*(self: View, chainId: int) {.slot.} = diff --git a/src/app_service/service/community_tokens/service.nim b/src/app_service/service/community_tokens/service.nim index 0b7fc0cf68..a3cb467ece 100644 --- a/src/app_service/service/community_tokens/service.nim +++ b/src/app_service/service/community_tokens/service.nim @@ -23,6 +23,11 @@ export deployment_parameters logScope: topics = "community-tokens-service" +type + CommunityTokenAndAmount* = object + communityToken*: CommunityTokenDto + amount*: int + type CommunityTokenDeployedStatusArgs* = ref object of Args communityId*: string @@ -155,4 +160,21 @@ QtObject: let price = self.tokenService.getTokenPrice(cryptoSymbol, currentCurrency) let value = parseFloat(cryptoBalance) * price - return fmt"{value:.2f}" \ No newline at end of file + return fmt"{value:.2f}" + + proc contractOwner*(self: Service, chainId: int, contractAddress: string): string = + try: + let response = tokens_backend.contractOwner(chainId, contractAddress) + return response.result.getStr() + except RpcException: + error "Error getting contract owner", message = getCurrentExceptionMsg() + + proc airdropCollectibles*(self: Service, communityId: string, password: string, collectiblesAndAmounts: seq[CommunityTokenAndAmount], walletAddresses: seq[string]) = + try: + for collectibleAndAmount in collectiblesAndAmounts: + let addressFrom = self.contractOwner(collectibleAndAmount.communityToken.chainId, collectibleAndAmount.communityToken.address) + let txData = TransactionDataDto(source: parseAddress(addressFrom)) #TODO estimate fee in UI + let response = tokens_backend.mintTo(collectibleAndAmount.communityToken.chainId, collectibleAndAmount.communityToken.address, %txData, password, walletAddresses, collectibleAndAmount.amount) + echo "!!! Transaction hash ", response.result.getStr() + except RpcException: + error "Error minting collectibles", message = getCurrentExceptionMsg() diff --git a/src/backend/community_tokens.nim b/src/backend/community_tokens.nim index ed81078afd..b75261eb67 100644 --- a/src/backend/community_tokens.nim +++ b/src/backend/community_tokens.nim @@ -18,4 +18,12 @@ proc addCommunityToken*(token: CommunityTokenDto): RpcResponse[JsonNode] {.raise proc updateCommunityTokenState*(contractAddress: string, deployState: DeployState): RpcResponse[JsonNode] {.raises: [Exception].} = let payload = %* [contractAddress, deployState.int] - return core.callPrivateRPC("wakuext_updateCommunityTokenState", payload) \ No newline at end of file + return core.callPrivateRPC("wakuext_updateCommunityTokenState", payload) + +proc mintTo*(chainId: int, contractAddress: string, txData: JsonNode, password: string, walletAddresses: seq[string], amount: int): RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [chainId, contractAddress, txData, utils.hashPassword(password), walletAddresses, amount] + return core.callPrivateRPC("collectibles_mintTo", payload) + +proc contractOwner*(chainId: int, contractAddress: string): RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [chainId, contractAddress] + return core.callPrivateRPC("collectibles_contractOwner", payload) \ No newline at end of file diff --git a/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml b/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml index 20f17cbb54..8c8d713093 100644 --- a/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml @@ -19,7 +19,7 @@ SettingsPageLayout { property int viewWidth: 560 // by design - signal airdropClicked(var airdropTokens, string address) + signal airdropClicked(var airdropTokens, var addresses) signal navigateToMintTokenSettings // TODO: Update with stackmanager when #8736 is integrated @@ -96,7 +96,7 @@ SettingsPageLayout { collectiblesModel: root.collectiblesModel onAirdropClicked: { - root.airdropClicked(airdropTokens, address) + root.airdropClicked(airdropTokens, addresses) stackManager.clear(d.welcomeViewState, StackView.Immediate) } onNavigateToMintTokenSettings: root.navigateToMintTokenSettings() diff --git a/ui/app/AppLayouts/Chat/stores/CommunityTokensStore.qml b/ui/app/AppLayouts/Chat/stores/CommunityTokensStore.qml index 45545a1d14..1927cab42b 100644 --- a/ui/app/AppLayouts/Chat/stores/CommunityTokensStore.qml +++ b/ui/app/AppLayouts/Chat/stores/CommunityTokensStore.qml @@ -67,7 +67,11 @@ QtObject { } // Airdrop tokens: - function airdrop(airdropTokens, address) { - console.warn("TODO: Airdrop backend call!") + function airdrop(communityId, airdropTokens, addresses) { + const addrArray = [] + for(var i = 0; i < addresses.length; i++) { + addrArray.push(addresses[i]["text"]) + } + communityTokensModuleInst.airdropCollectibles(communityId, JSON.stringify(airdropTokens), JSON.stringify(addrArray)) } } diff --git a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml index 915354e6b5..ca11bbbab1 100644 --- a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml @@ -345,7 +345,7 @@ StatusSectionLayout { collectiblesModel: rootStore.collectiblesModel onPreviousPageNameChanged: root.backButtonName = previousPageName - onAirdropClicked: communityTokensStore.airdrop(airdropTokens, chainId, address) + onAirdropClicked: communityTokensStore.airdrop(root.community.id, airdropTokens, addresses) onNavigateToMintTokenSettings: d.currentIndex = d.mintTokensSettingsIndex } diff --git a/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml b/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml index 23acc7845d..2ea9a2714b 100644 --- a/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml +++ b/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml @@ -31,7 +31,7 @@ StatusScrollView { readonly property bool isFullyFilled: selectedHoldingsModel.count > 0 && addressess.model.count > 0 - signal airdropClicked(var airdropTokens, string address) + signal airdropClicked(var airdropTokens, var addresses) signal navigateToMintTokenSettings QtObject { @@ -205,6 +205,7 @@ StatusScrollView { id: addressInput Layout.fillWidth: true + placeholderText: qsTr("Example: 0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7999") } @@ -249,7 +250,11 @@ StatusScrollView { root.selectedHoldingsModel, ["key", "type", "amount"]) - root.airdropClicked(airdropTokens, addressess.model) + const addresses = ModelUtils.modelToArray( + addressess.model, + ["text"]) + + root.airdropClicked(airdropTokens, addresses) } } }