diff --git a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/model/EvmTrigger.java b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/model/EvmTrigger.java index c9893c1..3632cb1 100644 --- a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/model/EvmTrigger.java +++ b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/model/EvmTrigger.java @@ -28,11 +28,13 @@ public class EvmTrigger { private String walletAddress; + private String contractAddress; + private String type; private String transactionHash; public EvmTrigger clone() { - return new EvmTrigger(trigger, walletAddress, type, transactionHash); + return new EvmTrigger(trigger, walletAddress, contractAddress, type, transactionHash); } } diff --git a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/plugin/EvmEventPlugin.java b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/plugin/EvmEventPlugin.java new file mode 100644 index 0000000..8143554 --- /dev/null +++ b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/plugin/EvmEventPlugin.java @@ -0,0 +1,47 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package io.meeds.gamification.evm.plugin; + +import io.meeds.gamification.plugin.EventPlugin; + +import java.util.List; +import java.util.Map; + +import static io.meeds.gamification.evm.utils.Utils.*; + +public class EvmEventPlugin extends EventPlugin{ + public static final String EVENT_TYPE = "evm"; + + @Override + public String getEventType() { + return EVENT_TYPE; + } + + public List getTriggers() { + return List.of(HOLD_TOKEN_EVENT); + } + + @Override + public boolean isValidEvent(Map eventProperties, String triggerDetails) { + String desiredContractAddress = eventProperties.get(CONTRACT_ADDRESS); + Map triggerDetailsMop = stringToMap(triggerDetails); + return desiredContractAddress != null && desiredContractAddress.equals(triggerDetailsMop.get(CONTRACT_ADDRESS)); + } + +} diff --git a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/scheduling/task/ERC20TransferTask.java b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/scheduling/task/ERC20TransferTask.java index 716343b..b561b8e 100644 --- a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/scheduling/task/ERC20TransferTask.java +++ b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/scheduling/task/ERC20TransferTask.java @@ -21,7 +21,6 @@ import io.meeds.common.ContainerTransactional; import io.meeds.gamification.constant.DateFilterType; import io.meeds.gamification.constant.EntityStatusType; -import io.meeds.gamification.evm.blockchain.BlockchainConfigurationProperties; import io.meeds.gamification.evm.model.EvmTrigger; import io.meeds.gamification.evm.service.EvmTriggerService; import io.meeds.gamification.evm.service.BlockchainService; @@ -30,6 +29,7 @@ import io.meeds.gamification.service.EventService; import io.meeds.gamification.service.RuleService; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.exoplatform.commons.api.settings.SettingService; import org.exoplatform.commons.api.settings.data.Context; import org.exoplatform.commons.api.settings.data.Scope; @@ -70,9 +70,6 @@ public class ERC20TransferTask { @Autowired private EvmTriggerService evmTriggerService; - @Autowired - private BlockchainConfigurationProperties blockchainProperties; - @Autowired private RuleService ruleService; @@ -87,34 +84,42 @@ public synchronized void listenTokenTransfer() { ruleFilter.setProgramStatus(EntityStatusType.ENABLED); ruleFilter.setDateFilterType(DateFilterType.STARTED); List rules = ruleService.getRules(ruleFilter, 0, -1); - if (CollectionUtils.isNotEmpty(rules)) { - long lastBlock = blockchainService.getLastBlock(); - long lastCheckedBlock = getLastCheckedBlock(blockchainProperties.getMeedAddress()); - if (lastCheckedBlock == 0) { - // If this is the first time that it's started, save the last block as - // last checked one - saveLastCheckedBlock(lastBlock, blockchainProperties.getMeedAddress()); - return; - } - Set events = blockchainService.getTransferredTokensTransactions(lastCheckedBlock + 1, - lastBlock, - blockchainProperties.getMeedAddress()); - if (!CollectionUtils.isEmpty(events)) { - events.forEach(event -> { - try { - EvmTrigger evmTrigger = new EvmTrigger(); - evmTrigger.setTrigger(HOLD_TOKEN_EVENT); - evmTrigger.setType(CONNECTOR_NAME); - evmTrigger.setWalletAddress(event.getTo()); - evmTrigger.setTransactionHash(event.getTransactionHash()); - evmTriggerService.handleTriggerAsync(evmTrigger); - } catch (Exception e) { - LOG.warn("Error broadcasting event '" + event, e); - } - }); - } - saveLastCheckedBlock(lastBlock, blockchainProperties.getMeedAddress()); - LOG.info("End listening erc20 token transfers"); + List filteredRules = rules.stream() + .filter(r -> !r.getEvent().getProperties().isEmpty() + && StringUtils.isNotBlank(r.getEvent().getProperties().get(CONTRACT_ADDRESS))) + .toList(); + if (CollectionUtils.isNotEmpty(filteredRules)) { + filteredRules.forEach(rule -> { + String contractAddress = rule.getEvent().getProperties().get(CONTRACT_ADDRESS); + long lastBlock = blockchainService.getLastBlock(); + long lastCheckedBlock = getLastCheckedBlock(contractAddress); + if (lastCheckedBlock == 0) { + // If this is the first time that it's started, save the last block as + // last checked one + saveLastCheckedBlock(lastBlock, contractAddress); + return; + } + Set events = blockchainService.getTransferredTokensTransactions(lastCheckedBlock + 1, + lastBlock, + contractAddress); + if (!CollectionUtils.isEmpty(events)) { + events.forEach(event -> { + try { + EvmTrigger evmTrigger = new EvmTrigger(); + evmTrigger.setTrigger(HOLD_TOKEN_EVENT); + evmTrigger.setType(CONNECTOR_NAME); + evmTrigger.setWalletAddress(event.getTo()); + evmTrigger.setTransactionHash(event.getTransactionHash()); + evmTrigger.setContractAddress(contractAddress); + evmTriggerService.handleTriggerAsync(evmTrigger); + } catch (Exception e) { + LOG.warn("Error broadcasting event '" + event, e); + } + }); + } + saveLastCheckedBlock(lastBlock, contractAddress); + LOG.info("End listening erc20 token transfers"); + }); } } catch (Exception e) { LOG.error("An error occurred while listening erc20 token transfers", e); diff --git a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/service/BlockchainService.java b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/service/BlockchainService.java index e8453b3..751cc4f 100644 --- a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/service/BlockchainService.java +++ b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/service/BlockchainService.java @@ -18,12 +18,15 @@ import io.meeds.gamification.evm.blockchain.BlockchainConfigurationProperties; import io.meeds.gamification.evm.model.TokenTransferEvent; import org.apache.commons.collections.CollectionUtils; -import org.exoplatform.wallet.contract.MeedsToken; +import org.exoplatform.wallet.contract.ERC20; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.web3j.abi.EventEncoder; +import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Event; +import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameterNumber; import org.web3j.protocol.core.methods.request.EthFilter; @@ -38,7 +41,6 @@ import java.util.*; import java.util.stream.Stream; - @Component public class BlockchainService { @@ -49,6 +51,9 @@ public class BlockchainService { @Autowired BlockchainConfigurationProperties blockchainProperties; + public static final Event TRANSFER_EVENT = new Event("Transfer", + Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference(false) {})); + /** * Retrieves the list of ERC20 Token transfer transactions * starting from a block to another @@ -61,21 +66,21 @@ public Set getTransferredTokensTransactions(long fromBlock, EthFilter ethFilter = new EthFilter(new DefaultBlockParameterNumber(fromBlock), new DefaultBlockParameterNumber(toBlock), contractAddress); - ethFilter.addSingleTopic(EventEncoder.encode(MeedsToken.TRANSFER_EVENT)); + ethFilter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT)); try { EthLog ethLog = polygonWeb3j.ethGetLogs(ethFilter).send(); @SuppressWarnings("rawtypes") List ethLogs = ethLog.getLogs(); if (CollectionUtils.isEmpty(ethLogs)) { return Collections.emptySet(); - } + } List transferEvents = ethLogs.stream() .map(logResult -> (EthLog.LogObject) logResult.get()) .filter(logObject -> !logObject.isRemoved()) .map(EthLog.LogObject::getTransactionHash) .map(this::getTransactionReceipt) .filter(TransactionReceipt::isStatusOK) - .flatMap(this::getTransferEvents) + .flatMap(transactionReceipt -> getTransferEvents(transactionReceipt, contractAddress)) .filter(Objects::nonNull) .toList(); return new LinkedHashSet<>(transferEvents); @@ -95,18 +100,18 @@ public long getLastBlock() { } } - private Stream getTransferEvents(TransactionReceipt transactionReceipt) { - MeedsToken meedsToken = MeedsToken.load(blockchainProperties.getMeedAddress(), - polygonWeb3j, - new ReadonlyTransactionManager(polygonWeb3j, Address.DEFAULT.toString()), - new StaticGasProvider(BigInteger.valueOf(20000000000l), BigInteger.valueOf(300000l))); - List transferEvents = meedsToken.getTransferEvents(transactionReceipt); + private Stream getTransferEvents(TransactionReceipt transactionReceipt, String contractAddress) { + ERC20 erc20Token = ERC20.load(contractAddress, + polygonWeb3j, + new ReadonlyTransactionManager(polygonWeb3j, Address.DEFAULT.toString()), + new StaticGasProvider(BigInteger.valueOf(20000000000l), BigInteger.valueOf(300000l))); + List transferEvents = erc20Token.getTransferEvents(transactionReceipt); if (transferEvents != null && !transferEvents.isEmpty()) { return transferEvents.stream() .map(transferEventResponse -> new TokenTransferEvent(transferEventResponse.from, - transferEventResponse.to, - transferEventResponse.value, - transferEventResponse.log.getTransactionHash())); + transferEventResponse.to, + transferEventResponse.value, + transferEventResponse.log.getTransactionHash())); } return Stream.empty(); } diff --git a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/service/EvmTriggerService.java b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/service/EvmTriggerService.java index 10cd626..56f9116 100644 --- a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/service/EvmTriggerService.java +++ b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/service/EvmTriggerService.java @@ -112,7 +112,7 @@ private void processEvent(EvmTrigger evmTrigger) { Identity socialIdentity = identityManager.getOrCreateUserIdentity(receiverId); if (socialIdentity != null) { String eventDetails = "{" + WALLET_ADDRESS + ": " + evmTrigger.getWalletAddress() + ", " + TRANSACTION_HASH + ": " - + evmTrigger.getTransactionHash() + "}"; + + evmTrigger.getTransactionHash() + ", " + CONTRACT_ADDRESS + ": " + evmTrigger.getContractAddress() + "}"; broadcastEvmEvent(evmTrigger.getTrigger(), receiverId, evmTrigger.getTransactionHash(), diff --git a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/utils/Utils.java b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/utils/Utils.java index 327d55f..56dfe74 100644 --- a/gamification-evm-services/src/main/java/io/meeds/gamification/evm/utils/Utils.java +++ b/gamification-evm-services/src/main/java/io/meeds/gamification/evm/utils/Utils.java @@ -15,17 +15,35 @@ */ package io.meeds.gamification.evm.utils; +import java.util.HashMap; +import java.util.Map; + public class Utils { public static final String CONNECTOR_NAME = "evm"; - public static final String HOLD_TOKEN_EVENT = "holdtoken"; + public static final String HOLD_TOKEN_EVENT = "holdToken"; public static final String WALLET_ADDRESS = "walletAddress"; + public static final String CONTRACT_ADDRESS = "contractAddress"; + public static final String TRANSACTION_HASH = "transactionHash"; private Utils() { } + + public static Map stringToMap(String mapAsString) { + Map map = new HashMap<>(); + mapAsString = mapAsString.substring(1, mapAsString.length() - 1); + String[] pairs = mapAsString.split(", "); + for (String pair : pairs) { + String[] keyValue = pair.split(": "); + String key = keyValue[0].trim(); + String value = keyValue[1].trim(); + map.put(key, value); + } + return map; + } } diff --git a/gamification-evm-services/src/main/resources/conf/portal/configuration.xml b/gamification-evm-services/src/main/resources/conf/portal/configuration.xml index d22bbc0..1131d67 100644 --- a/gamification-evm-services/src/main/resources/conf/portal/configuration.xml +++ b/gamification-evm-services/src/main/resources/conf/portal/configuration.xml @@ -43,6 +43,15 @@ + + io.meeds.gamification.service.EventService + + evm + addPlugin + io.meeds.gamification.evm.plugin.EvmEventPlugin + + + jar:/conf/portal/gamification-evm-connector-configuration.xml \ No newline at end of file diff --git a/gamification-evm-services/src/main/resources/conf/portal/gamification-evm-connector-configuration.xml b/gamification-evm-services/src/main/resources/conf/portal/gamification-evm-connector-configuration.xml index 2d57b11..9003f52 100644 --- a/gamification-evm-services/src/main/resources/conf/portal/gamification-evm-connector-configuration.xml +++ b/gamification-evm-services/src/main/resources/conf/portal/gamification-evm-connector-configuration.xml @@ -39,7 +39,7 @@ evm - holdtoken + holdToken diff --git a/gamification-evm-webapp/src/main/resources/locale/addon/Gamification_en.properties b/gamification-evm-webapp/src/main/resources/locale/addon/Gamification_en.properties index 6a75186..107e1b3 100644 --- a/gamification-evm-webapp/src/main/resources/locale/addon/Gamification_en.properties +++ b/gamification-evm-webapp/src/main/resources/locale/addon/Gamification_en.properties @@ -1,3 +1,5 @@ -gamification.event.title.holdtoken=EVM: Hold Token +gamification.event.title.holdToken=EVM: Hold Token +gamification.event.form.contractAddress=Contract address +gamification.event.detail.invalidContractAddress.error=Please enter a valis contract address gamification.admin.evm.label.description=Listen to any smart contract transaction on Ethereum Virtual Machine blockchains \ No newline at end of file diff --git a/gamification-evm-webapp/src/main/webapp/WEB-INF/gatein-resources.xml b/gamification-evm-webapp/src/main/webapp/WEB-INF/gatein-resources.xml index 9477e84..6d8b9e7 100644 --- a/gamification-evm-webapp/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/gamification-evm-webapp/src/main/webapp/WEB-INF/gatein-resources.xml @@ -66,5 +66,25 @@ + + engagementCenterConnectorEventsEvmExtensions + engagement-center-connector-event-extensions + + + vue + + + vuetify + + + eXoVueI18n + + + extensionRegistry + + + diff --git a/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/components/EvmEventForm.vue b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/components/EvmEventForm.vue new file mode 100644 index 0000000..be4a60f --- /dev/null +++ b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/components/EvmEventForm.vue @@ -0,0 +1,92 @@ + + + + \ No newline at end of file diff --git a/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/extensions.js b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/extensions.js new file mode 100644 index 0000000..ee0c6c5 --- /dev/null +++ b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/extensions.js @@ -0,0 +1,30 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +import '../connectorEventExtensions/initComponents'; + +export function init() { + extensionRegistry.registerComponent('engagementCenterEvent', 'connector-event-extensions', { + id: 'evm-event', + name: 'evm', + vueComponent: Vue.options.components['evm-connector-event-form'], + isEnabled: (params) => [ + 'holdToken', + ].includes(params?.trigger), + }); +} \ No newline at end of file diff --git a/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/initComponents.js b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/initComponents.js new file mode 100644 index 0000000..cbdf229 --- /dev/null +++ b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/initComponents.js @@ -0,0 +1,26 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import EvmConnectorEventForm from './components/EvmEventForm.vue'; + +const components = { + 'evm-connector-event-form': EvmConnectorEventForm, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} \ No newline at end of file diff --git a/gamification-evm-webapp/src/main/webapp/vue-app/engagementCenterExtensions/extensions.js b/gamification-evm-webapp/src/main/webapp/vue-app/engagementCenterExtensions/extensions.js index 089f27a..6701a18 100644 --- a/gamification-evm-webapp/src/main/webapp/vue-app/engagementCenterExtensions/extensions.js +++ b/gamification-evm-webapp/src/main/webapp/vue-app/engagementCenterExtensions/extensions.js @@ -23,7 +23,7 @@ export function init() { rank: 60, image: '/gamification-evm/images/EVM.png', match: (actionLabel) => [ - 'holdtoken', + 'holdToken', ].includes(actionLabel), getLink: realization => { if (realization.objectType === 'evm' && realization.objectId !== '') { @@ -31,6 +31,7 @@ export function init() { return realization.link; } }, + isExtensible: true } }); } \ No newline at end of file diff --git a/gamification-evm-webapp/webpack.prod.js b/gamification-evm-webapp/webpack.prod.js index 6eab67a..190edd3 100644 --- a/gamification-evm-webapp/webpack.prod.js +++ b/gamification-evm-webapp/webpack.prod.js @@ -7,6 +7,7 @@ const config = merge(webpackCommonConfig, { entry: { engagementCenterExtensions: './src/main/webapp/vue-app/engagementCenterExtensions/extensions.js', connectorExtensions: './src/main/webapp/vue-app/connectorExtensions/extensions.js', + connectorEventExtensions: './src/main/webapp/vue-app/connectorEventExtensions/extensions.js', }, output: { path: path.join(__dirname, 'target/gamification-evm/'),