diff --git a/contracts/javascore/xcall-lib/src/main/java/foundation/icon/xcall/messages/PersistentMessage.java b/contracts/javascore/xcall-lib/src/main/java/foundation/icon/xcall/messages/PersistentMessage.java new file mode 100644 index 00000000..0df370fb --- /dev/null +++ b/contracts/javascore/xcall-lib/src/main/java/foundation/icon/xcall/messages/PersistentMessage.java @@ -0,0 +1,27 @@ + +package foundation.icon.xcall.messages; + +public class PersistentMessage extends Message { + public static final int TYPE = 3; + private byte[] data; + + public PersistentMessage(byte[] data) { + this.data = data; + } + + public int getType() { + return TYPE; + } + + public byte[] getData() { + return data; + } + + public byte[] toBytes() { + return data; + } + + public static CallMessage fromBytes(byte[] bytes) { + return new CallMessage(bytes); + } +} \ No newline at end of file diff --git a/contracts/javascore/xcall/src/main/java/foundation/icon/xcall/CallServiceImpl.java b/contracts/javascore/xcall/src/main/java/foundation/icon/xcall/CallServiceImpl.java index 94f9ff7b..33bdb5f7 100644 --- a/contracts/javascore/xcall/src/main/java/foundation/icon/xcall/CallServiceImpl.java +++ b/contracts/javascore/xcall/src/main/java/foundation/icon/xcall/CallServiceImpl.java @@ -32,6 +32,7 @@ import foundation.icon.xcall.messages.CallMessage; import foundation.icon.xcall.messages.CallMessageWithRollback; import foundation.icon.xcall.messages.Message; +import foundation.icon.xcall.messages.PersistentMessage; import foundation.icon.xcall.messages.XCallEnvelope; @@ -316,7 +317,6 @@ public void RollbackExecuted(BigInteger _sn) { public void CallMessageSent(Address _from, String _to, BigInteger _sn) { } - private void sendMessage(String[] _sources, String netTo, int msgType, BigInteger sn, byte[] data) { Address[] sources = prepareProtocols(_sources, netTo); CSMessage msg = new CSMessage(msgType, data); @@ -383,6 +383,7 @@ public ProcessResult(boolean needResponse, byte[] data) { private ProcessResult preProcessMessage(BigInteger sn, NetworkAddress to, XCallEnvelope envelope) { switch (envelope.getType()) { case CallMessage.TYPE: + case PersistentMessage.TYPE: return new ProcessResult(false, envelope.getMessage()); case CallMessageWithRollback.TYPE: Address caller = Context.getCaller(); @@ -404,6 +405,9 @@ private void executeMessage(BigInteger reqId, CSMessageRequest req, byte[] data) case CallMessage.TYPE: tryExecuteCall(reqId, to, req.getFrom(), data, protocols); break; + case PersistentMessage.TYPE: + _executeCall(reqId, to, req.getFrom(), data, protocols); + break; case CallMessageWithRollback.TYPE: { int code = tryExecuteCall(reqId, to, req.getFrom(), data, protocols); BigInteger sn = req.getSn().negate(); @@ -451,10 +455,7 @@ private void handleResult(byte[] data) { BigInteger resSn = msgRes.getSn(); RollbackData req = rollbacks.get(resSn); - if (req == null) { - Context.println("handleResult: no request for " + resSn); - return; // just ignore - } + Context.require(req != null, "CallRequest Not Found For " + resSn.toString()); byte[] hash = Context.hash("sha-256", data); DictDB pending = pendingResponses.at(hash); diff --git a/contracts/javascore/xcall/src/test/java/foundation/icon/xcall/CallServiceTest.java b/contracts/javascore/xcall/src/test/java/foundation/icon/xcall/CallServiceTest.java index 5940c1cb..e78d9d30 100644 --- a/contracts/javascore/xcall/src/test/java/foundation/icon/xcall/CallServiceTest.java +++ b/contracts/javascore/xcall/src/test/java/foundation/icon/xcall/CallServiceTest.java @@ -25,6 +25,9 @@ import com.iconloop.score.test.TestBase; import foundation.icon.xcall.messages.CallMessageWithRollback; +import foundation.icon.xcall.messages.Message; +import foundation.icon.xcall.messages.PersistentMessage; +import foundation.icon.xcall.messages.XCallEnvelope; import score.UserRevertedException; import xcall.icon.test.MockContract; @@ -125,6 +128,25 @@ public void sendMessage_multiProtocol() throws Exception { verify(xcallSpy).CallMessageSent(dapp.getAddress(), ethDapp.toString(), BigInteger.ONE); } + + @Test + public void sendMessage_persistent() { + // Arrange + byte[] data = "test".getBytes(); + Message message = new PersistentMessage(data); + XCallEnvelope envelope = new XCallEnvelope(message); + xcall.invoke(owner, "setDefaultConnection", ethDapp.net(), baseConnection.getAddress()); + + // Act + xcall.invoke(dapp.account, "sendCall", ethDapp.toString(), envelope.toBytes()); + + // Assert + CSMessageRequest request = new CSMessageRequest(iconDappAddress.toString(), ethDapp.account.toString(), BigInteger.ONE, PersistentMessage.TYPE, data, null); + CSMessage msg = new CSMessage(CSMessage.REQUEST, request.toBytes()); + verify(baseConnection.mock).sendMessage(eq(ethNid), eq(CallService.NAME), eq(BigInteger.ZERO), aryEq(msg.toBytes())); + verify(xcallSpy).CallMessageSent(dapp.getAddress(), ethDapp.toString(), BigInteger.ONE); + } + @Test public void handleResponse_singleProtocol() { // Arrange @@ -343,6 +365,39 @@ public void executeCall_failedExecution() { verify(xcallSpy).CallExecuted(BigInteger.ONE, 0, "score.RevertedException"); } + @Test + public void executeCall_persistent_failedExecution() { + // Arrange + byte[] data = "test".getBytes(); + CSMessageRequest request = new CSMessageRequest(ethDapp.toString(), dapp.getAddress().toString(), BigInteger.ONE, PersistentMessage.TYPE, data, baseSource); + CSMessage msg = new CSMessage(CSMessage.REQUEST, request.toBytes()); + xcall.invoke(baseConnection.account, "handleMessage", ethNid, msg.toBytes()); + // Act + doThrow(new UserRevertedException()).when(dapp.mock).handleCallMessage(ethDapp.toString(), data, new String[]{baseConnection.getAddress().toString()}); + assertThrows(Exception.class, () -> xcall.invoke(user, "executeCall", BigInteger.ONE, data)); + } + + @Test + public void executeCall_persistent() throws Exception { + // Arrange + byte[] data = "test".getBytes(); + MockContract defaultDapp = new MockContract<>(DefaultCallServiceReceiverScoreInterface.class, DefaultCallServiceReceiver.class, sm, owner); + CSMessageRequest request = new CSMessageRequest(ethDapp.toString(), defaultDapp.getAddress().toString(), BigInteger.ONE, PersistentMessage.TYPE, data, null); + CSMessage msg = new CSMessage(CSMessage.REQUEST, request.toBytes()); + + xcall.invoke(owner, "setDefaultConnection", ethDapp.net(), baseConnection.getAddress()); + xcall.invoke(baseConnection.account, "handleMessage", ethNid, msg.toBytes()); + + // Act + xcall.invoke(user, "executeCall", BigInteger.ONE, data); + + // Assert + verify(defaultDapp.mock).handleCallMessage(ethDapp.toString(), data); + verify(xcallSpy).CallExecuted(BigInteger.ONE, 1, ""); + Exception e = assertThrows(Exception.class, () -> xcall.invoke(user, "executeCall", BigInteger.ONE, data)); + assertEquals("Reverted(0): InvalidRequestId", e.getMessage()); + } + @Test public void rollback_singleProtocol() { // Arrange diff --git a/docs/adr/xcall.md b/docs/adr/xcall.md index 051ce35e..490141af 100644 --- a/docs/adr/xcall.md +++ b/docs/adr/xcall.md @@ -503,13 +503,13 @@ payable external sendCall(String _to, byte[] _data) returns Integer { msgReq = CSMessageRequest(from, to.account(), sn, envelope.type, msg, envelope.destinations) msg = CSMessage(CSMessage.REQUEST, msgReq.toBytes()).toBytes() assert msg.length <= MAX_DATA_SIZE - + if isReply(_to.netId,envelope.sources) && !needResponse: replyState = null callReply[caller]=msg emit CallMessageSent(caller, dst.toString(), sn) return sn - + sendSn = needResponse ? sn : 0 if protocolConfig.sources == []: src = defaultConnection[to.net()] @@ -671,7 +671,7 @@ internal function handleResult(data byte[]) { req = rollbacks[resSn] if req == null: - return + throw "CallRequest Not Found For {resSn}" if !verifyProtocols(req.netTo, req.protocolConfig, hash(data)): return @@ -681,7 +681,7 @@ internal function handleResult(data byte[]) { case CSMessageResult.SUCCESS: if result.getMessage()!=null: handleReply(req,result.getMessage()) - + rollbacks[resSn] = null successfulResponses[resSn] = 1 break @@ -778,7 +778,7 @@ internal function executeCall(int id, String from, byte[] data, String[] protoco internal function isReply(String netId, String[] sources) { if replyState != null: return replyState.fromNid == netid && replyState.protocols.equals(sources) - + return false } ``` @@ -831,7 +831,7 @@ external readonly function getFee(String _net, if isReply(_net, sources) && !_rollback { return 0 - } + } fee = protocolFee if _sources == [] { return defaultConnection[_net]->getFee(_net, _rollback) + fee