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 index a8aa69b..8143554 100644 --- 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 @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; -import java.util.HashMap; import static io.meeds.gamification.evm.utils.Utils.*; @@ -38,4 +37,11 @@ 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 e2be47a..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 @@ -70,7 +70,7 @@ engagementCenterConnectorEventsEvmExtensions engagement-center-connector-event-extensions vue 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 index 99fee0f..be4a60f 100644 --- 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 @@ -19,8 +19,22 @@ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. @@ -31,6 +45,48 @@ export default { type: Object, default: null } - } + }, + data() { + return { + contractAddress: null, + startTypingKeywordTimeout: 0, + startSearchAfterInMilliseconds: 300, + endTypingKeywordTimeout: 50, + isValidAddress: true + }; + }, + methods: { + handleAddress() { + if (this.contractAddress) { + this.startTypingKeywordTimeout = Date.now() + this.startSearchAfterInMilliseconds; + if (!this.typing) { + this.typing = true; + this.waitForEndTyping(); + } + } + }, + waitForEndTyping() { + window.setTimeout(() => { + if (Date.now() > this.startTypingKeywordTimeout) { + this.typing = false; + if (this.checkContractAddress(this.contractAddress) && this.contractAddress !== this.properties?.contractAddress) { + const eventProperties = { + contractAddress: this.contractAddress + }; + document.dispatchEvent(new CustomEvent('event-form-filled', {detail: eventProperties})); + } else { + document.dispatchEvent(new CustomEvent('event-form-unfilled')); + } + } else { + this.waitForEndTyping(); + } + }, this.endTypingKeywordTimeout); + }, + checkContractAddress(contractAddress) { + const addressUrlRegex = /^(0x)?[0-9a-f]{40}$/i; + this.isValidAddress = addressUrlRegex.test(contractAddress); + return this.isValidAddress; + }, + }, }; \ 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 index eabb67f..ee0c6c5 100644 --- a/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/extensions.js +++ b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/extensions.js @@ -16,6 +16,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ +import '../connectorEventExtensions/initComponents'; export function init() { extensionRegistry.registerComponent('engagementCenterEvent', 'connector-event-extensions', { @@ -23,8 +24,7 @@ export function init() { name: 'evm', vueComponent: Vue.options.components['evm-connector-event-form'], isEnabled: (params) => [ - 'holdtoken', + 'holdToken', ].includes(params?.trigger), - rank: 1, }); } \ 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 265fdac..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 !== '') {