Compare commits

..
85 changed files with 3110 additions and 320 deletions
+50 -40
View File
@@ -1,9 +1,5 @@
# Contributing to thirdweb
You can find a full video tutorial on how to contribute to thirdweb below:
https://www.youtube.com/watch?v=TXsQ3qok3B0
## Getting Started
To get started, read the [How this repo works](#how-this-repo-works) section below to learn about the structure of this repo.
@@ -12,23 +8,19 @@ From there, you can take a look at our [Good First Issues](https://github.com/th
If you have any questions about the issue, feel free to ask on our [Discord server](https://discord.gg/thirdweb) in the `#contributors` channel; where you'll be able to get help from our team and other contributors.
<br/>
<br />
## How this repo works
[@thirdweb-dev/js](https://github.com/thirdweb-dev/js) is a monorepo, meaning it contains many projects within it.
We use [Turborepo](https://turbo.build/repo/docs) to manage the repository, and help speed up the [CI/CD](https://www.atlassian.com/continuous-delivery/principles/continuous-integration-vs-delivery-vs-deployment) pipeline to ship to production faster 🚢. Turborepo is a project dependency, and doesn't need to be installed separately.
We use [Turborepo](https://turborepo.org/docs/getting-started) to manage the monorepo, and help speed up the [CI/CD](https://www.atlassian.com/continuous-delivery/principles/continuous-integration-vs-delivery-vs-deployment) pipeline to ship to production faster 🚢.
We use [pnpm](https://pnpm.io) for package management across the repo. `pnpm` is similar to `npm` or `yarn` but with more efficient disk space usage.
You can see a quick outline of each of the projects within this repo below, each living within the [/packages](/packages) directory:
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](./packages/thirdweb). You can still find the legacy packages at [/legacy_packages](./legacy_packages).**
| Package | Description | Latest Version |
| ------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [/sdk](./legacy_packages/sdk) | Best in class web3 SDK for Browser, Node and Mobile apps | <a href="https://www.npmjs.com/package/@thirdweb-dev/sdk"><img src="https://img.shields.io/npm/v/@thirdweb-dev/sdk?color=red&label=npm&logo=npm" alt="npm version"/></a> |
| [/react](./legacy_packages/react) | Ultimate collection of React hooks for your web3 apps | <a href="https://www.npmjs.com/package/@thirdweb-dev/react"><img src="https://img.shields.io/npm/v/@thirdweb-dev/react?color=red&label=npm&logo=npm" alt="npm version"/></a> |
| [/auth](./legacy_packages/auth) | Best in class wallet authentication for Node backends | <a href="https://www.npmjs.com/package/@thirdweb-dev/auth"><img src="https://img.shields.io/npm/v/@thirdweb-dev/auth?color=red&label=npm&logo=npm" alt="npm version"/></a> |
| [/storage](./legacy_packages/storage) | Best in class decentralized storage SDK for Browser and Node | <a href="https://www.npmjs.com/package/@thirdweb-dev/storage"><img src="https://img.shields.io/npm/v/@thirdweb-dev/storage?color=red&label=npm&logo=npm" alt="npm version"/></a> |
| [/cli](./legacy_packages/cli) | Publish and deploy smart contracts without dealing with private keys | <a href="https://www.npmjs.com/package/thirdweb"><img src="https://img.shields.io/npm/v/thirdweb?color=red&label=npm&logo=npm" alt="npm version"/></a> |
This single package provides a performant & lightweight SDK to interact with any EVM chain across Node, React, and React Native. Learn more about how to use the thirdweb SDK in our [documentation](https://portal.thirdweb.com/typescript/v5).
<br />
## How to contribute
@@ -36,7 +28,7 @@ Let's explore how you can set up the repo on your local machine and start contri
This section requires some existing knowledge of [Git](https://git-scm.com/), [Node.js](https://nodejs.org/en/) and [pnpm](https://pnpm.io/).
<br/>
<br />
### Getting the repo
@@ -50,26 +42,50 @@ To begin:
3. Create a new branch on your fork to start working on your changes:
```
git checkout -b MY_BRANCH_NAME
```bash
git checkout -b <YOUR BRANCH NAME>
```
4. Install the dependencies:
```
```bash
pnpm install
```
If you are on windows, use the `--ignore-scripts` flag
```
```bash
pnpm install --ignore-scripts
```
Now you have got the repo on your local machine, and you're ready to start making your changes!
Now you have the repo on your local machine, and you're ready to start making your changes!
<br/>
### Test Your Changes
We use [yalc](https://github.com/wclr/yalc) to test changes locally.
#### Writing Unit Tests
Try to include unit test coverage with your changes where appropriate. We use `vitest` for testing, look for files with the `.test.ts` extension for examples of how to structure your unit tests.
To run your tests, run the following from the package directory (most likely /packages/thirdweb):
```bash
pnpm test:dev <YOUR TEST FILE PATH>
```
> Specifying your test file path is optional, but will save time by only running specific tests each time. Before opening your PR, run `pnpm test` from the monorepo root (without specifying your test file) to ensure your changes didn't break any existing tests.
#### Linting
We use a linter to maintain best practices across projects. Once your changes are complete (or periodically while making changes), run the linter with the following command from the repo root:
```bash
pnpm lint
```
If there are errors, try running `pnpm fix` to auto-fix any basic errors. Other linter errors, like missing documentation, will need to be fixed manually. See existing files for examples of inline documentation.
#### Creating a Local Test Project
You can use [yalc](https://github.com/wclr/yalc) to test changes on a local project.
Install the yalc CLI globally:
@@ -77,40 +93,35 @@ Install the yalc CLI globally:
pnpm add yalc -g
```
First, create a test project where you can experiment with your changes:
First, create a test project where you can experiment with your changes. This can be any node project that uses your changes.
You can create a basic starter project with the `sdk` and `react` packages installed using the CLI:
Push your local version to your `yalc` store with:
```bash
npx thirdweb create --app
pnpm build && pnpm push
```
Use `yalc` to link your local changes in the monorepo to the test project, by running the following command from your test repo:
Link your local changes in the monorepo to the test project by running the following command from your test repo:
```bash
yalc add @thirdweb-dev/react # Link the react package
yalc add @thirdweb-dev/sdk # Link the sdk package
# etc...
yalc add thirdweb
```
From the monorepo, run the following command to publish your local changes to the test project:
Now, each time you make a change to the monorepo, you can run `pnpm push` from the monorepo then `yalc update thirdweb` from your test project to publish your latest changes to the test project.
```bash
pnpm push
```
If your changes modify dependencies, you may need to reinstall dependencies on your test project with `pnpm install`
Now, each time you make a change to the monorepo, you can run `pnpm push` to publish your changes to the test project.
To make sure your latest changes are used, in your test project you may need to:
In your test project, you need to:
1. Delete the dependencies cache. If you're using Next.js, that is the `.next` directory, and if you're using CRA, that is the `node_modules/.cache` directory.
1. Delete the dependencies cache. If you're using Next.js, that's the `.next` directory, and if you're using CRA, that's the `node_modules/.cache` directory.
2. Restart the development server.
#### Testing from npm
You can also test your changes by publishing a dev package to npm and adding your package as you normally do in your projects.
Once you have your PR up you can add a comment with the text: `/release-pr`. This will trigger a GitHub action that will publish a dev version to npm.
**Once you have your PR up** you can add a comment with the text: `/release-pr`. This will trigger a GitHub action that will publish a dev version to npm.
You can see the action progress in GitHub's `Actions` tab, look for the workflow: `release-pr`.
@@ -154,9 +165,8 @@ git commit -am "My commit message"
3. Push your changes to the SDK:
```
git push origin MY_BRANCH_NAME
git push origin <YOUR BRANCH NAME>
```
4. Create a [pull request](https://www.atlassian.com/git/tutorials/making-a-pull-request) to the `main` branch of the official (not your fork) SDK repo.
It's helpful to tag PRs with `[SDK]`, `[REACT]`, `[AUTH]`, (the name of the package you're modifying) to indicate the package that you are engaging with.
+3
View File
@@ -8,5 +8,8 @@
"typescript.preferences.importModuleSpecifier": "relative",
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+7
View File
@@ -1,5 +1,12 @@
# @thirdweb-dev/auth
## 4.1.67
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/wallets@2.5.9
## 4.1.66
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "4.1.66",
"version": "4.1.67",
"main": "dist/thirdweb-dev-auth.cjs.js",
"module": "dist/thirdweb-dev-auth.esm.js",
"browser": {
+9
View File
@@ -1,5 +1,14 @@
# thirdweb
## 0.13.75
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.69
- @thirdweb-dev/wallets@2.5.9
- @thirdweb-dev/auth@4.1.67
## 0.13.74
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@thirdweb-dev/cli",
"main": "dist/cli/index.js",
"version": "0.13.74",
"version": "0.13.75",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+9
View File
@@ -1,5 +1,14 @@
# @thirdweb-dev/react-core
## 4.6.8
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.69
- @thirdweb-dev/wallets@2.5.9
- @thirdweb-dev/auth@4.1.67
## 4.6.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-core",
"version": "4.6.7",
"version": "4.6.8",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react-core",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -1,5 +1,7 @@
# @thirdweb-dev/react-native-compat
## 0.7.40
## 0.7.39
## 0.7.38
@@ -1,7 +1,7 @@
{
"name": "@thirdweb-dev/react-native-compat",
"description": "Shims for Thirdweb in React Native Projects",
"version": "0.7.39",
"version": "0.7.40",
"author": "thirdweb eng <[email protected]>",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react-native-compat",
"license": "Apache-2.0",
@@ -1,5 +1,14 @@
# @thirdweb-dev/react-native
## 0.7.40
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.69
- @thirdweb-dev/react-core@4.6.8
- @thirdweb-dev/wallets@2.5.9
## 0.7.39
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-native",
"version": "0.7.39",
"version": "0.7.40",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react-native",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+11
View File
@@ -1,5 +1,16 @@
# @thirdweb-dev/react
## 4.6.8
### Patch Changes
- [#2679](https://github.com/thirdweb-dev/js/pull/2679) [`560d8ad`](https://github.com/thirdweb-dev/js/commit/560d8adafa011dc48e48e73d7eb6a36170d38cef) Thanks [@ElasticBottle](https://github.com/ElasticBottle)! - Fix In-App wallet country selector styles on windows
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.69
- @thirdweb-dev/react-core@4.6.8
- @thirdweb-dev/wallets@2.5.9
## 4.6.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "4.6.7",
"version": "4.6.8",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -12,3 +12,4 @@ export const StyledH2 = /* @__PURE__ */ styled.h2;
export const StyledP = /* @__PURE__ */ styled.p;
export const StyledUl = /* @__PURE__ */ styled.ul;
export const StyledSelect = /* @__PURE__ */ styled.select;
export const StyledOption = /* @__PURE__ */ styled.option;
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import React, { useRef } from "react";
import { fontSize, spacing } from "../../../design-system";
import { useCustomTheme } from "../../../design-system/CustomThemeProvider";
import { StyledSelect } from "../../../design-system/elements";
import { StyledOption, StyledSelect } from "../../../design-system/elements";
export function CountrySelector({
countryCode,
@@ -52,12 +52,12 @@ export function CountrySelector({
</option>
{supportedCountriesForSms.map((country) => {
return (
<option
<Option
key={country.countryIsoCode}
value={`${country.countryIsoCode} +${country.phoneNumberCode}`}
>
{country.countryName} +{country.phoneNumberCode}{" "}
</option>
</Option>
);
})}
</Select>
@@ -65,7 +65,20 @@ export function CountrySelector({
);
}
export const Select = /* @__PURE__ */ StyledSelect(() => {
const Option = /* @__PURE__ */ StyledOption(() => {
const theme = useCustomTheme();
return {
color: theme.colors.primaryText,
background: theme.colors.modalBg,
transition: "background 0.3s ease",
"&:hover": {
background: theme.colors.walletSelectorButtonHoverBg,
},
};
});
const Select = /* @__PURE__ */ StyledSelect(() => {
const theme = useCustomTheme();
return {
fontSize: fontSize.sm,
Binary file not shown.
+7
View File
@@ -1,5 +1,12 @@
# @thirdweb-dev/sdk
## 4.0.69
### Patch Changes
- Updated dependencies [[`560d8ad`](https://github.com/thirdweb-dev/js/commit/560d8adafa011dc48e48e73d7eb6a36170d38cef), [`d7e6671`](https://github.com/thirdweb-dev/js/commit/d7e6671c0a34cc8a3523f397a45d754617c9fbb0), [`948f155`](https://github.com/thirdweb-dev/js/commit/948f155e7c2406cc5a172a6c5abe18ba8da89e2a), [`3be61dd`](https://github.com/thirdweb-dev/js/commit/3be61ddc52cef168cda0bee13e93a926dedf479a), [`cfff7e8`](https://github.com/thirdweb-dev/js/commit/cfff7e838af1c464c9b5fbb547153eaae41e2c66)]:
- thirdweb@5.11.0
## 4.0.68
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "4.0.68",
"version": "4.0.69",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/sdk",
"license": "Apache-2.0",
Binary file not shown.
@@ -1,5 +1,14 @@
# @thirdweb-dev/unity-js-bridge
## 0.6.18
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.69
- @thirdweb-dev/wallets@2.5.9
- @thirdweb-dev/auth@4.1.67
## 0.6.17
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.6.17",
"version": "0.6.18",
"main": "dist/thirdweb-unity-bridge.js",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/unity-js-bridge",
"license": "Apache-2.0",
+7
View File
@@ -1,5 +1,12 @@
# @thirdweb-dev/wallets
## 2.5.9
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.69
## 2.5.8
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wallets",
"version": "2.5.8",
"version": "2.5.9",
"main": "dist/thirdweb-dev-wallets.cjs.js",
"module": "dist/thirdweb-dev-wallets.esm.js",
"browser": {
+124
View File
@@ -1,5 +1,129 @@
# thirdweb
## 5.11.0
### Minor Changes
- [#2679](https://github.com/thirdweb-dev/js/pull/2679) [`560d8ad`](https://github.com/thirdweb-dev/js/commit/560d8adafa011dc48e48e73d7eb6a36170d38cef) Thanks [@ElasticBottle](https://github.com/ElasticBottle)! - Add support for connecting in-app wallet using phone number
## Usage in TypeScript
```ts
import { createThirdwebClient, createWallet } from "thirdweb";
import { preAuthenticate } from "thirdweb/wallets/in-app";
const client = createThirdwebClient({ clientId: "..." });
const phoneNumber = '+123456789';
// Send OTP to given phone number
async function sendOTP() {
await preAuthenticate({
strategy: "phone",
phoneNumber,
client,
});
}
async function connect() {
// create a in-app wallet instance
const wallet = createWallet('inApp');
// if the OTP is correct, the wallet will be connected else an error will be thrown
const account = await wallet.connect({
client,
strategy: "phone";
phoneNumber,
verificationCode: '...' // Pass the OTP entered by the user
});
console.log('connected to', account);
}
```
## Usage in React
```tsx
import { createThirdwebClient } from "thirdweb";
import { preAuthenticate } from "thirdweb/wallets/in-app";
import { useConnect } from "thirdweb/react";
const client = createThirdwebClient({ clientId: "..." });
function Component() {
const { connect } = useConnect();
const [phoneNumber, setPhoneNumber] = useState(''); // get phone number from user
const [otp, setOtp] = useState(''); // get OTP from user
// Send OTP to given phone number
async function sendOTP() {
await preAuthenticate({
strategy: "phone",
phoneNumber,
client,
});
}
async function connect() {
// create a in-app wallet instance
const wallet = createWallet('inApp');
// if the OTP is correct, the wallet will be connected else an error will be thrown
await wallet.connect({
client,
strategy: "phone";
phoneNumber,
verificationCode: otp
});
// set the wallet as active
connect(wallet)
}
// render UI to get OTP and phone number from user
return <div> ... </div>
}
```
- [#2818](https://github.com/thirdweb-dev/js/pull/2818) [`948f155`](https://github.com/thirdweb-dev/js/commit/948f155e7c2406cc5a172a6c5abe18ba8da89e2a) Thanks [@jnsdls](https://github.com/jnsdls)! - **Gasless transactions in Typescript**
```ts
import { sendTransaction } from "thirdweb";
const result = sendTransaction({
transaction,
account,
gasless: {
provider: "engine",
relayerUrl: "https://...",
relayerForwarderAddress: "0x...",
},
});
```
**Gasless transactions in React**
```jsx
import { useSendTransaction } from "thirdweb/react";
const { mutate } = useSendTransaction({
gasless: {
provider: "engine",
relayerUrl: "https://...",
relayerForwarderAddress: "0x...",
},
});
// Call mutate with the transaction object
mutate(transaction);
```
### Patch Changes
- [#2842](https://github.com/thirdweb-dev/js/pull/2842) [`d7e6671`](https://github.com/thirdweb-dev/js/commit/d7e6671c0a34cc8a3523f397a45d754617c9fbb0) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - fix: handle signature minting with price and currency
- [#2839](https://github.com/thirdweb-dev/js/pull/2839) [`3be61dd`](https://github.com/thirdweb-dev/js/commit/3be61ddc52cef168cda0bee13e93a926dedf479a) Thanks [@MananTank](https://github.com/MananTank)! - Enable Buy Modal by default with useSendTransaction hook
- [#2768](https://github.com/thirdweb-dev/js/pull/2768) [`cfff7e8`](https://github.com/thirdweb-dev/js/commit/cfff7e838af1c464c9b5fbb547153eaae41e2c66) Thanks [@MananTank](https://github.com/MananTank)! - Fix custom chain not being used
## 5.10.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
"version": "5.10.1",
"version": "5.11.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -18,7 +18,7 @@ export type SetClaimConditionsParams = {
* ```ts
* import { setClaimConditions } from "thirdweb/extensions/erc1155";
*
* const tx = await setClaimConditions({
* const tx = setClaimConditions({
* contract,
* tokenId: 0n,
* phases: [
@@ -1,14 +1,21 @@
import type { AbiParameterToPrimitiveType, Address } from "abitype";
import { maxUint256 } from "viem";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import {
NATIVE_TOKEN_ADDRESS,
isNativeTokenAddress,
} from "../../../constants/addresses.js";
import type { ThirdwebContract } from "../../../contract/contract.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { toBigInt } from "../../../utils/bigint.js";
import { dateToSeconds, tenYearsFromNow } from "../../../utils/date.js";
import type { Hex } from "../../../utils/encoding/hex.js";
import type { NFTInput } from "../../../utils/nft/parseNft.js";
import { randomBytes32 } from "../../../utils/uuid.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { mintWithSignature as generatedMintWithSignature } from "../__generated__/ISignatureMintERC1155/write/mintWithSignature.js";
import {
type MintWithSignatureParams,
mintWithSignature as generatedMintWithSignature,
} from "../__generated__/ISignatureMintERC1155/write/mintWithSignature.js";
/**
* Mints a new ERC1155 token with the given minter signature
@@ -29,7 +36,19 @@ import { mintWithSignature as generatedMintWithSignature } from "../__generated_
* @extension ERC1155
* @returns A promise that resolves to the transaction result.
*/
export const mintWithSignature = generatedMintWithSignature;
export function mintWithSignature(
options: BaseTransactionOptions<MintWithSignatureParams>,
) {
const value = isNativeTokenAddress(options.payload.currency)
? options.payload.pricePerToken * options.payload.quantity
: 0n;
return generatedMintWithSignature({
...options,
overrides: {
value,
},
});
}
export type GenerateMintSignatureOptions = {
account: Account;
@@ -61,6 +61,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("generateMintSignature1155", () => {
mintRequest: {
to: TEST_ACCOUNT_B.address,
quantity: 10n,
pricePerToken: "0.1",
metadata: {
name: "My NFT",
description: "This is my NFT",
@@ -1,12 +1,19 @@
import type { AbiParameterToPrimitiveType, Address } from "abitype";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import {
NATIVE_TOKEN_ADDRESS,
isNativeTokenAddress,
} from "../../../constants/addresses.js";
import type { ThirdwebContract } from "../../../contract/contract.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { dateToSeconds, tenYearsFromNow } from "../../../utils/date.js";
import type { Hex } from "../../../utils/encoding/hex.js";
import { randomBytes32 } from "../../../utils/uuid.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { name } from "../../common/read/name.js";
import { mintWithSignature as generatedMintWithSignature } from "../__generated__/ISignatureMintERC20/write/mintWithSignature.js";
import {
type MintWithSignatureParams,
mintWithSignature as generatedMintWithSignature,
} from "../__generated__/ISignatureMintERC20/write/mintWithSignature.js";
/**
* Mints a new ERC20 token with the given minter signature
@@ -27,7 +34,19 @@ import { mintWithSignature as generatedMintWithSignature } from "../__generated_
* @extension ERC20
* @returns A promise that resolves to the transaction result.
*/
export const mintWithSignature = generatedMintWithSignature;
export function mintWithSignature(
options: BaseTransactionOptions<MintWithSignatureParams>,
) {
const value = isNativeTokenAddress(options.payload.currency)
? options.payload.price
: 0n;
return generatedMintWithSignature({
...options,
overrides: {
value,
},
});
}
export type GenerateMintSignatureOptions = {
account: Account;
@@ -1,13 +1,20 @@
import type { AbiParameterToPrimitiveType, Address } from "abitype";
import type { Hex } from "viem";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import {
NATIVE_TOKEN_ADDRESS,
isNativeTokenAddress,
} from "../../../constants/addresses.js";
import type { ThirdwebContract } from "../../../contract/contract.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { toBigInt } from "../../../utils/bigint.js";
import { dateToSeconds, tenYearsFromNow } from "../../../utils/date.js";
import type { NFTInput } from "../../../utils/nft/parseNft.js";
import { randomBytes32 } from "../../../utils/uuid.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { mintWithSignature as generatedMintWithSignature } from "../__generated__/ISignatureMintERC721/write/mintWithSignature.js";
import {
type MintWithSignatureParams,
mintWithSignature as generatedMintWithSignature,
} from "../__generated__/ISignatureMintERC721/write/mintWithSignature.js";
/**
* Mints a new ERC721 token with the given minter signature
@@ -28,7 +35,19 @@ import { mintWithSignature as generatedMintWithSignature } from "../__generated_
* @extension ERC721
* @returns A promise that resolves to the transaction result.
*/
export const mintWithSignature = generatedMintWithSignature;
export function mintWithSignature(
options: BaseTransactionOptions<MintWithSignatureParams>,
) {
const value = isNativeTokenAddress(options.payload.currency)
? options.payload.price
: 0n;
return generatedMintWithSignature({
...options,
overrides: {
value,
},
});
}
export type GenerateMintSignatureOptions = {
account: Account;
@@ -10,6 +10,7 @@ import type {
// WalletWithPersonalAccount,
} from "../../../../wallets/interfaces/wallet.js";
import {
getLastConnectedChain,
getStoredActiveWalletId,
getStoredConnectedWalletIds,
} from "../../../../wallets/manager/index.js";
@@ -178,10 +179,13 @@ export function AutoConnect(props: AutoConnectProps) {
return;
}
const lastConnectedChain = await getLastConnectedChain(asyncLocalStorage);
async function handleWalletConnection(wallet: Wallet) {
setConnectionStatus("connecting");
return wallet.autoConnect({
client: props.client,
chain: lastConnectedChain ?? undefined,
});
}
@@ -1,5 +1,6 @@
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import { estimateGasCost } from "../../../../transaction/actions/estimate-gas-cost.js";
import type { GaslessOptions } from "../../../../transaction/actions/gasless/types.js";
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
@@ -34,6 +35,7 @@ type ShowModalData = {
*/
export function useSendTransactionCore(
showPayModal?: (data: ShowModalData) => void,
gasless?: GaslessOptions,
): UseMutationResult<WaitForReceiptOptions, Error, PreparedTransaction> {
const account = useActiveAccount();
@@ -47,6 +49,7 @@ export function useSendTransactionCore(
return sendTransaction({
transaction: tx,
account,
gasless,
});
}
@@ -56,6 +59,7 @@ export function useSendTransactionCore(
const res = await sendTransaction({
transaction: tx,
account,
gasless,
});
resolve(res);
@@ -171,13 +171,14 @@ export function useConnect() {
* Disconnect from given account
* @example
* ```jsx
* import { useDisconnect } from "thirdweb/react";
* import { useDisconnect, useActiveWallet } from "thirdweb/react";
*
* function Example() {
* const { disconnect } = useDisconnect();
* const wallet = useActiveWallet();
*
* return (
* <button onClick={() => disconnect(account)}>
* <button onClick={() => disconnect(wallet)}>
* Disconnect
* </button>
* );
@@ -1,5 +1,6 @@
import { useContext, useState } from "react";
import type { ThirdwebClient } from "../../../client/client.js";
import type { GaslessOptions } from "../../../transaction/actions/gasless/types.js";
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
import { useSendTransactionCore } from "../../core/hooks/contract/useSendTransaction.js";
import { SetRootElementContext } from "../../core/providers/RootElementContext.js";
@@ -47,6 +48,12 @@ export type SendTransactionConfig = {
theme?: Theme | "light" | "dark";
}
| false;
/**
* Configuration for gasless transactions.
* Refer to [`GaslessOptions`](https://portal.thirdweb.com/references/typescript/v5/GaslessOptions) for more details.
*/
gasless?: GaslessOptions;
};
/**
@@ -70,8 +77,9 @@ export function useSendTransaction(config: SendTransactionConfig = {}) {
const setRootEl = useContext(SetRootElementContext);
return useSendTransactionCore(
typeof payModal === "object"
? (data) => {
payModal === false
? undefined
: (data) => {
setRootEl(
<TxModal
tx={data.tx}
@@ -89,8 +97,8 @@ export function useSendTransaction(config: SendTransactionConfig = {}) {
nativeTokenSymbol={data.walletBalance.symbol}
/>,
);
}
: undefined,
},
config.gasless,
);
}
@@ -2,7 +2,6 @@ import styled from "@emotion/styled";
import {
ChevronRightIcon,
ExitIcon,
// EnterIcon,
PaperPlaneIcon,
PinBottomIcon,
PlusIcon,
@@ -16,7 +15,6 @@ import { getContract } from "../../../../contract/contract.js";
import { resolveAvatar } from "../../../../extensions/ens/resolve-avatar.js";
import { resolveName } from "../../../../extensions/ens/resolve-name.js";
import { isContractDeployed } from "../../../../utils/bytecode/is-contract-deployed.js";
import { getUserEmail } from "../../../../wallets/in-app/core/authentication/index.js";
import {
useChainQuery,
useChainsQuery,
@@ -27,7 +25,6 @@ import {
useActiveAccount,
useActiveWallet,
useActiveWalletChain,
// useConnect,
useDisconnect,
useSwitchActiveWalletChain,
} from "../../../core/hooks/wallets/wallet-hooks.js";
@@ -383,7 +380,10 @@ export const ConnectedWalletDetails: React.FC<{
<Container px="lg">
<ConnectedToSmartWallet />
<InAppWalletEmail />
{(activeWallet?.id === "embedded" || activeWallet?.id === "inApp") && (
<InAppWalletUserInfo />
)}
{/* Send, Receive, Swap */}
<Container
@@ -876,20 +876,31 @@ function ConnectedToSmartWallet() {
return null;
}
function InAppWalletEmail() {
function InAppWalletUserInfo() {
const { client } = useConnectUI();
const emailQuery = useQuery({
queryKey: ["in-app-wallet-user", client],
queryFn: async () => {
const data = await getUserEmail({
client: client,
});
const account = useActiveAccount();
return data || null;
const userInfoQuery = useQuery({
queryKey: ["in-app-wallet-user", client, account?.address],
queryFn: async () => {
const { getUserEmail, getUserPhoneNumber } = await import(
"../../../../wallets/in-app/core/authentication/index.js"
);
const [email, phone] = await Promise.all([
getUserEmail({
client: client,
}),
getUserPhoneNumber({
client: client,
}),
]);
return email || phone || null;
},
});
if (emailQuery.data) {
if (userInfoQuery.data) {
return (
<Container
flex="row"
@@ -898,7 +909,7 @@ function InAppWalletEmail() {
paddingBottom: spacing.md,
}}
>
<Text size="sm">{emailQuery.data}</Text>
<Text size="sm">{userInfoQuery.data}</Text>
</Container>
);
}
@@ -4,6 +4,7 @@ import { useActiveAccount } from "../../../../core/hooks/wallets/wallet-hooks.js
import {
useIsWalletModalOpen,
useSetIsWalletModalOpen,
useSetSelectionData,
} from "../../../providers/wallet-ui-states-provider.js";
import { Modal } from "../../components/Modal.js";
import { onModalUnmount, reservedScreens } from "../constants.js";
@@ -15,6 +16,7 @@ import { useSetupScreen } from "./screen.js";
*/
const ConnectModal = () => {
const screenSetup = useSetupScreen();
const setSelectionData = useSetSelectionData();
const { screen, setScreen, initialScreen } = screenSetup;
const isWalletModalOpen = useIsWalletModalOpen();
const setIsWalletModalOpen = useSetIsWalletModalOpen();
@@ -25,8 +27,9 @@ const ConnectModal = () => {
setIsWalletModalOpen(false);
onModalUnmount(() => {
setScreen(initialScreen);
setSelectionData({});
});
}, [initialScreen, setIsWalletModalOpen, setScreen]);
}, [initialScreen, setIsWalletModalOpen, setScreen, setSelectionData]);
const activeAccount = useActiveAccount();
@@ -72,16 +75,8 @@ const ConnectModal = () => {
return;
}
setIsWalletModalOpen(value);
if (!value) {
onModalUnmount(() => {
// if (connectionStatus === "connecting") {
// disconnect();
// }
setScreen(initialScreen);
});
closeModal();
}
}}
>
@@ -3,6 +3,7 @@ import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import { useSiweAuth } from "../../../../core/hooks/auth/useSiweAuth.js";
import { useConnectUI } from "../../../../core/hooks/others/useWalletConnectionCtx.js";
import { useConnect } from "../../../../core/hooks/wallets/wallet-hooks.js";
import { useSetSelectionData } from "../../../providers/wallet-ui-states-provider.js";
import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js";
import { WalletSelector } from "../WalletSelector.js";
import { onModalUnmount, reservedScreens } from "../constants.js";
@@ -39,6 +40,7 @@ export const ConnectModalContent = (props: {
client,
} = useConnectUI();
const { connect } = useConnect();
const setSelectionData = useSetSelectionData();
const siweAuth = useSiweAuth(auth);
const showSignatureScreen = siweAuth.requiresAuth && !siweAuth.isLoggedIn;
@@ -52,6 +54,7 @@ export const ConnectModalContent = (props: {
}
onModalUnmount(() => {
setSelectionData({});
setModalVisibility(true);
});
@@ -69,6 +72,7 @@ export const ConnectModalContent = (props: {
connect,
showSignatureScreen,
setScreen,
setSelectionData,
],
);
@@ -1,102 +0,0 @@
import { PinBottomIcon } from "@radix-ui/react-icons";
import type { LocalWallet } from "../../../../../wallets/local/index._ts";
import {
useActiveAccount,
useActiveWallet,
} from "../../../../core/hooks/wallets/wallet-hooks.js";
import { shortenString } from "../../../../core/utils/addresses.js";
import { downloadTextFile } from "../../../wallets/local/utils/downloadTextFile.js";
import { Spacer } from "../../components/Spacer.js";
import {
Container,
ModalHeader,
Line,
ScreenBottomContainer,
} from "../../components/basic.js";
import { Button } from "../../components/buttons.js";
import { Label } from "../../components/formElements.js";
import { spacing, iconSize } from "../../design-system/index.js";
import { Text } from "../../components/text.js";
import { useState, useEffect } from "react";
import { useWalletConnectionCtx } from "../../../../core/hooks/others/useWalletConnectionCtx.js";
import { getLocalWalletLocale } from "../../../wallets/local/locale/getLocalWalletLocale.js";
import type { LocalWalletLocale } from "../../../wallets/local/locale/types.js";
import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js";
/**
* Export the private key and download it as a text file for current active local wallet
* @internal
*/
export function ExportLocalWallet(props: {
onExport: () => void;
onBack: () => void;
}) {
const { locale: localeId } = useWalletConnectionCtx();
const [locale, setLocale] = useState<LocalWalletLocale | undefined>();
useEffect(() => {
getLocalWalletLocale(localeId).then((_local) => {
setLocale(_local);
});
}, [localeId]);
const isWideScreen = false;
const wallet = useActiveWallet() as LocalWallet;
const account = useActiveAccount();
if (!locale) {
return <LoadingScreen />;
}
const handleExport = async () => {
const privateKey = await wallet.export({
strategy: "privateKey",
encryption: false,
});
downloadTextFile(privateKey, "privateKey.txt");
props.onExport();
};
return (
<Container fullHeight animate="fadein">
<div>
<Container p="lg">
<ModalHeader
onBack={props.onBack}
title={locale.exportScreen.title}
/>
</Container>
<Line />
<Container expand p="lg">
<Text multiline>{locale.exportScreen.downloadMessage}</Text>
<Spacer y="xl" />
<Label>{locale.exportScreen.walletAddress}</Label>
<Spacer y="sm" />
<Text>{shortenString(account?.address || "")}</Text>
</Container>
<Spacer y="lg" />
<ScreenBottomContainer
style={{
borderTop: isWideScreen ? "none" : undefined,
}}
>
<Button
variant="accent"
fullWidth
style={{
display: "flex",
gap: spacing.sm,
}}
type="submit"
onClick={handleExport}
>
<PinBottomIcon width={iconSize.sm} height={iconSize.sm} />
{locale.exportScreen.download}
</Button>
</ScreenBottomContainer>
</div>
</Container>
);
}
@@ -11,3 +11,5 @@ export const StyledInput = /* @__PURE__ */ styled.input;
export const StyledH2 = /* @__PURE__ */ styled.h2;
export const StyledP = /* @__PURE__ */ styled.p;
export const StyledUl = /* @__PURE__ */ styled.ul;
export const StyledSelect = /* @__PURE__ */ styled.select;
export const StyledOption = /* @__PURE__ */ styled.option;
@@ -0,0 +1,111 @@
import { useQuery } from "@tanstack/react-query";
import { useRef } from "react";
import { useCustomTheme } from "../../ui/design-system/CustomThemeProvider.js";
import { StyledOption, StyledSelect } from "../../ui/design-system/elements.js";
import { fontSize, spacing } from "../../ui/design-system/index.js";
export function CountrySelector({
countryCode,
setCountryCode,
}: {
countryCode: string;
setCountryCode: React.Dispatch<React.SetStateAction<string>>;
}) {
const selectRef = useRef<HTMLSelectElement>(null);
const { data: supportedCountries } = useQuery({
queryKey: ["supported-sms-countries"],
queryFn: async () => {
const { supportedSmsCountries } = await import(
"./supported-sms-countries.js"
);
return supportedSmsCountries;
},
});
const supportedCountriesForSms = supportedCountries ?? [
{
countryIsoCode: "US",
countryName: "United States",
phoneNumberCode: 1,
},
];
return (
<>
<Select
ref={selectRef}
name="countries"
id="countries"
value={countryCode}
onChange={(e) => {
setCountryCode(e.target.value);
}}
>
<Option
style={{
display: "none",
}}
value={countryCode}
>
{countryCode}
</Option>
{supportedCountriesForSms.map((country) => {
return (
<Option
key={country.countryIsoCode}
value={`${country.countryIsoCode} +${country.phoneNumberCode}`}
>
{country.countryName} +{country.phoneNumberCode}{" "}
</Option>
);
})}
</Select>
</>
);
}
const Option = /* @__PURE__ */ StyledOption(() => {
const theme = useCustomTheme();
return {
color: theme.colors.primaryText,
background: theme.colors.modalBg,
transition: "background 0.3s ease",
"&:hover": {
background: theme.colors.tertiaryBg,
},
};
});
const Select = /* @__PURE__ */ StyledSelect(() => {
const theme = useCustomTheme();
return {
fontSize: fontSize.sm,
display: "block",
padding: spacing.sm,
boxSizing: "border-box",
outline: "none",
border: "none",
borderRadius: "6px",
color: theme.colors.primaryText,
WebkitAppearance: "none",
appearance: "none",
cursor: "pointer",
background: "transparent",
"&::placeholder": {
color: theme.colors.secondaryText,
},
boxShadow: `0 0 0 1.5px ${theme.colors.borderColor}`,
"&:focus, &:hover": {
boxShadow: `0 0 0 2px ${theme.colors.accentText}`,
},
"&[disabled]": {
cursor: "not-allowed",
},
minWidth: "0px",
maxWidth: "90px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
};
});
@@ -24,10 +24,16 @@ function InAppWalletConnectUI(props: {
return <LoadingScreen />;
}
if (state?.emailLogin) {
const otpUserInfo = state?.emailLogin
? { email: state.emailLogin }
: state?.phoneLogin
? { phone: state.phoneLogin }
: undefined;
if (otpUserInfo) {
return (
<InAppWalletOTPLoginUI
email={state.emailLogin}
userInfo={otpUserInfo}
locale={locale}
done={props.done}
goBack={props.goBack}
@@ -1,4 +1,5 @@
import styled from "@emotion/styled";
import { useCallback, useState } from "react";
import type {
InAppWalletAuth,
InAppWalletSocialAuth,
@@ -17,12 +18,16 @@ import { Button } from "../../ui/components/buttons.js";
import { useCustomTheme } from "../../ui/design-system/CustomThemeProvider.js";
import { fontSize, iconSize, spacing } from "../../ui/design-system/index.js";
import { InputSelectionUI } from "./InputSelectionUI.js";
import { LinkButton } from "./LinkButton.js";
import type { InAppWalletLocale } from "./locale/types.js";
import { openOauthSignInWindow } from "./openOauthSignInWindow.js";
import { socialIcons } from "./socialIcons.js";
import type { InAppWalletSelectUIState } from "./types.js";
import { validateEmail } from "./validateEmail.js";
const defaultAuthOptions: InAppWalletAuth[] = [
"email",
"phone",
"google",
"apple",
"facebook",
@@ -43,7 +48,9 @@ export const InAppWalletFormUI = (props: InAppWalletFormUIProps) => {
const locale = props.locale;
const { chain, client, connectModal } = useConnectUI();
const { done, wallet } = props;
const setData = useSetSelectionData();
const setData = useSetSelectionData() as (
value: InAppWalletSelectUIState,
) => void;
const themeObj = useCustomTheme();
@@ -54,12 +61,48 @@ export const InAppWalletFormUI = (props: InAppWalletFormUIProps) => {
};
const config = props.wallet.getConfig();
const authOptions = config?.auth?.options || defaultAuthOptions;
const enableEmailLogin = authOptions.includes("email");
const emailIndex = authOptions.indexOf("email");
const isEmailEnabled = emailIndex !== -1;
const phoneIndex = authOptions.indexOf("phone");
const isPhoneEnabled = phoneIndex !== -1;
const [inputMode, setInputMode] = useState<"email" | "phone" | "none">(() => {
if (isEmailEnabled && isPhoneEnabled) {
return emailIndex < phoneIndex ? "email" : "phone";
}
if (isEmailEnabled) {
return "email";
}
if (isPhoneEnabled) {
return "phone";
}
return "none";
});
const placeholder =
inputMode === "email" ? locale.emailPlaceholder : locale.phonePlaceholder;
const emptyErrorMessage =
inputMode === "email" ? locale.emailRequired : locale.phoneRequired;
let type = "text";
if (inputMode === "email") {
type = "email";
} else if (inputMode === "phone") {
type = "tel";
}
const switchInputModeText =
inputMode === "email" ? locale.signInWithPhone : locale.signInWithEmail;
const switchInputMode = useCallback(() => {
setInputMode((prev) => (prev === "email" ? "phone" : "email"));
}, []);
const allowSwitchInputMode = isEmailEnabled && isPhoneEnabled;
const socialLogins = authOptions.filter(
(x) => x !== "email",
(x) => x !== "email" && x !== "phone",
) as InAppWalletSocialAuth[];
const hasSocialLogins = socialLogins.length > 0;
@@ -100,7 +143,13 @@ export const InAppWalletFormUI = (props: InAppWalletFormUIProps) => {
const showOnlyIcons = socialLogins.length > 1;
return (
<Container flex="column" gap="lg">
<Container
flex="column"
gap="lg"
style={{
position: "relative",
}}
>
{/* Social Login */}
{hasSocialLogins && (
<Container
@@ -137,36 +186,67 @@ export const InAppWalletFormUI = (props: InAppWalletFormUIProps) => {
</Container>
)}
{connectModal.size === "wide" && hasSocialLogins && enableEmailLogin && (
<TextDivider text={locale.or} />
)}
{connectModal.size === "wide" &&
hasSocialLogins &&
(isEmailEnabled || isPhoneEnabled) && <TextDivider text={locale.or} />}
{/* Email Login */}
{enableEmailLogin && (
<InputSelectionUI
onSelect={(email) => {
setData({
emailLogin: email,
});
props.select();
}}
placeholder={locale.emailPlaceholder}
name="email"
type="email"
errorMessage={(_input) => {
const input = _input.replace(/\+/g, "").toLowerCase();
const emailRegex =
/^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,})$/g;
const isValidEmail = emailRegex.test(input);
if (!isValidEmail) {
return locale.invalidEmail;
}
{inputMode !== "none" && (
<Container>
{inputMode === "email" ? (
<InputSelectionUI
type={type}
onSelect={(value) => {
setData({ emailLogin: value });
props.select();
}}
placeholder={placeholder}
name="email"
errorMessage={(input) => {
const isValidEmail = validateEmail(input.toLowerCase());
if (!isValidEmail) {
return locale.invalidEmail;
}
return undefined;
}}
emptyErrorMessage={emptyErrorMessage}
submitButtonText={locale.submitEmail}
/>
) : (
<InputSelectionUI
format="phone"
type={type}
onSelect={(value) => {
// removes white spaces and special characters
setData({ phoneLogin: value.replace(/[-\(\) ]/g, "") });
props.select();
}}
placeholder={placeholder}
name="phone"
errorMessage={(_input) => {
// removes white spaces and special characters
const input = _input.replace(/[-\(\) ]/g, "");
const isPhone = /^[0-9]+$/.test(input);
return undefined;
}}
emptyErrorMessage={locale.emailRequired}
submitButtonText={locale.submitEmail}
/>
if (!isPhone && isPhoneEnabled) {
return locale.invalidPhone;
}
return undefined;
}}
emptyErrorMessage={emptyErrorMessage}
submitButtonText={locale.submitEmail}
/>
)}
{allowSwitchInputMode && (
<>
<Spacer y="md" />
<LinkButton onClick={switchInputMode} type="button">
{switchInputModeText}
</LinkButton>
</>
)}
</Container>
)}
</Container>
);
@@ -21,60 +21,89 @@ type VerificationStatus =
| "valid"
| "idle"
| "payment_required";
type EmailStatus = "sending" | SendEmailOtpReturnType | "error";
type ScreenToShow =
| "base"
| "create-password"
| "enter-password-or-recovery-code";
type AccountStatus = "sending" | SendEmailOtpReturnType | "error";
type ScreenToShow = "base" | "enter-password-or-recovery-code";
/**
* @internal
*/
export function InAppWalletOTPLoginUI(props: {
userInfo: { email: string } | { phone: string };
wallet: Wallet<"inApp">;
email: string;
locale: InAppWalletLocale;
done: () => void;
goBack?: () => void;
}) {
const { wallet, done, goBack } = props;
const email = props.email;
const { wallet, done, goBack, userInfo } = props;
const { client, chain, connectModal } = useConnectUI();
const isWideModal = connectModal.size === "wide";
const locale = props.locale;
const [otpInput, setOtpInput] = useState("");
const [verifyStatus, setVerifyStatus] = useState<VerificationStatus>("idle");
const [emailStatus, setEmailStatus] = useState<EmailStatus>("sending");
const [accountStatus, setAccountStatus] = useState<AccountStatus>("sending");
const [screen] = useState<ScreenToShow>("base");
const sendEmail = useCallback(async () => {
const sendEmailOrSms = useCallback(async () => {
setOtpInput("");
setVerifyStatus("idle");
setEmailStatus("sending");
setAccountStatus("sending");
try {
const status = await preAuthenticate({
email,
strategy: "email",
client,
});
setEmailStatus(status);
if ("email" in userInfo) {
const status = await preAuthenticate({
email: userInfo.email,
strategy: "email",
client,
});
setAccountStatus(status);
} else if ("phone" in userInfo) {
const status = await preAuthenticate({
phoneNumber: userInfo.phone,
strategy: "phone",
client,
});
setAccountStatus(status);
} else {
throw new Error("Invalid userInfo");
}
} catch (e) {
console.error(e);
setVerifyStatus("idle");
setEmailStatus("error");
setAccountStatus("error");
}
}, [client, email]);
}, [client, userInfo]);
async function connect(otp: string) {
if ("email" in userInfo) {
await wallet.connect({
chain,
strategy: "email",
email: userInfo.email,
verificationCode: otp,
client,
});
} else if ("phone" in userInfo) {
await wallet.connect({
chain,
strategy: "phone",
phoneNumber: userInfo.phone,
verificationCode: otp,
client,
});
} else {
throw new Error("Invalid userInfo");
}
}
const verify = async (otp: string) => {
if (typeof emailStatus !== "object" || otp.length !== 6) {
if (typeof accountStatus !== "object" || otp.length !== 6) {
return;
}
setVerifyStatus("idle");
if (typeof emailStatus !== "object") {
if (typeof accountStatus !== "object") {
return;
}
@@ -86,23 +115,16 @@ export function InAppWalletOTPLoginUI(props: {
setVerifyStatus("verifying");
const needsRecoveryCode =
emailStatus.recoveryShareManagement === "USER_MANAGED" &&
(emailStatus.isNewUser || emailStatus.isNewDevice);
accountStatus.recoveryShareManagement === "USER_MANAGED" &&
(accountStatus.isNewUser || accountStatus.isNewDevice);
// USER_MANAGED
if (needsRecoveryCode) {
if (emailStatus.isNewUser) {
if (accountStatus.isNewUser) {
try {
await wallet.connect({
chain,
strategy: "email",
email,
verificationCode: otp,
client,
});
await connect(otp);
} catch (e) {
if (e instanceof Error && e.message.includes("encryption key")) {
// TODO: do we need this?
// setScreen("create-password");
} else {
throw e;
@@ -111,13 +133,7 @@ export function InAppWalletOTPLoginUI(props: {
} else {
try {
// verifies otp for UI feedback
await wallet.connect({
chain,
strategy: "email",
email,
verificationCode: otp,
client,
});
await connect(otp);
} catch (e) {
if (e instanceof Error && e.message.includes("encryption key")) {
// TODO: do we need this?
@@ -131,17 +147,8 @@ export function InAppWalletOTPLoginUI(props: {
// AWS_MANAGED
else {
const authResult = await wallet.connect({
chain,
strategy: "email",
email,
verificationCode: otp,
client,
});
if (!authResult) {
throw new Error("Failed to verify OTP");
}
// verifies otp for UI feedback
await connect(otp);
done();
}
@@ -164,9 +171,9 @@ export function InAppWalletOTPLoginUI(props: {
useEffect(() => {
if (!emailSentOnMount.current) {
emailSentOnMount.current = true;
sendEmail();
sendEmailOrSms();
}
}, [sendEmail]);
}, [sendEmailOrSms]);
if (screen === "base") {
return (
@@ -185,7 +192,9 @@ export function InAppWalletOTPLoginUI(props: {
{!isWideModal && <Spacer y="xl" />}
<Text>{locale.emailLoginScreen.enterCodeSendTo}</Text>
<Spacer y="sm" />
<Text color="primaryText">{email}</Text>
<Text color="primaryText">
{"email" in userInfo ? userInfo.email : userInfo.phone}
</Text>
<Spacer y="xl" />
</Container>
@@ -251,7 +260,7 @@ export function InAppWalletOTPLoginUI(props: {
{!isWideModal && <Line />}
<Container p={isWideModal ? undefined : "lg"}>
{emailStatus === "error" && (
{accountStatus === "error" && (
<>
<Text size="sm" center color="danger">
{locale.emailLoginScreen.failedToSendCode}
@@ -259,7 +268,7 @@ export function InAppWalletOTPLoginUI(props: {
</>
)}
{emailStatus === "sending" && (
{accountStatus === "sending" && (
<Container
flex="row"
center="both"
@@ -273,8 +282,8 @@ export function InAppWalletOTPLoginUI(props: {
</Container>
)}
{typeof emailStatus === "object" && (
<LinkButton onClick={sendEmail} type="button">
{typeof accountStatus === "object" && (
<LinkButton onClick={sendEmailOrSms} type="button">
{locale.emailLoginScreen.resendCode}
</LinkButton>
)}
@@ -3,10 +3,9 @@ import { Spacer } from "../../ui/components/Spacer.js";
import { Button } from "../../ui/components/buttons.js";
import { Input } from "../../ui/components/formElements.js";
import { Text } from "../../ui/components/text.js";
import { spacing } from "../../ui/design-system/index.js";
import { CountrySelector } from "./CountrySelector.js";
/**
* @internal
*/
export function InputSelectionUI(props: {
onSelect: (data: string) => void;
placeholder: string;
@@ -15,7 +14,9 @@ export function InputSelectionUI(props: {
errorMessage?: (input: string) => string | undefined;
emptyErrorMessage?: string;
submitButtonText: string;
format?: "phone";
}) {
const [countryCodeInfo, setCountryCodeInfo] = useState("US +1");
const [input, setInput] = useState("");
const [error, setError] = useState<string | undefined>();
const [showError, setShowError] = useState(false);
@@ -26,7 +27,11 @@ export function InputSelectionUI(props: {
return;
}
props.onSelect(input);
props.onSelect(
props.format === "phone"
? `+${countryCodeInfo.split("+")[1]}${input}`
: input,
);
};
const renderingError =
@@ -34,15 +39,31 @@ export function InputSelectionUI(props: {
(!input && !!props.emptyErrorMessage && showError);
return (
<div>
<div
style={{
width: "100%",
}}
>
<div
style={{
position: "relative",
display: "flex",
flexDirection: "row",
gap: spacing.sm,
}}
>
{props.format === "phone" && (
<CountrySelector
countryCode={countryCodeInfo}
setCountryCode={setCountryCodeInfo}
/>
)}
<Input
tabIndex={-1}
placeholder={props.placeholder}
style={{
flexGrow: 1,
}}
variant="outline"
type={props.type}
name={props.name}
@@ -0,0 +1,19 @@
import { useCustomTheme } from "../../ui/design-system/CustomThemeProvider.js";
import { StyledButton } from "../../ui/design-system/elements.js";
import { fontSize } from "../../ui/design-system/index.js";
export const LinkButton = /* @__PURE__ */ StyledButton(() => {
const theme = useCustomTheme();
return {
all: "unset",
color: theme.colors.accentText,
fontSize: fontSize.sm,
cursor: "pointer",
textAlign: "center",
fontWeight: 500,
width: "100%",
"&:hover": {
color: theme.colors.primaryText,
},
};
});
@@ -48,4 +48,9 @@ export default {
submitButton: "Verify",
wrongPassword: "Wrong password",
},
signInWithEmail: "Sign in with Email",
invalidPhone: "Invalid phone number",
phonePlaceholder: "Enter phone number",
signInWithPhone: "Sign in with phone number",
phoneRequired: "phone number is required",
} satisfies InAppWalletLocale;
@@ -48,4 +48,9 @@ export default {
submitButton: "Verificar",
wrongPassword: "Contraseña incorrecta",
},
invalidPhone: "Número de teléfono inválido",
phonePlaceholder: "Ingresa tu número de teléfono",
signInWithPhone: "Iniciar sesión con número de teléfono",
phoneRequired: "Se requiere número de teléfono",
signInWithEmail: "Iniciar sesión con correo electrónico",
} satisfies InAppWalletLocale;
@@ -47,4 +47,9 @@ export default {
submitButton: "確認",
wrongPassword: "パスワードが違います",
},
invalidPhone: "無効な電話番号",
signInWithEmail: "電子メールでサインインする",
phonePlaceholder: "電話番号を入力してください",
signInWithPhone: "電話番号でログイン",
phoneRequired: "電話番号が必要です",
} satisfies InAppWalletLocale;
@@ -48,4 +48,9 @@ export default {
submitButton: "Patunayan",
wrongPassword: "Maling password",
},
signInWithEmail: "Mag-sign in gamit ang email",
invalidPhone: "Hindi wastong numero ng telepono",
phonePlaceholder: "Ilagay ang iyong numero ng telepono",
signInWithPhone: "Mag-login gamit ang numero ng telepono",
phoneRequired: "Kinakailangan ang numero ng telepono",
} satisfies InAppWalletLocale;
@@ -29,6 +29,11 @@ export type InAppWalletLocale = {
title: string;
wrongPassword: string;
};
phonePlaceholder: string;
phoneRequired: string;
signInWithPhone: string;
signInWithEmail: string;
invalidPhone: string;
or: string;
invalidEmail: string;
signIn: string;
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,7 @@ export type InAppWalletSelectUIState =
| undefined
| {
emailLogin?: string;
phoneLogin?: string;
socialLogin?: {
type: InAppWalletSocialAuth;
connectionPromise: Promise<Account>;
@@ -0,0 +1,7 @@
// A super basic email validation function that is very forgiving to allow for a wide range emails
export function validateEmail(str: string) {
// <string> + @ + <string> + . + <string>
const emailRegex = /^\S+@\S+\.\S+$/;
return emailRegex.test(str.replace(/\+/g, ""));
}
@@ -0,0 +1,82 @@
import { beforeAll, describe, expect, it } from "vitest";
import { FORKED_ETHEREUM_CHAIN_WITH_MINING } from "../../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../../test/src/test-wallets.js";
import {
type ThirdwebContract,
getContract,
} from "../../../../contract/contract.js";
import { mintTo } from "../../../../extensions/erc20/write/mintTo.js";
import { transfer } from "../../../../extensions/erc20/write/transfer.js";
import { deployERC20Contract } from "../../../../extensions/prebuilts/deploy-erc20.js";
import { isHex } from "../../../../utils/encoding/hex.js";
import { sendAndConfirmTransaction } from "../../send-and-confirm-transaction.js";
import { toSerializableTransaction } from "../../to-serializable-transaction.js";
import { prepareBiconomyTransaction } from "./biconomy.js";
describe.runIf(process.env.TW_SECRET_KEY)("prepareBiconomyTransaction", () => {
let erc20Contract: ThirdwebContract;
beforeAll(async () => {
erc20Contract = getContract({
address: await deployERC20Contract({
account: TEST_ACCOUNT_A,
chain: FORKED_ETHEREUM_CHAIN_WITH_MINING,
client: TEST_CLIENT,
params: { name: "Test Token", symbol: "TST" },
type: "TokenERC20",
}),
chain: FORKED_ETHEREUM_CHAIN_WITH_MINING,
client: TEST_CLIENT,
});
// mint some tokens to the account
await sendAndConfirmTransaction({
transaction: mintTo({
contract: erc20Contract,
amount: "1000",
to: TEST_ACCOUNT_A.address,
}),
account: TEST_ACCOUNT_A,
});
}, 60_000);
it("should prepare a Biconomy transaction", async () => {
const transaction = transfer({
amount: "100",
contract: erc20Contract,
to: TEST_ACCOUNT_B.address,
});
// seralizable transaction
const serializableTransaction = await toSerializableTransaction({
transaction,
from: TEST_ACCOUNT_A.address,
});
const result = await prepareBiconomyTransaction({
account: TEST_ACCOUNT_A,
serializableTransaction,
transaction,
gasless: {
apiId: "TEST_ID",
apiKey: "TEST_KEY",
// mainnet forwarder address
relayerForwarderAddress: "0x84a0856b038eaAd1cC7E297cF34A7e72685A8693",
provider: "biconomy",
},
});
const [request, signature] = result;
expect(isHex(signature)).toBe(true);
expect(request.batchId).toBe(0n);
expect(request.batchNonce).toBe(0n);
expect(request.data).toMatchInlineSnapshot(
`"0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000"`,
);
expect(request.deadline).toBeGreaterThan(0);
expect(request.from).toBe(TEST_ACCOUNT_A.address);
expect(request.to).toBe(erc20Contract.address);
});
});
@@ -0,0 +1,150 @@
import type { Address } from "abitype";
import { type TransactionSerializable, encodeAbiParameters } from "viem";
import { ADDRESS_ZERO } from "../../../../constants/addresses.js";
import { getContract } from "../../../../contract/contract.js";
import { isHex } from "../../../../utils/encoding/helpers/is-hex.js";
import { keccak256 } from "../../../../utils/hashing/keccak256.js";
import { stringify } from "../../../../utils/json.js";
import type { Account } from "../../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../../prepare-transaction.js";
import { readContract } from "../../../read-contract.js";
import type { WaitForReceiptOptions } from "../../wait-for-tx-receipt.js";
export type BiconomyOptions = {
provider: "biconomy";
// you can find the correct forwarder for your network here: https://docs-gasless.biconomy.io/misc/contract-addresses
relayerForwarderAddress: Address;
apiId: string;
apiKey: string;
deadlineSeconds?: number; // default: 3600
};
type SendBiconomyTransactionOptions = {
account: Account;
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction<any>;
serializableTransaction: TransactionSerializable;
gasless: BiconomyOptions;
};
// we do not send multiple batches so this stays consistent
const BATCH_ID = 0n;
/**
* @internal - only exported for testing
*/
export async function prepareBiconomyTransaction({
account,
serializableTransaction,
transaction,
gasless,
}: SendBiconomyTransactionOptions) {
const forwarderContract = getContract({
address: gasless.relayerForwarderAddress,
chain: transaction.chain,
client: transaction.client,
});
// get the nonce
const nonce = await readContract({
contract: forwarderContract,
method: "function getNonce(address,uint256) view returns (uint256)",
params: [account.address, BATCH_ID],
});
const deadline =
Math.floor(Date.now() / 1000) + (gasless.deadlineSeconds ?? 3600);
const request = {
from: account.address,
to: serializableTransaction.to,
token: ADDRESS_ZERO,
txGas: serializableTransaction.gas,
tokenGasPrice: 0n,
batchId: BATCH_ID,
batchNonce: nonce,
deadline: deadline,
data: serializableTransaction.data,
};
if (!request.to) {
throw new Error("Cannot send a transaction without a `to` address");
}
if (!request.txGas) {
throw new Error("Cannot send a transaction without a `gas` value");
}
if (!request.data) {
throw new Error("Cannot send a transaction without a `data` value");
}
// create the hash
const message = encodeAbiParameters(
[
{ type: "address" },
{ type: "address" },
{ type: "address" },
{ type: "uint256" },
{ type: "uint256" },
{ type: "uint256" },
{ type: "uint256" },
{ type: "bytes32" },
],
[
request.from,
request.to,
request.token,
request.txGas,
request.tokenGasPrice,
request.batchId,
request.batchNonce,
keccak256(request.data),
],
);
const signature = await account.signMessage({ message });
return [request, signature] as const;
}
/**
* @internal
*/
export async function relayBiconomyTransaction(
options: SendBiconomyTransactionOptions,
): Promise<WaitForReceiptOptions> {
const [request, signature] = await prepareBiconomyTransaction(options);
// send the transaction to the biconomy api
const response = await fetch(
"https://api.biconomy.io/api/v2/meta-tx/native",
{
method: "POST",
body: stringify({
apiId: options.gasless.apiId,
params: [request, signature],
from: request.from,
to: request.to,
gasLimit: request.txGas,
}),
headers: {
"x-api-key": options.gasless.apiKey,
"Content-Type": "application/json;charset=utf-8",
},
},
);
if (!response.ok) {
response.body?.cancel();
throw new Error(`Failed to send transaction: ${await response.text()}`);
}
const json = await response.json();
const transactionHash = json.txHash;
if (isHex(transactionHash)) {
return {
transactionHash: transactionHash,
chain: options.transaction.chain,
client: options.transaction.client,
};
}
throw new Error(`Failed to send transaction: ${stringify(json)}`);
}
@@ -0,0 +1,118 @@
import { beforeAll, describe, expect, it } from "vitest";
import { FORKED_ETHEREUM_CHAIN_WITH_MINING } from "../../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../../test/src/test-wallets.js";
import {
type ThirdwebContract,
getContract,
} from "../../../../contract/contract.js";
import { polygon } from "../../../../exports/chains.js";
import { generateAccount } from "../../../../exports/wallets.js";
import { mintTo } from "../../../../extensions/erc20/write/mintTo.js";
import { transfer } from "../../../../extensions/erc20/write/transfer.js";
import { deployERC20Contract } from "../../../../extensions/prebuilts/deploy-erc20.js";
import { add } from "../../../../extensions/thirdweb/__generated__/ITWMultichainRegistry/write/add.js";
import { isHex } from "../../../../utils/encoding/hex.js";
import { sendAndConfirmTransaction } from "../../send-and-confirm-transaction.js";
import { sendTransaction } from "../../send-transaction.js";
import { toSerializableTransaction } from "../../to-serializable-transaction.js";
import { prepareEngineTransaction } from "./engine.js";
describe.runIf(process.env.TW_SECRET_KEY)("prepareengineTransaction", () => {
let erc20Contract: ThirdwebContract;
beforeAll(async () => {
erc20Contract = getContract({
address: await deployERC20Contract({
account: TEST_ACCOUNT_A,
chain: FORKED_ETHEREUM_CHAIN_WITH_MINING,
client: TEST_CLIENT,
params: { name: "Test Token", symbol: "TST" },
type: "TokenERC20",
}),
chain: FORKED_ETHEREUM_CHAIN_WITH_MINING,
client: TEST_CLIENT,
});
// mint some tokens to the account
await sendAndConfirmTransaction({
transaction: mintTo({
contract: erc20Contract,
amount: "1000",
to: TEST_ACCOUNT_A.address,
}),
account: TEST_ACCOUNT_A,
});
}, 60_000);
it("should prepare an engine relay transaction", async () => {
const transaction = transfer({
amount: "100",
contract: erc20Contract,
to: TEST_ACCOUNT_B.address,
});
// seralizable transaction
const serializableTransaction = await toSerializableTransaction({
transaction,
from: TEST_ACCOUNT_A.address,
});
const result = await prepareEngineTransaction({
account: TEST_ACCOUNT_A,
serializableTransaction,
transaction,
gasless: {
provider: "engine",
relayerUrl: "https://safe-transaction.engine.com/api/v1",
// mainnet forwarder address
relayerForwarderAddress: "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81",
},
});
const { message, messageType, signature } = result;
expect(isHex(signature)).toBe(true);
expect(message).toMatchInlineSnapshot(`
{
"data": "0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"gas": 64338n,
"nonce": 0n,
"to": "${erc20Contract.address}",
"value": 0n,
}
`);
expect(messageType).toMatchInlineSnapshot(`"forward"`);
});
// This is more of an integration test, re-enable to test relaying functionality
it.skip("should send a real tx", async () => {
const multichainRegistry = getContract({
address: "0xcdAD8FA86e18538aC207872E8ff3536501431B73",
chain: polygon,
client: TEST_CLIENT,
});
const transaction = add({
contract: multichainRegistry,
chainId: 1337n,
deployer: TEST_ACCOUNT_A.address,
deployment: (await generateAccount({ client: TEST_CLIENT })).address,
metadataUri: "",
});
const res = await sendTransaction({
account: TEST_ACCOUNT_A,
transaction,
gasless: {
provider: "engine",
relayerUrl:
"https://checkout.engine.thirdweb.com/relayer/0c2bdd3a-307f-4243-b6e5-5ba495222d2b",
relayerForwarderAddress: "0x409d530a6961297ece29121dbee2c917c3398659",
experimentalChainlessSupport: true,
},
});
expect(res.transactionHash).toBeDefined();
expect(res.transactionHash.length).toBe(66);
});
});
@@ -0,0 +1,223 @@
import type { Address } from "abitype";
import type { TransactionSerializable } from "viem";
import { getContract } from "../../../../contract/contract.js";
import { stringify } from "../../../../utils/json.js";
import type { Account } from "../../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../../prepare-transaction.js";
import { readContract } from "../../../read-contract.js";
import {
type WaitForReceiptOptions,
waitForReceipt,
} from "../../wait-for-tx-receipt.js";
export type EngineOptions = {
provider: "engine";
relayerUrl: string;
relayerForwarderAddress: Address;
domainName?: string; // default: "GSNv2 Forwarder"
domainVersion?: string; // default: "0.0.1"
domainSeparatorVersion?: string; // default: "1"
experimentalChainlessSupport?: boolean; // default: false
};
type SendengineTransactionOptions = {
account: Account;
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction<any>;
serializableTransaction: TransactionSerializable;
gasless: EngineOptions;
};
/**
* @internal - only exported for testing
*/
export async function prepareEngineTransaction({
account,
serializableTransaction,
transaction,
gasless,
}: SendengineTransactionOptions) {
const forrwaderContract = getContract({
address: gasless.relayerForwarderAddress,
chain: transaction.chain,
client: transaction.client,
});
const nonce = await readContract({
contract: forrwaderContract,
method: "function getNonce(address) view returns (uint256)",
params: [account.address],
});
const [signature, message] = await (async () => {
// TODO: handle special case for `approve` -> `permit` transactions
if (!serializableTransaction.to) {
throw new Error("engine transactions must have a 'to' address");
}
if (!serializableTransaction.gas) {
throw new Error("engine transactions must have a 'gas' value");
}
if (!serializableTransaction.data) {
throw new Error("engine transactions must have a 'data' value");
}
// chainless support!
if (gasless.experimentalChainlessSupport) {
const message = {
from: account.address,
to: serializableTransaction.to,
value: 0n,
gas: serializableTransaction.gas,
nonce: nonce,
data: serializableTransaction.data,
chainid: BigInt(transaction.chain.id),
} as const;
return [
await account.signTypedData({
domain: {
name: "GSNv2 Forwarder",
version: "0.0.1",
verifyingContract: forrwaderContract.address,
},
message,
primaryType: "ForwardRequest",
types: { ForwardRequest: ChainAwareForwardRequest },
}),
message,
] as const;
}
// else non-chainless support
const message = {
from: account.address,
to: serializableTransaction.to,
value: 0n,
gas: serializableTransaction.gas,
nonce: nonce,
data: serializableTransaction.data,
} as const;
return [
await account.signTypedData({
domain: {
name: gasless.domainName ?? "GSNv2 Forwarder",
version: gasless.domainVersion ?? "0.0.1",
chainId: transaction.chain.id,
verifyingContract: forrwaderContract.address,
},
message,
primaryType: "ForwardRequest",
types: { ForwardRequest },
}),
message,
] as const;
})();
// TODO: handle special case for `approve` -> `permit`
const messageType = "forward";
return { message, signature, messageType } as const;
}
export const ForwardRequest = [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "gas", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "data", type: "bytes" },
] as const;
export const ChainAwareForwardRequest = [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "gas", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "data", type: "bytes" },
{ name: "chainid", type: "uint256" },
] as const;
/**
* @internal
*/
export async function relayEngineTransaction(
options: SendengineTransactionOptions,
): Promise<WaitForReceiptOptions> {
const { message, messageType, signature } =
await prepareEngineTransaction(options);
const response = await fetch(options.gasless.relayerUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: stringify({
request: message,
type: messageType,
signature,
forwarderAddress: options.gasless.relayerForwarderAddress,
}),
});
if (!response.ok) {
throw new Error(`Failed to send transaction: ${await response.text()}`);
}
const json = await response.json();
if (!json.result) {
throw new Error(`Relay transaction failed: ${json.message}`);
}
const queueId = json.result.queueId;
// poll for transactionHash
const timeout = 60000;
const interval = 1000;
const endtime = Date.now() + timeout;
while (Date.now() < endtime) {
const receipt = await fetchReceipt({ options, queueId });
if (receipt) {
return {
transactionHash: receipt.transactionHash,
chain: options.transaction.chain,
client: options.transaction.client,
};
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error(`Failed to find relayed transaction after ${timeout}ms`);
}
async function fetchReceipt(args: {
options: SendengineTransactionOptions;
queueId: string;
}) {
const { options, queueId } = args;
const url = options.gasless.relayerUrl.split("/relayer/")[0];
const res = await fetch(`${url}/transaction/status/${queueId}`, {
method: "GET",
});
const resJson = await res.json();
if (!res.ok) {
return null;
}
const result = resJson.result;
if (!result) {
return null;
}
switch (result.status) {
case "errored":
throw new Error(
`Transaction errored with reason: ${result.errorMessage}`,
);
case "cancelled":
throw new Error("Transaction execution cancelled.");
case "mined": {
const receipt = await waitForReceipt({
client: options.transaction.client,
chain: options.transaction.chain,
transactionHash: result.transactionHash,
});
return receipt;
}
default: {
return null;
}
}
}
@@ -0,0 +1,88 @@
import { beforeAll, describe, expect, it } from "vitest";
import { FORKED_ETHEREUM_CHAIN_WITH_MINING } from "../../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../../test/src/test-wallets.js";
import {
type ThirdwebContract,
getContract,
} from "../../../../contract/contract.js";
import { mintTo } from "../../../../extensions/erc20/write/mintTo.js";
import { transfer } from "../../../../extensions/erc20/write/transfer.js";
import { deployERC20Contract } from "../../../../extensions/prebuilts/deploy-erc20.js";
import { isHex } from "../../../../utils/encoding/hex.js";
import { sendAndConfirmTransaction } from "../../send-and-confirm-transaction.js";
import { toSerializableTransaction } from "../../to-serializable-transaction.js";
import { prepareOpenZeppelinTransaction } from "./openzeppelin.js";
describe.runIf(process.env.TW_SECRET_KEY)(
"prepareOpenZeppelinTransaction",
() => {
let erc20Contract: ThirdwebContract;
beforeAll(async () => {
erc20Contract = getContract({
address: await deployERC20Contract({
account: TEST_ACCOUNT_A,
chain: FORKED_ETHEREUM_CHAIN_WITH_MINING,
client: TEST_CLIENT,
params: { name: "Test Token", symbol: "TST" },
type: "TokenERC20",
}),
chain: FORKED_ETHEREUM_CHAIN_WITH_MINING,
client: TEST_CLIENT,
});
// mint some tokens to the account
await sendAndConfirmTransaction({
transaction: mintTo({
contract: erc20Contract,
amount: "1000",
to: TEST_ACCOUNT_A.address,
}),
account: TEST_ACCOUNT_A,
});
}, 60_000);
it("should prepare an Openzeppelin relay transaction", async () => {
const transaction = transfer({
amount: "100",
contract: erc20Contract,
to: TEST_ACCOUNT_B.address,
});
// seralizable transaction
const serializableTransaction = await toSerializableTransaction({
transaction,
from: TEST_ACCOUNT_A.address,
});
const result = await prepareOpenZeppelinTransaction({
account: TEST_ACCOUNT_A,
serializableTransaction,
transaction,
gasless: {
provider: "openzeppelin",
relayerUrl: "https://safe-transaction.openzeppelin.com/api/v1",
// mainnet forwarder address
relayerForwarderAddress: "0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81",
},
});
const { message, messageType, signature } = result;
expect(isHex(signature)).toBe(true);
expect(message).toMatchInlineSnapshot(`
{
"data": "0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"gas": 64338n,
"nonce": 0n,
"to": "${erc20Contract.address}",
"value": 0n,
}
`);
expect(messageType).toMatchInlineSnapshot(`"forward"`);
});
},
);
@@ -0,0 +1,174 @@
import type { Address } from "abitype";
import type { TransactionSerializable } from "viem";
import { getContract } from "../../../../contract/contract.js";
import { isHex } from "../../../../utils/encoding/helpers/is-hex.js";
import { stringify } from "../../../../utils/json.js";
import type { Account } from "../../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../../prepare-transaction.js";
import { readContract } from "../../../read-contract.js";
import type { WaitForReceiptOptions } from "../../wait-for-tx-receipt.js";
export type OpenZeppelinOptions = {
provider: "openzeppelin";
relayerUrl: string;
relayerForwarderAddress: Address;
domainName?: string; // default: "GSNv2 Forwarder"
domainVersion?: string; // default: "0.0.1"
domainSeparatorVersion?: string; // default: "1"
experimentalChainlessSupport?: boolean; // default: false
};
type SendOpenZeppelinTransactionOptions = {
account: Account;
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction<any>;
serializableTransaction: TransactionSerializable;
gasless: OpenZeppelinOptions;
};
/**
* @internal - only exported for testing
*/
export async function prepareOpenZeppelinTransaction({
account,
serializableTransaction,
transaction,
gasless,
}: SendOpenZeppelinTransactionOptions) {
const forrwaderContract = getContract({
address: gasless.relayerForwarderAddress,
chain: transaction.chain,
client: transaction.client,
});
const nonce = await readContract({
contract: forrwaderContract,
method: "function getNonce(address) view returns (uint256)",
params: [account.address],
});
const [signature, message] = await (async () => {
// TODO: handle special case for `approve` -> `permit` transactions
if (!serializableTransaction.to) {
throw new Error("OpenZeppelin transactions must have a 'to' address");
}
if (!serializableTransaction.gas) {
throw new Error("OpenZeppelin transactions must have a 'gas' value");
}
if (!serializableTransaction.data) {
throw new Error("OpenZeppelin transactions must have a 'data' value");
}
// chainless support!
if (gasless.experimentalChainlessSupport) {
const message = {
from: account.address,
to: serializableTransaction.to,
value: 0n,
gas: serializableTransaction.gas,
nonce: nonce,
data: serializableTransaction.data,
chainid: BigInt(transaction.chain.id),
} as const;
return [
await account.signTypedData({
domain: {
name: "GSNv2 Forwarder",
version: "0.0.1",
verifyingContract: forrwaderContract.address,
},
message,
primaryType: "ForwardRequest",
types: { ForwardRequest: ChainAwareForwardRequest },
}),
message,
] as const;
}
// else non-chainless support
const message = {
from: account.address,
to: serializableTransaction.to,
value: 0n,
gas: serializableTransaction.gas,
nonce: nonce,
data: serializableTransaction.data,
} as const;
return [
await account.signTypedData({
domain: {
name: gasless.domainName ?? "GSNv2 Forwarder",
version: gasless.domainVersion ?? "0.0.1",
chainId: transaction.chain.id,
verifyingContract: forrwaderContract.address,
},
message,
primaryType: "ForwardRequest",
types: { ForwardRequest },
}),
message,
] as const;
})();
// TODO: handle special case for `approve` -> `permit`
const messageType = "forward";
return { message, signature, messageType } as const;
}
export const ForwardRequest = [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "gas", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "data", type: "bytes" },
] as const;
export const ChainAwareForwardRequest = [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "gas", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "data", type: "bytes" },
{ name: "chainid", type: "uint256" },
] as const;
/**
* @internal
*/
export async function relayOpenZeppelinTransaction(
options: SendOpenZeppelinTransactionOptions,
): Promise<WaitForReceiptOptions> {
const { message, messageType, signature } =
await prepareOpenZeppelinTransaction(options);
const response = await fetch(options.gasless.relayerUrl, {
method: "POST",
body: stringify({
request: message,
type: messageType,
signature,
forwarderAddress: options.gasless.relayerForwarderAddress,
}),
});
if (!response.ok) {
response.body?.cancel();
throw new Error(`Failed to send transaction: ${await response.text()}`);
}
const json = await response.json();
if (!json.result) {
throw new Error(`Relay transaction failed: ${json.message}`);
}
const transactionHash = JSON.parse(json.result).txHash;
if (isHex(transactionHash)) {
return {
transactionHash,
chain: options.transaction.chain,
client: options.transaction.client,
};
}
throw new Error(`Failed to send transaction: ${stringify(json)}`);
}
@@ -0,0 +1,72 @@
import type { TransactionSerializable } from "viem";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../prepare-transaction.js";
import type { WaitForReceiptOptions } from "../wait-for-tx-receipt.js";
import type { GaslessOptions } from "./types.js";
export type SendGaslessTransactionOptions = {
account: Account;
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction<any>;
serializableTransaction: TransactionSerializable;
gasless: GaslessOptions;
};
export async function sendGaslessTransaction({
account,
transaction,
serializableTransaction,
gasless,
}: SendGaslessTransactionOptions): Promise<WaitForReceiptOptions> {
// TODO: handle special case for mutlicall transactions!
// Steps:
// 1. check if the method is `multicall` by comparing the 4bytes data with the `multicall` selector
// 2. split the rest of the data into its "parts"
// 3. solidityPack the parts with the part data + the `account.address`
// see v4: `core/classes/transactions.ts>Transaction>prepareGasless:L551`
if (serializableTransaction.value && serializableTransaction.value > 0n) {
throw new Error("Gasless transactions cannot have a value");
}
// TODO: multiply gas by 2 for some reason(?) - we do in v4, *should* we?
// biconomy
if (gasless.provider === "biconomy") {
const { relayBiconomyTransaction } = await import(
"./providers/biconomy.js"
);
return relayBiconomyTransaction({
account,
transaction,
serializableTransaction,
gasless,
});
}
// openzeppelin
if (gasless.provider === "openzeppelin") {
const { relayOpenZeppelinTransaction } = await import(
"./providers/openzeppelin.js"
);
return relayOpenZeppelinTransaction({
account,
transaction,
serializableTransaction,
gasless,
});
}
if (gasless.provider === "engine") {
const { relayEngineTransaction } = await import("./providers/engine.js");
return relayEngineTransaction({
account,
transaction,
serializableTransaction,
gasless,
});
}
throw new Error("Unsupported gasless provider");
}
@@ -0,0 +1,8 @@
import type { BiconomyOptions } from "./providers/biconomy.js";
import type { EngineOptions } from "./providers/engine.js";
import type { OpenZeppelinOptions } from "./providers/openzeppelin.js";
export type GaslessOptions =
| EngineOptions
| OpenZeppelinOptions
| BiconomyOptions;
@@ -1,14 +1,15 @@
import type { Account } from "../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../prepare-transaction.js";
import type { WaitForReceiptOptions } from "./wait-for-tx-receipt.js";
import type { GaslessOptions } from "./gasless/types.js";
import { toSerializableTransaction } from "./to-serializable-transaction.js";
import type { WaitForReceiptOptions } from "./wait-for-tx-receipt.js";
export type SendTransactionOptions = {
account: Account;
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction<any>;
gasless?: GaslessOptions;
};
/**
@@ -30,12 +31,27 @@ export type SendTransactionOptions = {
export async function sendTransaction(
options: SendTransactionOptions,
): Promise<WaitForReceiptOptions> {
const { account, transaction } = options;
const { account, transaction, gasless } = options;
const serializableTx = await toSerializableTransaction({
const serializableTransaction = await toSerializableTransaction({
transaction: transaction,
from: account.address,
});
const result = await account.sendTransaction(serializableTx);
// branch for gasless transactions
if (gasless) {
// lazy load the gasless tx function because it's only needed for gasless transactions
const { sendGaslessTransaction } = await import(
"./gasless/send-gasless-transaction.js"
);
return sendGaslessTransaction({
account,
transaction,
serializableTransaction,
gasless,
});
}
const result = await account.sendTransaction(serializableTransaction);
return { ...result, chain: transaction.chain, client: transaction.client };
}
@@ -280,7 +280,10 @@ export async function connectCoinbaseWalletSDK(
})) as string | number;
const chainId = normalizeChainId(connectedChainId);
let chain = defineChain(chainId);
let chain =
options.chain && options.chain.id === chainId
? options.chain
: defineChain(chainId);
// Switch to chain if provided
if (
connectedChainId &&
@@ -318,7 +321,10 @@ export async function autoConnectCoinbaseWalletSDK(
method: "eth_chainId",
})) as string | number;
const chainId = normalizeChainId(connectedChainId);
const chain = defineChain(chainId);
const chain =
options.chain && options.chain.id === chainId
? options.chain
: defineChain(chainId);
return onConnect(address, chain, provider, emitter);
}
@@ -8,6 +8,7 @@ import type { Account, Wallet } from "./interfaces/wallet.js";
import type {
CreateWalletArgs,
InjectedConnectOptions,
WalletAutoConnectionOption,
WalletId,
} from "./wallet-types.js";
@@ -104,7 +105,11 @@ export function createWallet<const ID extends WalletId>(
getConfig: () => args[1],
getChain: () => chain,
getAccount: () => account,
autoConnect: async (options) => {
autoConnect: async (
options: WalletAutoConnectionOption<
WCSupportedWalletIds | InjectedSupportedWalletIds
>,
) => {
// injected wallet priority for autoConnect
if (id !== "walletConnect" && injectedProvider(id)) {
const { autoConnectInjectedWallet } = await import(
@@ -119,6 +124,7 @@ export function createWallet<const ID extends WalletId>(
] = await autoConnectInjectedWallet(
id as InjectedSupportedWalletIds,
emitter,
options.chain,
);
// set the states
account = connectedAccount;
@@ -73,12 +73,32 @@ export async function getAuthenticatedUser(
*/
export async function getUserEmail(options: GetAuthenticatedUserParams) {
const user = await getAuthenticatedUser(options);
if (user) {
if (user && "email" in user.authDetails) {
return user.authDetails.email;
}
return undefined;
}
/**
* Retrieves the authenticated user phone number for the active embedded wallet.
* @param options - The arguments for retrieving the authenticated user.
* @returns The authenticated user phone number if authenticated with phone number, otherwise undefined.
* @example
* ```ts
* import { getUserPhoneNumber } from "thirdweb/wallets/embedded";
*
* const phoneNumber = await getUserPhoneNumber({ client });
* console.log(phoneNumber);
* ```
*/
export async function getUserPhoneNumber(options: GetAuthenticatedUserParams) {
const user = await getAuthenticatedUser(options);
if (user && "phoneNumber" in user.authDetails) {
return user.authDetails.phoneNumber;
}
return undefined;
}
/**
* Pre-authenticates the user based on the provided authentication strategy.
* @param args - The arguments required for pre-authentication.
@@ -102,8 +122,12 @@ export async function preAuthenticate(args: PreAuthArgsType) {
case "email": {
return ewSDK.auth.sendEmailLoginOtp({ email: args.email });
}
case "phone": {
return ewSDK.auth.sendSmsLoginOtp({ phoneNumber: args.phoneNumber });
}
default:
throw new Error(
assertUnreachable(
strategy,
`Provider: ${strategy} doesnt require pre-authentication`,
);
}
@@ -137,6 +161,12 @@ export async function authenticate(
otp: args.verificationCode,
});
}
case "phone": {
return await ewSDK.auth.verifySmsLoginOtp({
otp: args.verificationCode,
phoneNumber: args.phoneNumber,
});
}
case "apple":
case "facebook":
case "google": {
@@ -172,8 +202,8 @@ export async function authenticate(
}
}
function assertUnreachable(x: never): never {
throw new Error(`Invalid param: ${x}`);
function assertUnreachable(x: never, message?: string): never {
throw new Error(message ?? `Invalid param: ${x}`);
}
const oauthStrategyToAuthProvider: Record<
@@ -1,9 +1,14 @@
import type { ThirdwebClient } from "../../../../client/client.js";
export type MultiStepAuthProviderType = {
strategy: "email";
email: string;
};
export type MultiStepAuthProviderType =
| {
strategy: "email";
email: string;
}
| {
strategy: "phone";
phoneNumber: string;
};
export type PreAuthArgsType = MultiStepAuthProviderType & {
client: ThirdwebClient;
};
@@ -17,7 +17,7 @@ export type InAppWalletConnectionOptions = (
export type InAppWalletSocialAuth = "google" | "apple" | "facebook";
export type InAppWalletAuth = "email" | InAppWalletSocialAuth;
export type InAppWalletAuth = "email" | "phone" | InAppWalletSocialAuth;
/**
* @internal
@@ -29,8 +29,14 @@ export type ClientIdWithQuerierType = ClientIdConstructorType & {
};
// Auth Types
export type AuthDetails = {
email?: string;
export type AuthDetails = (
| {
email?: string;
}
| {
phoneNumber?: string;
}
) & {
userWalletId: string;
encryptionKey?: string;
backupRecoveryCodes?: string[];
@@ -14,12 +14,18 @@ export type LoginQuerierTypes = {
loginWithCustomAuthEndpoint: { payload: string; encryptionKey: string };
loginWithCustomJwt: { jwt: string; encryptionKey?: string };
loginWithThirdwebModal: undefined | { email: string };
sendThirdwebSmsLoginOtp: { phoneNumber: string };
sendThirdwebEmailLoginOtp: { email: string };
verifyThirdwebEmailLoginOtp: {
email: string;
otp: string;
recoveryCode?: string;
};
verifyThirdwebSmsLoginOtp: {
phoneNumber: string;
otp: string;
recoveryCode?: string;
};
injectDeveloperClientId: undefined;
getHeadlessOauthLoginLink: { authProvider: AuthProvider };
};
@@ -101,7 +107,28 @@ export abstract class AbstractLogin<
return result;
}
/**
*
* @internal
*/
async sendSmsLoginOtp({
phoneNumber,
}: LoginQuerierTypes["sendThirdwebSmsLoginOtp"]): Promise<SendEmailOtpReturnType> {
await this.preLogin();
const result = await this.LoginQuerier.call<SendEmailOtpReturnType>({
procedureName: "sendThirdwebSmsLoginOtp",
params: { phoneNumber },
});
return result;
}
abstract verifyEmailLoginOtp(
args: EMAIL_VERIFICATION,
): Promise<AuthLoginReturnType>;
abstract verifySmsLoginOtp(args: {
phoneNumber: string;
otp: string;
recoveryCode?: string;
}): Promise<AuthLoginReturnType>;
}
@@ -235,4 +235,19 @@ export class BaseLogin extends AbstractLogin<
});
return this.postLogin(result);
}
/**
* @internal
*/
override async verifySmsLoginOtp({
phoneNumber,
otp,
recoveryCode,
}: LoginQuerierTypes["verifyThirdwebSmsLoginOtp"]): Promise<AuthLoginReturnType> {
const result = await this.LoginQuerier.call<AuthAndWalletRpcReturnType>({
procedureName: "verifyThirdwebSmsLoginOtp",
params: { phoneNumber, otp, recoveryCode },
});
return this.postLogin(result);
}
}
@@ -193,6 +193,19 @@ export class Auth {
});
}
/**
* @internal
*/
async sendSmsLoginOtp({
phoneNumber,
}: Parameters<
BaseLogin["sendSmsLoginOtp"]
>[0]): Promise<SendEmailOtpReturnType> {
return this.BaseLogin.sendSmsLoginOtp({
phoneNumber,
});
}
/**
* Used to verify the otp that the user receives from thirdweb
*
@@ -208,6 +221,13 @@ export class Auth {
return this.BaseLogin.verifyEmailLoginOtp(args);
}
/**
* @internal
*/
async verifySmsLoginOtp(args: Parameters<BaseLogin["verifySmsLoginOtp"]>[0]) {
return this.BaseLogin.verifySmsLoginOtp(args);
}
/**
* Logs any existing user out of their wallet.
* @returns `{{success: boolean}}` true if a user is successfully logged out. false if there's no user currently logged in.
@@ -63,7 +63,10 @@ export async function connectInjectedWallet(
.request({ method: "eth_chainId" })
.then(normalizeChainId);
let connectedChain = defineChain(chainId);
let connectedChain =
options.chain && options.chain.id === chainId
? options.chain
: defineChain(chainId);
// if we want a specific chainId and it is not the same as the provider chainId, trigger switchChain
if (options.chain && options.chain.id !== chainId) {
@@ -80,6 +83,7 @@ export async function connectInjectedWallet(
export async function autoConnectInjectedWallet(
id: InjectedSupportedWalletIds,
emitter: WalletEmitter<InjectedSupportedWalletIds>,
chain?: Chain,
): Promise<ReturnType<typeof onConnect>> {
const provider = getInjectedProvider(id);
@@ -101,7 +105,8 @@ export async function autoConnectInjectedWallet(
.request({ method: "eth_chainId" })
.then(normalizeChainId);
const connectedChain = defineChain(chainId);
const connectedChain =
chain && chain.id === chainId ? chain : defineChain(chainId);
return onConnect(provider, address, connectedChain, emitter);
}
@@ -11,6 +11,7 @@ export type ConnectionStatus = "connected" | "disconnected" | "connecting";
const CONNECTED_WALLET_IDS = "thirdweb:connected-wallet-ids";
const ACTIVE_WALLET_ID = "thirdweb:active-wallet-id";
const LAST_ACTIVE_CHAIN = "thirdweb:active-chain";
export type ConnectionManager = ReturnType<typeof createConnectionManager>;
@@ -126,6 +127,19 @@ export function createConnectionManager(storage: AsyncStorage) {
// side effects
effect(
() => {
const _chain = activeWalletChainStore.getValue();
if (_chain) {
storage.setItem(LAST_ACTIVE_CHAIN, JSON.stringify(_chain));
} else {
storage.removeItem(LAST_ACTIVE_CHAIN);
}
},
[activeWalletChainStore],
false,
);
// save last connected wallet ids to storage
effect(
() => {
@@ -218,3 +232,21 @@ export async function getStoredActiveWalletId(
return null;
}
}
/**
* @internal
*/
export async function getLastConnectedChain(
storage: AsyncStorage,
): Promise<Chain | null> {
try {
const value = await storage.getItem(LAST_ACTIVE_CHAIN);
if (value) {
return JSON.parse(value) as Chain;
}
return null;
} catch {
return null;
}
}
@@ -31,7 +31,7 @@ export type GetWalletBalanceResult = {
/**
* Retrieves the balance of a token or native currency for a given wallet.
* @param options - The options for retrieving the token balance.
* @param options.account - The account for which to retrieve the balance.
* @param options.address - The address for which to retrieve the balance.
* @param options.client - The Thirdweb client to use for the request.
* @param options.chain - The chain for which to retrieve the balance.
* @param options.tokenAddress - (Optional) The address of the token to retrieve the balance for. If not provided, the balance of the native token will be retrieved.
@@ -102,7 +102,12 @@ export async function connectWC(
throw new Error("No accounts found on provider.");
}
const chain = defineChain(normalizeChainId(provider.chainId));
const providerChainId = normalizeChainId(provider.chainId);
const chain =
options.chain && options.chain.id === providerChainId
? options.chain
: defineChain(providerChainId);
if (options) {
const savedParams: SavedConnectParams = {
@@ -160,7 +165,12 @@ export async function autoConnectWC(
throw new Error("No accounts found on provider.");
}
const chain = defineChain(normalizeChainId(provider.chainId));
const providerChainId = normalizeChainId(provider.chainId);
const chain =
options.chain && options.chain.id === providerChainId
? options.chain
: defineChain(providerChainId);
return onConnect(address, chain, provider, emitter);
}
@@ -179,6 +179,8 @@ export type WCAutoConnectOptions = {
client: ThirdwebClient;
savedConnectParams?: SavedConnectParams;
chain?: Chain;
};
type SavedConnectParams = {
+19 -2
View File
@@ -26,8 +26,24 @@ export default async function globalSetup() {
},
});
const shutdownOptimism = await startProxy({
const shutdownMainnetWithMining = await startProxy({
port: 8646,
options: {
chainId: 1,
forkUrl: SECRET_KEY
? `https://1.rpc.thirdweb.com/${clientId}`
: "https://mainnet.gateway.tenderly.co",
forkHeader: SECRET_KEY ? { "x-secret-key": SECRET_KEY } : {},
forkChainId: 1,
forkBlockNumber: FORK_BLOCK_NUMBER,
// ey, i'm mining here!
noMining: false,
startTimeout: 20000,
},
});
const shutdownOptimism = await startProxy({
port: 8647,
options: {
chainId: 10,
forkUrl: SECRET_KEY
@@ -42,11 +58,12 @@ export default async function globalSetup() {
});
const shutdownAnvil = await startProxy({
port: 8647,
port: 8648,
});
return async () => {
await shutdownMainnet();
await shutdownMainnetWithMining();
await shutdownOptimism();
await shutdownAnvil();
};
+9 -2
View File
@@ -6,8 +6,9 @@ import { defineChain } from "../../src/chains/utils.js";
export const poolId = Number(process.env.VITEST_POOL_ID ?? 1);
export const FORKED_ETHEREUM_RPC = `http://127.0.0.1:8645/${poolId}`;
export const FORKED_OPTIMISM_RPC = `http://127.0.0.1:8646/${poolId}`;
export const ANVIL_RPC = `http://127.0.0.1:8647/${poolId}`;
export const FORKED_ETHEREUM_WITH_MINING_RPC = `http://127.0.0.1:8646/${poolId}`;
export const FORKED_OPTIMISM_RPC = `http://127.0.0.1:8647/${poolId}`;
export const ANVIL_RPC = `http://127.0.0.1:8648/${poolId}`;
export const FORKED_ETHEREUM_CHAIN = defineChain({
...ethereum,
@@ -15,6 +16,12 @@ export const FORKED_ETHEREUM_CHAIN = defineChain({
rpc: FORKED_ETHEREUM_RPC,
});
export const FORKED_ETHEREUM_CHAIN_WITH_MINING = defineChain({
...ethereum,
// override the rpc url
rpc: FORKED_ETHEREUM_WITH_MINING_RPC,
});
export const FORKED_OPTIMISM_CHAIN = defineChain({
...optimism,
// override the rpc url
Binary file not shown.