You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I've two different complex RPC request payloads (for eth_sendTransaction and personal_sign methods):
[RpcMethod("personal_sign"), RpcRequestOptions(Clock.ONE_MINUTE, 99994)]
public class EthPersonalSignRequest : List<string>
[RpcMethod("eth_sendTransaction"), RpcRequestOptions(Clock.ONE_MINUTE, 99997)]
public class EthSendTransactionRequest : List<TransactionInput>
// handlers
client.Engine.SignClient.SessionRequestEvents<EthSendTransactionRequest, EthSendTransactionResponse>().OnRequest += OnEthSendTransactionRequested;
client.Engine.SignClient.SessionRequestEvents<EthPersonalSignRequest, EthPersonalSignResponse>().OnRequest += OnEthPersonalSignRequested;
The Issue
The wallet client throws a JSON deserialization exception when it receives a personal_sign (or eth_sendTransaction) request.
Reason
The RelayerMessageCallback function of the TypedMessageHandler invokes all session request handlers for each session request event (regardless of method type).
When the client receives a personal_sign request (an array of strings), it tries to deserialize it to the payload of the eth_sendTransaction method (an array of objects) and throws an exception.
Actual Root Cause
When registering session request handlers, the HandleMessageType function of the TypedMessageHandler obtains the method name by calling RpcMethodAttribute.MethodForType<T>
RpcMethodAttribute.MethodForType<T>, finds the method name from the RpcMethodAttribute of the given type (T). But, for session request events, the type (T) will always be SessionRequest<TR> (TR: the actual RPC request type) and it always returns wc_sessionRequest:
[RpcMethod("wc_sessionRequest")]
public class SessionRequest<T> : IWcMethod
It registers all session request event handlers (to the messageEventHandlerMap) with the key of wc_sessionRequest.
When an event is received, the RelayerMessageCallback function retrieves all handlers with the key of wc_sessionRequest and invokes all of them. In the RequestCallback local function (of the HandleMessageType function of the TypedMessageHandler), it tries to decode the message to the target request type, and it throws a JSON deserialization exception when the payload schema does not match:
var payload = await this.Core.Crypto.Decode<JsonRpcRequest<T>>(topic, message, options);
The WrappedRefOnOnRequest function of the SessionRequestEventHandler checks the method of the event, and it does not execute the RequestCallback function of the TypedEventHandler (not TypedMessageHandler) if it does not match, but it is not enough to prevent this issue.
Proposed Solution
RpcMethodAttribute.MethodForType<T> should check if the type T is a generic type:
If so, it should check whether any of the generic arguments of type T has the RpcMethodAttribute attribute and return the method name of the generic argument.
In this way, the event handler of the personal_sign request will be registered as personal_sign instead of wc_sessionRequest (in messageEventHandlerMap)
The Method property of the JsonRpcPayload class should check whether the param.request.method value exists in the _extraStuff dictionary. If so, it should return the param.request.method instead of the method.
You can add this integration test to the SignTests.cs test file of the WalletConnectSharp.Sign.Test project.
// represents array of strings requests, similar to personal_sign
[RpcMethod("complex_test_method"), RpcRequestOptions(Clock.ONE_MINUTE, 99990)]
public class ComplexTestRequest: List<string>
{
public ComplexTestRequest()
{
}
public ComplexTestRequest(params string[] args) : base(args)
{
}
public int A
{
get
{
if (Count != 2 || !int.TryParse(this[0], out var a))
{
return 0;
}
return a;
}
}
public int B
{
get
{
if (Count != 2 || !int.TryParse(this[1], out var b))
{
return 0;
}
return b;
}
}
}
// represents array of objects requests, similar to eth_sendTransaction
[RpcMethod("complex_test_method_2"),
RpcRequestOptions(Clock.ONE_MINUTE, 99991),
RpcResponseOptions(Clock.ONE_MINUTE, 99992)]
public class ComplexTestRequest2 : List<TestRequest2>
{
public ComplexTestRequest2()
{
}
public ComplexTestRequest2(params TestRequest2[] args): base(args)
{
}
public string X => this.FirstOrDefault()?.x ?? string.Empty;
public int Y => this.FirstOrDefault()?.y ?? -1;
}
[Fact, Trait("Category", "integration")]
public async Task TestTwoUniqueComplexSessionRequestResponse()
{
await _cryptoFixture.WaitForClientsReady();
var testAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
var testMethod = "complex_test_method";
var testMethod2 = "complex_test_method_2";
var dappConnectOptions = new ConnectOptions()
{
RequiredNamespaces = new RequiredNamespaces()
{
{
"eip155",
new ProposedNamespace()
{
Methods = new[] {testMethod, testMethod2},
Chains = new[] {"eip155:1", "eip155:10"},
Events = new[] {"chainChanged", "accountsChanged"}
}
}
}
};
var dappClient = ClientA;
var connectData = await dappClient.Connect(dappConnectOptions);
var walletClient = ClientB;
var proposal = await walletClient.Pair(connectData.Uri);
var approveData = await walletClient.Approve(proposal, testAddress);
var sessionData = await connectData.Approval;
await approveData.Acknowledged();
var rnd = new Random();
var a = rnd.Next(100);
var b = rnd.Next(100);
var x = rnd.NextStrings(AllowedChars, (Math.Min(a, b), Math.Max(a, b)), 1).First();
var y = x.Length;
var testData = new ComplexTestRequest(a.ToString(), b.ToString());
var testData2 = new ComplexTestRequest2(new TestRequest2() {x = x, y = y});
var pending = new TaskCompletionSource<int>();
// Step 1. Setup event listener for request
// The wallet client will listen for the request with the "test_method" rpc method
walletClient.Engine.SessionRequestEvents<ComplexTestRequest, TestResponse>()
.OnRequest += ( requestData) =>
{
var request = requestData.Request;
var data = request.Params;
requestData.Response = new TestResponse()
{
result = data.A * data.B
};
return Task.CompletedTask;
};
// The wallet client will listen for the request with the "test_method" rpc method
walletClient.Engine.SessionRequestEvents<ComplexTestRequest2, bool>()
.OnRequest += ( requestData) =>
{
var request = requestData.Request;
var data = request.Params;
requestData.Response = data.X.Length == data.Y;
return Task.CompletedTask;
};
// The dapp client will listen for the response
// Normally, we wouldn't do this and just rely on the return value
// from the dappClient.Engine.Request function call (the response Result or throws an Exception)
// We do it here for the sake of testing
dappClient.Engine.SessionRequestEvents<ComplexTestRequest, TestResponse>()
.FilterResponses((r) => r.Topic == sessionData.Topic)
.OnResponse += (responseData) =>
{
var response = responseData.Response;
var data = response.Result;
pending.TrySetResult(data.result);
return Task.CompletedTask;
};
// 2. Send the request from the dapp client
var responseReturned = await dappClient.Engine.Request<ComplexTestRequest, TestResponse>(sessionData.Topic, testData);
var responseReturned2 = await dappClient.Engine.Request<ComplexTestRequest2, bool>(sessionData.Topic, testData2);
// 3. Wait for the response from the event listener
var eventResult = await pending.Task.WithTimeout(TimeSpan.FromSeconds(5));
Assert.Equal(eventResult, a * b);
Assert.Equal(eventResult, testData.A * testData.B);
Assert.Equal(eventResult, responseReturned.result);
Assert.True(responseReturned2);
}
The text was updated successfully, but these errors were encountered:
Context (Web3Wallet)
I've two different complex RPC request payloads (for
eth_sendTransaction
andpersonal_sign
methods):The Issue
personal_sign
(oreth_sendTransaction
) request.Reason
RelayerMessageCallback
function of theTypedMessageHandler
invokes all session request handlers for each session request event (regardless of method type).personal_sign
request (an array of strings), it tries to deserialize it to the payload of theeth_sendTransaction
method (an array of objects) and throws an exception.Actual Root Cause
HandleMessageType
function of theTypedMessageHandler
obtains the method name by callingRpcMethodAttribute.MethodForType<T>
RpcMethodAttribute.MethodForType<T>
, finds the method name from theRpcMethodAttribute
of the given type (T). But, for session request events, the type (T) will always beSessionRequest<TR>
(TR: the actual RPC request type) and it always returnswc_sessionRequest
:messageEventHandlerMap
) with the key ofwc_sessionRequest
.RelayerMessageCallback
function retrieves all handlers with the key ofwc_sessionRequest
and invokes all of them. In theRequestCallback
local function (of theHandleMessageType
function of theTypedMessageHandler
), it tries to decode the message to the target request type, and it throws a JSON deserialization exception when the payload schema does not match:WrappedRefOnOnRequest
function of theSessionRequestEventHandler
checks themethod
of the event, and it does not execute theRequestCallback
function of theTypedEventHandler
(notTypedMessageHandler
) if it does not match, but it is not enough to prevent this issue.Proposed Solution
RpcMethodAttribute.MethodForType<T>
should check if thetype T
is a generic type:type T
has the RpcMethodAttribute attribute and return the method name of the generic argument.personal_sign
instead ofwc_sessionRequest
(inmessageEventHandlerMap
)Method
property of theJsonRpcPayload
class should check whether theparam.request.method
value exists in the_extraStuff
dictionary. If so, it should return theparam.request.method
instead of themethod
.How to Reproduce the Issue?
You can add this integration test to the
SignTests.cs
test file of theWalletConnectSharp.Sign.Test
project.The text was updated successfully, but these errors were encountered: