@@ -3,12 +3,14 @@ package com.jd.blockchain.contract.engine; | |||
import com.jd.blockchain.contract.ContractEventContext; | |||
import com.jd.blockchain.utils.Bytes; | |||
import java.util.concurrent.CompletableFuture; | |||
import java.util.concurrent.Future; | |||
public interface ContractCode { | |||
Bytes getAddress(); | |||
long getVersion(); | |||
void processEvent(ContractEventContext eventContext); | |||
void processEvent(ContractEventContext eventContext, CompletableFuture<String> execReturn); | |||
} |
@@ -14,6 +14,8 @@ import org.springframework.util.ReflectionUtils; | |||
import java.lang.reflect.Method; | |||
import java.util.List; | |||
import java.util.concurrent.CompletableFuture; | |||
import java.util.concurrent.Future; | |||
/** | |||
* contract code based jvm | |||
@@ -46,9 +48,9 @@ public class JavaContractCode implements ContractCode { | |||
} | |||
@Override | |||
public void processEvent(ContractEventContext eventContext) { | |||
public void processEvent(ContractEventContext eventContext, CompletableFuture<String> execReturn) { | |||
this.contractEventContext = eventContext; | |||
codeModule.execute(new ContractExecution()); | |||
codeModule.execute(new ContractExecution(execReturn)); | |||
} | |||
private Object resolveArgs(byte[] args, List<DataContract> dataContractList) { | |||
@@ -59,6 +61,13 @@ public class JavaContractCode implements ContractCode { | |||
} | |||
class ContractExecution implements Runnable { | |||
private CompletableFuture<String> contractReturn; | |||
public ContractExecution(CompletableFuture<String> contractReturn) { | |||
this.contractReturn = contractReturn; | |||
} | |||
public void run() { | |||
LOGGER.info("ContractThread execute()."); | |||
try { | |||
@@ -66,12 +75,16 @@ public class JavaContractCode implements ContractCode { | |||
long startTime = System.currentTimeMillis(); | |||
String contractClassName = codeModule.getMainClass(); | |||
Class myClass = codeModule.loadClass(contractClassName); | |||
Object contractMainClassObj = myClass.newInstance();// 合约主类生成的类实例; | |||
Method beforeMth_ = myClass.getMethod("beforeEvent", | |||
codeModule.loadClass(ContractEventContext.class.getName())); | |||
ReflectionUtils.invokeMethod(beforeMth_, contractMainClassObj, contractEventContext); | |||
LOGGER.info("beforeEvent,耗时:" + (System.currentTimeMillis() - startTime)); | |||
// Method eventMethod = this.getMethodByAnno(contractMainClassObj, contractEventContext.getEvent()); | |||
@@ -79,25 +92,38 @@ public class JavaContractCode implements ContractCode { | |||
// 反序列化参数; | |||
contractType = ContractType.resolve(myClass); | |||
Method handleMethod = contractType.getHandleMethod(contractEventContext.getEvent()); | |||
if (handleMethod == null){ | |||
throw new IllegalDataException("don't get this method by it's @ContractEvent."); | |||
} | |||
Object args = resolveArgs(contractEventContext.getArgs(), | |||
contractType.getDataContractMap().get(handleMethod)); | |||
Object[] params = null; | |||
if(args.getClass().isArray()){ | |||
params = (Object[])args; | |||
} | |||
ReflectionUtils.invokeMethod(handleMethod, contractMainClassObj, params); | |||
String contractReturn = ReflectionUtils.invokeMethod(handleMethod, contractMainClassObj, params).toString(); | |||
LOGGER.info("合约执行,耗时:" + (System.currentTimeMillis() - startTime)); | |||
Method mth2 = myClass.getMethod("postEvent"); | |||
startTime = System.currentTimeMillis(); | |||
ReflectionUtils.invokeMethod(mth2, contractMainClassObj); | |||
LOGGER.info("postEvent,耗时:" + (System.currentTimeMillis() - startTime)); | |||
// 填充return结果 | |||
if (this.contractReturn != null) { | |||
this.contractReturn.complete(contractReturn); | |||
} | |||
} catch (NoSuchMethodException e) { | |||
throw new IllegalArgumentException(e.getMessage()); | |||
} catch (Exception e) { | |||
@@ -0,0 +1,62 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |||
<parent> | |||
<artifactId>contract</artifactId> | |||
<groupId>com.jd.blockchain</groupId> | |||
<version>0.9.0-SNAPSHOT</version> | |||
</parent> | |||
<modelVersion>4.0.0</modelVersion> | |||
<artifactId>contract-samples</artifactId> | |||
<name>contract-samples</name> | |||
<dependencies> | |||
<dependency> | |||
<groupId>com.jd.blockchain</groupId> | |||
<artifactId>ledger-model</artifactId> | |||
<version>${project.version}</version> | |||
<scope>provided</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.jd.blockchain</groupId> | |||
<artifactId>crypto-framework</artifactId> | |||
<version>${project.version}</version> | |||
<scope>provided</scope> | |||
</dependency> | |||
</dependencies> | |||
<build> | |||
<plugins> | |||
<plugin> | |||
<artifactId>maven-assembly-plugin</artifactId> | |||
<configuration> | |||
<finalName>contract</finalName> | |||
<appendAssemblyId>false</appendAssemblyId> | |||
<archive> | |||
<manifest> | |||
<mainClass>com.jd.blockchain.contract.ReadContractImpl</mainClass> | |||
</manifest> | |||
</archive> | |||
<descriptorRefs> | |||
<descriptorRef>jar-with-dependencies</descriptorRef> | |||
</descriptorRefs> | |||
</configuration> | |||
<executions> | |||
<execution> | |||
<id>make-assembly</id> | |||
<phase>package</phase> | |||
<goals> | |||
<goal>single</goal> | |||
</goals> | |||
</execution> | |||
</executions> | |||
</plugin> | |||
</plugins> | |||
</build> | |||
</project> |
@@ -0,0 +1,10 @@ | |||
package com.jd.blockchain.contract; | |||
@Contract | |||
public interface ReadContract { | |||
@ContractEvent(name = "read-key") | |||
String read(String address, String key); | |||
} | |||
@@ -0,0 +1,43 @@ | |||
package com.jd.blockchain.contract; | |||
import com.jd.blockchain.crypto.HashDigest; | |||
import com.jd.blockchain.ledger.KVDataEntry; | |||
@Contract | |||
public class ReadContractImpl implements EventProcessingAwire, ReadContract { | |||
private ContractEventContext eventContext; | |||
@Override | |||
public void beforeEvent(ContractEventContext eventContext) { | |||
this.eventContext = eventContext; | |||
} | |||
@Override | |||
public void postEvent(ContractEventContext eventContext, ContractException error) { | |||
} | |||
@Override | |||
public void postEvent(ContractException error) { | |||
} | |||
@Override | |||
public void postEvent() { | |||
} | |||
@Override | |||
@ContractEvent(name = "read-key") | |||
public String read(String address, String key) { | |||
HashDigest ledgerHash = eventContext.getCurrentLedgerHash(); | |||
KVDataEntry[] kvDataEntries = eventContext.getLedger().getDataEntries(ledgerHash, address, key); | |||
if (kvDataEntries != null && kvDataEntries.length == 1) { | |||
return kvDataEntries[0].getValue().toString(); | |||
} | |||
return null; | |||
} | |||
} |
@@ -13,6 +13,7 @@ | |||
<module>contract-framework</module> | |||
<module>contract-jvm</module> | |||
<module>contract-maven-plugin</module> | |||
</modules> | |||
<module>contract-samples</module> | |||
</modules> | |||
</project> |
@@ -3,7 +3,9 @@ package com.jd.blockchain.ledger.core.impl; | |||
import java.util.ArrayList; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import java.util.concurrent.CompletableFuture; | |||
import com.jd.blockchain.ledger.core.impl.handles.ContractEventSendOperationHandle; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
@@ -74,6 +76,7 @@ public class TransactionBatchProcessor implements TransactionBatchProcess { | |||
// 此调用将会验证交易签名,验签失败将会抛出异常,同时,不记录签名错误的交易到链上; | |||
LedgerTransactionContext txCtx = newBlockEditor.newTransaction(request); | |||
TransactionState result; | |||
List<CompletableFuture<String>> contractReturn = new ArrayList<>(); | |||
try { | |||
LedgerDataSet dataset = txCtx.getDataSet(); | |||
TransactionRequestContext reqCtx = new TransactionRequestContextImpl(request); | |||
@@ -102,7 +105,14 @@ public class TransactionBatchProcessor implements TransactionBatchProcess { | |||
OperationHandle opHandle; | |||
for (Operation op : ops) { | |||
opHandle = opHandles.getHandle(op.getClass()); | |||
opHandle.process(op, dataset, reqCtx, previousBlockDataset, handleContext, ledgerService); | |||
// 合约执行需要填充执行结果 | |||
if (opHandle instanceof ContractEventSendOperationHandle) { | |||
CompletableFuture<String> currContractReturn = new CompletableFuture<>(); | |||
contractReturn.add(currContractReturn); | |||
((ContractEventSendOperationHandle) opHandle).process(op, dataset, reqCtx, previousBlockDataset, handleContext, ledgerService, currContractReturn); | |||
} else { | |||
opHandle.process(op, dataset, reqCtx, previousBlockDataset, handleContext, ledgerService); | |||
} | |||
} | |||
// 提交交易(事务); | |||
@@ -120,8 +130,21 @@ public class TransactionBatchProcessor implements TransactionBatchProcess { | |||
LOGGER.warn(String.format("Transaction rollback caused by the system exception! --[TxHash=%s] --%s", | |||
request.getHash().toBase58(), e.getMessage()), e); | |||
} | |||
TxResponseHandle resp = new TxResponseHandle(request, result); | |||
if (!contractReturn.isEmpty()) { | |||
// 获取结果中的字符串 | |||
String[] returnValue = new String[contractReturn.size()]; | |||
try { | |||
for (int i = 0; i < contractReturn.size(); i++) { | |||
returnValue[i] = contractReturn.get(i).get(); | |||
} | |||
resp.setContractReturn(returnValue); | |||
} catch (Exception e) { | |||
throw new IllegalStateException(e); | |||
} | |||
} | |||
responseList.add(resp); | |||
return resp; | |||
@@ -201,6 +224,8 @@ public class TransactionBatchProcessor implements TransactionBatchProcess { | |||
private TransactionState result; | |||
private String[] contractReturn; | |||
public TxResponseHandle(TransactionRequest request, TransactionState result) { | |||
this.request = request; | |||
this.result = result; | |||
@@ -231,6 +256,14 @@ public class TransactionBatchProcessor implements TransactionBatchProcess { | |||
return globalResult == null ? result == TransactionState.SUCCESS : globalResult == TransactionState.SUCCESS; | |||
} | |||
@Override | |||
public String[] getContractReturn() { | |||
return contractReturn; | |||
} | |||
public void setContractReturn(String[] contractReturn) { | |||
this.contractReturn = contractReturn; | |||
} | |||
} | |||
private class TransactionBatchResultImpl implements TransactionBatchResult { | |||
@@ -20,6 +20,9 @@ import com.jd.blockchain.ledger.core.TransactionRequestContext; | |||
import com.jd.blockchain.ledger.core.impl.LedgerQueryService; | |||
import com.jd.blockchain.ledger.core.impl.OperationHandleContext; | |||
import java.util.concurrent.CompletableFuture; | |||
import java.util.concurrent.Future; | |||
@Service | |||
public class ContractEventSendOperationHandle implements OperationHandle { | |||
@@ -32,6 +35,18 @@ public class ContractEventSendOperationHandle implements OperationHandle { | |||
@Override | |||
public void process(Operation op, LedgerDataSet dataset, TransactionRequestContext requestContext, | |||
LedgerDataSet previousBlockDataset, OperationHandleContext opHandleContext, LedgerService ledgerService) { | |||
process(op, dataset, requestContext, previousBlockDataset, opHandleContext, ledgerService, null); | |||
} | |||
@Override | |||
public boolean support(Class<?> operationType) { | |||
return ContractEventSendOperation.class.isAssignableFrom(operationType); | |||
} | |||
public void process(Operation op, LedgerDataSet dataset, TransactionRequestContext requestContext, | |||
LedgerDataSet previousBlockDataset, OperationHandleContext opHandleContext, | |||
LedgerService ledgerService, CompletableFuture<String> contractReturn) { | |||
ContractEventSendOperation contractOP = (ContractEventSendOperation) op; | |||
// 先从账本校验合约的有效性; | |||
// 注意:必须在前一个区块的数据集中进行校验,因为那是经过共识的数据;从当前新区块链数据集校验则会带来攻击风险:未经共识的合约得到执行; | |||
@@ -66,12 +81,8 @@ public class ContractEventSendOperationHandle implements OperationHandle { | |||
} | |||
// 处理合约事件; | |||
contractCode.processEvent(localContractEventContext); | |||
contractCode.processEvent(localContractEventContext, contractReturn); | |||
} | |||
@Override | |||
public boolean support(Class<?> operationType) { | |||
return ContractEventSendOperation.class.isAssignableFrom(operationType); | |||
} | |||
} |
@@ -27,6 +27,8 @@ public class TransactionRespHandle implements TransactionResponse { | |||
private TransactionState globalResult; | |||
private String[] contractReturn; | |||
public TransactionRespHandle(TransactionRequest request, TransactionState result, TransactionState globalResult) { | |||
this.request = request; | |||
this.result = result; | |||
@@ -49,6 +51,10 @@ public class TransactionRespHandle implements TransactionResponse { | |||
this.result = result; | |||
} | |||
public void setContractReturn(String[] contractReturn) { | |||
this.contractReturn = contractReturn; | |||
} | |||
public LedgerBlock getBlock() { | |||
return block; | |||
} | |||
@@ -89,4 +95,9 @@ public class TransactionRespHandle implements TransactionResponse { | |||
public boolean isSuccess() { | |||
return globalResult == null ? result == TransactionState.SUCCESS : globalResult == TransactionState.SUCCESS; | |||
} | |||
@Override | |||
public String[] getContractReturn() { | |||
return contractReturn; | |||
} | |||
} |
@@ -49,8 +49,20 @@ public interface TransactionResponse { | |||
*/ | |||
@DataField(order=4, primitiveType=PrimitiveType.INT64) | |||
long getBlockHeight(); | |||
/** | |||
* 交易是否执行成功 | |||
* | |||
* @return | |||
*/ | |||
@DataField(order=5, primitiveType=PrimitiveType.BOOLEAN) | |||
boolean isSuccess(); | |||
/** | |||
* 合约返回值 | |||
* | |||
* @return | |||
*/ | |||
@DataField(order=6, list=true, primitiveType=PrimitiveType.TEXT) | |||
String[] getContractReturn(); | |||
} |
@@ -17,6 +17,8 @@ public class TxResponseMessage implements TransactionResponse { | |||
private long blockHeight; | |||
private TransactionState executionState; | |||
private String[] contractReturn; | |||
public TxResponseMessage() { | |||
} | |||
@@ -39,9 +41,6 @@ public class TxResponseMessage implements TransactionResponse { | |||
this.executionState = executionState; | |||
} | |||
/* (non-Javadoc) | |||
* @see com.jd.blockchain.ledger.TransactionResponse#getBlockHash() | |||
*/ | |||
@Override | |||
public HashDigest getBlockHash() { | |||
return blockHash; | |||
@@ -55,13 +54,23 @@ public class TxResponseMessage implements TransactionResponse { | |||
public long getBlockHeight() { | |||
return blockHeight; | |||
} | |||
public void setBlockHeight(long blockHeight) { | |||
this.blockHeight = blockHeight; | |||
} | |||
public void setContractReturn(String[] contractReturn) { | |||
this.contractReturn = contractReturn; | |||
} | |||
@Override | |||
public boolean isSuccess() { | |||
return blockHash != null & executionState == TransactionState.SUCCESS; | |||
} | |||
@Override | |||
public String[] getContractReturn() { | |||
return contractReturn; | |||
} | |||
} |
@@ -307,6 +307,11 @@ public class ConsensusMessageDispatcher implements MessageHandle { | |||
public boolean isSuccess() { | |||
return this.txResp.isSuccess(); | |||
} | |||
@Override | |||
public String[] getContractReturn() { | |||
return txResp.getContractReturn(); | |||
} | |||
} | |||
private final class BlockStateSnapshot implements StateSnapshot { | |||
@@ -50,15 +50,12 @@ public abstract class AbstractModule implements Module { | |||
if (origClassLoader != moduleClassLoader) { | |||
Thread.currentThread().setContextClassLoader(moduleClassLoader); | |||
} | |||
return CompletableAsyncFuture.runAsync(new Runnable() { | |||
@Override | |||
public void run() { | |||
try { | |||
runnable.run(); | |||
} finally { | |||
if (origClassLoader != Thread.currentThread().getContextClassLoader()) { | |||
Thread.currentThread().setContextClassLoader(origClassLoader); | |||
} | |||
return CompletableAsyncFuture.runAsync(() -> { | |||
try { | |||
runnable.run(); | |||
} finally { | |||
if (origClassLoader != Thread.currentThread().getContextClassLoader()) { | |||
Thread.currentThread().setContextClassLoader(origClassLoader); | |||
} | |||
} | |||
}); | |||
@@ -40,6 +40,13 @@ | |||
<artifactId>sdk-client</artifactId> | |||
<version>${project.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.jd.blockchain</groupId> | |||
<artifactId>contract-samples</artifactId> | |||
<version>${project.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>io.nats</groupId> | |||
<artifactId>jnats</artifactId> | |||
@@ -9,6 +9,7 @@ | |||
package test.com.jd.blockchain.intgr; | |||
import com.jd.blockchain.binaryproto.DataContractRegistry; | |||
import com.jd.blockchain.contract.ReadContract; | |||
import com.jd.blockchain.crypto.AddressEncoding; | |||
import com.jd.blockchain.crypto.AsymmetricKeypair; | |||
import com.jd.blockchain.crypto.HashDigest; | |||
@@ -441,7 +442,7 @@ public class IntegrationBase { | |||
static BlockchainKeypair contractDeployKey = BlockchainKeyGenerator.getInstance().generate(); | |||
// 保存资产总数的键; | |||
// 第二个参数; | |||
private static String contractZipName = "contract.jar"; | |||
private static String contractZipName = "contract-read.jar"; | |||
static HashDigest txContentHash; | |||
public static LedgerBlock testSDK_Contract(AsymmetricKeypair adminKey, HashDigest ledgerHash, | |||
BlockchainService blockchainService,LedgerRepository ledgerRepository) { | |||
@@ -473,9 +474,11 @@ public class IntegrationBase { | |||
.getChainCode(); | |||
assertArrayEquals(contractCode, contractCodeInDb); | |||
testExeReadContract(adminKey, ledgerHash, blockchainService); | |||
// execute the contract; | |||
testContractExe(adminKey, ledgerHash, keyPairResponse.keyPair, blockchainService, ledgerRepository); | |||
testContractExe1(adminKey, ledgerHash, keyPairResponse.keyPair, blockchainService, ledgerRepository); | |||
// testContractExe(adminKey, ledgerHash, keyPairResponse.keyPair, blockchainService, ledgerRepository); | |||
// testContractExe1(adminKey, ledgerHash, keyPairResponse.keyPair, blockchainService, ledgerRepository); | |||
return block; | |||
} | |||
@@ -509,7 +512,7 @@ public class IntegrationBase { | |||
assertEquals("100",kvDataEntries[0].getValue().toString()); | |||
} | |||
private static <T> void testContractExe1(AsymmetricKeypair adminKey, HashDigest ledgerHash, BlockchainKeypair dataKey, | |||
private static <T> void testContractExe1(AsymmetricKeypair adminKey, HashDigest ledgerHash, BlockchainKeypair dataKey, | |||
BlockchainService blockchainService,LedgerRepository ledgerRepository) { | |||
LedgerInfo ledgerInfo = blockchainService.getLedger(ledgerHash); | |||
LedgerBlock previousBlock = blockchainService.getBlock(ledgerHash, ledgerInfo.getLatestBlockHeight() - 1); | |||
@@ -537,6 +540,68 @@ public class IntegrationBase { | |||
assertEquals(888L,kvDataEntries[1].getValue()); | |||
} | |||
private static <T> void testExeReadContract(AsymmetricKeypair adminKey, HashDigest ledgerHash, BlockchainService blockchainService) { | |||
// 首先注册一个数据账户 | |||
BlockchainKeypair newDataAccount = BlockchainKeyGenerator.getInstance().generate(); | |||
TransactionTemplate txTpl = blockchainService.newTransaction(ledgerHash); | |||
txTpl.dataAccounts().register(newDataAccount.getIdentity()); | |||
PreparedTransaction ptx = txTpl.prepare(); | |||
ptx.sign(adminKey); | |||
// 提交并等待共识返回; | |||
ptx.commit(); | |||
// 再提交一个KV写入 | |||
String key1 = "JingDong", value1 = "www.jd.com"; | |||
String key2 = "JD", value2 = "JingDong"; | |||
TransactionTemplate txKv = blockchainService.newTransaction(ledgerHash); | |||
txKv.dataAccount(newDataAccount.getAddress()).set(key1, value1, -1).set(key2, value2, -1); | |||
PreparedTransaction kvPtx = txKv.prepare(); | |||
kvPtx.sign(adminKey); | |||
// 提交并等待共识返回; | |||
kvPtx.commit(); | |||
// 下面才是执行Read交易 | |||
// 定义交易; | |||
TransactionTemplate txContract = blockchainService.newTransaction(ledgerHash); | |||
ReadContract readContract1 = txContract.contract(contractDeployKey.getAddress(), ReadContract.class); | |||
readContract1.read(newDataAccount.getAddress().toBase58(), key1); | |||
ReadContract readContract2 = txContract.contract(contractDeployKey.getAddress(), ReadContract.class); | |||
readContract2.read(newDataAccount.getAddress().toBase58(), key2); | |||
// 签名; | |||
PreparedTransaction contractPtx = txContract.prepare(); | |||
contractPtx.sign(adminKey); | |||
// 提交并等待共识返回; | |||
TransactionResponse readTxResp = contractPtx.commit(); | |||
String[] contractReturn = readTxResp.getContractReturn(); | |||
// 打印结果 | |||
for (String cr : contractReturn) { | |||
System.out.printf("----- Return Value = [%s] ----- \r\n", cr); | |||
} | |||
// 验证结果 | |||
assertNotNull(contractReturn); | |||
assertEquals(contractReturn.length, 2); | |||
String returnVal1 = contractReturn[0]; | |||
assertEquals(value1, returnVal1); | |||
String returnVal2 = contractReturn[1]; | |||
assertEquals(value2, returnVal2); | |||
} | |||
/** | |||
* 根据合约构建字节数组; | |||
* | |||
@@ -15,9 +15,11 @@ import com.jd.blockchain.tools.keygen.KeyGenCommand; | |||
import com.jd.blockchain.utils.concurrent.ThreadInvoker; | |||
import org.junit.Test; | |||
import org.springframework.core.io.ClassPathResource; | |||
import test.com.jd.blockchain.intgr.initializer.LedgerInitializeTest; | |||
import test.com.jd.blockchain.intgr.initializer.LedgerInitializeWeb4Nodes; | |||
import java.io.File; | |||
import java.util.concurrent.CountDownLatch; | |||
import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.Executors; | |||
@@ -85,6 +87,20 @@ public class IntegrationTest4Contract { | |||
BlockchainService blockchainService = gwsrvFact.getBlockchainService(); | |||
if(isContractDeployAndExe){ | |||
// 合约测试需要runtime路径 | |||
try { | |||
ClassPathResource contractPath = new ClassPathResource(""); | |||
File file = new File(contractPath.getURI()); | |||
String runTimePath = file.getParentFile().getParent() + File.separator + "runtime"; | |||
File runTime = new File(runTimePath); | |||
if (!runTime.exists()) { | |||
runTime.mkdir(); | |||
} | |||
} catch (Exception e) { | |||
e.printStackTrace(); | |||
} | |||
IntegrationBase.testSDK_Contract(adminKey,ledgerHash,blockchainService,ledgerRepository); | |||
} | |||