Skip to content

Commit 7bc68f1

Browse files
committed
Offline userop/tx signing w/ tx builder for engine
1 parent ebbc38b commit 7bc68f1

File tree

4 files changed

+128
-57
lines changed

4 files changed

+128
-57
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/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/Wallet.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -510,16 +510,6 @@ public async Task<string> SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain>
510510
if (!await IsConnected())
511511
throw new Exception("No account connected!");
512512

513-
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet)
514-
{
515-
var sw = ThirdwebManager.Instance.SDK.session.ActiveWallet as Wallets.ThirdwebSmartWallet;
516-
if (!sw.SmartWallet.IsDeployed && !sw.SmartWallet.IsDeploying)
517-
{
518-
ThirdwebDebug.Log("SmartWallet not deployed, deploying before signing...");
519-
await sw.SmartWallet.ForceDeploy();
520-
}
521-
}
522-
523513
if (ThirdwebManager.Instance.SDK.session.ActiveWallet.GetLocalAccount() != null)
524514
{
525515
var signer = new Eip712TypedDataSigner();

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using UnityEngine;
34

@@ -56,5 +57,44 @@ public async void CreateSessionKey()
5657
Debugger.Instance.Log("[CreateSessionKey] Error", e.Message);
5758
}
5859
}
60+
61+
public async void PreSignSessionKeyTxAsUserOpForLaterBroadcastingThroughThirdwebEngine()
62+
{
63+
try
64+
{
65+
var accountAddress = await ThirdwebManager.Instance.SDK.wallet.GetAddress();
66+
var accountContract = ThirdwebManager.Instance.SDK.GetContract(
67+
accountAddress,
68+
"[{\"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\"}]"
69+
);
70+
var contractsAllowedForInteraction = new List<string>() { "0x450b943729Ddba196Ab58b589Cea545551DF71CC" };
71+
var request = new Contracts.Account.ContractDefinition.SignerPermissionRequest()
72+
{
73+
Signer = "0x22b79AD6c6009525933ac2FF40bC9F30dF14Ecfb",
74+
IsAdmin = 0,
75+
ApprovedTargets = contractsAllowedForInteraction,
76+
NativeTokenLimitPerTransaction = 0,
77+
PermissionStartTimestamp = 0,
78+
PermissionEndTimestamp = Utils.GetUnixTimeStampNow() + 86400,
79+
ReqValidityStartTimestamp = 0,
80+
ReqValidityEndTimestamp = Utils.GetUnixTimeStampIn10Years(),
81+
Uid = Guid.NewGuid().ToByteArray()
82+
};
83+
var signature = await EIP712.GenerateSignature_SmartAccount(
84+
"Account",
85+
"1",
86+
await ThirdwebManager.Instance.SDK.wallet.GetChainId(),
87+
await ThirdwebManager.Instance.SDK.wallet.GetAddress(),
88+
request
89+
);
90+
var tx = await accountContract.Prepare("setPermissionsForSigner", request, signature.HexStringToByteArray());
91+
var signedTx = await tx.Sign();
92+
Debugger.Instance.Log("[Pre-Sign Session Key User Op] Sucess", signedTx.ToString());
93+
}
94+
catch (System.Exception e)
95+
{
96+
Debugger.Instance.Log("[Pre-Sign Session Key User Op] Error", e.Message);
97+
}
98+
}
5999
}
60100
}

0 commit comments

Comments
 (0)