Skip to content

Commit 6a0072e

Browse files
committed
MetaMask WebGL Integration
1 parent 5cefcd9 commit 6a0072e

17 files changed

+1110
-110
lines changed
Binary file not shown.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
%YAML 1.1
2+
%TAG !u! tag:unity3d.com,2011:
3+
--- !u!1 &2136098963133507156
4+
GameObject:
5+
m_ObjectHideFlags: 0
6+
m_CorrespondingSourceObject: {fileID: 0}
7+
m_PrefabInstance: {fileID: 0}
8+
m_PrefabAsset: {fileID: 0}
9+
serializedVersion: 6
10+
m_Component:
11+
- component: {fileID: 4385959303137876471}
12+
- component: {fileID: 1696570382154790093}
13+
m_Layer: 0
14+
m_Name: MetaMaskWallet
15+
m_TagString: Untagged
16+
m_Icon: {fileID: 0}
17+
m_NavMeshLayer: 0
18+
m_StaticEditorFlags: 0
19+
m_IsActive: 1
20+
--- !u!4 &4385959303137876471
21+
Transform:
22+
m_ObjectHideFlags: 0
23+
m_CorrespondingSourceObject: {fileID: 0}
24+
m_PrefabInstance: {fileID: 0}
25+
m_PrefabAsset: {fileID: 0}
26+
m_GameObject: {fileID: 2136098963133507156}
27+
serializedVersion: 2
28+
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
29+
m_LocalPosition: {x: 0, y: 0, z: 0}
30+
m_LocalScale: {x: 1, y: 1, z: 1}
31+
m_ConstrainProportionsScale: 0
32+
m_Children: []
33+
m_Father: {fileID: 0}
34+
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
35+
--- !u!114 &1696570382154790093
36+
MonoBehaviour:
37+
m_ObjectHideFlags: 0
38+
m_CorrespondingSourceObject: {fileID: 0}
39+
m_PrefabInstance: {fileID: 0}
40+
m_PrefabAsset: {fileID: 0}
41+
m_GameObject: {fileID: 2136098963133507156}
42+
m_Enabled: 1
43+
m_EditorHideFlags: 0
44+
m_Script: {fileID: 11500000, guid: fb4896ebd314cc5479290757d60f513e, type: 3}
45+
m_Name:
46+
m_EditorClassIdentifier:

Assets/Thirdweb/Core/Unity/Prefabs/MetaMaskWallet.prefab.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Thirdweb/Core/Unity/Prefabs/ThirdwebManager.prefab

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ PrefabInstance:
175175
objectReference: {fileID: 0}
176176
- target: {fileID: 5852007486238492023, guid: 6393a0b7364627248830caa747134c99, type: 3}
177177
propertyPath: m_Name
178-
value: WalletConnectModal
178+
value: WalletConnectWallet
179179
objectReference: {fileID: 0}
180180
- target: {fileID: 6065279674905622718, guid: 6393a0b7364627248830caa747134c99, type: 3}
181181
propertyPath: m_AnchorMax.y

Assets/Thirdweb/Core/Unity/Prefabs/WalletConnectModal.prefab renamed to Assets/Thirdweb/Core/Unity/Prefabs/WalletConnectWallet.prefab

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3730,7 +3730,7 @@ GameObject:
37303730
- component: {fileID: 927399162371239047}
37313731
- component: {fileID: 5769675580277394246}
37323732
m_Layer: 0
3733-
m_Name: WalletConnectModal
3733+
m_Name: WalletConnectWallet
37343734
m_TagString: Untagged
37353735
m_Icon: {fileID: 0}
37363736
m_NavMeshLayer: 0

Assets/Thirdweb/Core/Unity/ThirdwebManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public void Initialize()
5454

5555
Client = ThirdwebClient.Create(
5656
clientId: ClientId,
57-
bundleId: BundleId ?? Application.identifier,
57+
bundleId: BundleId,
5858
httpClient: Application.platform == RuntimePlatform.WebGLPlayer ? new UnityThirdwebHttpClient() : new ThirdwebHttpClient(),
5959
headers: new Dictionary<string, string>
6060
{
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net.Http;
5+
using System.Numerics;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Nethereum.ABI.EIP712;
9+
using Nethereum.Hex.HexTypes;
10+
using Nethereum.RPC.Eth.DTOs;
11+
using Nethereum.RPC.HostWallet;
12+
using Newtonsoft.Json;
13+
using UnityEngine;
14+
15+
namespace Thirdweb.Unity
16+
{
17+
public class MetaMaskWallet : IThirdwebWallet
18+
{
19+
public ThirdwebClient Client => _client;
20+
public ThirdwebAccountType AccountType => ThirdwebAccountType.ExternalAccount;
21+
22+
private static ThirdwebClient _client;
23+
24+
protected MetaMaskWallet() { }
25+
26+
public static async Task<MetaMaskWallet> Create(ThirdwebClient client, BigInteger activeChainId)
27+
{
28+
_client = client;
29+
30+
if (Application.platform != RuntimePlatform.WebGLPlayer || Application.isEditor)
31+
{
32+
throw new Exception("MetaMaskWallet is only available in WebGL Builds. Please use a different wallet provider on native platforms.");
33+
}
34+
35+
var metaMaskInstance = WebGLMetaMask.Instance;
36+
37+
if (metaMaskInstance.IsConnected() && !string.IsNullOrEmpty(metaMaskInstance.GetAddress()))
38+
{
39+
ThirdwebDebug.Log("MetaMask already initialized.");
40+
await EnsureCorrectNetwork(activeChainId);
41+
return new MetaMaskWallet();
42+
}
43+
44+
if (metaMaskInstance.IsMetaMaskAvailable())
45+
{
46+
ThirdwebDebug.Log("MetaMask is available. Enabling Ethereum...");
47+
var isEnabled = await metaMaskInstance.EnableEthereumAsync();
48+
ThirdwebDebug.Log($"Ethereum enabled: {isEnabled}");
49+
if (isEnabled && !string.IsNullOrEmpty(metaMaskInstance.GetAddress()))
50+
{
51+
ThirdwebDebug.Log("MetaMask initialized successfully.");
52+
await EnsureCorrectNetwork(activeChainId);
53+
return new MetaMaskWallet();
54+
}
55+
else
56+
{
57+
throw new Exception("MetaMask initialization failed or address is empty.");
58+
}
59+
}
60+
else
61+
{
62+
throw new Exception("MetaMask is not available.");
63+
}
64+
}
65+
66+
#region IThirdwebWallet
67+
68+
public Task<string> EthSign(byte[] rawMessage)
69+
{
70+
throw new NotImplementedException("MetaMask does not support signing raw messages.");
71+
}
72+
73+
public Task<string> EthSign(string message)
74+
{
75+
throw new NotImplementedException("MetaMask does not support signing messages.");
76+
}
77+
78+
public Task<string> GetAddress()
79+
{
80+
return Task.FromResult(WebGLMetaMask.Instance.GetAddress());
81+
}
82+
83+
public Task<bool> IsConnected()
84+
{
85+
return Task.FromResult(WebGLMetaMask.Instance.IsConnected());
86+
}
87+
88+
public Task<string> PersonalSign(byte[] rawMessage)
89+
{
90+
if (rawMessage == null)
91+
{
92+
throw new ArgumentNullException(nameof(rawMessage), "Message to sign cannot be null.");
93+
}
94+
95+
var message = Encoding.UTF8.GetString(rawMessage);
96+
return PersonalSign(message);
97+
}
98+
99+
public async Task<string> PersonalSign(string message)
100+
{
101+
if (string.IsNullOrEmpty(message))
102+
{
103+
throw new ArgumentNullException(nameof(message), "Message to sign cannot be null or empty.");
104+
}
105+
106+
var rpcRequest = new RpcRequest { Method = "personal_sign", Params = new object[] { message, WebGLMetaMask.Instance.GetAddress() } };
107+
return await WebGLMetaMask.Instance.RequestAsync<string>(rpcRequest);
108+
}
109+
110+
public async Task<string> SendTransaction(ThirdwebTransactionInput transaction)
111+
{
112+
await EnsureCorrectNetwork(transaction.ChainId);
113+
114+
var rpcRequest = new RpcRequest
115+
{
116+
Method = "eth_sendTransaction",
117+
Params = new object[]
118+
{
119+
new TransactionInput()
120+
{
121+
Nonce = transaction.Nonce,
122+
From = transaction.From,
123+
To = transaction.To,
124+
Gas = transaction.Gas,
125+
GasPrice = transaction.GasPrice,
126+
Value = transaction.Value,
127+
Data = transaction.Data,
128+
MaxFeePerGas = transaction.MaxFeePerGas,
129+
MaxPriorityFeePerGas = transaction.MaxPriorityFeePerGas,
130+
ChainId = transaction.ChainId,
131+
}
132+
}
133+
};
134+
return await WebGLMetaMask.Instance.RequestAsync<string>(rpcRequest);
135+
}
136+
137+
public Task<string> SignTransaction(ThirdwebTransactionInput transaction)
138+
{
139+
throw new NotImplementedException("Offline transaction signing is not supported by MetaMask.");
140+
}
141+
142+
public async Task<string> SignTypedDataV4(string json)
143+
{
144+
if (string.IsNullOrEmpty(json))
145+
{
146+
throw new ArgumentNullException(nameof(json), "Json to sign cannot be null.");
147+
}
148+
149+
var rpcRequest = new RpcRequest { Method = "eth_signTypedData_v4", Params = new object[] { await GetAddress(), json } };
150+
return await WebGLMetaMask.Instance.RequestAsync<string>(rpcRequest);
151+
}
152+
153+
public Task<string> SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData)
154+
where TDomain : IDomain
155+
{
156+
if (data == null)
157+
{
158+
throw new ArgumentNullException(nameof(data), "Data to sign cannot be null.");
159+
}
160+
161+
var json = typedData.ToJson(data);
162+
return SignTypedDataV4(json);
163+
}
164+
165+
public virtual async Task<string> Authenticate(
166+
string domain,
167+
BigInteger chainId,
168+
string authPayloadPath = "/auth/payload",
169+
string authLoginPath = "/auth/login",
170+
IThirdwebHttpClient httpClientOverride = null
171+
)
172+
{
173+
await EnsureCorrectNetwork(chainId);
174+
175+
var payloadURL = domain + authPayloadPath;
176+
var loginURL = domain + authLoginPath;
177+
178+
var payloadBodyRaw = new { address = await GetAddress(), chainId = chainId.ToString() };
179+
var payloadBody = JsonConvert.SerializeObject(payloadBodyRaw);
180+
181+
var httpClient = httpClientOverride ?? _client.HttpClient;
182+
183+
var payloadContent = new StringContent(payloadBody, Encoding.UTF8, "application/json");
184+
var payloadResponse = await httpClient.PostAsync(payloadURL, payloadContent);
185+
_ = payloadResponse.EnsureSuccessStatusCode();
186+
var payloadString = await payloadResponse.Content.ReadAsStringAsync();
187+
188+
var loginBodyRaw = JsonConvert.DeserializeObject<LoginPayload>(payloadString);
189+
var payloadToSign = Utils.GenerateSIWE(loginBodyRaw.payload);
190+
191+
loginBodyRaw.signature = await PersonalSign(payloadToSign);
192+
var loginBody = JsonConvert.SerializeObject(new { payload = loginBodyRaw });
193+
194+
var loginContent = new StringContent(loginBody, Encoding.UTF8, "application/json");
195+
var loginResponse = await httpClient.PostAsync(loginURL, loginContent);
196+
_ = loginResponse.EnsureSuccessStatusCode();
197+
var responseString = await loginResponse.Content.ReadAsStringAsync();
198+
return responseString;
199+
}
200+
201+
public Task<string> RecoverAddressFromEthSign(string message, string signature)
202+
{
203+
throw new NotImplementedException();
204+
}
205+
206+
public Task<string> RecoverAddressFromPersonalSign(string message, string signature)
207+
{
208+
throw new NotImplementedException();
209+
}
210+
211+
public Task<string> RecoverAddressFromTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData, string signature)
212+
where TDomain : IDomain
213+
{
214+
throw new NotImplementedException();
215+
}
216+
217+
#endregion
218+
219+
#region Network Switching
220+
221+
public static async Task EnsureCorrectNetwork(BigInteger chainId)
222+
{
223+
if (WebGLMetaMask.Instance.GetActiveChainId() != chainId)
224+
{
225+
await AddNetwork(chainId);
226+
if (WebGLMetaMask.Instance.GetActiveChainId() == chainId)
227+
return;
228+
await SwitchNetwork(chainId);
229+
}
230+
}
231+
232+
private static async Task SwitchNetwork(BigInteger chainId)
233+
{
234+
var switchEthereumChainParameter = new SwitchEthereumChainParameter { ChainId = new HexBigInteger(chainId) };
235+
var rpcRequest = new RpcRequest { Method = "wallet_switchEthereumChain", Params = new object[] { switchEthereumChainParameter } };
236+
_ = await WebGLMetaMask.Instance.RequestAsync<string>(rpcRequest);
237+
}
238+
239+
private static async Task AddNetwork(BigInteger chainId)
240+
{
241+
var twChainData = await Utils.FetchThirdwebChainDataAsync(_client, chainId);
242+
var addEthereumChainParameter = new AddEthereumChainParameter
243+
{
244+
ChainId = new HexBigInteger(chainId),
245+
BlockExplorerUrls = twChainData.Explorers.Select(e => e.Url).ToList(),
246+
ChainName = twChainData.Name,
247+
IconUrls = new List<string> { twChainData.Icon.Url },
248+
NativeCurrency = new NativeCurrency
249+
{
250+
Name = twChainData.NativeCurrency.Name,
251+
Symbol = twChainData.NativeCurrency.Symbol,
252+
Decimals = (uint)twChainData.NativeCurrency.Decimals,
253+
},
254+
RpcUrls = new List<string> { $"https://{chainId}.rpc.thirdweb.com/" },
255+
};
256+
var rpcRequest = new RpcRequest { Method = "wallet_addEthereumChain", Params = new object[] { addEthereumChainParameter } };
257+
_ = await WebGLMetaMask.Instance.RequestAsync<string>(rpcRequest);
258+
}
259+
260+
#endregion
261+
}
262+
}

Assets/Thirdweb/Core/Unity/Wallets/MetaMaskWallet.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)