Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Automate the transfer of any ERC20 token rewarding - MEED-3206 - Meeds-io/MIPs#118 #3

Merged
merged 2 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -0,0 +1,47 @@
/*
* This file is part of the Meeds project (https://meeds.io/).
*
* Copyright (C) 2020 - 2024 Meeds Association [email protected]
*
* 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<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 @@ -66,5 +66,25 @@
</depends>
</module>

<module>
<name>engagementCenterConnectorEventsEvmExtensions</name>
<load-group>engagement-center-connector-event-extensions</load-group>
<script>
<path>/js/connectorEventExtensions.bundle.js</path>
</script>
<depends>
<module>vue</module>
</depends>
<depends>
<module>vuetify</module>
</depends>
<depends>
<module>eXoVueI18n</module>
</depends>
<depends>
<module>extensionRegistry</module>
</depends>
</module>

</gatein-resources>

Loading
Loading