Skip to content

Commit ad4ca35

Browse files
authored
Offline userop/tx signing w/ tx builder for engine + WebGL SignTypedData (#154)
1 parent ebbc38b commit ad4ca35

File tree

7 files changed

+87377
-105433
lines changed

7 files changed

+87377
-105433
lines changed

Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/SmartWallet.cs

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,14 @@ internal async Task<RpcResponseMessage> Request(RpcRequestMessage requestMessage
155155
{
156156
ThirdwebDebug.Log("Requesting: " + requestMessage.Method + "...");
157157

158-
if (requestMessage.Method == "eth_sendTransaction")
158+
if (requestMessage.Method == "eth_signTransaction")
159+
{
160+
var parameters = JsonConvert.DeserializeObject<object[]>(JsonConvert.SerializeObject(requestMessage.RawParameters));
161+
var txInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(parameters[0]));
162+
var partialUserOp = await SignTransactionAsUserOp(txInput, requestMessage.Id);
163+
return new RpcResponseMessage(requestMessage.Id, JsonConvert.SerializeObject(partialUserOp.EncodeUserOperation()));
164+
}
165+
else if (requestMessage.Method == "eth_sendTransaction")
159166
{
160167
return await CreateUserOpAndSend(requestMessage);
161168
}
@@ -185,23 +192,13 @@ internal async Task<RpcResponseMessage> Request(RpcRequestMessage requestMessage
185192
}
186193
}
187194

188-
private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage requestMessage)
195+
private async Task<EntryPointContract.UserOperation> SignTransactionAsUserOp(TransactionInput transactionInput, object requestId = null)
189196
{
190-
await new WaitUntil(() => !_deploying);
191-
192-
await UpdateDeploymentStatus();
193-
if (!_deployed)
194-
{
195-
_deploying = true;
196-
}
197+
requestId ??= SmartWalletClient.GenerateRpcId();
197198

198199
string apiKey = ThirdwebManager.Instance.SDK.session.Options.clientId;
199200

200-
// Deserialize the transaction input from the request message
201-
202-
var paramList = JsonConvert.DeserializeObject<List<object>>(JsonConvert.SerializeObject(requestMessage.RawParameters));
203-
var transactionInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(paramList[0]));
204-
var dummySig = Constants.DUMMY_SIG;
201+
// Create the user operation and its safe (hexified) version
205202

206203
var executeFn = new AccountContract.ExecuteFunction
207204
{
@@ -212,35 +209,6 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
212209
};
213210
var executeInput = executeFn.CreateTransactionInput(Accounts[0]);
214211

215-
// Approve ERC20 tokens if any
216-
217-
if (!string.IsNullOrEmpty(Config.erc20PaymasterAddress) && !_approved && !_approving)
218-
{
219-
try
220-
{
221-
_approving = true;
222-
var tokenContract = ThirdwebManager.Instance.SDK.GetContract(Config.erc20TokenAddress);
223-
var approvedAmount = await tokenContract.ERC20.AllowanceOf(Accounts[0], Config.erc20PaymasterAddress);
224-
if (BigInteger.Parse(approvedAmount.value) == 0)
225-
{
226-
ThirdwebDebug.Log($"Approving tokens for ERC20Paymaster spending");
227-
_deploying = false;
228-
await tokenContract.ERC20.SetAllowance(Config.erc20PaymasterAddress, (BigInteger.Pow(2, 96) - 1).ToString().ToEth());
229-
}
230-
_approved = true;
231-
_approving = false;
232-
await UpdateDeploymentStatus();
233-
}
234-
catch (Exception e)
235-
{
236-
_approving = false;
237-
_approved = false;
238-
throw new Exception($"Approving tokens for ERC20Paymaster spending failed: {e.Message}");
239-
}
240-
}
241-
242-
// Create the user operation and its safe (hexified) version
243-
244212
var (initCode, gas) = await GetInitCode();
245213

246214
var gasPrices = await Utils.GetGasPriceAsync(ThirdwebManager.Instance.SDK.session.ChainId);
@@ -257,16 +225,16 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
257225
MaxFeePerGas = gasPrices.MaxFeePerGas,
258226
MaxPriorityFeePerGas = gasPrices.MaxPriorityFeePerGas,
259227
PaymasterAndData = new byte[] { },
260-
Signature = dummySig.HexStringToByteArray(),
228+
Signature = Constants.DUMMY_SIG.HexStringToByteArray(),
261229
};
262230

263231
// Update paymaster data if any
264232

265-
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestMessage.Id, partialUserOp.EncodeUserOperation(), apiKey);
233+
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, partialUserOp.EncodeUserOperation(), apiKey);
266234

267235
// Estimate gas
268236

269-
var gasEstimates = await BundlerClient.EthEstimateUserOperationGas(Config.bundlerUrl, apiKey, requestMessage.Id, partialUserOp.EncodeUserOperation(), Config.entryPointAddress);
237+
var gasEstimates = await BundlerClient.EthEstimateUserOperationGas(Config.bundlerUrl, apiKey, requestId, partialUserOp.EncodeUserOperation(), Config.entryPointAddress);
270238
partialUserOp.CallGasLimit = 50000 + new HexBigInteger(gasEstimates.CallGasLimit).Value;
271239
partialUserOp.VerificationGasLimit = string.IsNullOrEmpty(Config.erc20PaymasterAddress)
272240
? new HexBigInteger(gasEstimates.VerificationGas).Value
@@ -275,12 +243,63 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
275243

276244
// Update paymaster data if any
277245

278-
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestMessage.Id, partialUserOp.EncodeUserOperation(), apiKey);
246+
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, partialUserOp.EncodeUserOperation(), apiKey);
279247

280248
// Hash, sign and encode the user operation
281249

282250
partialUserOp.Signature = await partialUserOp.HashAndSignUserOp(Config.entryPointAddress);
283251

252+
return partialUserOp;
253+
}
254+
255+
private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage requestMessage)
256+
{
257+
await new WaitUntil(() => !_deploying);
258+
259+
await UpdateDeploymentStatus();
260+
if (!_deployed)
261+
{
262+
_deploying = true;
263+
}
264+
265+
string apiKey = ThirdwebManager.Instance.SDK.session.Options.clientId;
266+
267+
// Deserialize the transaction input from the request message
268+
269+
var paramList = JsonConvert.DeserializeObject<List<object>>(JsonConvert.SerializeObject(requestMessage.RawParameters));
270+
var transactionInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(paramList[0]));
271+
272+
// Approve ERC20 tokens if any
273+
274+
if (!string.IsNullOrEmpty(Config.erc20PaymasterAddress) && !_approved && !_approving)
275+
{
276+
try
277+
{
278+
_approving = true;
279+
var tokenContract = ThirdwebManager.Instance.SDK.GetContract(Config.erc20TokenAddress);
280+
var approvedAmount = await tokenContract.ERC20.AllowanceOf(Accounts[0], Config.erc20PaymasterAddress);
281+
if (BigInteger.Parse(approvedAmount.value) == 0)
282+
{
283+
ThirdwebDebug.Log($"Approving tokens for ERC20Paymaster spending");
284+
_deploying = false;
285+
await tokenContract.ERC20.SetAllowance(Config.erc20PaymasterAddress, (BigInteger.Pow(2, 96) - 1).ToString().ToEth());
286+
}
287+
_approved = true;
288+
_approving = false;
289+
await UpdateDeploymentStatus();
290+
}
291+
catch (Exception e)
292+
{
293+
_approving = false;
294+
_approved = false;
295+
throw new Exception($"Approving tokens for ERC20Paymaster spending failed: {e.Message}");
296+
}
297+
}
298+
299+
// Create and sign the user operation
300+
301+
var partialUserOp = await SignTransactionAsUserOp(transactionInput, requestMessage.Id);
302+
284303
// Send the user operation
285304

286305
ThirdwebDebug.Log("Valid UserOp: " + JsonConvert.SerializeObject(partialUserOp));

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/Transaction.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,28 @@ public async Task<string> Simulate()
298298
}
299299
}
300300

301+
/// <summary>
302+
/// Signs the transaction asynchronously, if the wallet supports it. Useful for smart wallet user op delayed broadcasting through thirdweb Engine. Otherwise not recommended.
303+
/// </summary>
304+
/// <returns>The signed transaction a string.</returns>
305+
public async Task<string> Sign()
306+
{
307+
if (Input.Value == null)
308+
Input.Value = new HexBigInteger(0);
309+
310+
if (Utils.IsWebGLBuild())
311+
{
312+
return await Bridge.InvokeRoute<string>(GetTxBuilderRoute("sign"), Utils.ToJsonStringArray(Input, fnName, fnArgs));
313+
}
314+
else
315+
{
316+
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetProvider() != WalletProvider.SmartWallet && ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount() != null)
317+
return await ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount().TransactionManager.SignTransactionAsync(Input);
318+
else
319+
return await ThirdwebManager.Instance.SDK.session.Request<string>("eth_signTransaction", Input);
320+
}
321+
}
322+
301323
/// <summary>
302324
/// Sends the transaction asynchronously.
303325
/// </summary>

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: 67 additions & 40 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,64 +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.GetProvider() == WalletProvider.SmartWallet)
514+
if (Utils.IsWebGLBuild())
514515
{
515-
var sw = ThirdwebManager.Instance.SDK.session.ActiveWallet as Wallets.ThirdwebSmartWallet;
516-
if (!sw.SmartWallet.IsDeployed && !sw.SmartWallet.IsDeploying)
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)
517527
{
518-
ThirdwebDebug.Log("SmartWallet not deployed, deploying before signing...");
519-
await sw.SmartWallet.ForceDeploy();
528+
if (type.Key.Contains("EIP712Domain"))
529+
continue;
530+
531+
types.Add(type.Key, type.Value);
520532
}
521-
}
522533

523-
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount() != null)
524-
{
525-
var signer = new Eip712TypedDataSigner();
526-
var key = new EthECKey(ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount().PrivateKey);
527-
return signer.SignTypedDataV4(data, typedData, key);
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>();
528546
}
529547
else
530548
{
531-
var json = typedData.ToJson(data);
532-
var jsonObject = JObject.Parse(json);
533-
534-
var uidToken = jsonObject.SelectToken("$.message.uid");
535-
if (uidToken != null)
549+
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount() != null)
536550
{
537-
var uidBase64 = uidToken.Value<string>();
538-
var uidBytes = Convert.FromBase64String(uidBase64);
539-
var uidHex = uidBytes.ByteArrayToHexString();
540-
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);
541554
}
542-
543-
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet)
555+
else
544556
{
545-
// Smart accounts
546-
var hashToken = jsonObject.SelectToken("$.message.message");
547-
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)
548562
{
549-
var hashBase64 = hashToken.Value<string>();
550-
var hashBytes = Convert.FromBase64String(hashBase64);
551-
var hashHex = hashBytes.ByteArrayToHexString();
552-
hashToken.Replace(hashHex);
563+
var uidBase64 = uidToken.Value<string>();
564+
var uidBytes = Convert.FromBase64String(uidBase64);
565+
var uidHex = uidBytes.ByteArrayToHexString();
566+
uidToken.Replace(uidHex);
553567
}
554-
}
555568

556-
var messageObject = jsonObject.GetValue("message") as JObject;
557-
foreach (var property in messageObject.Properties())
558-
{
559-
if (property.Value.Type == JTokenType.Array)
569+
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet)
560570
{
561-
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+
}
562580
}
563-
else
581+
582+
var messageObject = jsonObject.GetValue("message") as JObject;
583+
foreach (var property in messageObject.Properties())
564584
{
565-
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+
}
566593
}
567-
}
568594

569-
string safeJson = jsonObject.ToString();
570-
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+
}
571598
}
572599
}
573600

0 commit comments

Comments
 (0)