Skip to content

Commit 2348b5d

Browse files
committed
Support WebGL SignTypedData + fix serialization
1 parent 7bc68f1 commit 2348b5d

File tree

5 files changed

+88104
-106435
lines changed

5 files changed

+88104
-106435
lines changed

Assets/Thirdweb/Core/Scripts/Contract.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ public async Task<Transaction> Prepare(string functionName, params object[] args
112112
public async Task<Transaction> Prepare(string functionName, string from = null, params object[] args)
113113
{
114114
var initialInput = new TransactionInput();
115-
if (!Utils.IsWebGLBuild())
115+
if (Utils.IsWebGLBuild())
116+
{
117+
initialInput.From = from ?? await ThirdwebManager.Instance.SDK.wallet.GetAddress();
118+
initialInput.To = address;
119+
}
120+
else
116121
{
117122
var contract = Utils.GetWeb3().Eth.GetContract(this.abi, this.address);
118123
var function = contract.GetFunction(functionName);

Assets/Thirdweb/Core/Scripts/Utils.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Globalization;
1515
using System.Linq;
1616
using System.Net.Http;
17+
using System.Collections;
1718

1819
namespace Thirdweb
1920
{
@@ -32,8 +33,26 @@ public static string[] ToJsonStringArray(params object[] args)
3233
{
3334
continue;
3435
}
36+
// if array or list, check if bytes and convert to hex
37+
if (args[i].GetType().IsArray || args[i] is IList)
38+
{
39+
var enumerable = args[i] as IEnumerable;
40+
var enumerableArgs = new List<object>();
41+
foreach (var item in enumerable)
42+
{
43+
if (item is byte[])
44+
{
45+
enumerableArgs.Add(ByteArrayToHexString(item as byte[]));
46+
}
47+
else
48+
{
49+
enumerableArgs.Add(item);
50+
}
51+
}
52+
stringArgs.Add(ToJson(enumerableArgs));
53+
}
3554
// if bytes, make hex
36-
if (args[i] is byte[])
55+
else if (args[i] is byte[])
3756
{
3857
stringArgs.Add(ByteArrayToHexString(args[i] as byte[]));
3958
}
@@ -44,7 +63,7 @@ public static string[] ToJsonStringArray(params object[] args)
4463
}
4564
else
4665
{
47-
stringArgs.Add(Utils.ToJson(args[i]));
66+
stringArgs.Add(ToJson(args[i]));
4867
}
4968
}
5069
return stringArgs.ToArray();

Assets/Thirdweb/Core/Scripts/Wallet.cs

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Newtonsoft.Json.Linq;
1212
using Nethereum.Hex.HexTypes;
1313
using System.Linq;
14+
using Newtonsoft.Json;
1415

1516
namespace Thirdweb
1617
{
@@ -510,54 +511,90 @@ public async Task<string> SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain>
510511
if (!await IsConnected())
511512
throw new Exception("No account connected!");
512513

513-
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount() != null)
514+
if (Utils.IsWebGLBuild())
514515
{
515-
var signer = new Eip712TypedDataSigner();
516-
var key = new EthECKey(ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount().PrivateKey);
517-
return signer.SignTypedDataV4(data, typedData, key);
516+
var domainType = typedData.Domain.GetType();
517+
var domain = new
518+
{
519+
name = domainType.GetProperty("Name").GetValue(typedData.Domain).ToString(),
520+
version = domainType.GetProperty("Version").GetValue(typedData.Domain).ToString(),
521+
chainId = domainType.GetProperty("ChainId").GetValue(typedData.Domain).ToString(),
522+
verifyingContract = domainType.GetProperty("VerifyingContract").GetValue(typedData.Domain).ToString()
523+
};
524+
525+
var types = new Dictionary<string, object>();
526+
foreach (var type in typedData.Types)
527+
{
528+
if (type.Key.Contains("EIP712Domain"))
529+
continue;
530+
531+
types.Add(type.Key, type.Value);
532+
}
533+
534+
var message = new Dictionary<string, object>();
535+
foreach (var member in data.GetType().GetProperties())
536+
{
537+
string n = char.ToLower(member.Name[0]) + member.Name.Substring(1);
538+
object v = member.GetValue(data);
539+
// hexify bytes to avoid base64 json serialization, mostly useful for bytes32 uid
540+
if (member.PropertyType == typeof(byte[]))
541+
v = Utils.ToBytes32HexString((byte[])v);
542+
message.Add(n, v);
543+
}
544+
var result = await Bridge.InvokeRoute<JToken>(getRoute("signTypedData"), Utils.ToJsonStringArray(domain, types, message));
545+
return result["signature"].Value<string>();
518546
}
519547
else
520548
{
521-
var json = typedData.ToJson(data);
522-
var jsonObject = JObject.Parse(json);
523-
524-
var uidToken = jsonObject.SelectToken("$.message.uid");
525-
if (uidToken != null)
549+
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount() != null)
526550
{
527-
var uidBase64 = uidToken.Value<string>();
528-
var uidBytes = Convert.FromBase64String(uidBase64);
529-
var uidHex = uidBytes.ByteArrayToHexString();
530-
uidToken.Replace(uidHex);
551+
var signer = new Eip712TypedDataSigner();
552+
var key = new EthECKey(ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount().PrivateKey);
553+
return signer.SignTypedDataV4(data, typedData, key);
531554
}
532-
533-
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet)
555+
else
534556
{
535-
// Smart accounts
536-
var hashToken = jsonObject.SelectToken("$.message.message");
537-
if (hashToken != null)
557+
var json = typedData.ToJson(data);
558+
var jsonObject = JObject.Parse(json);
559+
560+
var uidToken = jsonObject.SelectToken("$.message.uid");
561+
if (uidToken != null)
538562
{
539-
var hashBase64 = hashToken.Value<string>();
540-
var hashBytes = Convert.FromBase64String(hashBase64);
541-
var hashHex = hashBytes.ByteArrayToHexString();
542-
hashToken.Replace(hashHex);
563+
var uidBase64 = uidToken.Value<string>();
564+
var uidBytes = Convert.FromBase64String(uidBase64);
565+
var uidHex = uidBytes.ByteArrayToHexString();
566+
uidToken.Replace(uidHex);
543567
}
544-
}
545568

546-
var messageObject = jsonObject.GetValue("message") as JObject;
547-
foreach (var property in messageObject.Properties())
548-
{
549-
if (property.Value.Type == JTokenType.Array)
569+
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet)
550570
{
551-
continue;
571+
// Smart accounts
572+
var hashToken = jsonObject.SelectToken("$.message.message");
573+
if (hashToken != null)
574+
{
575+
var hashBase64 = hashToken.Value<string>();
576+
var hashBytes = Convert.FromBase64String(hashBase64);
577+
var hashHex = hashBytes.ByteArrayToHexString();
578+
hashToken.Replace(hashHex);
579+
}
552580
}
553-
else
581+
582+
var messageObject = jsonObject.GetValue("message") as JObject;
583+
foreach (var property in messageObject.Properties())
554584
{
555-
property.Value = property.Value.ToString();
585+
if (property.Value.Type == JTokenType.Array)
586+
{
587+
continue;
588+
}
589+
else
590+
{
591+
property.Value = property.Value.ToString();
592+
}
556593
}
557-
}
558594

559-
string safeJson = jsonObject.ToString();
560-
return await ThirdwebManager.Instance.SDK.session.Request<string>("eth_signTypedData_v4", await GetSignerAddress(), safeJson);
595+
string safeJson = jsonObject.ToString();
596+
return await ThirdwebManager.Instance.SDK.session.Request<string>("eth_signTypedData_v4", await GetSignerAddress(), safeJson);
597+
}
561598
}
562599
}
563600

Assets/Thirdweb/Examples/Scripts/Prefabs/Prefab_SmartWallet.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using Newtonsoft.Json;
34
using UnityEngine;
45

56
namespace Thirdweb.Examples
@@ -62,11 +63,16 @@ public async void PreSignSessionKeyTxAsUserOpForLaterBroadcastingThroughThirdweb
6263
{
6364
try
6465
{
66+
// Get smart account predicted address
6567
var accountAddress = await ThirdwebManager.Instance.SDK.wallet.GetAddress();
68+
69+
// Treat it as a contract, abi can be full or just contain setPermissionsForSigner for this use case
6670
var accountContract = ThirdwebManager.Instance.SDK.GetContract(
6771
accountAddress,
6872
"[{\"type\": \"function\",\"name\": \"setPermissionsForSigner\",\"inputs\": [{\"type\": \"tuple\",\"name\": \"_req\",\"components\": [{\"type\": \"address\",\"name\": \"signer\",\"internalType\": \"address\"},{\"type\": \"uint8\",\"name\": \"isAdmin\",\"internalType\": \"uint8\"},{\"type\": \"address[]\",\"name\": \"approvedTargets\",\"internalType\": \"address[]\"},{\"type\": \"uint256\",\"name\": \"nativeTokenLimitPerTransaction\",\"internalType\": \"uint256\"},{\"type\": \"uint128\",\"name\": \"permissionStartTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"uint128\",\"name\": \"permissionEndTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"uint128\",\"name\": \"reqValidityStartTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"uint128\",\"name\": \"reqValidityEndTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"bytes32\",\"name\": \"uid\",\"internalType\": \"bytes32\"}],\"internalType\": \"struct IAccountPermissions.SignerPermissionRequest\"},{\"type\": \"bytes\",\"name\": \"_signature\",\"internalType\": \"bytes\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"}]"
6973
);
74+
75+
// Setup the request using the correct types and values
7076
var contractsAllowedForInteraction = new List<string>() { "0x450b943729Ddba196Ab58b589Cea545551DF71CC" };
7177
var request = new Contracts.Account.ContractDefinition.SignerPermissionRequest()
7278
{
@@ -80,15 +86,26 @@ public async void PreSignSessionKeyTxAsUserOpForLaterBroadcastingThroughThirdweb
8086
ReqValidityEndTimestamp = Utils.GetUnixTimeStampIn10Years(),
8187
Uid = Guid.NewGuid().ToByteArray()
8288
};
89+
90+
// Sign the typed data related to session keys
8391
var signature = await EIP712.GenerateSignature_SmartAccount(
8492
"Account",
8593
"1",
8694
await ThirdwebManager.Instance.SDK.wallet.GetChainId(),
8795
await ThirdwebManager.Instance.SDK.wallet.GetAddress(),
8896
request
8997
);
98+
99+
// Prepare the transaction
90100
var tx = await accountContract.Prepare("setPermissionsForSigner", request, signature.HexStringToByteArray());
101+
102+
// Set gas limit to avoid any potential estimation/simulation namely in WebGL
103+
tx.SetGasLimit("1500000");
104+
105+
// Sign the transaction, since a smart wallet is connected this returns a stringified and hexified user op ready for bundling
91106
var signedTx = await tx.Sign();
107+
108+
// You can use engine's send-signed-user-op endpoint to broadcast or directly call eth_sendUserOperation on a bundler
92109
Debugger.Instance.Log("[Pre-Sign Session Key User Op] Sucess", signedTx.ToString());
93110
}
94111
catch (System.Exception e)

0 commit comments

Comments
 (0)