Skip to content

Commit

Permalink
MEED-3206 Automate the Check of any ERC20 transfer rewarding
Browse files Browse the repository at this point in the history
  • Loading branch information
MayTekayaa committed Mar 5, 2024
1 parent e59d8fc commit 662069c
Show file tree
Hide file tree
Showing 13 changed files with 161 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

import java.util.List;
import java.util.Map;
import java.util.HashMap;

import static io.meeds.gamification.evm.utils.Utils.*;

Expand All @@ -38,4 +37,11 @@ public List<String> getTriggers() {
return List.of(HOLD_TOKEN_EVENT);
}

@Override
public boolean isValidEvent(Map<String, String> eventProperties, String triggerDetails) {
String desiredContractAddress = eventProperties.get(CONTRACT_ADDRESS);
Map<String, String> triggerDetailsMop = stringToMap(triggerDetails);
return desiredContractAddress != null && desiredContractAddress.equals(triggerDetailsMop.get(CONTRACT_ADDRESS));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -70,9 +70,6 @@ public class ERC20TransferTask {
@Autowired
private EvmTriggerService evmTriggerService;

@Autowired
private BlockchainConfigurationProperties blockchainProperties;

@Autowired
private RuleService ruleService;

Expand All @@ -87,34 +84,42 @@ public synchronized void listenTokenTransfer() {
ruleFilter.setProgramStatus(EntityStatusType.ENABLED);
ruleFilter.setDateFilterType(DateFilterType.STARTED);
List<RuleDTO> 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<TokenTransferEvent> 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<RuleDTO> 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<TokenTransferEvent> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,7 +41,6 @@
import java.util.*;
import java.util.stream.Stream;


@Component
public class BlockchainService {

Expand All @@ -49,6 +51,9 @@ public class BlockchainService {
@Autowired
BlockchainConfigurationProperties blockchainProperties;

public static final Event TRANSFER_EVENT = new Event("Transfer",
Arrays.<TypeReference<?>>asList(new TypeReference<Address>(true) {}, new TypeReference<Address>(true) {}, new TypeReference<Uint256>(false) {}));

/**
* Retrieves the list of ERC20 Token transfer transactions
* starting from a block to another
Expand All @@ -61,21 +66,21 @@ public Set<TokenTransferEvent> 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<EthLog.LogResult> ethLogs = ethLog.getLogs();
if (CollectionUtils.isEmpty(ethLogs)) {
return Collections.emptySet();
}
}
List<TokenTransferEvent> 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);
Expand All @@ -95,18 +100,18 @@ public long getLastBlock() {
}
}

private Stream<TokenTransferEvent> 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<MeedsToken.TransferEventResponse> transferEvents = meedsToken.getTransferEvents(transactionReceipt);
private Stream<TokenTransferEvent> 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<ERC20.TransferEventResponse> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> stringToMap(String mapAsString) {
Map<String, String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
</component-plugin>
</external-component-plugins>

<external-component-plugins>
<target-component>io.meeds.gamification.service.EventService</target-component>
<component-plugin>
<name>evm</name>
<set-method>addPlugin</set-method>
<type>io.meeds.gamification.evm.plugin.EvmEventPlugin</type>
</component-plugin>
</external-component-plugins>

<import>jar:/conf/portal/gamification-evm-connector-configuration.xml</import>

</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<string>evm</string>
</field>
<field name="trigger">
<string>holdtoken</string>
<string>holdToken</string>
</field>
</object>
</object-param>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
<name>engagementCenterConnectorEventsEvmExtensions</name>
<load-group>engagement-center-connector-event-extensions</load-group>
<script>
<path>/js/connectorExtensions.bundle.js</path>
<path>/js/connectorEventExtensions.bundle.js</path>
</script>
<depends>
<module>vue</module>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,22 @@ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<template>
<v-app>
<v-card-text class="px-0 dark-grey-color font-weight-bold">
Evm connector properties
{{ $t('gamification.event.form.contractAddress') }}
</v-card-text>
<v-card-text class="ps-0 py-0">
<input
ref="contractAddress"
v-model="contractAddress"
placeholder="Enter the contract address"
type="text"
class="ignore-vuetify-classes full-width"
required
@input="handleAddress"
@change="checkContractAddress(contractAddress)">
</v-card-text>
<v-list-item-action-text v-if="!isValidAddress" class="d-flex py-0 me-0 me-sm-8">
<span class="error--text">{{ $t('gamification.event.detail.invalidContractAddress.error') }}</span>
</v-list-item-action-text>
</v-app>
</template>

Expand All @@ -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;
},
},
};
</script>
Loading

0 comments on commit 662069c

Please sign in to comment.