diff --git a/gamification-evm-services/src/main/java/io/meeds/evm/gamification/plugin/EvmEventPlugin.java b/gamification-evm-services/src/main/java/io/meeds/evm/gamification/plugin/EvmEventPlugin.java index bfb9215..87bfe54 100644 --- a/gamification-evm-services/src/main/java/io/meeds/evm/gamification/plugin/EvmEventPlugin.java +++ b/gamification-evm-services/src/main/java/io/meeds/evm/gamification/plugin/EvmEventPlugin.java @@ -33,7 +33,7 @@ public String getEventType() { } public List<String> getTriggers() { - return List.of(Utils.HOLD_TOKEN_EVENT); + return List.of(Utils.TRANSFER_TOKEN_EVENT); } @Override diff --git a/gamification-evm-services/src/main/java/io/meeds/evm/gamification/scheduling/task/ERC20TransferTask.java b/gamification-evm-services/src/main/java/io/meeds/evm/gamification/scheduling/task/ERC20TransferTask.java index 342004e..63d71d5 100644 --- a/gamification-evm-services/src/main/java/io/meeds/evm/gamification/scheduling/task/ERC20TransferTask.java +++ b/gamification-evm-services/src/main/java/io/meeds/evm/gamification/scheduling/task/ERC20TransferTask.java @@ -15,8 +15,10 @@ */ package io.meeds.evm.gamification.scheduling.task; +import java.math.BigInteger; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import io.meeds.common.ContainerTransactional; import io.meeds.evm.gamification.model.TokenTransferEvent; @@ -81,10 +83,14 @@ public synchronized void listenTokenTransfer() { .toList(); if (CollectionUtils.isNotEmpty(filteredRules)) { filteredRules.forEach(rule -> { + BigInteger minAmount; + String recipientAddress; + BigInteger base = new BigInteger("10"); String blockchainNetwork = rule.getEvent().getProperties().get(Utils.BLOCKCHAIN_NETWORK); String contractAddress = rule.getEvent().getProperties().get(Utils.CONTRACT_ADDRESS); String tokenName = rule.getEvent().getProperties().get(Utils.NAME); String tokenSymbol = rule.getEvent().getProperties().get(Utils.SYMBOL); + Integer tokenDecimals = Integer.parseInt(rule.getEvent().getProperties().get(Utils.DECIMALS)); long lastBlock = blockchainService.getLastBlock(blockchainNetwork); long lastCheckedBlock = getLastCheckedBlock(contractAddress); if (lastCheckedBlock == 0) { @@ -97,24 +103,36 @@ public synchronized void listenTokenTransfer() { lastBlock, contractAddress, blockchainNetwork); - if (!CollectionUtils.isEmpty(events)) { - events.forEach(event -> { - try { - EvmTrigger evmTrigger = new EvmTrigger(); - evmTrigger.setTrigger(Utils.HOLD_TOKEN_EVENT); - evmTrigger.setType(Utils.CONNECTOR_NAME); - evmTrigger.setWalletAddress(event.getTo()); - evmTrigger.setTransactionHash(event.getTransactionHash()); - evmTrigger.setContractAddress(contractAddress); - evmTrigger.setBlockchainNetwork(blockchainNetwork); - evmTrigger.setTokenName(tokenName); - evmTrigger.setTokenSymbol(tokenSymbol); - evmTriggerService.handleTriggerAsync(evmTrigger); - } catch (Exception e) { - LOG.warn("Error broadcasting event '" + event, e); - } - }); - } + if(!CollectionUtils.isEmpty(events) && StringUtils.isNotBlank(rule.getEvent().getProperties().get(Utils.MIN_AMOUNT))) { + minAmount = base.pow(tokenDecimals).multiply(new BigInteger(rule.getEvent().getProperties().get(Utils.MIN_AMOUNT))); + events = events.stream() + .filter(event -> event.getAmount().compareTo(minAmount) > 0) + .collect(Collectors.toSet()); + } + if(!CollectionUtils.isEmpty(events) && StringUtils.isNotBlank(rule.getEvent().getProperties().get(Utils.RECIPIENT_ADDRESS))) { + recipientAddress = rule.getEvent().getProperties().get(Utils.RECIPIENT_ADDRESS); + events = events.stream() + .filter(event -> recipientAddress.toUpperCase().equals(event.getTo().toUpperCase())) + .collect(Collectors.toSet()); + } + if (!CollectionUtils.isEmpty(events)) { + events.forEach(event -> { + try { + EvmTrigger evmTrigger = new EvmTrigger(); + evmTrigger.setTrigger(Utils.TRANSFER_TOKEN_EVENT); + evmTrigger.setType(Utils.CONNECTOR_NAME); + evmTrigger.setWalletAddress(event.getTo()); + evmTrigger.setTransactionHash(event.getTransactionHash()); + evmTrigger.setContractAddress(contractAddress); + evmTrigger.setBlockchainNetwork(blockchainNetwork); + evmTrigger.setTokenName(tokenName); + evmTrigger.setTokenSymbol(tokenSymbol); + evmTriggerService.handleTriggerAsync(evmTrigger); + } catch (Exception e) { + LOG.warn("Error broadcasting event '" + event, e); + } + }); + } saveLastCheckedBlock(lastBlock, contractAddress); LOG.info("End listening erc20 token transfers"); }); diff --git a/gamification-evm-services/src/main/java/io/meeds/evm/gamification/utils/Utils.java b/gamification-evm-services/src/main/java/io/meeds/evm/gamification/utils/Utils.java index d069013..97e78fd 100644 --- a/gamification-evm-services/src/main/java/io/meeds/evm/gamification/utils/Utils.java +++ b/gamification-evm-services/src/main/java/io/meeds/evm/gamification/utils/Utils.java @@ -22,7 +22,7 @@ public class Utils { public static final String CONNECTOR_NAME = "evm"; - public static final String HOLD_TOKEN_EVENT = "holdToken"; + public static final String TRANSFER_TOKEN_EVENT = "transferToken"; public static final String WALLET_ADDRESS = "walletAddress"; @@ -34,6 +34,12 @@ public class Utils { public static final String SYMBOL = "tokenSymbol"; + public static final String DECIMALS = "tokenDecimals"; + + public static final String MIN_AMOUNT = "minAmount"; + + public static final String RECIPIENT_ADDRESS = "recipientAddress"; + public static final String TRANSACTION_HASH = "transactionHash"; private Utils() { diff --git a/gamification-evm-services/src/main/resources/conf/portal/evm-gamification-connector-configuration.xml b/gamification-evm-services/src/main/resources/conf/portal/evm-gamification-connector-configuration.xml index 9003f52..438cc8d 100644 --- a/gamification-evm-services/src/main/resources/conf/portal/evm-gamification-connector-configuration.xml +++ b/gamification-evm-services/src/main/resources/conf/portal/evm-gamification-connector-configuration.xml @@ -25,7 +25,7 @@ <external-component-plugins> <target-component>io.meeds.gamification.service.EventRegistry</target-component> <component-plugin> - <name>HoldToken</name> + <name>TransferToken</name> <set-method>addPlugin</set-method> <type>io.meeds.gamification.plugin.EventConfigPlugin</type> <init-params> @@ -33,13 +33,13 @@ <name>event</name> <object type="io.meeds.gamification.model.EventDTO"> <field name="title"> - <string>holdToken</string> + <string>transferToken</string> </field> <field name="type"> <string>evm</string> </field> <field name="trigger"> - <string>holdToken</string> + <string>transferToken</string> </field> </object> </object-param> 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 878b192..efc1065 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,11 +1,15 @@ -gamification.event.title.holdToken=EVM: Hold Token +gamification.event.title.transferToken=Transfer a Token gamification.event.form.networks=Blockchain -gamification.event.form.contractAddress=Smart Contract -gamification.event.form.contractAddress.placeholder=Enter the contract address +gamification.event.form.contractAddress=Token Address +gamification.event.form.contractAddress.placeholder=Enter the token address gamification.event.form.contractAddress.tooltip=Verif addresss on {0} +gamification.event.form.recipientAddress=Recipient +gamification.event.form.recipientAddress.placeholder=Enter the recipient address +gamification.event.form.minAmount=Minimum amount +gamification.event.form.minAmount.placeholder=Enter the minimum amount gamification.event.detail.invalidContractAddress.error=Please enter a valid contract address gamification.event.detail.invalidERC20ContractAddress.error=No smart contract found at this address gamification.event.detail.verifyToken.message=Click the Checkmark icon to verify this address -gamification.event.detail.display.holdToken=Contract address token +gamification.event.detail.display.transferToken=Contract address token 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/vue-app/connectorEventExtensions/components/EvmEventDisplay.vue b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/components/EvmEventDisplay.vue index e7edb85..15555b3 100644 --- a/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/components/EvmEventDisplay.vue +++ b/gamification-evm-webapp/src/main/webapp/vue-app/connectorEventExtensions/components/EvmEventDisplay.vue @@ -29,6 +29,18 @@ {{ contractAddress }} </div> </a> + <div class="subtitle-1 font-weight-bold mb-2 mt-4"> + {{ $t('gamification.event.form.recipientAddress') }} + </div> + <div class="text-font-size align-self-start"> + {{ recipientAddress }} + </div> + <div class="subtitle-1 font-weight-bold mb-2 mt-4"> + {{ $t('gamification.event.form.minAmount') }} + </div> + <div class="text-font-size align-self-start"> + {{ minAmount }} + </div> </div> </template> <script> @@ -50,8 +62,24 @@ export default { titleTriggerProps() { return this.$t(`gamification.event.detail.display.${this.trigger}`); }, + blockchainNetwork() { + return this.properties?.blockchainNetwork; + }, explorerLink() { - return `https://polygonscan.com/address/${this.contractAddress}`; + const url = this.blockchainNetwork?.substring(this.blockchainNetwork.indexOf('//') + 2, this.blockchainNetwork.indexOf('.g.alchemy.com')); + switch (url) { + case 'polygon-mainnet': return `https://polygonscan.com/address/${this.contractAddress}`; + case 'polygon-mumbai': return `https://mumbai.polygonscan.com/address/${this.contractAddress}`; + case 'eth-mainnet': return `https://etherscan.io/address/${this.contractAddress}`; + case 'eth-sepolia': return `https://sepolia.etherscan.io/address/${this.contractAddress}`; + } + return ''; + }, + minAmount() { + return this.properties?.minAmount; + }, + recipientAddress() { + return this.properties?.recipientAddress; } }, }; 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 62e8205..b02155c 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 @@ -104,6 +104,32 @@ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. <span v-if="isInValidAddressFormat" class="error--text">{{ $t('gamification.event.detail.invalidContractAddress.error') }}</span> <span v-else-if="isInvalidERC20Address" class="error--text">{{ $t('gamification.event.detail.invalidERC20ContractAddress.error') }}</span> <span v-else-if="emptyERC20Token">{{ $t('gamification.event.detail.verifyToken.message') }}</span> + <div v-if="erc20Token"> + <v-card-text class="px-0 dark-grey-color font-weight-bold"> + {{ $t('gamification.event.form.recipientAddress') }} + </v-card-text> + <v-text-field + ref="recipientAddress" + v-model="recipientAddress" + :placeholder="$t('gamification.event.form.recipientAddress.placeholder')" + class="pa-0" + type="text" + outlined + dense + @change="selectedRecipient" /> + <v-card-text class="px-0 dark-grey-color font-weight-bold"> + {{ $t('gamification.event.form.minAmount') }} + </v-card-text> + <v-text-field + ref="minAmount" + v-model="minAmount" + :placeholder="$t('gamification.event.form.minAmount.placeholder')" + class="pa-0" + type="text" + outlined + dense + @change="selectedAmount" /> + </div> </template> </div> </template> @@ -203,21 +229,12 @@ export default { return this.$evmConnectorService.getTokenDetailsByAddress({contractAddress: this.contractAddress, blockchainNetwork: this.selected?.providerUrl}) .then(token => { this.erc20Token = token; - this.eventProperties = { - contractAddress: this.contractAddress, - blockchainNetwork: this.selected?.providerUrl, - tokenName: token.name, - tokenSymbol: token.symbol - }; }) .then(() => this.loading = false ) .catch(() => { this.isValidERC20Address = false; this.erc20Token = null; this.loading = false; - }) - .finally(() => { - this.submitEventProperties(); }); }, resetERC20Token() { @@ -237,13 +254,61 @@ export default { name: this.properties?.tokenName, symbol: this.properties?.tokenSymbol }; + this.minAmount = this.properties?.minAmount; + this.recipientAddress = this.properties?.recipientAddress; this.readOnly = true; this.isValidAddress = true; } document.dispatchEvent(new CustomEvent('event-form-unfilled')); this.loadingNetworks = false; }); + }, + selectedAmount(minAmount) { + if (this.recipientAddress) { + this.eventProperties = { + contractAddress: this.contractAddress, + blockchainNetwork: this.selected?.providerUrl, + tokenName: this.erc20Token.name, + tokenSymbol: this.erc20Token.symbol, + tokenDecimals: this.erc20Token.decimals, + recipientAddress: this.recipientAddress, + minAmount: minAmount + }; + } else { + this.eventProperties = { + contractAddress: this.contractAddress, + blockchainNetwork: this.selected?.providerUrl, + tokenName: this.erc20Token.name, + tokenSymbol: this.erc20Token.symbol, + tokenDecimals: this.erc20Token.decimals, + minAmount: minAmount + }; + } + document.dispatchEvent(new CustomEvent('event-form-filled', {detail: this.eventProperties})); + }, + selectedRecipient(recipientAddress) { + if (this.minAmount) { + this.eventProperties = { + contractAddress: this.contractAddress, + blockchainNetwork: this.selected?.providerUrl, + tokenName: this.erc20Token.name, + tokenSymbol: this.erc20Token.symbol, + tokenDecimals: this.erc20Token.decimals, + recipientAddress: recipientAddress, + minAmount: this.minAmount + }; + } else { + this.eventProperties = { + contractAddress: this.contractAddress, + blockchainNetwork: this.selected?.providerUrl, + tokenName: this.erc20Token.name, + tokenSymbol: this.erc20Token.symbol, + tokenDecimals: this.erc20Token.decimals, + recipientAddress: recipientAddress + }; + } + document.dispatchEvent(new CustomEvent('event-form-filled', {detail: this.eventProperties})); } - }, + } }; </script> \ 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 3183626..7668e0b 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 @@ -25,7 +25,7 @@ export function init() { name: 'evm', vueComponent: Vue.options.components['evm-connector-event'], isEnabled: (params) => [ - 'holdToken', + 'transferToken', ].includes(params?.trigger), }); } \ 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 789f61f..08a73fa 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', + 'transferToken', ].includes(actionLabel), getLink: realization => { if (realization.objectType === 'evm' && realization.objectId !== '') {