Compare commits

..
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
00b9eea9ec Version Packages (#1980)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-21 13:37:37 -08:00
iketwandGitHub bf27b88728 [RN] Improves error messages for embedded wallet (#1989) 2023-11-21 19:41:23 +00:00
Tair AsimandGitHub 17fe41d42b fix: api key caching ttl (#1988) 2023-11-21 17:38:59 +00:00
Tair AsimandGitHub 36600decef chore: add logging to authenticate (#1973) 2023-11-21 17:08:22 +00:00
FirekeeperandGitHub e09221f1e0 [Unity] SW Account Override Support (#1986) 2023-11-21 12:16:28 +00:00
FirekeeperandGitHub 62cc785c4a [Unity] Support Add/Remove Admin and Create Session Key for SW (#1985) 2023-11-21 10:42:57 +00:00
Adam MajmudarandGitHub 7c45941b57 Add support for executeMetaTransaction (#1974) 2023-11-20 19:12:11 +00:00
FirekeeperandGitHub 7a2e0edab4 [Unity] Apple and Facebook logins (#1981) 2023-11-20 18:45:33 +00:00
Manan TankandGitHub fb0cb676b9 add ConnectEmbed + Fix double connection issue with Safe/Smart in ConnectWallet (#1952) 2023-11-20 17:56:55 +00:00
12d85dec10 [React/RN] Move usePersonalWalletAddress hook to core (#1971)
Co-authored-by: Manan Tank <[email protected]>
2023-11-20 17:29:32 +00:00
1094a03a04 [wallet, react] Add Coin98 Wallet (#1921)
Co-authored-by: Manan Tank <[email protected]>
2023-11-20 16:27:00 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
6cc38b0680 Version Packages (#1970)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-18 14:30:05 -08:00
Joaquim VergesandGitHub 7abb36a7ec [Wallets] Enforce authenticated users when getting EW signers (#1978) 2023-11-18 17:21:15 +00:00
Joaquim VergesandGitHub 6832b19fc5 [Wallets] Fix potential embedded wallet autoconnect race condition (#1977) 2023-11-18 07:18:30 +00:00
nkrishangandGitHub 1c4cbedb6a Override signMessage for SmartWallet class (#1953) 2023-11-18 06:04:02 +00:00
Phillip HoandGitHub 8ff87be631 fix: Reuse response body in service utils helper (#1976) 2023-11-18 01:50:37 +00:00
YashandGitHub e43beec36a [SDK] Resolve diamond pattern facets abi (#1935) 2023-11-17 15:56:29 +00:00
70 changed files with 2125 additions and 359 deletions
+16
View File
@@ -1,5 +1,21 @@
# @thirdweb-dev/auth
## 4.1.11
### Patch Changes
- [#1973](https://github.com/thirdweb-dev/js/pull/1973) [`36600dec`](https://github.com/thirdweb-dev/js/commit/36600decefca7b9ebb1924757b4186b559e439f6) Thanks [@assimovt](https://github.com/assimovt)! - add logging to authenticate
- Updated dependencies [[`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1)]:
- @thirdweb-dev/wallets@2.1.8
## 4.1.10
### Patch Changes
- Updated dependencies [[`6832b19f`](https://github.com/thirdweb-dev/js/commit/6832b19fc5eedc7c17e69940a361840de0a84881), [`7abb36a7`](https://github.com/thirdweb-dev/js/commit/7abb36a7ec39766dd7da03015ccef401ef7a7863), [`1c4cbedb`](https://github.com/thirdweb-dev/js/commit/1c4cbedb6a3c9f9014ffbf83c02b998768958d96)]:
- @thirdweb-dev/wallets@2.1.7
## 4.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "4.1.9",
"version": "4.1.11",
"main": "dist/thirdweb-dev-auth.cjs.js",
"module": "dist/thirdweb-dev-auth.esm.js",
"browser": {
@@ -62,6 +62,7 @@ export async function getUser<
const token = getToken(req);
if (!token) {
console.error("Error: No auth token found");
return null;
}
@@ -75,6 +76,7 @@ export async function getUser<
},
});
} catch (err) {
console.error(`Authenticate Error: ${(err as Error)?.message}`);
return null;
}
+18
View File
@@ -1,5 +1,23 @@
# thirdweb
## 0.13.19
### Patch Changes
- Updated dependencies [[`36600dec`](https://github.com/thirdweb-dev/js/commit/36600decefca7b9ebb1924757b4186b559e439f6), [`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1), [`7c45941b`](https://github.com/thirdweb-dev/js/commit/7c45941b57fb5a6f83be507ee64dbf885d93c9d4)]:
- @thirdweb-dev/auth@4.1.11
- @thirdweb-dev/wallets@2.1.8
- @thirdweb-dev/sdk@4.0.16
## 0.13.18
### Patch Changes
- Updated dependencies [[`6832b19f`](https://github.com/thirdweb-dev/js/commit/6832b19fc5eedc7c17e69940a361840de0a84881), [`7abb36a7`](https://github.com/thirdweb-dev/js/commit/7abb36a7ec39766dd7da03015ccef401ef7a7863), [`1c4cbedb`](https://github.com/thirdweb-dev/js/commit/1c4cbedb6a3c9f9014ffbf83c02b998768958d96), [`e43beec3`](https://github.com/thirdweb-dev/js/commit/e43beec36a569211b6fe708107aa7dc04bff08ec)]:
- @thirdweb-dev/wallets@2.1.7
- @thirdweb-dev/sdk@4.0.15
- @thirdweb-dev/auth@4.1.10
## 0.13.17
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.13.17",
"version": "0.13.19",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+40
View File
@@ -1,5 +1,45 @@
# @thirdweb-dev/react-core
## 4.1.9
### Patch Changes
- [#1952](https://github.com/thirdweb-dev/js/pull/1952) [`fb0cb676`](https://github.com/thirdweb-dev/js/commit/fb0cb676b996fe386dcc53b5a6dab03f88e76175) Thanks [@MananTank](https://github.com/MananTank)! - - Add `ConnectEmbed`` component to embed the ConnectWallet's Modal UI directly in page
- Fix `ConnectWallet` component setting personal wallet as "connected" wallet when connecting a Safe / Smart Wallet
- [#1921](https://github.com/thirdweb-dev/js/pull/1921) [`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Add Coin98 wallet
- [#1971](https://github.com/thirdweb-dev/js/pull/1971) [`12d85dec`](https://github.com/thirdweb-dev/js/commit/12d85dec10f47d6520b737a23576e4f832abb47f) Thanks [@iketw](https://github.com/iketw)! - Adds usePersonalWalletAddress hook to React/RN
```typescript
// React
import { usePersonalWalletAddress } from "@thirdweb-dev/react";
const personalWalletAddress = usePersonalWalletAddress();
```
```typescript
// React-Native
import { usePersonalWalletAddress } from "@thirdweb-dev/react-native";
const personalWalletAddress = usePersonalWalletAddress();
```
- Updated dependencies [[`36600dec`](https://github.com/thirdweb-dev/js/commit/36600decefca7b9ebb1924757b4186b559e439f6), [`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1), [`7c45941b`](https://github.com/thirdweb-dev/js/commit/7c45941b57fb5a6f83be507ee64dbf885d93c9d4)]:
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
## 4.1.8
### Patch Changes
- Updated dependencies [[`6832b19f`](https://github.com/thirdweb-dev/js/commit/6832b19fc5eedc7c17e69940a361840de0a84881), [`7abb36a7`](https://github.com/thirdweb-dev/js/commit/7abb36a7ec39766dd7da03015ccef401ef7a7863), [`1c4cbedb`](https://github.com/thirdweb-dev/js/commit/1c4cbedb6a3c9f9014ffbf83c02b998768958d96), [`e43beec3`](https://github.com/thirdweb-dev/js/commit/e43beec36a569211b6fe708107aa7dc04bff08ec)]:
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
## 4.1.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-core",
"version": "4.1.7",
"version": "4.1.9",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-core",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -2,6 +2,7 @@ import { useWalletContext } from "../providers/thirdweb-wallet-provider";
import invariant from "tiny-invariant";
import type {
BloctoWallet,
Coin98Wallet,
CoinbaseWallet,
CoreWallet,
EmbeddedWallet,
@@ -26,6 +27,7 @@ export type WalletId = (typeof walletIds)[keyof typeof walletIds];
type WalletIdToWalletTypeMap = {
metamask: MetaMaskWallet;
coin98: Coin98Wallet;
coinbase: CoinbaseWallet;
coreWallet: CoreWallet;
rainbowWallet: RainbowWallet;
@@ -65,6 +65,16 @@ const walletInstanceToConfig: Map<
WalletConfig<any>
> = new Map();
type PersonalWalletInfo = {
signer?: Signer;
wallet?: WalletInstance;
walletConfig?: WalletConfig;
connectionStatus: ConnectionStatus;
chainId?: number;
disconnect: () => Promise<void>;
switchChain: (chainId: number) => Promise<void>;
};
type ThirdwebWalletContextData = {
wallets: WalletConfig[];
signer?: Signer;
@@ -92,6 +102,20 @@ type ThirdwebWalletContextData = {
getWalletConfig: (walletInstance: WalletInstance) => WalletConfig | undefined;
activeChainSetExplicitly: boolean;
clientId?: string;
/**
* flag indicating whether to treat any wallet that gets connected after setting this flag as a "personal" wallet
* If a wallet is treated as a "personal" wallet, It's details like signer, connectionStatus, wallet, walletConfig will be saved in `personalWalletInfo` object instead of the main context
*/
isConnectingToPersonalWallet: boolean;
/**
* Set value of `isConnectingToPersonalWallet` flag
*/
setIsConnectingToPersonalWallet: (value: boolean) => void;
/**
* object containing info about the connected personal wallet
* When `isConnectingToPersonalWallet` is set to true, and a wallet is connected after that, all the info about the connected wallet will be stored in this object
*/
personalWalletInfo: PersonalWalletInfo;
};
const ThirdwebWalletContext = /* @__PURE__ */ createContext<
@@ -113,12 +137,32 @@ export function ThirdwebWalletProvider(
signerWallet?: WalletConfig<SignerWallet>;
}>,
) {
const [signer, setSigner] = useState<Signer | undefined>(undefined);
const [connectionStatus, setConnectionStatus] =
useState<ConnectionStatus>("unknown");
const autoConnectTimeout = props.autoConnectTimeout || 15000;
if (!lastConnectedWalletStorage) {
lastConnectedWalletStorage =
props.createWalletStorage("coordinatorStorage");
}
// if autoSwitch is enabled - enforce connection to activeChain
const chainToConnect = props.autoSwitch ? props.activeChain : undefined;
const [isConnectingToPersonalWallet, setIsConnectingToPersonalWallet] =
useState(false);
const personalWalletConnection = usePersonalWalletConnection();
const {
wallet: personalWallet,
setWallet: setPersonalWallet,
setSigner: setPersonalWalletSigner,
setStatus: setPersonalWalletStatus,
setWalletConfig: setPersonalWalletConfig,
} = personalWalletConnection;
const [signer, setSigner] = useState<Signer | undefined>(undefined);
const [status, setStatus] = useState<ConnectionStatus>("unknown");
const [activeWallet, setActiveWallet] = useState<
WalletInstance | undefined
>();
@@ -131,14 +175,6 @@ export function ThirdwebWalletProvider(
WalletConfig | undefined
>();
if (!lastConnectedWalletStorage) {
lastConnectedWalletStorage =
props.createWalletStorage("coordinatorStorage");
}
// if autoSwitch is enabled - enforce connection to activeChain
const chainToConnect = props.autoSwitch ? props.activeChain : undefined;
const walletParams: WalletOptions = useMemo(() => {
return {
chains: props.chains,
@@ -165,12 +201,25 @@ export function ThirdwebWalletProvider(
[walletParams],
);
// if props.chains is updated, update the active wallet's chains
// if props.chains is updated, update the wallet's chains
useEffect(() => {
if (activeWallet) {
activeWallet.updateChains(props.chains);
const _personalWallet = activeWallet.getPersonalWallet();
if (_personalWallet instanceof AbstractClientWallet) {
_personalWallet.updateChains(props.chains);
}
}
}, [activeWallet, props.chains]);
if (isConnectingToPersonalWallet && personalWallet) {
personalWallet.updateChains(props.chains);
}
}, [
activeWallet,
props.chains,
personalWallet,
isConnectingToPersonalWallet,
]);
const setConnectedWallet = useCallback(
async (
@@ -178,23 +227,45 @@ export function ThirdwebWalletProvider(
connectParams?: ConnectParams<Record<string, any>>,
isAutoConnect = false,
) => {
setActiveWallet(wallet);
if (isConnectingToPersonalWallet) {
setPersonalWallet(wallet);
} else {
setActiveWallet(wallet);
}
const walletConfig = walletInstanceToConfig.get(wallet);
if (!walletConfig) {
throw new Error(
"Wallet config not found for given wallet instance. Do not create a wallet instance manually - use the useCreateWalletInstance() hook instead",
);
}
setActiveWalletConfig(walletConfig);
setConnectionStatus("connected");
const _signer = await wallet.getSigner();
setSigner(_signer);
// it autoconnected, then the details is already saved in storage, no need to store again
if (isConnectingToPersonalWallet) {
setPersonalWalletConfig(walletConfig);
setPersonalWalletStatus("connected");
} else {
setActiveWalletConfig(walletConfig);
setStatus("connected");
}
const _signer = await wallet.getSigner();
if (isConnectingToPersonalWallet) {
setPersonalWalletSigner(_signer);
} else {
setSigner(_signer);
}
// it auto-connected, then the details is already saved in storage, no need to store again
if (isAutoConnect) {
return;
}
// do not save connection details for personal wallet
if (isConnectingToPersonalWallet) {
return;
}
// save to storage
const walletInfo: LastConnectedWalletInfo = {
@@ -202,16 +273,18 @@ export function ThirdwebWalletProvider(
connectParams: connectParams || wallet.getConnectParams(),
};
// if personal wallet exists, we need to replace the connectParams.personalWallet to a stringifiable version
const personalWallet = wallet.getPersonalWallet() as AbstractClientWallet;
const personalWalletConfig = walletInstanceToConfig.get(personalWallet);
// if personal wallet exists, we need to replace the connectParams.personalWallet to a serializable version
const _personalWallet =
wallet.getPersonalWallet() as AbstractClientWallet;
if (personalWallet && personalWalletConfig) {
const _personalWalletConfig = walletInstanceToConfig.get(_personalWallet);
if (_personalWallet && _personalWalletConfig) {
walletInfo.connectParams = {
...walletInfo.connectParams,
personalWallet: {
walletId: personalWalletConfig.id,
connectParams: personalWallet.getConnectParams(),
walletId: _personalWalletConfig.id,
connectParams: _personalWallet.getConnectParams(),
},
};
@@ -220,7 +293,13 @@ export function ThirdwebWalletProvider(
saveLastConnectedWalletInfo(walletInfo);
}
},
[],
[
isConnectingToPersonalWallet,
setPersonalWallet,
setPersonalWalletConfig,
setPersonalWalletStatus,
setPersonalWalletSigner,
],
);
const storeLastActiveChainId = useCallback(async (chainId: number) => {
@@ -250,22 +329,35 @@ export function ThirdwebWalletProvider(
const switchChain = useCallback(
async (chainId: number) => {
if (!activeWallet) {
const wallet = isConnectingToPersonalWallet
? personalWallet
: activeWallet;
if (!wallet) {
throw new Error("No active wallet");
}
await activeWallet.switchChain(chainId);
const _signer = await activeWallet.getSigner();
await storeLastActiveChainId(chainId);
await wallet.switchChain(chainId);
const _signer = await wallet.getSigner();
setSigner(_signer);
if (!isConnectingToPersonalWallet) {
await storeLastActiveChainId(chainId);
setSigner(_signer);
} else {
setPersonalWalletSigner(_signer);
}
},
[activeWallet, storeLastActiveChainId],
[
isConnectingToPersonalWallet,
personalWallet,
activeWallet,
storeLastActiveChainId,
setPersonalWalletSigner,
],
);
const autoConnectTriggered = useRef(false);
// Auto Connect
const autoConnectTriggered = useRef(false);
useEffect(() => {
// do not auto connect if signerWallet is given
if (props.signerWallet) {
@@ -278,7 +370,7 @@ export function ThirdwebWalletProvider(
// if explicitly set to false, don't auto connect
// by default, auto connect
if (props.shouldAutoConnect === false) {
setConnectionStatus("disconnected");
setStatus("disconnected");
return;
}
@@ -287,18 +379,18 @@ export function ThirdwebWalletProvider(
return;
}
if (connectionStatus !== "unknown") {
if (status !== "unknown") {
// only try to auto connect if we're in the unknown state
return;
}
autoConnectTriggered.current = true;
async function autoconnect() {
async function autoConnect() {
const walletInfo = await getLastConnectedWalletInfo();
if (!walletInfo) {
setConnectionStatus("disconnected");
setStatus("disconnected");
return;
}
@@ -308,7 +400,7 @@ export function ThirdwebWalletProvider(
if (!walletObj) {
// last connected wallet is no longer present in the supported wallets
setConnectionStatus("disconnected");
setStatus("disconnected");
return;
}
@@ -317,12 +409,13 @@ export function ThirdwebWalletProvider(
if (personalWalletInfo) {
const personalWallets = walletObj.personalWallets || [];
const personalWalleObj = personalWallets.find(
const personalWalletObj = personalWallets.find(
(W) => W.id === personalWalletInfo.walletId,
);
if (personalWalleObj) {
if (personalWalletObj) {
// create a personal wallet instance and auto connect it
const personalWalletInstance = createWalletInstance(personalWalleObj);
const personalWalletInstance =
createWalletInstance(personalWalletObj);
try {
await timeoutPromise(
@@ -337,7 +430,7 @@ export function ThirdwebWalletProvider(
} catch (e) {
console.error("Failed to auto connect personal wallet");
console.error(e);
setConnectionStatus("disconnected");
setStatus("disconnected");
return;
}
@@ -348,7 +441,7 @@ export function ThirdwebWalletProvider(
};
} else {
// last used personal wallet is no longer present in the supported wallets
setConnectionStatus("disconnected");
setStatus("disconnected");
return;
}
}
@@ -357,7 +450,7 @@ export function ThirdwebWalletProvider(
const wallet = createWalletInstance(walletObj);
try {
setConnectionStatus("connecting");
setStatus("connecting");
await timeoutPromise(wallet.autoConnect(walletInfo.connectParams), {
ms: autoConnectTimeout,
message: autoConnectTimeoutErrorMessage,
@@ -374,20 +467,21 @@ export function ThirdwebWalletProvider(
LAST_CONNECTED_WALLET_STORAGE_KEY,
);
}
setConnectionStatus("disconnected");
setStatus("disconnected");
}
}
autoconnect();
autoConnect();
}, [
createWalletInstance,
props.supportedWallets,
setConnectedWallet,
props.shouldAutoConnect,
activeWallet,
connectionStatus,
status,
autoConnectTimeout,
props.signerWallet,
setStatus,
]);
const connectWallet = useCallback(
@@ -400,7 +494,12 @@ export function ThirdwebWalletProvider(
};
const wallet = createWalletInstance(WalletObj);
setConnectionStatus("connecting");
if (isConnectingToPersonalWallet) {
setPersonalWalletStatus("connecting");
} else {
setStatus("connecting");
}
try {
// if magic is using social login - it will redirect the page - so need to save walletInfo before connecting
// TODO: find a better way to handle this
@@ -414,41 +513,65 @@ export function ThirdwebWalletProvider(
setConnectedWallet(wallet, _connectedParams);
} catch (e: any) {
console.error(`Error connecting to wallet: ${e}`);
setConnectionStatus("disconnected");
if (isConnectingToPersonalWallet) {
setPersonalWalletStatus("disconnected");
} else {
setStatus("disconnected");
}
throw e;
}
return wallet;
},
[createWalletInstance, setConnectedWallet, chainToConnect],
[
chainToConnect?.chainId,
createWalletInstance,
isConnectingToPersonalWallet,
setPersonalWalletStatus,
setConnectedWallet,
],
);
const onWalletDisconnect = useCallback(async () => {
await lastConnectedWalletStorage.removeItem(
LAST_CONNECTED_WALLET_STORAGE_KEY,
);
setConnectionStatus("disconnected");
setSigner(undefined);
setActiveWallet(undefined);
setActiveWalletConfig(undefined);
}, []);
if (isConnectingToPersonalWallet) {
setPersonalWalletStatus("disconnected");
setPersonalWalletSigner(undefined);
setPersonalWallet(undefined);
setPersonalWalletConfig(undefined);
} else {
setStatus("disconnected");
setSigner(undefined);
setActiveWallet(undefined);
setActiveWalletConfig(undefined);
}
}, [
isConnectingToPersonalWallet,
setPersonalWallet,
setPersonalWalletStatus,
setPersonalWalletSigner,
setPersonalWalletConfig,
]);
const disconnectWallet = useCallback(async () => {
// if disconnect is called before the wallet is connected
if (!activeWallet) {
onWalletDisconnect();
return;
const wallet = isConnectingToPersonalWallet ? personalWallet : activeWallet;
if (wallet) {
const _personalWallet = wallet.getPersonalWallet();
await wallet.disconnect();
if (_personalWallet instanceof AbstractClientWallet) {
await _personalWallet.disconnect();
}
}
const personalWallet = activeWallet.getPersonalWallet();
await activeWallet.disconnect();
if (personalWallet) {
await (personalWallet as AbstractClientWallet)?.disconnect();
}
onWalletDisconnect();
}, [activeWallet, onWalletDisconnect]);
}, [
activeWallet,
isConnectingToPersonalWallet,
onWalletDisconnect,
personalWallet,
]);
// when wallet's network or account is changed using the extension, update UI
useEffect(() => {
@@ -461,19 +584,20 @@ export function ThirdwebWalletProvider(
setSigner(_signer);
};
activeWallet.addListener("change", () => {
update();
});
activeWallet.addListener("disconnect", () => {
onWalletDisconnect();
});
activeWallet.addListener("change", update);
activeWallet.addListener("disconnect", onWalletDisconnect);
return () => {
activeWallet.removeListener("change");
activeWallet.removeListener("disconnect");
activeWallet.removeListener("change", update);
activeWallet.removeListener("disconnect", onWalletDisconnect);
};
}, [activeWallet, onWalletDisconnect]);
}, [
activeWallet,
isConnectingToPersonalWallet,
onWalletDisconnect,
personalWallet,
setPersonalWalletSigner,
setSigner,
]);
// connect signerWallet immediately if it's passed
// and disconnect it if it's not passed
@@ -502,6 +626,17 @@ export function ThirdwebWalletProvider(
disconnectWallet,
]);
const setConnectionStatus = useCallback(
(value: ConnectionStatus) => {
if (isConnectingToPersonalWallet) {
setPersonalWalletStatus(value);
} else {
setStatus(value);
}
},
[isConnectingToPersonalWallet, setPersonalWalletStatus],
);
return (
<ThirdwebWalletContext.Provider
value={{
@@ -511,8 +646,8 @@ export function ThirdwebWalletProvider(
signer,
activeWallet,
activeWalletConfig,
connectionStatus,
setConnectionStatus,
connectionStatus: status,
setConnectionStatus: setConnectionStatus,
createWalletInstance: createWalletInstance,
createdWalletInstance: createdWalletInstance,
createWalletStorage: props.createWalletStorage,
@@ -525,6 +660,17 @@ export function ThirdwebWalletProvider(
},
activeChainSetExplicitly: props.activeChainSetExplicitly,
clientId: props.clientId,
isConnectingToPersonalWallet,
setIsConnectingToPersonalWallet,
personalWalletInfo: {
wallet: personalWalletConnection.wallet,
signer: personalWalletConnection.signer,
walletConfig: personalWalletConnection.walletConfig,
connectionStatus: personalWalletConnection.status,
chainId: personalWalletConnection.chainId,
disconnect: personalWalletConnection.disconnect,
switchChain: personalWalletConnection.switchChain,
},
}}
>
{props.children}
@@ -603,3 +749,79 @@ function timeoutPromise<T>(
}
const autoConnectTimeoutErrorMessage = `Failed to Auto connect. Auto connect timed out. You can increase the timeout duration using the autoConnectTimeout prop on <ThirdwebProvider />`;
function usePersonalWalletConnection() {
const [signer, setSigner] = useState<Signer | undefined>(undefined);
const [status, setStatus] = useState<ConnectionStatus>("unknown");
const [wallet, setWallet] = useState<WalletInstance | undefined>();
const [walletConfig, setWalletConfig] = useState<WalletConfig | undefined>();
const [chainId, setChainId] = useState<number | undefined>(undefined);
const handleDisconnect = useCallback(async () => {
setStatus("disconnected");
setSigner(undefined);
setWallet(undefined);
setWalletConfig(undefined);
}, []);
const disconnect = useCallback(async () => {
if (wallet) {
await wallet.disconnect();
}
handleDisconnect();
}, [wallet, handleDisconnect]);
useEffect(() => {
if (wallet) {
const updateChainId = () => {
wallet?.getChainId().then((_chainId) => {
setChainId(_chainId);
});
};
const update = async () => {
updateChainId();
const _signer = await wallet.getSigner();
setSigner(_signer);
};
updateChainId();
wallet.addListener("change", update);
wallet.addListener("disconnect", handleDisconnect);
return () => {
wallet.removeListener("change", update);
wallet.removeListener("disconnect", handleDisconnect);
};
} else {
setChainId(undefined);
}
}, [disconnect, handleDisconnect, wallet]);
const switchChain = useCallback(
async (_chainId: number) => {
if (!wallet) {
throw new Error("No active wallet");
}
await wallet.switchChain(_chainId);
const _signer = await wallet.getSigner();
setSigner(_signer);
},
[wallet],
);
return {
signer: signer,
setSigner: setSigner,
wallet: wallet,
setWallet: setWallet,
walletConfig: walletConfig,
setWalletConfig: setWalletConfig,
status: status,
setStatus: setStatus,
chainId: chainId,
disconnect,
switchChain,
};
}
@@ -0,0 +1,27 @@
import { useEffect, useState } from "react";
import { useWallet } from "../../core/hooks/wallet-hooks";
export const usePersonalWalletAddress = () => {
const [personalWalletAddress, setPersonalWalletAddress] = useState<
string | undefined
>();
const activeWallet = useWallet();
useEffect(() => {
(async () => {
const possiblePersonalWallet = activeWallet?.getPersonalWallet();
if (!possiblePersonalWallet) {
setPersonalWalletAddress(undefined);
return;
}
const address = await (
await possiblePersonalWallet?.getSigner()
)?.getAddress();
setPersonalWalletAddress(address);
})();
}, [activeWallet]);
return personalWalletAddress;
};
+1
View File
@@ -22,6 +22,7 @@ export {
} from "../core/hooks/wallet-hooks";
export { useNetwork } from "../core/hooks/useNetwork";
export { useWalletContext } from "../core/providers/thirdweb-wallet-provider";
export { usePersonalWalletAddress } from "./hooks/usePersonalWalletAddress";
// connected wallet hooks
export {
@@ -1,5 +1,9 @@
# @thirdweb-dev/react-native-compat
## 0.4.9
## 0.4.8
## 0.4.7
## 0.4.6
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@thirdweb-dev/react-native-compat",
"description": "Shims for Thirdweb in React Native Projects",
"version": "0.4.7",
"version": "0.4.9",
"author": "thirdweb eng <[email protected]>",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-native-compat",
"license": "Apache-2.0",
+36
View File
@@ -1,5 +1,41 @@
# @thirdweb-dev/react-native
## 0.4.9
### Patch Changes
- [#1971](https://github.com/thirdweb-dev/js/pull/1971) [`12d85dec`](https://github.com/thirdweb-dev/js/commit/12d85dec10f47d6520b737a23576e4f832abb47f) Thanks [@iketw](https://github.com/iketw)! - Adds usePersonalWalletAddress hook to React/RN
```typescript
// React
import { usePersonalWalletAddress } from "@thirdweb-dev/react";
const personalWalletAddress = usePersonalWalletAddress();
```
```typescript
// React-Native
import { usePersonalWalletAddress } from "@thirdweb-dev/react-native";
const personalWalletAddress = usePersonalWalletAddress();
```
- [#1989](https://github.com/thirdweb-dev/js/pull/1989) [`bf27b887`](https://github.com/thirdweb-dev/js/commit/bf27b8872870c08bb19934d87e574e0477c9550f) Thanks [@iketw](https://github.com/iketw)! - Improves EmbeddedWallet error messages in React Native
- Updated dependencies [[`fb0cb676`](https://github.com/thirdweb-dev/js/commit/fb0cb676b996fe386dcc53b5a6dab03f88e76175), [`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1), [`7c45941b`](https://github.com/thirdweb-dev/js/commit/7c45941b57fb5a6f83be507ee64dbf885d93c9d4), [`12d85dec`](https://github.com/thirdweb-dev/js/commit/12d85dec10f47d6520b737a23576e4f832abb47f)]:
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
## 0.4.8
### Patch Changes
- Updated dependencies [[`6832b19f`](https://github.com/thirdweb-dev/js/commit/6832b19fc5eedc7c17e69940a361840de0a84881), [`7abb36a7`](https://github.com/thirdweb-dev/js/commit/7abb36a7ec39766dd7da03015ccef401ef7a7863), [`1c4cbedb`](https://github.com/thirdweb-dev/js/commit/1c4cbedb6a3c9f9014ffbf83c02b998768958d96), [`e43beec3`](https://github.com/thirdweb-dev/js/commit/e43beec36a569211b6fe708107aa7dc04bff08ec)]:
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
## 0.4.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-native",
"version": "0.4.7",
"version": "0.4.9",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-native",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -13,12 +13,12 @@ import { ModalHeaderTextClose } from "../base/modal/ModalHeaderTextClose";
import {
shortenAddress,
useAddress,
usePersonalWalletAddress,
useWallet,
} from "@thirdweb-dev/react-core";
import { PasswordInput } from "../PasswordInput";
import * as FileSystem from "expo-file-system";
import { SmartWallet } from "@thirdweb-dev/wallets";
import { usePersonalWalletAddress } from "../../wallets/hooks/usePersonalWalletAddress";
import { LocalWallet } from "../../wallets/wallets/LocalWallet";
import { TWModal } from "../base/modal/TWModal";
import { useLocale } from "../../providers/ui-context-provider";
@@ -25,11 +25,13 @@ import { getCognitoUser, setCognitoUser } from "./helpers/storage/state";
import { isDeviceSharePresentForUser } from "./helpers/storage/local";
import { Auth } from "aws-amplify";
import {
DOMAIN_URL_2023,
ROUTE_AUTH_JWT_CALLBACK,
ROUTE_HEADLESS_OAUTH_LOGIN,
} from "./helpers/constants";
import { AuthOptions, OauthOption, VerifiedTokenResponse } from "../types";
import { InAppBrowser } from "react-native-inappbrowser-reborn";
import { createErrorMessage } from "./helpers/errors";
export async function sendVerificationEmail(options: {
email: string;
@@ -50,7 +52,7 @@ export async function sendVerificationEmail(options: {
});
} catch (e) {
throw new Error(
`Malformed response from the send email OTP API: ${JSON.stringify(e)}`,
createErrorMessage("Malformed response from the send email OTP API", e),
);
}
@@ -102,7 +104,7 @@ export async function validateEmailOTP(options: {
});
} catch (e) {
throw new Error(
`Malformed response from the send email OTP API: ${JSON.stringify(e)}`,
createErrorMessage("Malformed response validating the OTP", e),
);
}
let verifiedTokenResponse: VerifiedTokenResponse;
@@ -163,9 +165,10 @@ export async function validateEmailOTP(options: {
return { storedToken };
} catch (e) {
throw new Error(
`Malformed response from the verify one time password: ${
(e as Error).message
}}`,
createErrorMessage(
"Malformed response from the verify one time password",
e,
),
);
}
}
@@ -173,7 +176,7 @@ export async function validateEmailOTP(options: {
export async function socialLogin(oauthOptions: OauthOption, clientId: string) {
const encodedProvider = encodeURIComponent(oauthOptions.provider);
const headlessLoginLinkWithParams = `${ROUTE_HEADLESS_OAUTH_LOGIN}?authProvider=${encodedProvider}&baseUrl=${encodeURIComponent(
`https://embedded-wallet.thirdweb.com`,
DOMAIN_URL_2023,
)}&platform=${encodeURIComponent("mobile")}`;
const resp = await fetch(headlessLoginLinkWithParams);
@@ -188,7 +191,7 @@ export async function socialLogin(oauthOptions: OauthOption, clientId: string) {
const { platformLoginLink } = json;
const completeLoginUrl = `${platformLoginLink}?developerClientId=${encodeURIComponent(
clientId || "",
clientId,
)}&platform=${encodeURIComponent("mobile")}&redirectUrl=${encodeURIComponent(
oauthOptions.redirectUrl,
)}&authOption=${encodedProvider}`;
@@ -243,7 +246,7 @@ export async function socialLogin(oauthOptions: OauthOption, clientId: string) {
return { storedToken, email: storedToken.authDetails.email };
} catch (e) {
throw new Error(
`Malformed response from post authentication: ${JSON.stringify(e)}`,
createErrorMessage("Malformed response from post authentication", e),
);
}
}
@@ -285,7 +288,7 @@ export async function customJwt(authOptions: AuthOptions, clientId: string) {
return { verifiedToken, email: verifiedToken.authDetails.email };
} catch (e) {
throw new Error(
`Malformed response from post authentication: ${JSON.stringify(e)}`,
createErrorMessage("Malformed response from post authentication", e),
);
}
}
@@ -18,6 +18,7 @@ import {
IsValidUserManagedEmailOTPResponse,
VerifiedTokenResponse,
} from "../../../types";
import { createErrorMessage } from "../errors";
const EMBEDDED_WALLET_TOKEN_HEADER = "embedded-wallet-token";
const PAPER_CLIENT_ID_HEADER = "x-thirdweb-client-id";
@@ -281,7 +282,7 @@ export async function getUserShares(clientId: string, getShareUrl: URL) {
};
} catch (e) {
throw new Error(
`Malformed response from the ews user wallet API: ${JSON.stringify(e)}`,
createErrorMessage("Malformed response from the ews user wallet API", e),
);
}
}
@@ -19,7 +19,8 @@ export const COGNITO_IDENTITY_POOL_ID =
export const GENERATE_RECOVERY_PASSWORD_LAMBDA_FUNCTION =
"arn:aws:lambda:us-west-2:324457261097:function:recovery-share-password-GenerateRecoverySharePassw-bbE5ZbVAToil";
const BASE_URL_2023 = "https://embedded-wallet.thirdweb.com/";
export const DOMAIN_URL_2023 = "https://embedded-wallet.thirdweb.com";
const BASE_URL_2023 = `${DOMAIN_URL_2023}/`;
const ROUTE_2023_10_20_API_BASE_PATH = `${BASE_URL_2023}api/2023-10-20`;
export const ROUTE_GET_EMBEDDED_WALLET_DETAILS = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/embedded-wallet-user-details`;
@@ -3,3 +3,11 @@ export const ErrorMessages = {
"Your OTP code is invalid or expired. Please request a new code or try again.",
missingRecoveryCode: "Missing encryption key for user",
};
export const createErrorMessage = (message: string, error: any): string => {
if (error instanceof Error) {
return `${message}: ${error.message}`;
} else {
return `${message}: ${JSON.stringify(error)}`;
}
};
@@ -12,6 +12,7 @@ import {
} from "../constants";
import { setDeviceShare } from "../storage/local";
import { encryptShareWeb } from "./encryption";
import { createErrorMessage } from "../errors";
export async function setUpNewUserWallet(
recoveryCode: string,
@@ -145,9 +146,10 @@ export async function storeShares<R extends string | undefined>({
}
} catch (e) {
throw new Error(
`Malformed response from the ews store user share API: ${JSON.stringify(
createErrorMessage(
"Malformed response from the ews store user share API",
e,
)}`,
),
);
}
}
@@ -1,23 +0,0 @@
import { useWallet } from "@thirdweb-dev/react-core";
import { AbstractClientWallet, SmartWallet } from "@thirdweb-dev/wallets";
import { useEffect, useState } from "react";
export const usePersonalWalletAddress = () => {
const [personalWalletAddress, setPersonalWalletAddress] = useState<
string | undefined
>();
const activeWallet = useWallet();
useEffect(() => {
(async () => {
if (activeWallet?.walletId === SmartWallet.id) {
const address = await (
activeWallet.getPersonalWallet() as AbstractClientWallet
).getAddress();
setPersonalWalletAddress(address);
}
})();
}, [activeWallet]);
return personalWalletAddress;
};
+24
View File
@@ -1,5 +1,29 @@
# @thirdweb-dev/react
## 4.1.9
### Patch Changes
- [#1952](https://github.com/thirdweb-dev/js/pull/1952) [`fb0cb676`](https://github.com/thirdweb-dev/js/commit/fb0cb676b996fe386dcc53b5a6dab03f88e76175) Thanks [@MananTank](https://github.com/MananTank)! - - Add `ConnectEmbed`` component to embed the ConnectWallet's Modal UI directly in page
- Fix `ConnectWallet` component setting personal wallet as "connected" wallet when connecting a Safe / Smart Wallet
- [#1921](https://github.com/thirdweb-dev/js/pull/1921) [`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Add Coin98 wallet
- Updated dependencies [[`fb0cb676`](https://github.com/thirdweb-dev/js/commit/fb0cb676b996fe386dcc53b5a6dab03f88e76175), [`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1), [`7c45941b`](https://github.com/thirdweb-dev/js/commit/7c45941b57fb5a6f83be507ee64dbf885d93c9d4), [`12d85dec`](https://github.com/thirdweb-dev/js/commit/12d85dec10f47d6520b737a23576e4f832abb47f)]:
- @thirdweb-dev/react-core@4.1.9
- @thirdweb-dev/wallets@2.1.8
- @thirdweb-dev/sdk@4.0.16
## 4.1.8
### Patch Changes
- Updated dependencies [[`6832b19f`](https://github.com/thirdweb-dev/js/commit/6832b19fc5eedc7c17e69940a361840de0a84881), [`7abb36a7`](https://github.com/thirdweb-dev/js/commit/7abb36a7ec39766dd7da03015ccef401ef7a7863), [`1c4cbedb`](https://github.com/thirdweb-dev/js/commit/1c4cbedb6a3c9f9014ffbf83c02b998768958d96), [`e43beec3`](https://github.com/thirdweb-dev/js/commit/e43beec36a569211b6fe708107aa7dc04bff08ec)]:
- @thirdweb-dev/wallets@2.1.7
- @thirdweb-dev/sdk@4.0.15
- @thirdweb-dev/react-core@4.1.8
## 4.1.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "4.1.7",
"version": "4.1.9",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+10 -1
View File
@@ -1,3 +1,8 @@
export {
ConnectEmbed,
type ConnectEmbedProps,
} from "../wallet/ConnectWallet/Modal/ConnectEmbed";
export {
useIsWalletModalOpen,
useSetIsWalletModalOpen,
@@ -10,7 +15,10 @@ export {
ConnectWallet,
type ConnectWalletProps,
} from "../wallet/ConnectWallet/ConnectWallet";
export { ConnectModalInline } from "../wallet/ConnectWallet/Modal/ConnectModalInline";
export {
ConnectModalInline,
type ConnectModalInlineProps,
} from "../wallet/ConnectWallet/Modal/ConnectModalInline";
export { NetworkSelector } from "../wallet/ConnectWallet/NetworkSelector";
export type { NetworkSelectorProps } from "../wallet/ConnectWallet/NetworkSelector";
@@ -73,4 +81,5 @@ export {
InjectedWallet,
setWalletAnalyticsEnabled,
CoreWallet,
Coin98Wallet,
} from "@thirdweb-dev/wallets";
+2
View File
@@ -52,6 +52,7 @@ export function enDefault() {
personalWallet: "Personal Wallet",
smartWallet: "Smart Wallet",
or: "OR",
goBackButton: "Back",
download: {
chrome: "Download Chrome Extension",
android: "Download on Google Play",
@@ -168,6 +169,7 @@ export function enDefault() {
trustWallet: extensionAndQRScanScreens("Trust"),
zerionWallet: extensionAndQRScanScreens("Zerion"),
coreWallet: extensionAndQRScanScreens("Core"),
coin98Wallet: extensionAndQRScanScreens("Coin98"),
paperWallet: {
signIn: "Sign in",
signInWithGoogle: "Sign in with Google",
+2
View File
@@ -54,6 +54,7 @@ export function esDefault(): ThirdwebLocale {
personalWallet: "Cartera personal",
smartWallet: "Cartera inteligente",
or: "O",
goBackButton: "Atras",
download: {
chrome: "Descargar extensión para Chrome",
android: "Descargar en Google Play",
@@ -168,6 +169,7 @@ export function esDefault(): ThirdwebLocale {
metamaskWallet: extensionAndQRScanScreens("MetaMask"),
okxWallet: extensionAndQRScanScreens("OKX"),
coreWallet: extensionAndQRScanScreens("Core"),
coin98Wallet: extensionAndQRScanScreens("Coin98"),
phantomWallet: extensionAndQRScanScreens("Phantom"),
rainbowWallet: extensionAndQRScanScreens("Rainbow"),
trustWallet: extensionAndQRScanScreens("Trust"),
+2
View File
@@ -53,6 +53,7 @@ export function jaDefault(): ThirdwebLocale {
personalWallet: "パーソナルウォレット",
smartWallet: "スマートウォレット",
or: "または",
goBackButton: "戻る", // TODO - check translation
download: {
chrome: "Chrome拡張をダウンロード",
android: "Google Playでダウンロード",
@@ -168,6 +169,7 @@ export function jaDefault(): ThirdwebLocale {
metamaskWallet: extensionAndQRScanScreens("MetaMask"),
okxWallet: extensionAndQRScanScreens("OKX"),
coreWallet: extensionAndQRScanScreens("Core"),
coin98Wallet: extensionAndQRScanScreens("Coin98"),
phantomWallet: extensionAndQRScanScreens("Phantom"),
rainbowWallet: extensionAndQRScanScreens("Rainbow"),
trustWallet: extensionAndQRScanScreens("Trust"),
@@ -20,6 +20,7 @@ export type ModalConfig = {
onLogin?: (token: string) => void;
onLogout?: () => void;
};
isEmbed?: boolean;
};
const WalletModalOpen = /* @__PURE__ */ createContext(false);
@@ -47,6 +48,7 @@ export const WalletUIStatesProvider = (
termsOfServiceUrl?: string;
privacyPolicyUrl?: string;
welcomeScreen?: WelcomeScreen;
isEmbed?: boolean;
}>,
) => {
const [isWalletModalOpen, setIsWalletModalOpen] = useState(false);
@@ -62,6 +64,7 @@ export const WalletUIStatesProvider = (
privacyPolicyUrl: props.privacyPolicyUrl,
welcomeScreen: props.welcomeScreen,
titleIconUrl: props.titleIconUrl,
isEmbed: props.isEmbed,
});
return (
+1
View File
@@ -20,6 +20,7 @@ export { walletConnectV1 } from "./wallet/wallets/walletConnectV1";
export { zerionWallet } from "./wallet/wallets/zerion/zerionWallet";
export { okxWallet } from "./wallet/wallets/okx/okxWallet";
export { coreWallet } from "./wallet/wallets/coreWallet/coreWallet";
export { coin98Wallet } from "./wallet/wallets/coin98/coin98Wallet";
export { darkTheme, lightTheme } from "./design-system/index";
export type { Theme, ThemeOverrides } from "./design-system/index";
@@ -0,0 +1,140 @@
import {
SetModalConfigCtx,
WalletUIStatesProvider,
} from "../../../evm/providers/wallet-ui-states-provider";
import { modalMaxWidthCompact, defaultTheme } from "../constants";
import { ConnectModalContent } from "./ConnectModal";
import { useScreen } from "./screen";
import { isMobile } from "../../../evm/utils/isMobile";
import { useConnectionStatus } from "@thirdweb-dev/react-core";
import { DynamicHeight } from "../../../components/DynamicHeight";
import {
CustomThemeProvider,
useCustomTheme,
} from "../../../design-system/CustomThemeProvider";
import { ComponentProps, useContext, useEffect } from "react";
import { useTWLocale } from "../../../evm/providers/locale-provider";
import { StyledDiv } from "../../../design-system/elements";
import { Theme, radius } from "../../../design-system";
export type ConnectEmbedProps = {
className?: string;
theme?: "dark" | "light" | Theme;
style?: React.CSSProperties;
/**
* If provided, Embed will show a Terms of Service message at the bottom with below link
*/
termsOfServiceUrl?: string;
/**
* If provided, Embed will show a Privacy Policy message at the bottom with below link
*/
privacyPolicyUrl?: string;
};
export const ConnectEmbed = (props: ConnectEmbedProps) => {
const connectionStatus = useConnectionStatus();
if (connectionStatus === "connected") {
return null;
}
return <ConnectEmbedContent {...props} />;
};
const ConnectEmbedContent = (props: ConnectEmbedProps) => {
const { screen, setScreen, initialScreen } = useScreen();
const modalSize = "compact" as const;
const content = (
<ConnectModalContent
initialScreen={initialScreen}
screen={screen}
setScreen={setScreen}
setHideModal={() => {
// no op
}}
/>
);
const walletUIStatesProps = {
theme: props.theme || defaultTheme,
modalSize: modalSize,
title: undefined,
termsOfServiceUrl: props.termsOfServiceUrl,
privacyPolicyUrl: props.privacyPolicyUrl,
isEmbed: true,
};
return (
<WalletUIStatesProvider {...walletUIStatesProps}>
<CustomThemeProvider theme={walletUIStatesProps.theme}>
<EmbedContainer
className={props.className}
style={{
height: "auto",
maxWidth: modalMaxWidthCompact,
...props.style,
}}
>
<DynamicHeight> {content} </DynamicHeight>
<SyncedWalletUIStates {...walletUIStatesProps} />
</EmbedContainer>
</CustomThemeProvider>
</WalletUIStatesProvider>
);
};
export function SyncedWalletUIStates(
props: ComponentProps<typeof WalletUIStatesProvider>,
) {
const setModalConfig = useContext(SetModalConfigCtx);
const locale = useTWLocale();
// update modalConfig on props change
useEffect(() => {
setModalConfig((c) => ({
...c,
title: props.title || locale.connectWallet.defaultModalTitle,
theme: props.theme || "dark",
modalSize: (isMobile() ? "compact" : props.modalSize) || "wide",
termsOfServiceUrl: props.termsOfServiceUrl,
privacyPolicyUrl: props.privacyPolicyUrl,
welcomeScreen: props.welcomeScreen,
titleIconUrl: props.titleIconUrl,
}));
}, [
props.title,
props.theme,
props.modalSize,
props.termsOfServiceUrl,
props.privacyPolicyUrl,
props.welcomeScreen,
props.titleIconUrl,
setModalConfig,
locale.connectWallet.defaultModalTitle,
]);
return <WalletUIStatesProvider {...props} />;
}
const EmbedContainer = /* @__PURE__ */ StyledDiv(() => {
const theme = useCustomTheme();
return {
color: theme.colors.primaryText,
background: theme.colors.modalBg,
width: "100%",
boxSizing: "border-box",
position: "relative",
lineHeight: "normal",
borderRadius: radius.xl,
border: `1px solid ${theme.colors.borderColor}`,
overflow: "hidden",
fontFamily: theme.fontFamily,
"& *::selection": {
backgroundColor: theme.colors.primaryText,
color: theme.colors.modalBg,
},
};
});
@@ -7,6 +7,7 @@ import {
useThirdwebAuthContext,
useUser,
useWallet,
useWalletContext,
useWallets,
} from "@thirdweb-dev/react-core";
import {
@@ -43,6 +44,8 @@ export const ConnectModalContent = (props: {
const walletConfigs = useWallets();
const connectionStatus = useConnectionStatus();
const disconnect = useDisconnect();
const { setIsConnectingToPersonalWallet, personalWalletInfo } =
useWalletContext();
const isWalletModalOpen = useIsWalletModalOpen();
const setIsWalletModalOpen = useSetIsWalletModalOpen();
@@ -59,6 +62,9 @@ export const ConnectModalContent = (props: {
const authConfig = useThirdwebAuthContext();
const closeModal = () => {
personalWalletInfo.disconnect();
setIsConnectingToPersonalWallet(false);
setIsWalletModalOpen(false);
onModalUnmount(() => {
setScreen(initialScreen);
@@ -66,6 +72,7 @@ export const ConnectModalContent = (props: {
};
const { setHideModal } = props;
const handleConnected = useCallback(() => {
const requiresSignIn = modalConfig.auth?.loginOptional
? false
@@ -261,6 +268,9 @@ export const ConnectModal = () => {
wallet,
]);
const { setIsConnectingToPersonalWallet, personalWalletInfo } =
useWalletContext();
return (
<CustomThemeProvider theme={theme}>
<Modal
@@ -270,6 +280,9 @@ export const ConnectModal = () => {
setOpen={(value) => {
setIsWalletModalOpen(value);
if (!value) {
setIsConnectingToPersonalWallet(false);
personalWalletInfo.disconnect();
const requiresSignIn = auth?.loginOptional
? false
: !!authConfig?.authUrl && !user?.address;
@@ -1,11 +1,8 @@
import { Cross2Icon } from "@radix-ui/react-icons";
import { CrossContainer } from "../../../components/Modal";
import { IconButton } from "../../../components/buttons";
import { iconSize, radius, shadow } from "../../../design-system";
import {
SetModalConfigCtx,
WalletUIStatesProvider,
} from "../../../evm/providers/wallet-ui-states-provider";
import { Theme, iconSize, radius, shadow } from "../../../design-system";
import { WalletUIStatesProvider } from "../../../evm/providers/wallet-ui-states-provider";
import {
wideModalMaxHeight,
modalMaxWidthCompact,
@@ -20,26 +17,65 @@ import {
CustomThemeProvider,
useCustomTheme,
} from "../../../design-system/CustomThemeProvider";
import { ComponentProps, useContext, useEffect } from "react";
import { ConnectWalletProps } from "../ConnectWallet";
import { useTWLocale } from "../../../evm/providers/locale-provider";
import { StyledDiv } from "../../../design-system/elements";
import { SyncedWalletUIStates } from "./ConnectEmbed";
import { WelcomeScreen } from "../screens/types";
export const ConnectModalInline = (
props: Omit<
ConnectWalletProps,
| "detailsBtn"
| "dropdownPosition"
| "auth"
| "networkSelector"
| "hideTestnetFaucet"
| "switchToActiveChain"
| "supportedTokens"
| "hideSwitchToPersonalWallet"
> & {
onModalHide?: () => void;
},
) => {
/**
* @internal
*/
export type ConnectModalInlineProps = {
className?: string;
theme?: "dark" | "light" | Theme;
/**
* Set a custom title for the modal
* @defaultValue "Connect"
*/
modalTitle?: string;
/**
* Replace the thirdweb icon next to modalTitle and set your own iconUrl
*
* Set to empty string to hide the icon
*/
modalTitleIconUrl?: string;
style?: React.CSSProperties;
/**
* Set the size of the modal - `compact` or `wide` on desktop
*
* Modal size is always `compact` on mobile
*
* @defaultValue "wide"
*/
modalSize?: "compact" | "wide";
/**
* If provided, Modal will show a Terms of Service message at the bottom with below link
*/
termsOfServiceUrl?: string;
/**
* If provided, Modal will show a Privacy Policy message at the bottom with below link
*/
privacyPolicyUrl?: string;
/**
* Customize the welcome screen
*
* Either provide a component to replace the default screen entirely
*
* or an object with title, subtitle and imgSrc to change the content of the default screen
*/
welcomeScreen?: WelcomeScreen;
};
/**
* @internal
*/
export const ConnectModalInline = (props: ConnectModalInlineProps) => {
const { screen, setScreen, initialScreen } = useScreen();
const walletConfigs = useWallets();
const modalSize =
@@ -53,9 +89,7 @@ export const ConnectModalInline = (
screen={screen}
setScreen={setScreen}
setHideModal={() => {
if (props.onModalHide) {
props.onModalHide();
}
// no op
}}
/>
@@ -95,9 +129,10 @@ export const ConnectModalInline = (
modalSize === "compact"
? modalMaxWidthCompact
: modalMaxWidthWide,
...props.style,
}}
>
{props.modalSize === "compact" ? (
{modalSize === "compact" ? (
<DynamicHeight> {content} </DynamicHeight>
) : (
content
@@ -109,39 +144,6 @@ export const ConnectModalInline = (
);
};
function SyncedWalletUIStates(
props: ComponentProps<typeof WalletUIStatesProvider>,
) {
const setModalConfig = useContext(SetModalConfigCtx);
const locale = useTWLocale();
// update modalConfig on props change
useEffect(() => {
setModalConfig((c) => ({
...c,
title: props.title || locale.connectWallet.defaultModalTitle,
theme: props.theme || "dark",
modalSize: (isMobile() ? "compact" : props.modalSize) || "wide",
termsOfServiceUrl: props.termsOfServiceUrl,
privacyPolicyUrl: props.privacyPolicyUrl,
welcomeScreen: props.welcomeScreen,
titleIconUrl: props.titleIconUrl,
}));
}, [
props.title,
props.theme,
props.modalSize,
props.termsOfServiceUrl,
props.privacyPolicyUrl,
props.welcomeScreen,
props.titleIconUrl,
setModalConfig,
locale.connectWallet.defaultModalTitle,
]);
return <WalletUIStatesProvider {...props} />;
}
const ConnectModalInlineContainer = /* @__PURE__ */ StyledDiv(() => {
const theme = useCustomTheme();
return {
@@ -7,7 +7,7 @@ import {
ScreenBottomContainer,
Line,
} from "../../components/basic";
import { Button } from "../../components/buttons";
import { Button, IconButton } from "../../components/buttons";
import { ModalTitle } from "../../components/modalElements";
import { iconSize, radius, spacing } from "../../design-system";
import {
@@ -26,6 +26,7 @@ import { Spacer } from "../../components/Spacer";
import { TextDivider } from "../../components/TextDivider";
import { TOS } from "./Modal/TOS";
import { useTWLocale } from "../../evm/providers/locale-provider";
import { ChevronLeftIcon } from "@radix-ui/react-icons";
import { StyledButton, StyledUl } from "../../design-system/elements";
import { useCustomTheme } from "../../design-system/CustomThemeProvider";
@@ -343,28 +344,62 @@ export const WalletSelector: React.FC<{
return (
<Container scrollY flex="column" animate="fadein" fullHeight>
{/* Header */}
<Container p="lg">
{isWalletGroupExpanded ? (
<ModalHeader
title={twTitle}
onBack={() => {
setIsWalletGroupExpanded(false);
}}
/>
) : (
twTitle
)}
</Container>
{!modalConfig.isEmbed && (
<Container p="lg">
{isWalletGroupExpanded ? (
<ModalHeader
title={twTitle}
onBack={() => {
setIsWalletGroupExpanded(false);
}}
/>
) : (
twTitle
)}
</Container>
)}
{/* Body */}
<Container
expand
scrollY
px="md"
style={{
paddingTop: "2px",
}}
style={
modalConfig.isEmbed
? {
paddingTop: spacing.lg,
}
: {
paddingTop: "2px",
}
}
>
{modalConfig.isEmbed && isWalletGroupExpanded && (
<Container
flex="row"
center="y"
style={{
padding: spacing.sm,
paddingTop: 0,
}}
>
<IconButton
onClick={() => {
setIsWalletGroupExpanded(false);
}}
style={{
gap: spacing.xxs,
transform: `translateX(-${spacing.xs})`,
paddingBlock: spacing.xxs,
paddingRight: spacing.xs,
}}
>
<ChevronLeftIcon width={iconSize.sm} height={iconSize.sm} />
{locale.goBackButton}
</IconButton>
</Container>
)}
{topSection}
</Container>
@@ -0,0 +1,148 @@
import { ConnectUIProps, useConnect } from "@thirdweb-dev/react-core";
import { ConnectingScreen } from "../../ConnectWallet/screens/ConnectingScreen";
import { isMobile } from "../../../evm/utils/isMobile";
import { useCallback, useEffect, useRef, useState } from "react";
import { GetStartedScreen } from "../../ConnectWallet/screens/GetStartedScreen";
import type { Coin98Wallet } from "@thirdweb-dev/wallets";
import { wait } from "../../../utils/wait";
import { useTWLocale } from "../../../evm/providers/locale-provider";
import { Coin98Scan } from "./Coin98Scan";
import { WCOpenURI } from "../../ConnectWallet/screens/WCOpenUri";
import { coin98WalletUris } from "./coin98WalletUris";
export const Coin98ConnectUI = (props: ConnectUIProps<Coin98Wallet>) => {
const [screen, setScreen] = useState<
"connecting" | "scanning" | "get-started" | "open-wc-uri"
>("connecting");
const locale = useTWLocale().wallets.coin98Wallet;
const connectingLocale = {
getStartedLink: locale.getStartedLink,
instruction: locale.connectionScreen.instruction,
tryAgain: locale.connectionScreen.retry,
inProgress: locale.connectionScreen.inProgress,
failed: locale.connectionScreen.failed,
};
const { walletConfig, connected } = props;
const connect = useConnect();
const [errorConnecting, setErrorConnecting] = useState(false);
const hideBackButton = props.supportedWallets.length === 1;
const connectToExtension = useCallback(async () => {
try {
connectPrompted.current = true;
setErrorConnecting(false);
setScreen("connecting");
await wait(1000);
await connect(walletConfig);
connected();
} catch (e) {
setErrorConnecting(true);
console.error(e);
}
}, [connected, connect, walletConfig]);
const connectPrompted = useRef(false);
useEffect(() => {
if (connectPrompted.current) {
return;
}
const isInstalled = walletConfig.isInstalled
? walletConfig.isInstalled()
: false;
// if loading
(async () => {
if (isInstalled) {
connectToExtension();
}
// if wallet is not injected
else {
// on mobile, open the Coin98 Mobile via wallet connect
if (isMobile()) {
setScreen("open-wc-uri");
} else {
// on desktop, show the Coin98 app scan qr code
setScreen("scanning");
}
}
})();
}, [connectToExtension, walletConfig]);
if (screen === "connecting") {
return (
<ConnectingScreen
locale={{
getStartedLink: locale.getStartedLink,
instruction: locale.connectionScreen.instruction,
tryAgain: locale.connectionScreen.retry,
inProgress: locale.connectionScreen.inProgress,
failed: locale.connectionScreen.failed,
}}
errorConnecting={errorConnecting}
onGetStarted={() => {
setScreen("get-started");
}}
onRetry={connectToExtension}
hideBackButton={hideBackButton}
onBack={props.goBack}
walletName={walletConfig.meta.name}
walletIconURL={walletConfig.meta.iconURL}
/>
);
}
if (screen === "open-wc-uri") {
return (
<WCOpenURI
locale={connectingLocale}
onRetry={() => {
// NOOP - TODO make onRetry optional
}}
errorConnecting={errorConnecting}
onGetStarted={() => {
setScreen("get-started");
}}
hideBackButton={hideBackButton}
onBack={props.goBack}
onConnected={connected}
walletConfig={walletConfig}
appUriPrefix={coin98WalletUris}
/>
);
}
if (screen === "get-started") {
return (
<GetStartedScreen
locale={{
scanToDownload: locale.getStartedScreen.instruction,
}}
walletIconURL={walletConfig.meta.iconURL}
walletName={walletConfig.meta.name}
chromeExtensionLink={walletConfig.meta.urls?.chrome}
googlePlayStoreLink={walletConfig.meta.urls?.android}
appleStoreLink={walletConfig.meta.urls?.ios}
onBack={props.goBack}
/>
);
}
if (screen === "scanning") {
return (
<Coin98Scan
onBack={props.goBack}
onConnected={props.connected}
onGetStarted={() => {
setScreen("get-started");
}}
hideBackButton={hideBackButton}
walletConfig={walletConfig}
/>
);
}
return null;
};
@@ -0,0 +1,65 @@
import { ScanScreen } from "../../ConnectWallet/screens/ScanScreen";
import {
useCreateWalletInstance,
useWalletContext,
} from "@thirdweb-dev/react-core";
import { useEffect, useRef, useState } from "react";
import type { Coin98Wallet } from "@thirdweb-dev/wallets";
import type { WalletConfig } from "@thirdweb-dev/react-core";
import { useTWLocale } from "../../../evm/providers/locale-provider";
export const Coin98Scan: React.FC<{
onBack: () => void;
onGetStarted: () => void;
onConnected: () => void;
walletConfig: WalletConfig<Coin98Wallet>;
hideBackButton: boolean;
}> = ({ onBack, onConnected, onGetStarted, walletConfig, hideBackButton }) => {
const locale = useTWLocale().wallets.coin98Wallet;
const createInstance = useCreateWalletInstance();
const [qrCodeUri, setQrCodeUri] = useState<string | undefined>();
const { setConnectedWallet, chainToConnect, setConnectionStatus } =
useWalletContext();
const scanStarted = useRef(false);
useEffect(() => {
if (scanStarted.current) {
return;
}
scanStarted.current = true;
const wallet = createInstance(walletConfig);
setConnectionStatus("connecting");
wallet.connectWithQrCode({
chainId: chainToConnect?.chainId,
onQrCodeUri(uri) {
setQrCodeUri(uri);
},
onConnected() {
setConnectedWallet(wallet);
onConnected();
},
});
}, [
createInstance,
setConnectedWallet,
chainToConnect,
onConnected,
walletConfig,
setConnectionStatus,
]);
return (
<ScanScreen
qrScanInstruction={locale.scanScreen.instruction}
onBack={onBack}
onGetStarted={onGetStarted}
qrCodeUri={qrCodeUri}
walletName={walletConfig.meta.name}
walletIconURL={walletConfig.meta.iconURL}
hideBackButton={hideBackButton}
getStartedLink={locale.getStartedLink}
/>
);
};
@@ -0,0 +1,52 @@
import type { WalletOptions, WalletConfig } from "@thirdweb-dev/react-core";
import { Coin98Wallet, getInjectedCoin98Provider } from "@thirdweb-dev/wallets";
import { Coin98ConnectUI } from "./Coin98ConnectUI";
type Coin98WalletOptions = {
/**
* When connecting Coin98 using the QR Code - Wallet Connect connector is used which requires a project id.
* This project id is Your project’s unique identifier for wallet connect that can be obtained at cloud.walletconnect.com.
*
* https://docs.walletconnect.com/2.0/web3modal/options#projectid-required
*/
projectId?: string;
/**
* If true, the wallet will be tagged as "reccomended" in ConnectWallet Modal
*/
recommended?: boolean;
};
export const coin98Wallet = (
options?: Coin98WalletOptions,
): WalletConfig<Coin98Wallet> => {
return {
id: Coin98Wallet.id,
recommended: options?.recommended,
meta: {
name: "Coin98 Wallet",
urls: {
chrome:
"https://chrome.google.com/webstore/detail/coin98-wallet/aeachknmefphepccionboohckonoeemg",
android:
"https://play.google.com/store/apps/details?id=coin98.crypto.finance.media",
ios: "https://apps.apple.com/us/app/coin98-super-wallet/id1561969966",
},
iconURL:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAABGCAMAAABG8BK2AAACf1BMVEUAAAD//wD/gAD/qlW/v0DMzDPVqivbtiTfv0DjqjnmszPRuS7VqivYsTvbtjfduzPfrzDStC3VuDnXrjbesTfVtTXWuDPYsTHZsy/bti7csDXdszPWtTHYsi7atDXcsTLWszDXtS/YtzTZszPatDLbsi/XtTPZszHatTDbtjTbszPXtDLYtTHZsjDZszTatTPbtjLbszHXtDDYtjTZszPZtDLatTHbsjHXszTYtDPYtjLatDHatTTbszPXtDLYtTLZszTatDPatTLbszLYtDHZszPZszLatDLatTHYtDPZszLZtDHatTHYszLZtTHatTLaszLYtDHYtDHZtTPatTLYtDPZtDPZtTLZszLatTPYszPYtDLZszHatDPatTLYtDHZtTPZszPZtDLZtDPZtTLZszLZtDHatDHYszPYtDLZtTHZszHatDPatTLYszLZtDHZtTPZtDLatDLatTLYszHZtDPZtDLZszLZtDLYtTPYtDLZtDLZtDLZszHZtDPatDLZtDLZtDHZtTPZtDLatDLYszHZtDPZtDLZtDLatDLatDPYszLZtDLZtDLZtDPatDLYtTLZszLZtDLZtDHZszLZtDHZtDLZtDLYtTLZtDLZtDLZtDLZtDHZtDLYtDLZszLZtDLZtDHZtTLatDLZszLZtDPZtDLZtTLZtDLatDLZtDPZtDLZtDLZtDLZszLZtDPatDLZtDLZtDLZtDLZtDPZtDLZtDLYtDLZtTLZtDLZtDLZtDLZtDLZtDHZszLZtDLZtDLZtDLZtDHatDLZtDLZtDLZtDLZtDLZtDLYtDLZtDLZtDLZtDLZtDLZtDLZtDLZtDLZtDLZtDLZtDL///+sdBnoAAAA03RSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExcYGRobHB0eHyEiJCUmJygpKy0vMDEyMzQ1Njc4OTo7PD0+P0BBQkRFRkdISktMTU5QUVJTVVdYWVtdYGFiY2RnaWprbG5vcHJ0dXd4eXp+f4CBgoOEhoeIiYqMjY6PkJGSk5SVl5iZmpucnZ+goaOkpaanqaqrrK2usLGys7S1trq7vMDBwsPExcbHyMnKzM7P0NHS09TV1tfY2drb3N3e3+Dh4uTl5ufo6err7O3u7/Hy8/T19vf4+fr7/P3++uqKzgAAAAFiS0dE1Am7C4UAAAPWSURBVBgZrcGLf81lAMfxzzk2NrJJcoujG10QliiXhCRU0kWlUi4r1ySXmS5KF4nWbFKMUlic2pRmk5nNUouzNOf7D/X8dvbbeZ6zvV5edn7vN23C49YcrI7pOsROH1w9Noxj8k/qkoo5Idr12asuK82lTaRSaagYSqucqNJS2QfPXqWpGGOW0jYdwj8rbSfC5CkAY1irAKzikAJQRrUCUEVMAYihQKBAoECgQKBO/LVnc/7GXfVK0VS8MX/zvmZ1AnVwZGYPPN0mlMpy+qksPDmL6tUBShF7NkS7yXXyfZKFr+8+pUKuC+OwDT6uhMIQSRlFSoEcVx7ENficPN9lYutdKRdyvI5n+p6LaiyaiuchefIwbtpQq/OFN2PMlAvZzmQBWTuU8FF3jK8kHcYYVCXP2WEYUTmQ7VWMz+R7H2OcpOUYXyvhcAhYKQeyDQMeUdJ4IFwrzQFulW8UMFsOZDmDUayk7Rg7pCnAbPkWAGPlQJbvMRqV9BvGOmkWME2+ecAkOZClFAhdVVIjxlLpBeDGy0r4byCwQA5k+QHjvJIqMDZJhdB/ZZMSYqv6QaEcyFKL8YWStmKUSHU9FzdLV09+u6usWtLlxT0b5UC24cAEtWsZAWQ3SaqRKp/rj+e2JfVSjVzItgyjQL58jMfkubwwA1/O+rhSIFtDLpCxIS5PS34ICB+TcXYUtidiciHHejyjP6hojBYOx/O8jKYRuJ6UCzni83DlNcuYi+f+7b82HFs7BM9mOZArNhfbxAsyyjAy31Wr2EKMvhdlQynib+fg6/HaFXmmYnwo34sYK2RDHdQvvR3PLS9Vq9WfmcDDatc8FLhHNtSZMwd37z8l326MEiWtA0L1sqBrewejSUlHMY7Igq5tCZAty1mMUlnQta3B+EdJxzEOyYIcj0cikaflWxaJRO6TPsbYq6QtGNWyIMcsYHBcbR4A7paqQ8A0tWsZAQyRDTlWYBQp4WgYeEbSSIxt8r2BsUg25DiB0e+kPHV3YJRI2omRWRCXp2V5COj+u2zINQOjz1s1Or91AMZdVyXFx+MZ/V70XPmmO/G8IgdyVdyALWO/PH8MwJXXLAdKUZRBUqhACeUDsY2skwul2tcXX/an8tVOol1o/iWlQB00vJyLJ3t+jSwlE7vh6THzR3WAOvHvNwX5m0ovKUXDlxvzt5T8rU6gQKBAoECgQBBTAGKcVgBOcUgBOMBqBeBNxioA9xKOKm1HQ/Co0jYFo0Rp2omnd1RpKe9Fq6G/KA3lg2iTW6wu+7wXSZPL1SXRGTjCY1aVVcV0HWJVB1aMDJHwP77sAsSIUUvgAAAAAElFTkSuQmCC",
},
create: (walletOptions: WalletOptions) => {
const wallet = new Coin98Wallet({
...walletOptions,
projectId: options?.projectId,
qrcode: false,
});
return wallet;
},
connectUI: Coin98ConnectUI,
isInstalled() {
return !!getInjectedCoin98Provider();
},
};
};
@@ -0,0 +1,5 @@
export const coin98WalletUris = {
ios: "coin98://",
android: "coin98://",
other: "coin98://",
};
@@ -11,13 +11,10 @@ import {
ExclamationTriangleIcon,
} from "@radix-ui/react-icons";
import {
useChain,
useChainId,
useConnect,
useConnectionStatus,
useSupportedChains,
useSwitchChain,
useWallet,
useWalletContext,
} from "@thirdweb-dev/react-core";
import { SafeSupportedChainsSet } from "@thirdweb-dev/wallets";
import { utils } from "ethers";
@@ -38,22 +35,24 @@ export const SelectAccount: React.FC<{
renderBackButton?: boolean;
}> = (props) => {
const locale = useTWLocale().wallets.safeWallet.accountDetailsScreen;
const activeWallet = useWallet();
const connect = useConnect();
const activeChain = useChain();
const connectedChainId = useChainId();
const [safeAddress, setSafeAddress] = useState("");
const [safeChainId, setSafeChainId] = useState(-1);
const [safeConnectError, setSafeConnectError] = useState(false);
// personal wallet
const { personalWalletInfo } = useWalletContext();
const personalWallet = personalWalletInfo?.wallet;
const personalWalletChainId = personalWalletInfo?.chainId;
const switchChainPersonalWallet = personalWalletInfo?.switchChain;
const [switchError, setSwitchError] = useState(false);
const [switchingNetwork, setSwitchingNetwork] = useState(false);
// safe
const connectSafe = useConnect();
const [safeAddress, setSafeAddress] = useState("");
const [safeChainId, setSafeChainId] = useState(-1);
const [safeConnectError, setSafeConnectError] = useState(false);
const connectionStatus = useConnectionStatus();
const chains = useSupportedChains();
// put supported chains first
const supportedChains = chains.filter((c) =>
SafeSupportedChainsSet.has(c.chainId),
);
@@ -69,15 +68,14 @@ export const SelectAccount: React.FC<{
const useOptGroup = mainnets.length > 0 && testnets.length > 0;
const handleSubmit = async () => {
if (!selectedSafeChain || !activeWallet || !activeChain) {
if (!selectedSafeChain || !personalWallet) {
return;
}
setSafeConnectError(false);
try {
await connect(props.safeWalletConfig, {
await connectSafe(props.safeWalletConfig, {
chain: selectedSafeChain,
personalWallet: activeWallet,
personalWallet: personalWallet,
safeAddress,
});
props.onConnect();
@@ -87,12 +85,11 @@ export const SelectAccount: React.FC<{
}
};
const mismatch = safeChainId !== -1 && connectedChainId !== safeChainId;
const mismatch = safeChainId !== -1 && personalWalletChainId !== safeChainId;
const isValidAddress = utils.isAddress(safeAddress);
const disableNetworkSelection = supportedChains.length === 1;
const switchChain = useSwitchChain();
const modalConfig = useContext(ModalConfigCtx);
return (
@@ -326,14 +323,14 @@ export const SelectAccount: React.FC<{
width: modalConfig.modalSize === "compact" ? "100%" : undefined,
}}
onClick={async () => {
if (!activeWallet) {
throw new Error("No active wallet");
if (!personalWallet) {
throw new Error("No personal wallet connected");
}
setSafeConnectError(false);
setSwitchError(false);
setSwitchingNetwork(true);
try {
await switchChain(safeChainId);
await switchChainPersonalWallet(safeChainId);
} catch (e) {
setSwitchError(true);
} finally {
@@ -3,11 +3,10 @@ import {
WalletConfig,
ConnectUIProps,
WalletOptions,
useDisconnect,
useWallet,
useWalletContext,
} from "@thirdweb-dev/react-core";
import { defaultWallets } from "../defaultWallets";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { SelectpersonalWallet } from "./SelectPersonalWallet";
import type { SafeWalletConfigOptions, SafeWalletConfig } from "./types";
import { SelectAccount } from "./SelectAccount";
@@ -44,19 +43,57 @@ export const safeWallet = (
export const SafeConnectUI = (
props: ConnectUIProps<SafeWallet> & { personalWallets: WalletConfig[] },
) => {
const activeWallet = useWallet();
// selected personal wallet config
const [personalWalletConfig, setPersonalWalletConfig] = useState<
WalletConfig | undefined
>();
const disconnect = useDisconnect();
if (personalWalletConfig) {
const [isPersonalWalletConnected, setIsPersonalWalletConnected] =
useState(false);
const { setIsConnectingToPersonalWallet, personalWalletInfo } =
useWalletContext();
const goBackToMain = () => {
personalWalletInfo.disconnect();
setIsConnectingToPersonalWallet(false);
props.goBack();
};
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) {
return;
}
mounted.current = true;
setIsConnectingToPersonalWallet(true);
}, [setIsConnectingToPersonalWallet]);
// if personal wallet is not selected
if (!personalWalletConfig) {
return (
<SelectpersonalWallet
personalWallets={props.personalWallets}
onBack={goBackToMain}
safeWallet={props.walletConfig}
selectWallet={(walletConfig) => {
setPersonalWalletConfig(walletConfig);
}}
renderBackButton={props.supportedWallets.length > 1}
/>
);
}
// if the personal wallet is selected, but not connected
if (!isPersonalWalletConnected) {
const _props: ConnectUIProps = {
goBack: () => {
setIsPersonalWalletConnected(false);
setPersonalWalletConfig(undefined);
},
connected() {
setPersonalWalletConfig(undefined);
setIsPersonalWalletConnected(true);
setIsConnectingToPersonalWallet(false);
},
isOpen: props.isOpen,
hide: props.hide,
@@ -76,25 +113,11 @@ export const SafeConnectUI = (
return <HeadlessConnectUI {..._props} />;
}
if (!activeWallet) {
return (
<SelectpersonalWallet
personalWallets={props.personalWallets}
onBack={props.goBack}
safeWallet={props.walletConfig}
selectWallet={setPersonalWalletConfig}
renderBackButton={props.supportedWallets.length > 1}
/>
);
}
// if personal wallet is selected and connected
return (
<SelectAccount
renderBackButton={props.supportedWallets.length > 1}
onBack={() => {
disconnect();
props.goBack();
}}
onBack={goBackToMain}
onConnect={props.connected}
safeWalletConfig={props.walletConfig}
/>
@@ -4,13 +4,9 @@ import { Button } from "../../../components/buttons";
import { iconSize, spacing, fontSize } from "../../../design-system";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import {
useChain,
useConnect,
useConnectionStatus,
useNetworkMismatch,
useWalletContext,
useWallet,
useSwitchChain,
WalletConfig,
} from "@thirdweb-dev/react-core";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
@@ -24,31 +20,32 @@ export const SmartWalletConnecting: React.FC<{
onBack: () => void;
onConnect: () => void;
smartWallet: SmartWalletConfig;
personalWallet: WalletConfig;
personalWalletConfig: WalletConfig;
}> = (props) => {
const locale = useTWLocale().wallets.smartWallet;
const activeWallet = useWallet(); // personal wallet
const connect = useConnect();
const connectedChain = useChain();
const targetChain = useWalletContext().activeChain;
const mismatch = useNetworkMismatch();
const [connectError, setConnectError] = useState(false);
// personal wallet
const { personalWalletInfo } = useWalletContext();
const personalWallet = personalWalletInfo?.wallet;
const personalWalletChainId = personalWalletInfo?.chainId;
const [switchError, setSwitchError] = useState(false);
const switchPersonalWalletChain = personalWalletInfo.switchChain;
const [switchingNetwork, setSwitchingNetwork] = useState(false);
// smart wallet
const connect = useConnect();
const targetChain = useWalletContext().activeChain;
const mismatch = targetChain.chainId !== personalWalletChainId;
const [connectError, setConnectError] = useState(false);
const connectionStatus = useConnectionStatus();
const { onConnect } = props;
const connectStarted = useRef(false);
const switchChain = useSwitchChain();
const modalSize = useContext(ModalConfigCtx).modalSize;
const handleConnect = useCallback(async () => {
if (!activeWallet || !connectedChain || connectStarted.current) {
if (!personalWallet || !personalWalletChainId || connectStarted.current) {
return;
}
setConnectError(false);
@@ -56,20 +53,26 @@ export const SmartWalletConnecting: React.FC<{
try {
connectStarted.current = true;
await connect(props.smartWallet, {
personalWallet: activeWallet,
personalWallet: personalWallet,
});
onConnect();
} catch (e) {
console.error(e);
setConnectError(true);
}
}, [activeWallet, connectedChain, connect, props.smartWallet, onConnect]);
}, [
personalWallet,
personalWalletChainId,
connect,
props.smartWallet,
onConnect,
]);
useEffect(() => {
if (!mismatch) {
handleConnect();
}
}, [mismatch, handleConnect, activeWallet, connectedChain]);
}, [mismatch, handleConnect, personalWallet, personalWalletChainId]);
if (!connectError && (connectionStatus === "connecting" || !mismatch)) {
return (
@@ -111,8 +114,8 @@ export const SmartWalletConnecting: React.FC<{
<Container fullHeight animate="fadein" flex="column">
<Container p="lg">
<ModalHeader
title={props.personalWallet.meta.name}
imgSrc={props.personalWallet.meta.iconURL}
title={props.personalWalletConfig.meta.name}
imgSrc={props.personalWalletConfig.meta.iconURL}
onBack={props.onBack}
/>
</Container>
@@ -150,14 +153,14 @@ export const SmartWalletConnecting: React.FC<{
gap: spacing.sm,
}}
onClick={async () => {
if (!activeWallet) {
if (!personalWallet) {
throw new Error("No active wallet");
}
setConnectError(false);
setSwitchError(false);
setSwitchingNetwork(true);
try {
await switchChain(targetChain.chainId);
await switchPersonalWalletChain(targetChain.chainId);
} catch (e) {
setSwitchError(true);
} finally {
@@ -2,12 +2,15 @@ import {
WalletConfig,
ConnectUIProps,
WalletOptions,
useWallet,
useWalletContext,
} from "@thirdweb-dev/react-core";
import { SmartWallet } from "@thirdweb-dev/wallets";
import { SmartWalletConfig, SmartWalletConfigOptions } from "./types";
import { SmartWalletConnecting } from "./SmartWalletConnecting";
import { HeadlessConnectUI } from "../headlessConnectUI";
import { useRef, useEffect, useState } from "react";
import { Container } from "../../../components/basic";
import { Spinner } from "../../../components/Spinner";
export const smartWallet = (
wallet: WalletConfig<any>,
@@ -34,17 +37,53 @@ export const smartWallet = (
export const SmartConnectUI = (
props: ConnectUIProps<SmartWallet> & { personalWallet: WalletConfig },
) => {
const activeWallet = useWallet();
const {
setIsConnectingToPersonalWallet,
personalWalletInfo,
isConnectingToPersonalWallet,
} = useWalletContext();
const { walletConfig } = props;
const [isPersonalWalletConnected, setIsPersonalWalletConnected] =
useState(false);
// hide the connection of personal wallet
const mounted = useRef(false);
useEffect(() => {
if (!mounted.current) {
setIsConnectingToPersonalWallet(true);
mounted.current = true;
}
}, [setIsConnectingToPersonalWallet]);
const PersonalWalletConfig = props.personalWallet;
if (!activeWallet) {
if (!isPersonalWalletConnected) {
// if the isConnectingToPersonalWallet is not yet updated
if (!isConnectingToPersonalWallet) {
return (
<Container
flex="row"
center="both"
style={{
minHeight: "250px",
}}
>
<Spinner size="xl" color="accentText" />
</Container>
);
}
const _props: ConnectUIProps = {
...props,
walletConfig: PersonalWalletConfig,
connected: () => {
// override to no-op
setIsPersonalWalletConnected(true);
setIsConnectingToPersonalWallet(false);
},
goBack() {
personalWalletInfo.disconnect();
setIsConnectingToPersonalWallet(false);
props.goBack();
},
};
@@ -57,10 +96,14 @@ export const SmartConnectUI = (
return (
<SmartWalletConnecting
onBack={props.goBack}
onBack={() => {
personalWalletInfo.disconnect();
setIsConnectingToPersonalWallet(false);
props.goBack();
}}
onConnect={props.connected}
smartWallet={walletConfig}
personalWallet={props.personalWallet}
personalWalletConfig={props.personalWallet}
/>
);
};
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/sdk
## 4.0.16
### Patch Changes
- [#1974](https://github.com/thirdweb-dev/js/pull/1974) [`7c45941b`](https://github.com/thirdweb-dev/js/commit/7c45941b57fb5a6f83be507ee64dbf885d93c9d4) Thanks [@adam-maj](https://github.com/adam-maj)! - Add support for new relayer and polygon relayer
## 4.0.15
### Patch Changes
- [#1935](https://github.com/thirdweb-dev/js/pull/1935) [`e43beec3`](https://github.com/thirdweb-dev/js/commit/e43beec36a569211b6fe708107aa7dc04bff08ec) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Resolve diamond pattern facets ABIs
## 4.0.14
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "4.0.14",
"version": "4.0.16",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/sdk",
"license": "Apache-2.0",
@@ -7,7 +7,7 @@ import { Abi, AbiSchema } from "../../schema/contracts/custom";
import { SDKOptions } from "../../schema/sdk-options";
import { isExtensionEnabled } from "../feature-detection/isFeatureEnabled";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { providers } from "ethers";
import { Contract, providers } from "ethers";
import { joinABIs } from "./joinABIs";
import { getPluginABI } from "./getPluginABI";
@@ -24,7 +24,7 @@ export async function getCompositeABI(
let pluginABIs: Abi[] = [];
try {
// check if contract is plugin-pattern
// check if contract is plugin-pattern / dynamic
const isPluginRouter: boolean = isExtensionEnabled(
AbiSchema.parse(abi),
"PluginRouter",
@@ -33,6 +33,13 @@ export async function getCompositeABI(
AbiSchema.parse(abi),
"DynamicContract",
);
// check if the contract has fallback function - we'll further check for diamond pattern if needed
const isFallback: boolean = isExtensionEnabled(
AbiSchema.parse(abi),
"Fallback",
);
if (isbaseRouter) {
const contract = new ContractWrapper(
provider,
@@ -68,6 +75,21 @@ export async function getCompositeABI(
// get ABIs of extension contracts
pluginABIs = await getPluginABI(plugins as any[], provider, storage);
} else if (isFallback) {
// check if diamond pattern
const dimaondAbi = [
"function facets() external view returns (tuple(address,bytes4[])[])",
];
const contract = new Contract(address, dimaondAbi, provider);
// get facets
const facets = await contract.facets();
// filter facet addresses
const facetAddresses = facets.map((item: any) => item[0]);
// get ABI of facets
pluginABIs = await getPluginABI(facetAddresses, provider, storage);
}
} catch (err) {}
@@ -74,6 +74,7 @@ import {
FEATURE_ACCOUNT_FACTORY,
FEATURE_ACCOUNT_PERMISSIONS,
FEATURE_ACCOUNT_PERMISSIONS_V1,
FEATURE_FALLBACK,
} from "./thirdweb-features";
/**
@@ -148,7 +149,8 @@ export type Feature =
| typeof FEATURE_ACCOUNT
| typeof FEATURE_ACCOUNT_PERMISSIONS
| typeof FEATURE_ACCOUNT_PERMISSIONS_V1
| typeof FEATURE_ACCOUNT_FACTORY;
| typeof FEATURE_ACCOUNT_FACTORY
| typeof FEATURE_FALLBACK;
export type Extension = Feature;
@@ -197,4 +199,5 @@ export const SUPPORTED_FEATURES: Record<string, Feature> = /* @__PURE__ */ {
[FEATURE_ACCOUNT.name]: FEATURE_ACCOUNT,
[FEATURE_NFT_LOYALTY_CARD.name]: FEATURE_NFT_LOYALTY_CARD,
[FEATURE_NFT_UPDATABLE_METADATA.name]: FEATURE_NFT_UPDATABLE_METADATA,
[FEATURE_FALLBACK.name]: FEATURE_FALLBACK,
};
@@ -19,6 +19,13 @@ import IAccountPermissions from "@thirdweb-dev/contracts-js/dist/abis/IAccountPe
import IAccountPermissionsV1 from "@thirdweb-dev/contracts-js/dist/abis/IAccountPermissions_V1.json";
import IAccount from "@thirdweb-dev/contracts-js/dist/abis/IAccount.json";
export const fallbackAbi = [
{
stateMutability: "payable",
type: "fallback",
},
];
export const getAllPluginsAbi = [
{
inputs: [],
@@ -370,3 +377,14 @@ export const FEATURE_AIRDROP_ERC1155 = {
abis: [IAirdropERC1155],
features: {},
} as const;
export const FEATURE_FALLBACK = {
name: "Fallback",
namespace: "fallback",
docLinks: {
sdk: "",
contracts: "",
},
abis: [fallbackAbi],
features: {},
} as const;
@@ -333,7 +333,8 @@ export class ContractWrapper<
if (
this.options?.gasless &&
("openzeppelin" in this.options.gasless ||
"biconomy" in this.options.gasless)
"biconomy" in this.options.gasless ||
"engine" in this.options.gasless)
) {
if (fn === "multicall" && Array.isArray(args[0]) && args[0].length > 0) {
const from = await this.getSignerAddress();
@@ -632,8 +633,211 @@ export class ContractWrapper<
): Promise<string> {
if (this.options.gasless && "biconomy" in this.options.gasless) {
return this.biconomySendFunction(transaction);
} else if (this.options.gasless && "openzeppelin" in this.options.gasless) {
return this.defenderSendFunction(transaction);
}
return this.engineSendFunction(transaction);
}
private async engineSendFunction(
transaction: GaslessTransaction,
): Promise<string> {
invariant(
this.options.gasless && "engine" in this.options.gasless,
"calling engine gasless transaction without engine config in the SDK options",
);
const request = await this.enginePrepareRequest(transaction);
const res = await fetch(this.options.gasless.engine.relayerUrl, {
...request,
headers: {
"Content-Type": "application/json",
},
});
const data = await res.json();
if (data.error) {
throw new Error(data.error?.message || JSON.stringify(data.error));
}
const queueId = data.result.queueId as string;
const engineUrl =
this.options.gasless.engine.relayerUrl.split("/relayer/")[0];
const startTime = Date.now();
while (true) {
const txRes = await fetch(`${engineUrl}/transaction/status/${queueId}`);
const txData = await txRes.json();
if (txData.result.transactionHash) {
return txData.result.transactionHash as string;
}
// Time out after 30s
if (Date.now() - startTime > 30 * 1000) {
throw new Error("timeout");
}
// Poll to check if the transaction was mined
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
private async enginePrepareRequest(transaction: GaslessTransaction) {
const signer = this.getSigner();
const provider = this.getProvider();
const storage = this.storage;
invariant(signer, "signer is not set");
try {
const { abi } = await fetchContractMetadataFromAddress(
transaction.to,
provider,
storage,
);
const chainId = (await provider.getNetwork()).chainId;
const contract = new ContractWrapper(
signer,
transaction.to,
abi,
{},
storage,
);
if (abi.find((item) => item.name === "executeMetaTransaction")) {
const name: string = await contract.call("name", []);
const domain = {
name,
version: "1",
salt: "0x" + chainId.toString(16).padStart(64, "0"), // Use 64 length hex chain id as salt
verifyingContract: transaction.to,
};
const types = {
MetaTransaction: [
{ name: "nonce", type: "uint256" },
{ name: "from", type: "address" },
{ name: "functionSignature", type: "bytes" },
],
};
const nonce = await contract.call("getNonce", [transaction.from]);
const message = {
nonce: nonce,
from: transaction.from,
functionSignature: transaction.data,
};
const { signature } = await signTypedDataInternal(
signer,
domain,
types,
message,
);
return {
method: "POST",
body: JSON.stringify({
type: "execute-meta-transaction",
request: {
from: transaction.from,
to: transaction.to,
data: transaction.data,
},
signature,
}),
};
}
} catch {
// no-op
}
if (
transaction.functionName === "approve" &&
transaction.functionArgs.length === 2
) {
const spender = transaction.functionArgs[0];
const amount = transaction.functionArgs[1];
// TODO: support DAI permit by signDAIPermit
const { message: permit, signature: sig } = await signEIP2612Permit(
signer,
transaction.to,
transaction.from,
spender,
amount,
);
const message = {
to: transaction.to,
owner: permit.owner,
spender: permit.spender,
value: BigNumber.from(permit.value).toString(),
nonce: BigNumber.from(permit.nonce).toString(),
deadline: BigNumber.from(permit.deadline).toString(),
};
return {
method: "POST",
body: JSON.stringify({
type: "permit",
request: message,
signature: sig,
}),
};
} else {
const forwarderAddress =
CONTRACT_ADDRESSES[
transaction.chainId as keyof typeof CONTRACT_ADDRESSES
].openzeppelinForwarder ||
(await computeForwarderAddress(provider, storage));
const ForwarderABI = (
await import("@thirdweb-dev/contracts-js/dist/abis/Forwarder.json")
).default;
const forwarder = new Contract(forwarderAddress, ForwarderABI, provider);
const nonce = await getAndIncrementNonce(forwarder, "getNonce", [
transaction.from,
]);
const domain = {
name: "GSNv2 Forwarder",
version: "0.0.1",
chainId: transaction.chainId,
verifyingContract: forwarderAddress,
};
const types = {
ForwardRequest,
};
const message = {
from: transaction.from,
to: transaction.to,
value: BigNumber.from(0).toString(),
gas: BigNumber.from(transaction.gasLimit).toString(),
nonce: BigNumber.from(nonce).toString(),
data: transaction.data,
};
const { signature: sig } = await signTypedDataInternal(
signer,
domain,
types,
message,
);
const signature: BytesLike = sig;
return {
method: "POST",
body: JSON.stringify({
type: "forward",
request: message,
signature,
forwarderAddress,
}),
};
}
return this.defenderSendFunction(transaction);
}
private async biconomySendFunction(
@@ -44,6 +44,8 @@ import { signTypedDataInternal } from "../../common/sign";
import { BytesLike } from "ethers";
import { CONTRACT_ADDRESSES } from "../../constants/addresses/CONTRACT_ADDRESSES";
import { getContractAddressByChainId } from "../../constants/addresses/getContractAddressByChainId";
import { getCompositeABI } from "../../common/plugin/getCompositePluginABI";
import { ContractWrapper } from "./contract-wrapper";
abstract class TransactionContext {
protected args: any[];
@@ -1019,29 +1021,76 @@ async function enginePrepareRequest(
provider: providers.Provider,
storage: ThirdwebStorage,
) {
const forwarderAddress =
CONTRACT_ADDRESSES[transaction.chainId as keyof typeof CONTRACT_ADDRESSES]
.openzeppelinForwarder ||
(await computeForwarderAddress(provider, storage));
const ForwarderABI = (
await import("@thirdweb-dev/contracts-js/dist/abis/Forwarder.json")
).default;
try {
const metadata = await fetchContractMetadataFromAddress(
transaction.to,
provider,
storage,
);
const forwarder = new Contract(forwarderAddress, ForwarderABI, provider);
const nonce = await getAndIncrementNonce(forwarder, "getNonce", [
transaction.from,
]);
const chainId = (await provider.getNetwork()).chainId;
const abi = await getCompositeABI(
transaction.to,
metadata.abi,
provider,
{},
storage,
);
const contract = new ContractWrapper(
signer,
transaction.to,
abi,
{},
storage,
);
if (abi.find((item) => item.name === "executeMetaTransaction")) {
const name: string = await contract.call("name", []);
const domain = {
name: "GSNv2 Forwarder",
version: "0.0.1",
chainId: transaction.chainId,
verifyingContract: forwarderAddress,
};
const types = {
ForwardRequest,
};
let message: ForwardRequestMessage | PermitRequestMessage;
const domain = {
name,
version: "1",
salt: "0x" + chainId.toString(16).padStart(64, "0"), // Use 64 length hex chain id as salt
verifyingContract: transaction.to,
};
const types = {
MetaTransaction: [
{ name: "nonce", type: "uint256" },
{ name: "from", type: "address" },
{ name: "functionSignature", type: "bytes" },
],
};
const nonce = await contract.call("getNonce", [transaction.from]);
const message = {
nonce: nonce,
from: transaction.from,
functionSignature: transaction.data,
};
const { signature } = await signTypedDataInternal(
signer,
domain,
types,
message,
);
return {
method: "POST",
body: JSON.stringify({
type: "execute-meta-transaction",
request: {
from: transaction.from,
to: transaction.to,
data: transaction.data,
},
signature,
}),
};
}
} catch {
// no-op
}
if (
transaction.functionName === "approve" &&
@@ -1058,18 +1107,13 @@ async function enginePrepareRequest(
amount,
);
const { r, s, v } = utils.splitSignature(sig);
message = {
const message = {
to: transaction.to,
owner: permit.owner,
spender: permit.spender,
value: BigNumber.from(permit.value).toString(),
nonce: BigNumber.from(permit.nonce).toString(),
deadline: BigNumber.from(permit.deadline).toString(),
r,
s,
v,
};
return {
@@ -1077,10 +1121,34 @@ async function enginePrepareRequest(
body: JSON.stringify({
type: "permit",
request: message,
signature: sig,
}),
};
} else {
message = {
const forwarderAddress =
CONTRACT_ADDRESSES[transaction.chainId as keyof typeof CONTRACT_ADDRESSES]
.openzeppelinForwarder ||
(await computeForwarderAddress(provider, storage));
const ForwarderABI = (
await import("@thirdweb-dev/contracts-js/dist/abis/Forwarder.json")
).default;
const forwarder = new Contract(forwarderAddress, ForwarderABI, provider);
const nonce = await getAndIncrementNonce(forwarder, "getNonce", [
transaction.from,
]);
const domain = {
name: "GSNv2 Forwarder",
version: "0.0.1",
chainId: transaction.chainId,
verifyingContract: forwarderAddress,
};
const types = {
ForwardRequest,
};
const message = {
from: transaction.from,
to: transaction.to,
value: BigNumber.from(0).toString(),
+1
View File
@@ -173,6 +173,7 @@ export { fetchSnapshotEntryForAddress } from "./common/claim-conditions/fetchSna
export { getCachedAbiForContract } from "./common/abi";
export * from "./common/ens/resolveEns";
export * from "./common/ens/resolveAddress";
export * from "./common/sign";
//#endregion @r/packages/sdk/src/evm/common/*
//#region @r/packages/sdk/src/evm/constants/*
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/service-utils
## 0.4.10
### Patch Changes
- [#1988](https://github.com/thirdweb-dev/js/pull/1988) [`17fe41d4`](https://github.com/thirdweb-dev/js/commit/17fe41d42bf99ccc83669557dc48d6fd22a5a04e) Thanks [@assimovt](https://github.com/assimovt)! - fix api key caching ttl
## 0.4.9
### Patch Changes
- [#1976](https://github.com/thirdweb-dev/js/pull/1976) [`8ff87be6`](https://github.com/thirdweb-dev/js/commit/8ff87be631f1754b15e7ec4556d51e8eec5cbd18) Thanks [@arcoraven](https://github.com/arcoraven)! - Handle if error parsing JSON response in authorization helper
## 0.4.8
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/service-utils",
"version": "0.4.8",
"version": "0.4.10",
"main": "dist/thirdweb-dev-service-utils.cjs.js",
"module": "dist/thirdweb-dev-service-utils.esm.js",
"exports": {
+10 -12
View File
@@ -80,17 +80,16 @@ export async function fetchKeyMetadataFromApi(
"content-type": "application/json",
},
});
let json: ApiResponse;
let text = "";
try {
json = await response.json();
text = await response.text();
return JSON.parse(text);
} catch (e) {
throw new Error(
`Error fetching key metadata from API: ${response.status} - ${
response.statusText
} - ${await response.text()}`,
`Error fetching key metadata from API: ${response.status} - ${text}`,
);
}
return json;
}
export async function fetchAccountFromApi(
@@ -110,17 +109,16 @@ export async function fetchAccountFromApi(
authorization: `Bearer ${jwt}`,
},
});
let json: ApiAccountResponse;
let text = "";
try {
json = await response.json();
text = await response.text();
return JSON.parse(text);
} catch (e) {
throw new Error(
`Error fetching account from API: ${response.status} - ${
response.statusText
} - ${await response.text()}`,
`Error fetching account from API: ${response.status} - ${text}`,
);
}
return json;
}
export async function updateRateLimitedAt(
@@ -76,9 +76,9 @@ export async function authorize(
// if the difference is greater than the cacheTtl we want to ignore the cached data
const now = Date.now();
const diff = now - parsed.updatedAt;
const cacheTtl = cacheOptions.cacheTtlSeconds * 1000;
const cacheTtlMs = cacheOptions.cacheTtlSeconds * 1000;
// only if the diff is less than the cacheTtl do we want to use the cached key
if (diff < cacheTtl * 1000) {
if (diff < cacheTtlMs) {
accountMeta = parsed.apiKeyMeta;
}
} else {
@@ -160,9 +160,9 @@ export async function authorize(
// if the difference is greater than the cacheTtl we want to ignore the cached data
const now = Date.now();
const diff = now - parsed.updatedAt;
const cacheTtl = cacheOptions.cacheTtlSeconds * 1000;
const cacheTtlMs = cacheOptions.cacheTtlSeconds * 1000;
// only if the diff is less than the cacheTtl do we want to use the cached key
if (diff < cacheTtl * 1000) {
if (diff < cacheTtlMs) {
apiKeyMeta = parsed.apiKeyMeta;
}
} else {
+24
View File
@@ -1,5 +1,29 @@
# @thirdweb-dev/unity-js-bridge
## 0.2.105
### Patch Changes
- [#1985](https://github.com/thirdweb-dev/js/pull/1985) [`62cc785c`](https://github.com/thirdweb-dev/js/commit/62cc785c4ac6886fbc3b117bfb3d85dcf7523df1) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - [Unity] Support Add/Remove Admin and Create Session Key for SW
- [#1986](https://github.com/thirdweb-dev/js/pull/1986) [`e09221f1`](https://github.com/thirdweb-dev/js/commit/e09221f1e0cf89bf4a8b512c107cfa7cb9bcb82e) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - [Unity] Support Smart Wallet Account Override
- [#1981](https://github.com/thirdweb-dev/js/pull/1981) [`7a2e0eda`](https://github.com/thirdweb-dev/js/commit/7a2e0edab4e8c11d6c8bfc8970ed2768583a48d6) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - [Unity] Added Facebook and Apple providers
- Updated dependencies [[`36600dec`](https://github.com/thirdweb-dev/js/commit/36600decefca7b9ebb1924757b4186b559e439f6), [`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1), [`7c45941b`](https://github.com/thirdweb-dev/js/commit/7c45941b57fb5a6f83be507ee64dbf885d93c9d4)]:
- @thirdweb-dev/auth@4.1.11
- @thirdweb-dev/wallets@2.1.8
- @thirdweb-dev/sdk@4.0.16
## 0.2.104
### Patch Changes
- Updated dependencies [[`6832b19f`](https://github.com/thirdweb-dev/js/commit/6832b19fc5eedc7c17e69940a361840de0a84881), [`7abb36a7`](https://github.com/thirdweb-dev/js/commit/7abb36a7ec39766dd7da03015ccef401ef7a7863), [`1c4cbedb`](https://github.com/thirdweb-dev/js/commit/1c4cbedb6a3c9f9014ffbf83c02b998768958d96), [`e43beec3`](https://github.com/thirdweb-dev/js/commit/e43beec36a569211b6fe708107aa7dc04bff08ec)]:
- @thirdweb-dev/wallets@2.1.7
- @thirdweb-dev/sdk@4.0.15
- @thirdweb-dev/auth@4.1.10
## 0.2.103
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.2.103",
"version": "0.2.105",
"main": "dist/thirdweb-unity-bridge.js",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/unity-js-bridge",
"license": "Apache-2.0",
+78 -12
View File
@@ -5,6 +5,7 @@ import { ThirdwebSDK, ChainIdOrName } from "@thirdweb-dev/sdk";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import {
DAppMetaData,
EmbeddedWalletOauthStrategy,
SmartWalletConfig,
walletIds,
} from "@thirdweb-dev/wallets";
@@ -17,7 +18,7 @@ import { MetaMaskWallet } from "@thirdweb-dev/wallets/evm/wallets/metamask";
import { SmartWallet } from "@thirdweb-dev/wallets/evm/wallets/smart-wallet";
import { WalletConnect } from "@thirdweb-dev/wallets/evm/wallets/wallet-connect";
import { EmbeddedWallet } from "@thirdweb-dev/wallets/evm/wallets/embedded-wallet";
import { BigNumber } from "ethers";
import { BigNumber, ethers } from "ethers";
import {
Ethereum,
defaultChains,
@@ -74,6 +75,7 @@ interface TWBridge {
email?: string,
personalWallet?: PossibleWallet,
authOptions?: string,
smartWalletAccountOverride?: string,
) => Promise<string>;
disconnect: () => Promise<void>;
switchNetwork: (chainId: string) => Promise<void>;
@@ -87,6 +89,9 @@ interface TWBridge {
) => void;
fundWallet: (options: string) => Promise<void>;
exportWallet: (password: string) => Promise<string>;
smartWalletAddAdmin: (admin: string) => Promise<string | undefined>;
smartWalletRemoveAdmin: (admin: string) => Promise<string | undefined>;
smartWalletCreateSessionKey: (options: string) => Promise<string | undefined>;
}
const w = window;
@@ -252,6 +257,7 @@ class ThirdwebBridge implements TWBridge {
email?: string,
personalWallet: PossibleWallet = "localWallet",
authOptions?: string,
smartWalletAccountOverride?: string,
) {
if (!this.activeSDK) {
throw new Error("SDK not initialized");
@@ -281,15 +287,31 @@ class ThirdwebBridge implements TWBridge {
chainId: chainIdNumber,
authResult,
});
} else if (authOptionsParsed.authProvider === 1) {
// GoogleManaged
const googleWindow = this.openGoogleSignInWindow();
if (!googleWindow) {
throw new Error("Failed to open google login window");
} else if (authOptionsParsed.authProvider < 4) {
// OAuth
let authProvider: EmbeddedWalletOauthStrategy;
switch (authOptionsParsed.authProvider) {
case 1:
authProvider = "google";
break;
case 2:
authProvider = "apple";
break;
case 3:
authProvider = "facebook";
break;
default:
throw new Error(
"Invalid auth provider: " + authOptionsParsed.authProvider,
);
}
const popupWindow = this.openPopupWindow();
if (!popupWindow) {
throw new Error("Failed to open login window");
}
const authResult = await embeddedWallet.authenticate({
strategy: "google",
openedWindow: googleWindow,
strategy: authProvider,
openedWindow: popupWindow,
closeOpenedWindow: (openedWindow) => {
openedWindow.close();
},
@@ -298,7 +320,7 @@ class ThirdwebBridge implements TWBridge {
chainId: chainIdNumber,
authResult,
});
} else if (authOptionsParsed.authProvider === 2) {
} else if (authOptionsParsed.authProvider === 4) {
// CustomAuth
const authResult = await embeddedWallet.authenticate({
strategy: "jwt",
@@ -326,7 +348,7 @@ class ThirdwebBridge implements TWBridge {
);
if (this.activeWallet) {
// Pass EOA and reconnect to initialize smart wallet
await this.initializeSmartWallet(smartWallet, this.activeWallet);
await this.initializeSmartWallet(smartWallet, this.activeWallet, smartWalletAccountOverride);
} else {
// If EOA wallet is not connected, throw error
throw new Error(
@@ -603,11 +625,16 @@ class ThirdwebBridge implements TWBridge {
public async initializeSmartWallet(
sw: SmartWallet,
personalWallet: AbstractClientWallet,
accountAddress?: string,
) {
if(accountAddress) {
console.debug("Initializing smart wallet with account address override:", accountAddress);
}
const personalWalletAddress = await personalWallet.getAddress();
console.debug("Personal wallet address:", personalWalletAddress);
await sw.connect({
personalWallet,
accountAddress: accountAddress,
});
if (sw.listenerCount("disconnect") === 1) {
sw.on("disconnect", () => {
@@ -627,10 +654,49 @@ class ThirdwebBridge implements TWBridge {
return localWallet;
}
public openGoogleSignInWindow() {
public async smartWalletAddAdmin(admin: string) {
if (!this.activeWallet) {
throw new Error("No wallet connected");
}
const smartWallet = this.activeWallet as SmartWallet;
const result = await smartWallet.addAdmin(admin);
return JSON.stringify({ result: result }, bigNumberReplacer);
}
public async smartWalletRemoveAdmin(admin: string) {
if (!this.activeWallet) {
throw new Error("No wallet connected");
}
const smartWallet = this.activeWallet as SmartWallet;
const result = await smartWallet.removeAdmin(admin);
return JSON.stringify({ result: result }, bigNumberReplacer);
}
public async smartWalletCreateSessionKey(options: string) {
if (!this.activeWallet) {
throw new Error("No wallet connected");
}
const smartWallet = this.activeWallet as SmartWallet;
const optionsParsed = JSON.parse(options);
const approvedCallTargets = optionsParsed.approvedCallTargets;
const nativeTokenLimitPerTransaction = ethers.utils.formatEther(
optionsParsed.nativeTokenLimitPerTransactionInWei,
);
const startDate = BigNumber.from(optionsParsed.startDate).toNumber();
const expirationDate = BigNumber.from(optionsParsed.expirationDate).toNumber();
const result = await smartWallet.createSessionKey(optionsParsed.signerAddress, {
approvedCallTargets: approvedCallTargets,
nativeTokenLimitPerTransaction: nativeTokenLimitPerTransaction,
startDate: startDate,
expirationDate: expirationDate,
});
return JSON.stringify({ result: result }, bigNumberReplacer);
}
public openPopupWindow() {
const win = window.open("", undefined, "width=350, height=500");
if (win) {
win.document.title = "Sign In - Google Accounts";
win.document.title = "Sign In - OAuth";
win.document.body.innerHTML = `
<svg class="loader" viewBox="0 0 50 50">
<circle
+22
View File
@@ -1,5 +1,27 @@
# @thirdweb-dev/wallets
## 2.1.8
### Patch Changes
- [#1921](https://github.com/thirdweb-dev/js/pull/1921) [`1094a03a`](https://github.com/thirdweb-dev/js/commit/1094a03a04f3e7ade84af0f7a6073794c0904cb1) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Add Coin98 wallet
- Updated dependencies [[`7c45941b`](https://github.com/thirdweb-dev/js/commit/7c45941b57fb5a6f83be507ee64dbf885d93c9d4)]:
- @thirdweb-dev/sdk@4.0.16
## 2.1.7
### Patch Changes
- [#1977](https://github.com/thirdweb-dev/js/pull/1977) [`6832b19f`](https://github.com/thirdweb-dev/js/commit/6832b19fc5eedc7c17e69940a361840de0a84881) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix potential auto connection race condition
- [#1978](https://github.com/thirdweb-dev/js/pull/1978) [`7abb36a7`](https://github.com/thirdweb-dev/js/commit/7abb36a7ec39766dd7da03015ccef401ef7a7863) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Enforce authenticated users when getting embedded wallet signers
- [#1953](https://github.com/thirdweb-dev/js/pull/1953) [`1c4cbedb`](https://github.com/thirdweb-dev/js/commit/1c4cbedb6a3c9f9014ffbf83c02b998768958d96) Thanks [@nkrishang](https://github.com/nkrishang)! - Patched smart wallet signatures for updated eip-1271 sig verification
- Updated dependencies [[`e43beec3`](https://github.com/thirdweb-dev/js/commit/e43beec36a569211b6fe708107aa7dc04bff08ec)]:
- @thirdweb-dev/sdk@4.0.15
## 2.1.6
### Patch Changes
@@ -0,0 +1,7 @@
{
"main": "dist/thirdweb-dev-wallets-evm-connectors-coin98.cjs.js",
"module": "dist/thirdweb-dev-wallets-evm-connectors-coin98.esm.js",
"browser": {
"./dist/thirdweb-dev-wallets-evm-connectors-coin98.esm.js": "./dist/thirdweb-dev-wallets-evm-connectors-coin98.browser.esm.js"
}
}
@@ -0,0 +1,7 @@
{
"main": "dist/thirdweb-dev-wallets-evm-wallets-coin98.cjs.js",
"module": "dist/thirdweb-dev-wallets-evm-wallets-coin98.esm.js",
"browser": {
"./dist/thirdweb-dev-wallets-evm-wallets-coin98.esm.js": "./dist/thirdweb-dev-wallets-evm-wallets-coin98.browser.esm.js"
}
}
+15 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wallets",
"version": "2.1.6",
"version": "2.1.8",
"main": "dist/thirdweb-dev-wallets.cjs.js",
"module": "dist/thirdweb-dev-wallets.esm.js",
"browser": {
@@ -77,6 +77,13 @@
},
"default": "./evm/wallets/blocto/dist/thirdweb-dev-wallets-evm-wallets-blocto.cjs.js"
},
"./evm/wallets/coin98": {
"module": {
"browser": "./evm/wallets/coin98/dist/thirdweb-dev-wallets-evm-wallets-coin98.browser.esm.js",
"default": "./evm/wallets/coin98/dist/thirdweb-dev-wallets-evm-wallets-coin98.esm.js"
},
"default": "./evm/wallets/coin98/dist/thirdweb-dev-wallets-evm-wallets-coin98.cjs.js"
},
"./evm/wallets/engine": {
"module": {
"browser": "./evm/wallets/engine/dist/thirdweb-dev-wallets-evm-wallets-engine.browser.esm.js",
@@ -245,6 +252,13 @@
},
"default": "./evm/connectors/blocto/dist/thirdweb-dev-wallets-evm-connectors-blocto.cjs.js"
},
"./evm/connectors/coin98": {
"module": {
"browser": "./evm/connectors/coin98/dist/thirdweb-dev-wallets-evm-connectors-coin98.browser.esm.js",
"default": "./evm/connectors/coin98/dist/thirdweb-dev-wallets-evm-connectors-coin98.esm.js"
},
"default": "./evm/connectors/coin98/dist/thirdweb-dev-wallets-evm-connectors-coin98.cjs.js"
},
"./evm/connectors/signer": {
"module": {
"browser": "./evm/connectors/signer/dist/thirdweb-dev-wallets-evm-connectors-signer.browser.esm.js",
@@ -0,0 +1,20 @@
import { Ethereum } from "../injected/types";
import { assertWindowEthereum } from "../../utils/assertWindowEthereum";
declare global {
interface Window {
coin98?: Ethereum;
}
}
export function getInjectedCoin98Provider(): Ethereum | undefined {
if (typeof window === "undefined") {
return;
}
if (assertWindowEthereum(globalThis.window)) {
if (window.coin98) {
return window.ethereum;
}
}
}
@@ -0,0 +1,135 @@
import { AsyncStorage } from "../../../core/AsyncStorage";
import {
ConnectorNotFoundError,
ResourceUnavailableError,
RpcError,
UserRejectedRequestError,
} from "../../../lib/wagmi-core/errors";
import { walletIds } from "../../constants/walletIds";
import { InjectedConnector, InjectedConnectorOptions } from "../injected";
import type { Chain } from "@thirdweb-dev/chains";
import { utils } from "ethers";
import { getInjectedCoin98Provider } from "./getInjectedCoin98Provider";
export type Coin98ConnectorOptions = InjectedConnectorOptions;
type Coin98ConnectorConstructorArg = {
chains?: Chain[];
connectorStorage: AsyncStorage;
options?: Coin98ConnectorOptions;
};
export class Coin98Connector extends InjectedConnector {
readonly id = walletIds.coin98;
constructor(arg: Coin98ConnectorConstructorArg) {
const defaultOptions = {
name: "Coin98 Wallet",
shimDisconnect: true,
shimChainChangedDisconnect: true,
getProvider: getInjectedCoin98Provider,
};
const options = {
...defaultOptions,
...arg.options,
};
super({
chains: arg.chains,
options,
connectorStorage: arg.connectorStorage,
});
}
/**
* Connect to injected Coin98Wallet provider
*/
async connect(options: { chainId?: number } = {}) {
try {
const provider = await this.getProvider();
if (!provider) {
throw new ConnectorNotFoundError();
}
this.setupListeners();
// emit "connecting" event
this.emit("message", { type: "connecting" });
// Attempt to show wallet select prompt with `wallet_requestPermissions` when
// `shimDisconnect` is active and account is in disconnected state (flag in storage)
let account: string | null = null;
if (
this.options?.shimDisconnect &&
!Boolean(this.connectorStorage.getItem(this.shimDisconnectKey))
) {
account = await this.getAccount().catch(() => null);
const isConnected = !!account;
if (isConnected) {
// Attempt to show another prompt for selecting wallet if already connected
try {
await provider.request({
method: "wallet_requestPermissions",
params: [{ eth_accounts: {} }],
});
} catch (error) {
// Not all injected providers support `wallet_requestPermissions` (e.g. iOS).
// Only bubble up error if user rejects request
if (this.isUserRejectedRequestError(error)) {
throw new UserRejectedRequestError(error);
}
}
}
}
// if account is not already set, request accounts and use the first account
if (!account) {
const accounts = await provider.request({
method: "eth_requestAccounts",
});
account = utils.getAddress(accounts[0] as string);
}
// get currently connected chainId
let connectedChainId = await this.getChainId();
// check if connected chain is unsupported
let isUnsupported = this.isChainUnsupported(connectedChainId);
// if chainId is given, but does not match the currently connected chainId, switch to the given chainId
if (options.chainId && connectedChainId !== options.chainId) {
try {
await this.switchChain(options.chainId);
// recalculate the chainId and isUnsupported
connectedChainId = options.chainId;
isUnsupported = this.isChainUnsupported(options.chainId);
} catch (e) {
console.error(`Could not switch to chain id : ${options.chainId}`, e);
}
}
// if shimDisconnect is enabled
if (this.options?.shimDisconnect) {
// add shimDisconnectKey in storage - this signals that connector is "connected"
await this.connectorStorage.setItem(this.shimDisconnectKey, "true");
}
const connectionInfo = {
chain: { id: connectedChainId, unsupported: isUnsupported },
provider: provider,
account,
};
this.emit("connect", connectionInfo);
return connectionInfo;
} catch (error) {
if (this.isUserRejectedRequestError(error)) {
throw new UserRejectedRequestError(error);
}
if ((error as RpcError).code === -32002) {
throw new ResourceUnavailableError(error);
}
throw error;
}
}
}
@@ -89,7 +89,7 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
if (!this.user) {
throw new Error("Embedded Wallet is not connected");
}
return this.user.walletAddress;
return await this.getSigner().then((signer) => signer.getAddress());
}
async isConnected(): Promise<boolean> {
@@ -115,7 +115,7 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
}
const user = await this.getUser();
const signer = await user?.wallet.getEthersJsSigner({
const signer = await user.wallet.getEthersJsSigner({
rpcEndpoint: this.options.chain.rpc[0] || "", // TODO: handle chain.rpc being empty array
});
@@ -180,11 +180,11 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
this.emit("disconnect");
};
async getUser(): Promise<InitializedUser | null> {
private async getUser(): Promise<InitializedUser> {
if (
!this.user ||
!this.user.wallet ||
typeof this.user.wallet.getEthersJsSigner !== "function"
!this.user.wallet.getEthersJsSigner // when serializing, functions are lost, need to rehydrate
) {
const embeddedWalletSdk = this.getEmbeddedWalletSDK();
const user = await embeddedWalletSdk.getUser();
@@ -193,27 +193,25 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
this.user = user;
break;
}
default: {
// if logged out or unitialized, we can't get a signer, so throw an error
throw new Error(
"Embedded Wallet is not authenticated, please authenticate first",
);
}
}
}
return this.user;
}
async getEmail() {
// implicit call to set the user
await this.getSigner();
if (!this.user) {
throw new Error("No user found, Embedded Wallet is not connected");
}
return this.user.authDetails.email;
const user = await this.getUser();
return user.authDetails.email;
}
async getRecoveryInformation() {
// implicit call to set the user
await this.getSigner();
if (!this.user) {
throw new Error("No user found, Embedded Wallet is not connected");
}
return this.user.authDetails;
const user = await this.getUser();
return user.authDetails;
}
async sendVerificationEmail({
@@ -64,5 +64,4 @@ export type AuthResult = {
user?: InitializedUser;
isNewUser?: boolean;
needsRecoveryCode?: boolean;
verifyOTP?: (otp: string, recoveryCode?: string) => Promise<AuthResult>;
};
@@ -52,6 +52,7 @@ type InjectedProviderFlags = {
isZerion?: true;
isOkxWallet?: true;
isCoreWallet?: true;
isCoin98Wallet?: true;
};
type InjectedProviders = InjectedProviderFlags & {
isMetaMask: true;
@@ -1,5 +1,6 @@
export const walletIds = {
blocto: "blocto",
coin98: "coin98",
coinbase: "coinbase",
coreWallet: "coreWallet",
frame: "frame",
+3
View File
@@ -53,6 +53,9 @@ export { getInjectedOKXProvider } from "./connectors/okx/getInjectedOKXProvider"
export { CoreWallet, type CoreWalletOptions } from "./wallets/core-wallet";
export { getInjectedCoreWalletProvider } from "./connectors/core-wallet/getInjectedCoreWalletProvider";
export { Coin98Wallet, type Coin98Options } from "./wallets/coin98";
export { getInjectedCoin98Provider } from "./connectors/coin98/getInjectedCoin98Provider";
export type { Chain } from "@thirdweb-dev/chains";
// export the window ethereum util
+139
View File
@@ -0,0 +1,139 @@
import type { WalletConnectConnector as WalletConnectConnectorType } from "../connectors/wallet-connect";
import type { QRModalOptions } from "../connectors/wallet-connect/qrModalOptions";
import { Connector, WagmiAdapter } from "../interfaces/connector";
import { AbstractClientWallet, WalletOptions } from "./base";
import type { Coin98Connector as Coin98ConnectorType } from "../connectors/coin98";
import { walletIds } from "../constants/walletIds";
import { TW_WC_PROJECT_ID } from "../constants/wc";
import { getInjectedCoin98Provider } from "../connectors/coin98/getInjectedCoin98Provider";
type Coin98AdditionalOptions = {
/**
* Whether to open the default Wallet Connect QR code Modal for connecting to Coin98 app on mobile if Coin98 wallet is not injected when calling connect().
*/
qrcode?: boolean;
/**
* When connecting Coin98 wallet using the QR Code - Wallet Connect connector is used which requires a project id.
* This project id is Your project’s unique identifier for wallet connect that can be obtained at cloud.walletconnect.com.
*
* https://docs.walletconnect.com/2.0/web3modal/options#projectid-required
*/
projectId?: string;
/**
* options to customize the Wallet Connect QR Code Modal ( only relevant when qrcode is true )
*
* https://docs.walletconnect.com/2.0/web3modal/options
*/
qrModalOptions?: QRModalOptions;
};
export type Coin98Options = WalletOptions<Coin98AdditionalOptions>;
type ConnectWithQrCodeArgs = {
chainId?: number;
onQrCodeUri: (uri: string) => void;
onConnected: (accountAddress: string) => void;
};
export class Coin98Wallet extends AbstractClientWallet<Coin98AdditionalOptions> {
connector?: Connector;
walletConnectConnector?: WalletConnectConnectorType;
Coin98Connector?: Coin98ConnectorType;
isInjected: boolean;
static id = walletIds.coin98 as string;
public get walletName() {
return "Coin98 wallet" as const;
}
constructor(options: Coin98Options) {
super(Coin98Wallet.id, options);
this.isInjected = !!getInjectedCoin98Provider();
}
protected async getConnector(): Promise<Connector> {
if (!this.connector) {
// if Coin98 wallet is injected, use the injected connector
// otherwise, use the wallet connect connector for using the Coin98 app on mobile via QR code scan
if (this.isInjected) {
// import the connector dynamically
const { Coin98Connector } = await import("../connectors/coin98");
this.Coin98Connector = new Coin98Connector({
chains: this.chains,
connectorStorage: this.walletStorage,
options: {
shimDisconnect: true,
},
});
this.connector = new WagmiAdapter(this.Coin98Connector);
} else {
const { WalletConnectConnector } = await import(
"../connectors/wallet-connect"
);
const walletConnectConnector = new WalletConnectConnector({
chains: this.chains,
options: {
projectId: this.options?.projectId || TW_WC_PROJECT_ID, // TODO,
storage: this.walletStorage,
qrcode: this.options?.qrcode,
dappMetadata: this.dappMetadata,
qrModalOptions: this.options?.qrModalOptions,
},
});
walletConnectConnector.getProvider().then((provider) => {
provider.signer.client.on("session_request_sent", () => {
this.emit("wc_session_request_sent");
});
});
// need to save this for getting the QR code URI
this.walletConnectConnector = walletConnectConnector;
this.connector = new WagmiAdapter(walletConnectConnector);
}
}
return this.connector;
}
/**
* connect to wallet with QR code
*
* @example
* ```typescript
* wallet.connectWithQrCode({
* chainId: 1,
* onQrCodeUri(qrCodeUri) {
* // render the QR code with `qrCodeUri`
* },
* onConnected(accountAddress) {
* // update UI to show connected state
* },
* })
* ```
*/
async connectWithQrCode(options: ConnectWithQrCodeArgs) {
await this.getConnector();
const wcConnector = this.walletConnectConnector;
if (!wcConnector) {
throw new Error("WalletConnect connector not found");
}
const wcProvider = await wcConnector.getProvider();
// set a listener for display_uri event
wcProvider.on("display_uri", (uri) => {
options.onQrCodeUri(uri);
});
// trigger connect flow
this.connect({ chainId: options.chainId }).then(options.onConnected);
}
}
@@ -1,4 +1,5 @@
import { AbstractClientWallet, WalletOptions } from "./base";
import { checkContractWalletSignature } from "./abstract";
import type { ConnectParams } from "../interfaces/connector";
import type {
SmartWalletConfig,
@@ -14,7 +15,8 @@ import {
} from "@thirdweb-dev/sdk";
import { walletIds } from "../constants/walletIds";
import { getValidChainRPCs } from "@thirdweb-dev/chains";
import { providers, utils } from "ethers";
import { providers, utils, Bytes, Signer } from "ethers";
import { signTypedDataInternal } from "@thirdweb-dev/sdk";
// export types and utils for convenience
export type * from "../connectors/smart-wallet/types";
@@ -76,6 +78,67 @@ export class SmartWallet extends AbstractClientWallet<
return this.connector?.personalWallet;
}
/**
* @returns the signature of the message
*/
public async signMessage(message: Bytes | string): Promise<string> {
// Deploy smart wallet if needed
const connector = await this.getConnector();
await connector.deployIfNeeded();
const erc4337Signer = await this.getSigner();
const chainId = await erc4337Signer.getChainId();
const address = await connector.getAddress();
/**
* We first try to sign the EIP-712 typed data i.e. the message mixed with the smart wallet's domain separator.
* If this fails, we fallback to the legacy signing method.
*/
try {
const result = await signTypedDataInternal(
erc4337Signer,
{
name: "Account",
version: "1",
chainId,
verifyingContract: address,
},
{ AccountMessage: [{ name: "message", type: "bytes" }] },
{
message: utils.defaultAbiCoder.encode(
["bytes32"],
[utils.hashMessage(message)],
),
},
);
const isValid = await checkContractWalletSignature(
message as string,
result.signature,
address,
chainId,
);
if (!isValid) {
throw new Error("Invalid signature");
}
return result.signature;
} catch {
return await this.signMessageLegacy(erc4337Signer, message);
}
}
/**
* @returns the signature of the message (for legacy EIP-1271 signature verification)
*/
private async signMessageLegacy(
signer: Signer,
message: Bytes | string,
): Promise<string> {
return await signer.signMessage(message);
}
/**
* Check whether the connected signer can execute a given transaction using the smart wallet.
* @param transaction - the transaction to execute using the smart wallet.