Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
4cddbe775f Version Packages (#3197)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-01 16:19:36 +12:00
gregandGitHub a3461114c5 Headless WalletConnect Integration (#3181) 2024-06-01 03:29:37 +00:00
Joaquim VergesandGitHub 51e7adaeb0 feat: native zkync AA support (#3105) 2024-06-01 15:17:23 +12:00
gregandGitHub 3f669453d0 Fix estimateGas transaction type resolution (#3195) 2024-06-01 03:05:24 +00:00
Kien NgoandGitHub fe0c4ed00c Add unit tests (#3180) 2024-05-31 21:27:07 +00:00
gregandGitHub fb8a11654e Increase error log on smart account address generation (#3193) 2024-05-31 21:23:29 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>GitHub Action
c1e155d04f Version Packages (#3172)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <[email protected]>
2024-06-01 07:14:56 +12:00
gregandGitHub 91e658035d Add forked chains explainer to contributing guide (#3189) 2024-05-31 07:51:51 +00:00
gregandGitHub 8491675e2e Fix AutoConnect on MIPD wallets (#3190) 2024-05-31 19:50:38 +12:00
Joaquim VergesandGitHub 1b2edb4c09 show connect button if disconnected in pay playground (#3188) 2024-05-31 16:12:10 +12:00
Joaquim VergesandGitHub 874747b3e0 chore: update cb SDK (#3187) 2024-05-31 14:30:07 +12:00
FirekeeperandGitHub 01f883f637 Unity 4.14.1 (#3185) 2024-05-31 00:06:09 +00:00
gregandGitHub 123275bbe2 Catch any rogue poller errors in watchBlockNumber (#3186) 2024-05-30 17:05:52 -07:00
Manan TankandGitHub 7bb241f7db Improved OTP input in v4 SDK using input-otp lib (#3183) 2024-05-30 15:34:20 +00:00
Jonas DanielsandGitHub a1f53a2b9e refactor: Improve CI workflow configuration and setup (#3182) 2024-05-30 08:46:42 +00:00
gregandGitHub 46e9072576 Adds predefined hardhat chain (#3176) 2024-05-30 02:48:31 +00:00
501ef9339d fix: peerDependency version of react-native-quick-crypto (#3174)
Signed-off-by: Kacper <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2024-05-30 13:52:26 +12:00
Joaquim VergesandGitHub 3dca028a7e fix: transaction button gas estimation for pay (#3178) 2024-05-30 00:31:40 +00:00
Edward SunandGitHub fd777c71c2 Upgrade dependencies for legacy_packages/wallets, add new AuthProviders (#3175) 2024-05-29 21:42:53 +00:00
Joaquim VergesandGitHub fbec24e260 chore: add tsdoc tags (#3177) 2024-05-30 09:22:45 +12:00
bbf65acc80 Add some basic tests for React (#3165)
Co-authored-by: greg <[email protected]>
2024-05-29 20:46:08 +00:00
gregandGitHub b569eb48ad Adds EIP-5792 hooks and waitForBundle fn (#3161) 2024-05-29 00:00:29 +00:00
gregandGitHub b6b0eb9120 Improve connect wallet copy (#3171) 2024-05-28 23:37:51 +00:00
FirekeeperandGitHub 7cfe6745e8 Unity 4.14.0 (#3170) 2024-05-28 23:18:20 +00:00
137 changed files with 5065 additions and 455 deletions
+4
View File
@@ -73,6 +73,10 @@ 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.
Many of the tests use forked versions of live chains like mainnet. We fork these chains at a specific arbitrary block number so results are consistent across test runs. If you're interacting with on-chain data, you can write tests that interact with real contracts at the same block number by using `FORKED_ETHEREUM_CHAIN`. However, if you don't need to interact with existing contracts, we recommend running your tests against `ANVIL_CHAIN`.
If you need to use accounts in your tests, use the predefined accounts in `test/src/test-wallets.ts`. These are the default anvil accounts that are pre-funded on the local test forks.
#### 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:
+56 -1
View File
@@ -90,6 +90,10 @@ jobs:
timeout-minutes: 15
name: E2E Tests
runs-on: ubuntu-latest
strategy:
matrix:
package_manager: [npm, yarn, pnpm, bun]
bundler: [vite, webpack, esbuild]
steps:
- name: Check out the code
uses: actions/checkout@v4
@@ -97,7 +101,58 @@ jobs:
- name: Setup & Install
uses: ./.github/composite-actions/install
# TODO actually run e2e tests
- name: Build Packages
run: pnpm build:packages
- name: Install Yarn (if needed)
if: matrix.package_manager == 'yarn'
run: npm install -g yarn
- name: Create test project
run: |
mkdir test-project
cd test-project
npm init -y
${{ matrix.package_manager }} add react react-dom ../packages/thirdweb
- name: Create test file
run: |
cd test-project
echo "import { createThirdwebClient } from 'thirdweb'; console.log(createThirdwebClient({clientId: "foo_bar_baz"}));" > index.js
- name: Bundle with vite
if: matrix.bundler == 'vite'
run: |
cd test-project
${{matrix.package_manager}} add vite
echo 'import { defineConfig } from "vite"; import {resolve} from "path"; export default defineConfig({ build: { lib: { entry: resolve(__dirname, "index.js"), name: "e2e_test" }, outDir: "dist" }});' > vite.config.js
npx vite build
- name: Bundle with webpack
if: matrix.bundler == 'webpack'
run: |
cd test-project
${{matrix.package_manager}} add webpack webpack-cli
echo 'const path = require("path"); module.exports = { mode: "production", entry: "./index.js", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js" }};' > webpack.config.js
npx webpack
- name: Bundle with esbuild
if: matrix.bundler == 'esbuild'
run: |
cd test-project
${{matrix.package_manager}} add esbuild
npx esbuild index.js --bundle --outdir=dist
- name: Verify bundle
run: |
cd test-project/dist
file_count=$(find . -type f | wc -l)
if [ "$file_count" -gt 0 ]; then
echo "Bundling successful"
else
echo "Bundling failed"
exit 1
fi
benchmark:
# cannot run benchmarks on merge group
@@ -3,6 +3,8 @@ import { Button } from "@/components/ui/button";
import { metadataBase } from "@/lib/constants";
import type { Metadata } from "next";
import Link from "next/link";
import { CodeExample } from "../../../components/code/code-example";
import { StyledPayTransaction } from "../../../components/styled-pay-transaction";
export const metadata: Metadata = {
metadataBase,
@@ -55,6 +57,46 @@ export default function Page() {
</div>
</div>
</section>
<section className="container px-4 md:px-6 space-y-8">
<div className="space-y-2">
<h2 className="text-4xl font-semibold tracking-tight">
Transaction Button
</h2>
<p className="max-w-[600px]">
Transaction Button is a handy component that handles transactions.
<br />
If your user doesn&apos;t have enough funds for that transaction, a
pre-filled pay modal will appear with the exact amount needed.
</p>
</div>
<CodeExample
preview={<StyledPayTransaction />}
code={`import { TransactionButton } from "thirdweb/react";
function App() {
const account = useActiveAccount();
return (
<TransactionButton
transaction={() => {
// any transaction works
return claimTo({
contract,
quantity: 1n,
tokenId: 0n,
to: account?.address,
});
}}
>
Buy for 10 MATIC
</TransactionButton>
);
};`}
lang="tsx"
/>
</section>
</main>
);
}
@@ -0,0 +1,41 @@
"use client";
import { THIRDWEB_CLIENT } from "@/lib/client";
import { useTheme } from "next-themes";
import { getContract } from "thirdweb";
import { polygon } from "thirdweb/chains";
import { claimTo } from "thirdweb/extensions/erc1155";
import { TransactionButton, useActiveAccount } from "thirdweb/react";
import { StyledConnectButton } from "./styled-connect-button";
const contract = getContract({
address: "0x96B30d36f783c7BC68535De23147e2ce65788e93",
chain: polygon,
client: THIRDWEB_CLIENT,
});
export function StyledPayTransaction() {
const account = useActiveAccount();
const { theme } = useTheme();
return account ? (
<TransactionButton
transaction={() => {
if (!account) throw new Error("No active account");
return claimTo({
contract,
quantity: 1n,
tokenId: 0n,
to: account?.address,
});
}}
payModal={{
theme: theme === "light" ? "light" : "dark",
}}
>
Buy for 10 MATIC
</TransactionButton>
) : (
<StyledConnectButton />
);
}
+5 -18
View File
@@ -1,10 +1,6 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -22,19 +18,10 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
},
"target": "ES2017"
"target": "ES2020"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/auth
## 4.1.89
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/wallets@2.5.31
## 4.1.88
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/wallets@2.5.30
## 4.1.87
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "4.1.87",
"version": "4.1.89",
"main": "dist/thirdweb-dev-auth.cjs.js",
"module": "dist/thirdweb-dev-auth.esm.js",
"browser": {
+18
View File
@@ -1,5 +1,23 @@
# thirdweb
## 0.14.4
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.91
- @thirdweb-dev/wallets@2.5.31
- @thirdweb-dev/auth@4.1.89
## 0.14.3
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.90
- @thirdweb-dev/wallets@2.5.30
- @thirdweb-dev/auth@4.1.88
## 0.14.2
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@thirdweb-dev/cli",
"main": "dist/cli/index.js",
"version": "0.14.2",
"version": "0.14.4",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+18
View File
@@ -1,5 +1,23 @@
# @thirdweb-dev/react-core
## 4.6.30
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.91
- @thirdweb-dev/wallets@2.5.31
- @thirdweb-dev/auth@4.1.89
## 4.6.29
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.90
- @thirdweb-dev/wallets@2.5.30
- @thirdweb-dev/auth@4.1.88
## 4.6.28
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-core",
"version": "4.6.28",
"version": "4.6.30",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react-core",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+20
View File
@@ -1,5 +1,25 @@
# @thirdweb-dev/react
## 4.6.30
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.91
- @thirdweb-dev/react-core@4.6.30
- @thirdweb-dev/wallets@2.5.31
## 4.6.29
### Patch Changes
- [#3183](https://github.com/thirdweb-dev/js/pull/3183) [`7bb241f`](https://github.com/thirdweb-dev/js/commit/7bb241f7db12e97b7eff7583548638b7ab2e5b77) Thanks [@MananTank](https://github.com/MananTank)! - Improved OTP input using input-otp library
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.90
- @thirdweb-dev/react-core@4.6.29
- @thirdweb-dev/wallets@2.5.30
## 4.6.28
### Patch Changes
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "4.6.28",
"version": "4.6.30",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -122,6 +122,7 @@
"copy-to-clipboard": "^3.3.3",
"detect-browser": "^5.3.0",
"fuse.js": "^7.0.0",
"input-otp": "^1.2.4",
"qrcode": "^1.5.3",
"tiny-invariant": "^1.3.3"
},
+86 -117
View File
@@ -1,140 +1,109 @@
import { useEffect, useRef } from "react";
import { Input } from "./formElements";
import { Container } from "./basic";
import { media, fontSize, spacing } from "../design-system";
import styled from "@emotion/styled";
"use client";
import { keyframes } from "@emotion/react";
import { OTPInput as InputOTP, type SlotProps } from "input-otp";
import { fontSize, spacing, radius } from "../design-system";
import { useCustomTheme } from "../design-system/CustomThemeProvider";
import { StyledDiv } from "../design-system/elements";
import { Container } from "./basic";
/**
* @internal
*/
export function OTPInput(props: {
digits: number;
isInvalid?: boolean;
value: string;
setValue: (value: string) => void;
onEnter?: () => void;
onEnter: () => void;
}) {
const otp = props.value.split("").map(Number);
const setOTP = (newOTP: number[]) => {
props.setValue(newOTP.join(""));
};
const inputToFocusIndex = otp.length;
const boxEls = useRef<(HTMLInputElement | null)[]>([]);
useEffect(() => {
if (boxEls.current[inputToFocusIndex]) {
requestAnimationFrame(() => {
boxEls.current[inputToFocusIndex]?.focus();
});
}
}, [inputToFocusIndex]);
return (
<Container center="x" gap="sm" flex="row">
{new Array(props.digits).fill(null).map((_, i) => {
return (
<OTPInputBox
data-error={props.isInvalid}
ref={(e) => (boxEls.current[i] = e)}
key={i}
value={otp[i] ?? ""}
type="number"
pattern="[0-9]*"
variant="outline"
inputMode="numeric"
onPaste={(e) => {
const pastedData = e.clipboardData.getData("text/plain");
const newOTP = pastedData
.slice(0, props.digits)
.split("")
.filter((n) => /\d/.test(n))
.map(Number);
setOTP(newOTP);
e.preventDefault();
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (props.onEnter) {
props.onEnter();
return;
}
}
if (e.key === "ArrowLeft") {
if (i === 0) {
return;
}
boxEls.current[i - 1]?.focus();
return;
}
if (e.key === "ArrowRight") {
if (i === props.digits - 1) {
return;
}
boxEls.current[i + 1]?.focus();
return;
}
if (e.key === "e" || e.key === ".") {
e.preventDefault();
return;
}
if (e.key === "Backspace") {
if (i === 0) {
return;
}
const newOTP = otp.slice(0, -1);
setOTP(newOTP);
}
}}
onChange={(e) => {
const value = e.target.value;
if (value.length > 1) {
setOTP(value.split("").filter((n) => /\d/.test(n)).map(Number));
return;
}
if (!/\d/.test(value) && value !== "") {
e.preventDefault();
return;
}
const newOTP = [...otp];
const index = i > inputToFocusIndex - 1 ? inputToFocusIndex : i;
newOTP[index] = Number(value);
setOTP(newOTP);
}}
/>
);
})}
</Container>
<OTPInputContainer>
<InputOTP
onComplete={() => {
props.onEnter();
}}
maxLength={6}
value={props.value}
render={({ slots }) => (
<Container flex="row" gap="xs" center="both">
{slots.map((slot, idx) => (
<Slot key={idx} {...slot} isInvalid={props.isInvalid} />
))}
</Container>
)}
onChange={(newValue) => {
props.setValue(newValue);
}}
/>
</OTPInputContainer>
);
}
const OTPInputBox = /* @__PURE__ */ styled(Input)(() => {
const OTPInputContainer = /* @__PURE__ */ StyledDiv({
"& input": {
maxWidth: "100%",
},
});
function Slot(props: SlotProps & { isInvalid?: boolean }) {
return (
<OTPInputBox data-active={props.isActive} data-error={props.isInvalid}>
{props.char !== null && <div>{props.char}</div>}
{props.hasFakeCaret && <FakeCaret />}
</OTPInputBox>
);
}
const caretBlink = keyframes`
0%, 100% {
opacity: 0;
},
50% {
opacity: 1;
}
`;
const FakeCaret = /* @__PURE__ */StyledDiv(() => {
const theme = useCustomTheme();
return {
appearance: "none",
WebkitAppearance: "none",
position: "absolute",
pointerEvents: "none",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
animation: `${caretBlink} 1s infinite`,
"&::before": {
content: "''",
display: "block",
width: "1.5px",
height: "1em",
backgroundColor: theme.colors.primaryText,
},
};
});
const OTPInputBox = /* @__PURE__ */ StyledDiv(() => {
const theme = useCustomTheme();
return {
position: "relative",
width: "40px",
height: "40px",
textAlign: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: fontSize.md,
padding: spacing.xs,
[media.mobile]: {
width: "35px",
height: "35px",
boxSizing: "border-box",
transition: "color 200ms ease",
border: `2px solid ${theme.colors.borderColor}`,
"&[data-active='true']": {
borderColor: theme.colors.accentText,
},
"&[data-verify-status='invalid']": {
color: theme.colors.danger,
color: theme.colors.primaryText,
borderRadius: radius.lg,
"&[data-error='true']": {
borderColor: theme.colors.danger,
},
};
@@ -313,7 +313,6 @@ export const EmbeddedWalletOTPLoginUI: React.FC<
setValue={(value) => {
setOtpInput(value);
setVerifyStatus("idle"); // reset error
verify(value);
}}
onEnter={() => {
verify(otpInput);
Binary file not shown.
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/sdk
## 4.0.91
### Patch Changes
- Updated dependencies [[`3f66945`](https://github.com/thirdweb-dev/js/commit/3f669453d086487a217ae9af82745199e26550c6), [`a346111`](https://github.com/thirdweb-dev/js/commit/a3461114c5e9adb8d0ea2f36bc9fb3d96366c5b9), [`51e7ada`](https://github.com/thirdweb-dev/js/commit/51e7adaeb0fe050eb132510291ecac7a12e4c4ad), [`51e7ada`](https://github.com/thirdweb-dev/js/commit/51e7adaeb0fe050eb132510291ecac7a12e4c4ad)]:
- thirdweb@5.27.0
## 4.0.90
### Patch Changes
- Updated dependencies [[`3dca028`](https://github.com/thirdweb-dev/js/commit/3dca028a7eff926ae406eb29cc82016ae6c994b3), [`874747b`](https://github.com/thirdweb-dev/js/commit/874747b3e00714e9a7336dcfda34e7b92b82e7e1), [`123275b`](https://github.com/thirdweb-dev/js/commit/123275bbe275457b0be5205812ba0ef75b1f0e5b), [`b569eb4`](https://github.com/thirdweb-dev/js/commit/b569eb48ad018619e4589b9efb5aa774c7b77642), [`b569eb4`](https://github.com/thirdweb-dev/js/commit/b569eb48ad018619e4589b9efb5aa774c7b77642)]:
- thirdweb@5.26.0
## 4.0.89
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "4.0.89",
"version": "4.0.91",
"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,23 @@
# @thirdweb-dev/unity-js-bridge
## 0.6.40
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.91
- @thirdweb-dev/wallets@2.5.31
- @thirdweb-dev/auth@4.1.89
## 0.6.39
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.90
- @thirdweb-dev/wallets@2.5.30
- @thirdweb-dev/auth@4.1.88
## 0.6.38
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.6.38",
"version": "0.6.40",
"main": "dist/thirdweb-unity-bridge.js",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/unity-js-bridge",
"license": "Apache-2.0",
@@ -171,7 +171,7 @@ class ThirdwebBridge implements TWBridge {
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix use of any
(globalThis as any).X_SDK_PLATFORM = "unity";
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix use of any
(globalThis as any).X_SDK_VERSION = "4.13.3";
(globalThis as any).X_SDK_VERSION = "4.14.1";
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix use of any
(globalThis as any).X_SDK_OS = browser?.os ?? "unknown";
}
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/wallets
## 2.5.31
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.91
## 2.5.30
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@4.0.90
## 2.5.29
### Patch Changes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wallets",
"version": "2.5.29",
"version": "2.5.31",
"main": "dist/thirdweb-dev-wallets.cjs.js",
"module": "dist/thirdweb-dev-wallets.esm.js",
"browser": {
@@ -520,9 +520,9 @@
"sideEffects": false,
"dependencies": {
"@account-abstraction/contracts": "^0.5.0",
"@blocto/sdk": "0.5.5",
"@coinbase/wallet-sdk": "^3.7.2",
"@google-cloud/kms": "4.2.0",
"@blocto/sdk": "0.10.2",
"@coinbase/wallet-sdk": "^3.9.0",
"@google-cloud/kms": "^4.4.0",
"@magic-ext/connect": "^6.7.2",
"@magic-ext/oauth": "^7.6.2",
"@magic-sdk/provider": "^13.6.2",
@@ -530,7 +530,7 @@
"@paperxyz/embedded-wallet-service-sdk": "^1.2.5",
"@paperxyz/sdk-common-utilities": "^0.1.1",
"@safe-global/safe-core-sdk": "^3.3.5",
"@safe-global/safe-ethers-adapters": "0.1.0-alpha.17",
"@safe-global/safe-ethers-adapters": "0.1.0-alpha.19",
"@safe-global/safe-ethers-lib": "^1.9.4",
"@thirdweb-dev/chains": "workspace:*",
"@thirdweb-dev/contracts-js": "workspace:*",
@@ -114,7 +114,7 @@ export class BloctoConnector extends WagmiConnector<
getProvider({ chainId }: { chainId?: number } = {}): Promise<BloctoProvider> {
if (!this._provider) {
const _chainId =
chainId ?? this.options.chainId ?? this.chains[0]?.chainId ?? 1;
chainId ?? this.chains[0]?.chainId ?? 1;
const _rpc = this.chains.find((x) => x.chainId === _chainId)?.rpc[0];
this._provider = new BloctoSDK({
@@ -145,7 +145,7 @@ export class BloctoConnector extends WagmiConnector<
}
async isAuthorized(): Promise<boolean> {
return !!this._provider?._blocto?.sessionKey ?? false;
return !!this._provider?._blocto?.sessionKeyEnv ?? false;
}
async switchChain(chainId: number): Promise<Chain> {
@@ -157,8 +157,8 @@ export class BloctoConnector extends WagmiConnector<
throw new SwitchChainError(new Error("chain not found on connector."));
}
const isBloctoSupportChain =
provider._blocto.supportNetworkList[`${chainId}`];
const blocktoSupportedChainList = await provider.supportChainList();
const isBloctoSupportChain = blocktoSupportedChainList[`${chainId}`];
if (!isBloctoSupportChain) {
throw new SwitchChainError(new Error(`Blocto unsupported chain: ${id}`));
}
@@ -17,6 +17,9 @@ export enum AuthProvider {
CUSTOM_AUTH_ENDPOINT = "CustomAuthEndpoint",
FACEBOOK = "Facebook",
APPLE = "Apple",
PASSKEY = "Passkey",
EXTERNAL_WALLET = "ExternalWallet",
DISCORD = "Discord",
}
/**
-1
View File
@@ -37,7 +37,6 @@
"hotlink-init": "node ./scripts/hotlink/hotlink-init.mjs",
"hotlink-revert": "node ./scripts/hotlink/hotlink-revert.mjs"
},
"packageManager": "[email protected]",
"dependencies": {
"@changesets/changelog-github": "0.5.0",
"@changesets/cli": "2.27.1",
+2 -5
View File
@@ -23,10 +23,7 @@
},
"./package.json": "./package.json"
},
"files": [
"dist/*",
"src/*"
],
"files": ["dist/*", "src/*"],
"dependencies": {
"@aws-sdk/client-lambda": "3.577.0",
"@aws-sdk/credential-providers": "3.577.0",
@@ -43,7 +40,7 @@
"expo-application": "^5",
"expo-web-browser": "^13",
"react-native-aes-gcm-crypto": "^0.2",
"react-native-quick-crypto": ">=0.6.1",
"react-native-quick-crypto": ">=0.7.0-rc.6 || >=0.7",
"shamir-secret-sharing": "^0.0.3",
"react-native": ">=0.70",
"react-native-get-random-values": "^1"
+174
View File
@@ -1,5 +1,179 @@
# thirdweb
## 5.27.0
### Minor Changes
- [#3181](https://github.com/thirdweb-dev/js/pull/3181) [`a346111`](https://github.com/thirdweb-dev/js/commit/a3461114c5e9adb8d0ea2f36bc9fb3d96366c5b9) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Adds headless functions for creating and managing a WalletConnect session with a connected wallet
### `createWalletConnectClient`
```ts
import { createWalletConnectClient } from "thirdweb/wallets/wallet-connect";
createWalletConnectClient({
client: client,
wallet: wallet,
onConnect: (session: any) => {
alert("Connected");
},
onDisconnect: (session: any) => {
alert("Disconnected");
},
});
```
### `createWalletConnectSession`
```ts
import { createWalletConnectSession } from "thirdweb/wallets/wallet-connect";
createWalletConnectSession({
walletConnectClient: wcClient,
uri: "wc:...",
});
```
### `getWalletConnectSessions`
```ts
import {
getWalletConnectSession,
type WalletConnectSession,
} from "thirdweb/wallets/wallet-connect";
const sessions: WalletConnectSession[] = await getWalletConnectSessions();
```
### `disconnectWalletConnectClient`
```ts
import { disconnectWalletConnectClient } from "thirdweb/wallets/wallet-connect";
disconnectWalletConnectClient({ session, walletConnectClient: wcClient });
```
- [#3105](https://github.com/thirdweb-dev/js/pull/3105) [`51e7ada`](https://github.com/thirdweb-dev/js/commit/51e7adaeb0fe050eb132510291ecac7a12e4c4ad) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - First party support for zkSync native account abstraction
You can now use smart accounts on zkSync and zkSync sepolia without any extra setup.
```ts
const wallet = smartWallet({
chain: zkSync,
sponsorGas: true,
});
const smartAccount = await wallet.connect({
client,
personalAccount,
});
// now your can perform transactions normally, gas will be sponsored
sendTransaction({ transaction, account: smartAccount });
```
- [#3105](https://github.com/thirdweb-dev/js/pull/3105) [`51e7ada`](https://github.com/thirdweb-dev/js/commit/51e7adaeb0fe050eb132510291ecac7a12e4c4ad) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - ZkSync transaction support
### Patch Changes
- [#3195](https://github.com/thirdweb-dev/js/pull/3195) [`3f66945`](https://github.com/thirdweb-dev/js/commit/3f669453d086487a217ae9af82745199e26550c6) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Fix `estimateGas` type resolution
## 5.26.0
### Minor Changes
- [#3161](https://github.com/thirdweb-dev/js/pull/3161) [`b569eb4`](https://github.com/thirdweb-dev/js/commit/b569eb48ad018619e4589b9efb5aa774c7b77642) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Adds waitForBundle function to await a sendCalls bundle's complete confirmation.
### Usage
```ts
import { waitForBundle } from "thirdweb/wallets/eip5792";
const result = await waitForBundle({
client,
chain,
wallet,
bundleId: "0x123...",
});
```
- [#3161](https://github.com/thirdweb-dev/js/pull/3161) [`b569eb4`](https://github.com/thirdweb-dev/js/commit/b569eb48ad018619e4589b9efb5aa774c7b77642) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Adds EIP-5792 react hooks
## `useSendCalls`
`useSendCalls` will automatically revalidate all reads from contracts that are interacted with.
```tsx
import { useSendCalls } from "thirdweb/react";
const sendTx1 = approve({
contract: USDT_CONTRACT,
amount: 100,
spender: "0x33d9B8BEfE81027E2C859EDc84F5636cbb202Ed6",
});
const sendTx2 = approve({
contract: USDT_CONTRACT,
amount: 100,
spender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
});
const { mutate: sendCalls, data: bundleId } = useSendCalls({ client });
await sendCalls({
wallet,
client,
calls: [sendTx1, sendTx2],
});
```
Await the bundle's full confirmation:
```tsx
const { mutate: sendCalls, data: bundleId } = useSendCalls({
client,
waitForResult: true,
});
await sendCalls({
wallet,
client,
calls: [sendTx1, sendTx2],
});
```
Sponsor transactions with a paymaster:
```ts
const { mutate: sendCalls, data: bundleId } = useSendCalls();
await sendCalls({
client,
calls: [sendTx1, sendTx2],
capabilities: {
paymasterService: {
url: `https://${CHAIN.id}.bundler.thirdweb.com/${client.clientId}`,
},
},
});
```
## `useCapabilities`
```tsx
import { useCapabilities } from "thirdweb/react";
const { data: capabilities, isLoading } = useCapabilities();
```
## `useCallsStatus`
```tsx
import { useCallsStatus } from "thirdweb/react";
const { data: status, isLoading } = useCallsStatus({ bundleId, client });
```
### Patch Changes
- [#3178](https://github.com/thirdweb-dev/js/pull/3178) [`3dca028`](https://github.com/thirdweb-dev/js/commit/3dca028a7eff926ae406eb29cc82016ae6c994b3) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix gas estimation in transaction button
- [#3187](https://github.com/thirdweb-dev/js/pull/3187) [`874747b`](https://github.com/thirdweb-dev/js/commit/874747b3e00714e9a7336dcfda34e7b92b82e7e1) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Upgrade to latest coinbase wallet sdk
- [#3186](https://github.com/thirdweb-dev/js/pull/3186) [`123275b`](https://github.com/thirdweb-dev/js/commit/123275bbe275457b0be5205812ba0ef75b1f0e5b) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Catch any errors thrown while polling block numbers
## 5.25.1
### Patch Changes
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
"version": "5.25.1",
"version": "5.27.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -176,7 +176,7 @@
"!tsconfig.build.json"
],
"dependencies": {
"@coinbase/wallet-sdk": "4.0.0-rc.2",
"@coinbase/wallet-sdk": "4.0.3",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.0",
"@google/model-viewer": "2.1.1",
@@ -189,6 +189,7 @@
"@radix-ui/react-tooltip": "1.0.7",
"@tanstack/react-query": "5.29.2",
"@walletconnect/ethereum-provider": "2.12.2",
"@walletconnect/sign-client": "^2.13.1",
"abitype": "1.0.0",
"fast-text-encoding": "^1.0.6",
"fuse.js": "7.0.0",
@@ -210,7 +211,7 @@
"react": ">=18",
"react-native": ">=0.70",
"react-native-aes-gcm-crypto": "^0.2",
"react-native-quick-crypto": ">=0.6.1",
"react-native-quick-crypto": ">=0.7.0-rc.6 || >=0.7",
"shamir-secret-sharing": "^0.0.3",
"typescript": ">=5.0.4"
},
@@ -293,6 +294,7 @@
"@react-native-async-storage/async-storage": "^1.23.1",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.6",
"@testing-library/react-hooks": "^8.0.1",
"@types/cross-spawn": "^6.0.6",
"@types/react": "18.2.17",
"@vitejs/plugin-react": "^4.2.1",
@@ -11,6 +11,29 @@ export type AdapterWalletOptions = {
switchChain: (chain: Chain) => Promise<void> | void;
};
/**
* Creates a wallet from the given adapted account. Use this to convert a third party library wallet into a thirdweb wallet.
* @param options - The options for the adapter wallet.
* @returns a wallet instance.
*
* @example
* ```ts
* import { createWalletAdapter } from "thirdweb";
*
* const wallet = createWalletAdapter({
* client,
* adaptedAccount,
* chain,
* onDisconnect: () => {
* // disconnect logic
* },
* switchChain: async (chain) => {
* // switch chain logic
* },
* });
* ```
* @wallet
*/
export function createWalletAdapter(
options: AdapterWalletOptions,
): Wallet<"adapter"> {
@@ -0,0 +1,13 @@
import { defineChain } from "../utils.js";
export const hardhat = /* @__PURE__ */ defineChain({
id: 31337,
name: "Hardhat",
rpc: "http://127.0.0.1:8545",
testnet: true,
nativeCurrency: {
name: "Hardhat Ether",
symbol: "ETH",
decimals: 18,
},
});
@@ -0,0 +1,18 @@
import { defineChain } from "../utils.js";
export const zkSyncSepolia = /*@__PURE__*/ defineChain({
id: 300,
name: "ZkSync Sepolia",
nativeCurrency: {
decimals: 18,
name: "Ether",
symbol: "ETH",
},
blockExplorers: [
{
name: "zkSync Sepolia Block Explorer",
url: "https://sepolia.explorer.zksync.io",
apiUrl: "https://block-explorer-api.sepolia.zksync.dev/api",
},
],
});
@@ -0,0 +1,18 @@
import { defineChain } from "../utils.js";
export const zkSync = /*@__PURE__*/ defineChain({
id: 324,
name: "ZkSync Era",
nativeCurrency: {
decimals: 18,
name: "Ether",
symbol: "ETH",
},
blockExplorers: [
{
name: "zkSync Era Block Explorer",
url: "https://explorer.zksync.io",
apiUrl: "https://block-explorer-api.zksync.dev/api",
},
],
});
+3
View File
@@ -11,6 +11,7 @@ export {
* PRE_DEFINED CHAINS
*/
export { anvil } from "../chains/chain-definitions/anvil.js";
export { hardhat } from "../chains/chain-definitions/hardhat.js";
export { arbitrumNova } from "../chains/chain-definitions/arbitrum-nova.js";
export { arbitrumSepolia } from "../chains/chain-definitions/arbitrum-sepolia.js";
export { arbitrum } from "../chains/chain-definitions/arbitrum.js";
@@ -34,3 +35,5 @@ export { zoraSepolia } from "../chains/chain-definitions/zora-sepolia.js";
export { zora } from "../chains/chain-definitions/zora.js";
export { bsc } from "../chains/chain-definitions/bsc.js";
export { bscTestnet } from "../chains/chain-definitions/bsc-testnet.js";
export { zkSync } from "../chains/chain-definitions/zksync.js";
export { zkSyncSepolia } from "../chains/chain-definitions/zksync-sepolia.js";
@@ -210,6 +210,7 @@ export {
type StringToHexOpts,
type ToHexParameters,
type Uint8ArrayToHexOpts,
type Hex,
} from "../utils/encoding/hex.js";
export { concatHex } from "../utils/encoding/helpers/concat-hex.js";
@@ -10,8 +10,14 @@ export type {
PreparedSendCall,
PrepareCallOptions,
} from "../../wallets/eip5792/send-calls.js";
export { getCallsStatus } from "../../wallets/eip5792/get-calls-status.js";
export type { GetCallsStatusOptions } from "../../wallets/eip5792/get-calls-status.js";
export {
type GetCallsStatusOptions,
getCallsStatus,
} from "../../wallets/eip5792/get-calls-status.js";
export {
waitForBundle,
type WaitForBundleOptions,
} from "../../wallets/eip5792/wait-for-bundle.js";
export type {
WalletCapabilities,
WalletCapabilitiesRecord,
@@ -20,3 +26,6 @@ export type {
GetCallsStatusResponse,
WalletCallReceipt,
} from "../../wallets/eip5792/types.js";
export { useSendCalls } from "../../react/core/hooks/wallets/useSendCalls.js";
export { useCallsStatus } from "../../react/core/hooks/wallets/useCallsStatus.js";
export { useCapabilities } from "../../react/core/hooks/wallets/useCapabilities.js";
@@ -0,0 +1,10 @@
export {
createWalletConnectClient,
createWalletConnectSession,
disconnectWalletConnectSession,
getActiveWalletConnectSessions,
} from "../../wallets/wallet-connect/consumer/index.js";
export type {
WalletConnectClient,
WalletConnectSession,
} from "../../wallets/wallet-connect/consumer/types.js";
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { DOODLES_CONTRACT, DROP1155_CONTRACT } from "~test/test-contracts.js";
import { isERC1155 } from "./isERC1155.js";
describe("isERC1155", () => {
it("should return TRUE if contract is an ERC1155 contract", async () => {
const result = await isERC1155({ contract: DROP1155_CONTRACT });
expect(result).toBe(true);
});
it("should return FALSE if contract is NOT an ERC1155 contract", async () => {
const result = await isERC1155({ contract: DOODLES_CONTRACT });
expect(result).toBe(false);
});
});
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { DOODLES_CONTRACT } from "~test/test-contracts.js";
import { getOwnedNFTs } from "./getOwnedNFTs.js";
describe.runIf(process.env.TW_SECRET_KEY)("erc721.getOwnedNFTs", () => {
it("should return the correct data", async () => {
const owner = "0x3010775D16E7B79AF280035c64a1Df5F705CfdDb";
const nfts = await getOwnedNFTs({
contract: DOODLES_CONTRACT,
owner,
});
// The following code is based on the state of the forked chain
// so the data should not change
expect(nfts.length).toBe(81);
for (const item of nfts) {
expect(item.owner).toBe(owner);
}
});
});
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { DOODLES_CONTRACT } from "~test/test-contracts.js";
import { getOwnedTokenIds } from "./getOwnedTokenIds.js";
describe.runIf(process.env.TW_SECRET_KEY)("erc721.getOwnedTokenIds", () => {
it("should return the correct result", async () => {
const owner = "0x3010775D16E7B79AF280035c64a1Df5F705CfdDb";
const tokenIds = await getOwnedTokenIds({
contract: DOODLES_CONTRACT,
owner,
});
// The following code is based on the state of the forked chain
// so the data should not change
expect(tokenIds.length).toBe(81);
});
});
@@ -0,0 +1,84 @@
import { beforeEach, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
import { deployERC721Contract } from "../../prebuilts/deploy-erc721.js";
import { claimTo } from "../drops/write/claimTo.js";
import { setClaimConditions } from "../drops/write/setClaimConditions.js";
import { lazyMint } from "../write/lazyMint.js";
import { getTotalClaimedSupply } from "./getTotalClaimedSupply.js";
const account = TEST_ACCOUNT_A;
let contract: ThirdwebContract;
describe.runIf(process.env.TW_SECRET_KEY)(
"erc721: getTotalClaimedSupply",
() => {
beforeEach(async () => {
const address = await deployERC721Contract({
client: TEST_CLIENT,
account,
type: "DropERC721",
chain: ANVIL_CHAIN,
params: {
name: "",
contractURI: TEST_CONTRACT_URI,
},
});
contract = getContract({
chain: ANVIL_CHAIN,
address,
client: TEST_CLIENT,
});
const lazyMintTx = lazyMint({
contract,
// These are the pre-uploaded URIs file - to speed up the test
nfts: [
"ipfs://QmVQpAEkbzbqm7fxMJ5up68CppbP5fF7bLxnrtPRMuTaYT/0",
"ipfs://QmVQpAEkbzbqm7fxMJ5up68CppbP5fF7bLxnrtPRMuTaYT/1",
"ipfs://QmVQpAEkbzbqm7fxMJ5up68CppbP5fF7bLxnrtPRMuTaYT/2",
],
});
await sendAndConfirmTransaction({ transaction: lazyMintTx, account });
const setClaimTx = setClaimConditions({
contract,
phases: [
{
maxClaimableSupply: 100n,
maxClaimablePerWallet: 100n,
currencyAddress: NATIVE_TOKEN_ADDRESS,
price: 0,
startTime: new Date(),
},
],
});
await sendAndConfirmTransaction({
transaction: setClaimTx,
account,
});
});
it("should return the correct claimed amount", async () => {
const tx = claimTo({
contract,
to: account.address,
quantity: 1n,
});
await sendAndConfirmTransaction({ transaction: tx, account });
const result = await getTotalClaimedSupply({ contract });
expect(result).toBe(1n);
});
},
);
@@ -0,0 +1,84 @@
import { beforeEach, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
import { deployERC721Contract } from "../../prebuilts/deploy-erc721.js";
import { claimTo } from "../drops/write/claimTo.js";
import { setClaimConditions } from "../drops/write/setClaimConditions.js";
import { lazyMint } from "../write/lazyMint.js";
import { getTotalUnclaimedSupply } from "./getTotalUnclaimedSupply.js";
const account = TEST_ACCOUNT_A;
let contract: ThirdwebContract;
describe.runIf(process.env.TW_SECRET_KEY)(
"erc721: getTotalClaimedSupply",
() => {
beforeEach(async () => {
const address = await deployERC721Contract({
client: TEST_CLIENT,
account,
type: "DropERC721",
chain: ANVIL_CHAIN,
params: {
name: "",
contractURI: TEST_CONTRACT_URI,
},
});
contract = getContract({
chain: ANVIL_CHAIN,
address,
client: TEST_CLIENT,
});
const lazyMintTx = lazyMint({
contract,
// These are the pre-uploaded URIs file - to speed up the test
nfts: [
"ipfs://QmVQpAEkbzbqm7fxMJ5up68CppbP5fF7bLxnrtPRMuTaYT/0",
"ipfs://QmVQpAEkbzbqm7fxMJ5up68CppbP5fF7bLxnrtPRMuTaYT/1",
"ipfs://QmVQpAEkbzbqm7fxMJ5up68CppbP5fF7bLxnrtPRMuTaYT/2",
],
});
await sendAndConfirmTransaction({ transaction: lazyMintTx, account });
const setClaimTx = setClaimConditions({
contract,
phases: [
{
maxClaimableSupply: 100n,
maxClaimablePerWallet: 100n,
currencyAddress: NATIVE_TOKEN_ADDRESS,
price: 0,
startTime: new Date(),
},
],
});
await sendAndConfirmTransaction({
transaction: setClaimTx,
account,
});
});
it("should return the correct un-claimed amount", async () => {
const tx = claimTo({
contract,
to: account.address,
quantity: 1n,
});
await sendAndConfirmTransaction({ transaction: tx, account });
const result = await getTotalUnclaimedSupply({ contract });
expect(result).toBe(2n);
});
},
);
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { DOODLES_CONTRACT, DROP1155_CONTRACT } from "~test/test-contracts.js";
import { isERC721 } from "./isERC721.js";
describe("isERC721", () => {
it("should return TRUE if contract is an ERC721 contract", async () => {
const result = await isERC721({ contract: DOODLES_CONTRACT });
expect(result).toBe(true);
});
it("should return FALSE if contract is NOT an ERC721 contract", async () => {
const result = await isERC721({ contract: DROP1155_CONTRACT });
expect(result).toBe(false);
});
});
@@ -6,6 +6,12 @@ export type SetSharedMetadataParams = {
nft: NFTInput;
};
/**
* Sets the shared metadata for a OpenEdition contract.
* @param options - The options for the transaction.
* @returns The prepared transaction.
* @extension ERC721
*/
export function setSharedMetadata(
options: BaseTransactionOptions<SetSharedMetadataParams>,
) {
+4 -4
View File
@@ -41,15 +41,15 @@ export async function getGasOverridesForTransaction(
]);
// Exit early if the user explicitly provided enough options
if (gasPrice) {
return { gasPrice };
}
if (maxFeePerGas && maxPriorityFeePerGas) {
if (maxFeePerGas !== undefined && maxPriorityFeePerGas !== undefined) {
return {
maxFeePerGas,
maxPriorityFeePerGas,
};
}
if (gasPrice) {
return { gasPrice };
}
// If we don't have enough explicit values, get defaults
const defaultGasOverrides = await getDefaultGasOverrides(
@@ -33,7 +33,7 @@ export type BuyWithFiatHistoryParams = {
/**
* The results for [`getBuyWithFiatHistory`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithFiatHistory) function
* @buyFiat
* @buyCrypto
*/
export type BuyWithFiatHistoryData = {
page: BuyWithFiatStatus[];
@@ -60,7 +60,7 @@ export type BuyWithFiatHistoryData = {
* })
* ```
* @returns Object of type [`BuyWithFiatHistoryData`](https://portal.thirdweb.com/references/typescript/v5/BuyWithFiatHistoryData)
* @buyFiat
* @buyCrypto
*/
export async function getBuyWithFiatHistory(
params: BuyWithFiatHistoryParams,
@@ -54,7 +54,7 @@ export type GetPostOnRampQuoteParams = {
* });
* }
* ```
* @buyFiat
* @buyCrypto
*/
export async function getPostOnRampQuote({
client,
@@ -232,7 +232,7 @@ export type BuyWithFiatQuote = {
*
* window.open(quote.onRampLink, "_blank");
* ```
* @buyFiat
* @buyCrypto
*/
export async function getBuyWithFiatQuote(
params: GetBuyWithFiatQuoteParams,
@@ -158,7 +158,7 @@ export type BuyWithFiatStatus =
* // when the fiatStatus.status is "ON_RAMP_TRANSFER_COMPLETED" - the process is complete
* // when the fiatStatus.status is "CRYPTO_SWAP_REQUIRED" - start the swap process
* ```
* @buyFiat
* @buyCrypto
*/
export async function getBuyWithFiatStatus(
params: GetBuyWithFiatStatusParams,
@@ -11,7 +11,7 @@ import type { BuyWithFiatQuote } from "./getQuote.js";
*
* @param buyWithFiatQuote - The quote of type [`BuyWithFiatQuote`](https://portal.thirdweb.com/references/typescript/v5/BuyWithFiatQuote) returned
* by the [`getBuyWithFiatQuote`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithFiatQuote) function.
* @buyFiat
* @buyCrypto
*/
export function isSwapRequiredPostOnramp(
buyWithFiatQuote: Pick<BuyWithFiatQuote, "toToken" | "onRampToken">,
@@ -1,4 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { createWallet } from "../../../../wallets/create-wallet.js";
import { getInstalledWalletProviders } from "../../../../wallets/injected/mipdStore.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import {
getLastConnectedChain,
@@ -68,8 +70,13 @@ export function useAutoConnect(props: AutoConnectProps) {
});
}
const availableWallets = [
...wallets,
...getInstalledWalletProviders().map((p) => createWallet(p.info.rdns)),
];
const activeWallet =
lastActiveWalletId && wallets.find((w) => w.id === lastActiveWalletId);
lastActiveWalletId &&
availableWallets.find((w) => w.id === lastActiveWalletId);
if (activeWallet) {
try {
@@ -5,7 +5,6 @@ import { sendTransaction } from "../../../../transaction/actions/send-transactio
import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
import { resolvePromisedValue } from "../../../../utils/promise/resolve-promised-value.js";
import type { Account } from "../../../../wallets/interfaces/wallet.js";
import {
type GetWalletBalanceResult,
getWalletBalance,
@@ -98,15 +97,14 @@ export function useSendTransactionCore(
return;
}
// buy supported, check if there is enouch balance - if not show modal to buy tokens
// buy supported, check if there is enough balance - if not show modal to buy tokens
const [walletBalance, totalCostWei] = await Promise.all([
getWalletBalance({
address: account.address,
chain: tx.chain,
client: tx.client,
}),
getTotalTxCostForBuy(tx, account),
getTotalTxCostForBuy(tx, account?.address),
]);
const walletBalanceWei = walletBalance.value;
@@ -140,18 +138,31 @@ export function useSendTransactionCore(
export async function getTotalTxCostForBuy(
tx: PreparedTransaction,
account?: Account,
from?: string,
) {
const gasCost = await estimateGasCost({
transaction: tx,
account,
});
try {
const gasCost = await estimateGasCost({
transaction: tx,
from,
});
const bufferCost = gasCost.wei / 10n;
const bufferCost = gasCost.wei / 10n;
// Note: get tx.value AFTER estimateGasCost
const txValue = await resolvePromisedValue(tx.value);
// Note: get tx.value AFTER estimateGasCost
const txValue = await resolvePromisedValue(tx.value);
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
return gasCost.wei + bufferCost + (txValue || 0n);
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
return gasCost.wei + bufferCost + (txValue || 0n);
} catch (e) {
if (from) {
// try again without passing from
return await getTotalTxCostForBuy(tx);
}
// fallback if both fail, use the tx value + 1% buffer
const value = await resolvePromisedValue(tx.value);
if (!value) {
return 0n;
}
return value + value / 100n;
}
}
@@ -0,0 +1,79 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import type { ReactNode } from "react";
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { getContract } from "../../../../contract/contract.js";
import { mintTo } from "../../../../extensions/erc20/write/mintTo.js";
import { deployERC20Contract } from "../../../../extensions/prebuilts/deploy-erc20.js";
import { sendAndConfirmTransaction } from "../../../../transaction/actions/send-and-confirm-transaction.js";
import { useWalletBalance } from "./useWalletBalance.js";
const createWrapper = () => {
const queryClient = new QueryClient();
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe.runIf(process.env.TW_SECRET_KEY)("useWalletBalance", () => {
it("should return the correct balance for erc20", async () => {
const erc20Address = await deployERC20Contract({
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
account: TEST_ACCOUNT_A,
type: "TokenERC20",
params: {
name: "",
contractURI: TEST_CONTRACT_URI,
},
});
const erc20Contract = getContract({
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
address: erc20Address,
});
const amount = 1000;
// Mint some tokens
const tx = mintTo({
contract: erc20Contract,
to: TEST_ACCOUNT_A.address,
amount,
});
await sendAndConfirmTransaction({
transaction: tx,
account: TEST_ACCOUNT_A,
});
const { result } = renderHook(
() =>
useWalletBalance({
chain: ANVIL_CHAIN,
address: TEST_ACCOUNT_A.address,
client: TEST_CLIENT,
tokenAddress: erc20Address,
}),
{
wrapper: createWrapper(),
},
);
const defaultDecimal = 18;
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toBeDefined();
expect(result.current.data?.decimals).toBe(defaultDecimal);
expect(result.current.data?.symbol).toBeDefined();
expect(result.current.data?.name).toBeDefined();
expect(result.current.data?.displayValue).toBe(amount.toString());
expect(result.current.data?.value).toBe(
BigInt(amount) * 10n ** BigInt(defaultDecimal),
);
});
});
@@ -22,6 +22,7 @@ import {
*
* const { data, isLoading, isError } = useWalletBalance({ chain, address, client, tokenAddress });
* ```
* @wallet
*/
export function useWalletBalance(options: Partial<GetWalletBalanceOptions>) {
const { chain, address, tokenAddress, client } = options;
@@ -35,6 +35,7 @@ export type BuyHistoryQueryOptions = Omit<
* return <div> ... </div>
* }
* ```
* @buyCrypto
*/
export function useBuyHistory(
params?: BuyHistoryParams,
@@ -34,7 +34,7 @@ export type BuyWithFiatHistoryQueryOptions = Omit<
* return <div> ... </div>
* }
* ```
* @buyFiat
* @buyCrypto
*/
export function useBuyWithFiatHistory(
params?: BuyWithFiatHistoryParams,
@@ -60,7 +60,7 @@ export type BuyWithFiatQuoteQueryOptions = Omit<
* );
* }
* ```
* @buyFiat
* @buyCrypto
*/
export function useBuyWithFiatQuote(
params?: GetBuyWithFiatQuoteParams,
@@ -31,7 +31,7 @@ import {
* return <div>...</div>;
* }
* ```
* @buyFiat
* @buyCrypto
*/
export function useBuyWithFiatStatus(
params?: GetBuyWithFiatStatusParams,
@@ -32,7 +32,7 @@ export type PostOnRampQuoteQueryOptions = Omit<
*
* @param params - object of type [`GetPostOnRampQuoteParams`](https://portal.thirdweb.com/references/typescript/v5/GetPostOnRampQuoteParams)
* @returns Object of type [`BuyWithCryptoQuote`](https://portal.thirdweb.com/references/typescript/v5/BuyWithCryptoQuote) which contains the information about the quote such as processing fees, estimated time, converted token amounts, etc.
* @buyFiat
* @buyCrypto
*/
export function usePostOnRampQuote(
params?: GetPostOnRampQuoteParams,
@@ -0,0 +1,49 @@
import {
type UseQueryOptions,
type UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import type { ThirdwebClient } from "../../../../client/client.js";
import { getCallsStatus } from "../../../../wallets/eip5792/get-calls-status.js";
import type { GetCallsStatusResponse } from "../../../../wallets/eip5792/types.js";
import { useActiveWallet } from "./wallet-hooks.js";
/**
* A hook to get a call bundle's current status according to [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792).
*
* @note This function is dependent on the wallet's support for EIP-5792 and could throw an error if it's not supported.
*
* @returns a React Query object.
* @beta
* @example
* ```tsx
* import { useCallsStatus } from "thirdweb/react";
* const { data: status, isLoading } = useCallsStatus({ bundleId, client });
* ```
* @extension EIP5792
*/
export function useCallsStatus(options: {
bundleId: string;
client: ThirdwebClient;
queryOptions?: Pick<UseQueryOptions, "enabled" | "retry">;
}): UseQueryResult<GetCallsStatusResponse> {
const { client, bundleId } = options;
const wallet = useActiveWallet();
return useQuery({
queryKey: [
"getCapabilities",
wallet?.getChain()?.id || -1,
wallet?.id,
] as const,
queryFn: async () => {
if (!wallet) {
throw new Error("Failed to get calls status, no wallet connected");
}
return getCallsStatus({ wallet, client, bundleId });
},
retry: false,
enabled: !!bundleId && wallet !== undefined,
...options.queryOptions,
});
}
@@ -0,0 +1,49 @@
import {
type UseQueryOptions,
type UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import {
type GetCapabilitiesResult,
getCapabilities,
} from "../../../../wallets/eip5792/get-capabilities.js";
import { useActiveWallet } from "./wallet-hooks.js";
/**
* A hook to get the current wallet's capabilities according to [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792).
*
* @note This function is dependent on the wallet's support for EIP-5792, but will not throw.
* **The returned object contains a `message` field detailing any issues with the wallet's support for EIP-5792.**
*
* @returns a React Query object.
* @beta
* @example
* ```tsx
* import { useCapabilities } from "thirdweb/react";
* const { data: capabilities, isLoading } = useCapabilities();
* ```
* @extension EIP5792
*/
export function useCapabilities(options?: {
queryOptions?: Pick<UseQueryOptions, "enabled" | "retry">;
}): UseQueryResult<GetCapabilitiesResult> {
const wallet = useActiveWallet();
return useQuery({
queryKey: [
"getCapabilities",
wallet?.getChain()?.id || -1,
wallet?.id,
] as const,
queryFn: async () => {
if (!wallet) {
return {
message: "Can't get capabilities, no wallet connected",
} as const;
}
return getCapabilities({ wallet });
},
retry: false,
...options?.queryOptions,
});
}
@@ -0,0 +1,150 @@
import {
type UseMutationResult,
useMutation,
useQueryClient,
} from "@tanstack/react-query";
import type { ThirdwebClient } from "../../../../client/client.js";
import {
type SendCallsOptions,
sendCalls,
} from "../../../../wallets/eip5792/send-calls.js";
import type {
GetCallsStatusResponse,
WalletSendCallsId,
} from "../../../../wallets/eip5792/types.js";
import { waitForBundle } from "../../../../wallets/eip5792/wait-for-bundle.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import { invalidateWalletBalance } from "../../providers/invalidateWalletBalance.js";
import { useActiveWallet } from "../wallets/wallet-hooks.js";
/**
* A hook to send [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792) calls to a wallet.
* This hook works with all Thirdweb wallets (in-app and smart) and certain injected wallets that already support EIP-5792.
* Transactions will be bundled and sponsored when those capabilities are supported, otherwise they will be sent as individual transactions.
*
* When calls are sent, all contracts that are interacted with will have their corresponding reads revalidated via React Query.
*
* @note This hook is dependent on the wallet's support for EIP-5792 and could fail.
* @note The mutatuon function will use your currently connected wallet by default, but you can pass it a specific wallet to use if you'd like.
*
* @returns A React Query mutatuon object to interact with {@link sendCalls}
* @throws an error if the wallet does not support EIP-5792.
* @returns The ID of the bundle of the calls.
*
* @beta
* @example
* ```tsx
* import { useSendCalls } from "thirdweb/react";
*
* const sendTx1 = approve({
contract: USDT_CONTRACT,
amount: 100,
spender: "0x33d9B8BEfE81027E2C859EDc84F5636cbb202Ed6",
});
* const sendTx2 = approve({
contract: USDT_CONTRACT,
amount: 100,
spender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
});
* const { mutate: sendCalls, data: bundleId } = useSendCalls({ client });
* await sendCalls({
* wallet,
* client,
* calls: [sendTx1, sendTx2],
* });
* ```
* Await the bundle's full confirmation:
* ```tsx
* const { mutate: sendCalls, data: bundleId } = useSendCalls({ client, waitForResult: true });
* await sendCalls({
* wallet,
* client,
* calls: [sendTx1, sendTx2],
* });
* ```
* Sponsor transactions with a paymaster:
* ```ts
* const { mutate: sendCalls, data: bundleId } = useSendCalls();
* await sendCalls({
* client,
* calls: [sendTx1, sendTx2],
* capabilities: {
* paymasterService: {
* url: `https://${CHAIN.id}.bundler.thirdweb.com/${client.clientId}`
* }
* }
* });
* @note We recommend proxying any paymaster calls via an API route you setup and control.
* ```
* @extension EIP5792
*/
export function useSendCalls({
client,
waitForResult = true,
}: { client: ThirdwebClient; waitForResult?: boolean }): UseMutationResult<
GetCallsStatusResponse | WalletSendCallsId,
Error,
Omit<SendCallsOptions, "chain" | "wallet"> & { wallet?: Wallet } // Optional wallet override
> {
const connectedWallet = useActiveWallet();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (options) => {
const { wallet = connectedWallet } = options;
const chain = wallet?.getChain();
if (!wallet || !chain) {
throw new Error(
"Failed to send transactions, no connected wallet found.",
);
}
const callsPromise = sendCalls({ ...options, wallet });
if (!waitForResult) {
return callsPromise;
}
const result = await waitForBundle({
bundleId: await callsPromise,
wallet,
client,
chain,
});
return result;
},
onSettled: async (_result, _error, variables) => {
// Attempt to invalidate any reads related to the sent transactions
const chain = connectedWallet?.getChain();
if (!_result || !connectedWallet || !chain) {
return;
}
if (typeof _result === "string") {
await waitForBundle({
bundleId: _result,
wallet: connectedWallet,
client,
chain,
}).catch((error) => {
console.error(
"Failed to confirm sent bundle and invalidate queries",
_result,
error,
);
return undefined;
});
}
for (const call of variables.calls) {
queryClient.invalidateQueries({
queryKey: [
"readContract",
call.__contract?.chain.id,
call.__contract?.address,
],
});
}
invalidateWalletBalance(queryClient, chain.id);
},
});
}
@@ -0,0 +1,31 @@
import type { QueryClient } from "@tanstack/react-query";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { invalidateWalletBalance } from "./invalidateWalletBalance.js";
const chainId = 1;
const queryClient = {
invalidateQueries: vi.fn(),
} as unknown as QueryClient;
describe("invalidateWalletBalance", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should call invalidateQueries when chainId is provided", () => {
invalidateWalletBalance(queryClient, chainId);
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: ["walletBalance", chainId] as const,
});
});
it("should call invalidateQueries when chainId is not provided", () => {
invalidateWalletBalance(queryClient);
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: ["walletBalance", undefined] as const,
});
});
});
@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { shortenString } from "./addresses.js";
const testAddress = "0x0000000000000000000000000000000000000000";
describe("react: addresses test", () => {
it("should shorten string <extraShort === true>", () => {
expect(shortenString(testAddress, true)).toBe("0x00...000");
});
it("should shorten string <extraShort === false>", () => {
expect(shortenString(testAddress, false)).toBe("0x0000...0000");
});
});
@@ -0,0 +1,65 @@
import { describe, expect, it, vi } from "vitest";
import { timeoutPromise } from "./timeoutPromise.js";
const options = {
ms: 100,
message: "Timeout",
};
describe("timeoutPromise", () => {
it("should resolve if the promise resolves before the timeout", async () => {
const mockPromise = new Promise((resolve) => {
setTimeout(() => resolve("success"), 50);
});
const result = await timeoutPromise(mockPromise, options);
expect(result).toBe("success");
});
it("should reject if the promise rejects before the timeout", async () => {
const mockPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("failure")), 50);
});
try {
await timeoutPromise(mockPromise, options);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe("failure");
}
});
it("rejects with the timeout message if the promise took longer than the allowed time", async () => {
const mockPromise = new Promise((resolve) => {
setTimeout(() => resolve("success"), 1000);
});
try {
await timeoutPromise(mockPromise, options);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe("Timeout");
}
});
it("cleans up the timeout when the promise resolves", async () => {
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
const mockPromise = new Promise((resolve) => {
setTimeout(() => resolve("success"), 50);
});
await timeoutPromise(mockPromise, options);
expect(clearTimeoutSpy).toHaveBeenCalled();
clearTimeoutSpy.mockRestore();
});
it("cleans up the timeout when the promise rejects", async () => {
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
const mockPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("failure")), 50);
});
try {
await timeoutPromise(mockPromise, options);
} catch (error) {}
expect(clearTimeoutSpy).toHaveBeenCalled();
clearTimeoutSpy.mockRestore();
});
});
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { wait } from "./wait.js";
describe("wait", () => {
it("should wait", async () => {
const start = Date.now();
const waitTime = 1500;
await wait(waitTime);
const end = Date.now();
const elapsedTime = end - start;
expect(elapsedTime).toBeGreaterThanOrEqual(waitTime);
});
});
@@ -38,7 +38,10 @@ export function useBuyTxStates(options: {
}
try {
const totalCost = await getTotalTxCostForBuy(buyForTx.tx, account);
const totalCost = await getTotalTxCostForBuy(
buyForTx.tx,
account?.address,
);
if (!mounted) {
return;
@@ -42,7 +42,7 @@ import type { WelcomeScreen } from "./screens/types.js";
* The returned `connect` method takes an object of type [UseConnectModalOptions](https://portal.thirdweb.com/references/typescript/v5/ConnectButtonProps)
* as an argument to customize the Connect Modal UI. Refer to [UseConnectModalOptions](https://portal.thirdweb.com/references/typescript/v5/ConnectButtonProps) to see the available options.
*
* @connectWallet
* @walletConnection
*/
export function useConnectModal() {
const setRootEl = useContext(SetRootElementContext);
@@ -5,6 +5,7 @@ import type { WalletId } from "../../../../wallets/wallet-types.js";
/**
* Returns the wallet info for the provided wallet id.
* @wallet
*/
export function useWalletInfo(id: WalletId) {
return useQuery<WalletInfo>({
@@ -18,6 +19,7 @@ export function useWalletInfo(id: WalletId) {
/**
* Returns the wallet image for the provided wallet id.
* @wallet
*/
export function useWalletImage(id: WalletId) {
return useQuery({
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { wideModalScreenThreshold } from "../ui/ConnectWallet/constants.js";
import { canFitWideModal } from "./canFitWideModal.js";
describe("canFitWideModal", () => {
it("should return true if window width is >= wideModalScreenThreshold", () => {
// Mock the window object and set innerWidth
Object.defineProperty(window, "innerWidth", {
writable: true,
value: wideModalScreenThreshold,
});
expect(canFitWideModal()).toBe(true);
window.innerWidth = wideModalScreenThreshold + 1;
expect(canFitWideModal()).toBe(true);
});
it("returns false if window width is < wideModalScreenThreshold", () => {
// Mock the window object and set innerWidth
Object.defineProperty(window, "innerWidth", {
writable: true,
value: wideModalScreenThreshold - 1,
});
expect(canFitWideModal()).toBe(false);
});
it("returns false if window is undefined", () => {
// Mock the window object to be undefined
const originalWindow = global.window;
// @ts-ignore - Not sure if this is the best way to mock the undefined `window` object
global.window = undefined;
expect(canFitWideModal()).toBe(false);
global.window = originalWindow;
});
});
@@ -16,7 +16,7 @@ const injectedWalletLocaleEn = (wallet: string): InjectedWalletLocale => ({
scanScreen: {
instruction: `Scan the QR code with the ${wallet} app to connect`,
},
getStartedLink: `Don't have a ${wallet} wallet?`,
getStartedLink: `Don't have ${wallet}?`,
download: {
chrome: "Download Chrome Extension",
android: "Download on Google Play",
+53 -29
View File
@@ -32,6 +32,7 @@ function createBlockNumberPoller(
client: ThirdwebClient,
chain: Chain,
overPollRatio?: number,
onError?: (error: Error) => void,
) {
let subscribers: Array<(blockNumber: bigint) => void> = [];
let blockTimesWindow: number[] = [];
@@ -51,35 +52,48 @@ function createBlockNumberPoller(
if (!isActive) {
return;
}
const blockNumber = await eth_blockNumber(rpcRequest);
if (!lastBlockNumber || blockNumber > lastBlockNumber) {
let newBlockNumbers = [];
if (lastBlockNumber) {
for (let i = lastBlockNumber + 1n; i <= blockNumber; i++) {
newBlockNumbers.push(BigInt(i));
try {
const blockNumber = await eth_blockNumber(rpcRequest);
if (!lastBlockNumber || blockNumber > lastBlockNumber) {
let newBlockNumbers = [];
if (lastBlockNumber) {
for (let i = lastBlockNumber + 1n; i <= blockNumber; i++) {
newBlockNumbers.push(BigInt(i));
}
} else {
newBlockNumbers = [blockNumber];
}
lastBlockNumber = blockNumber;
const currentTime = new Date().getTime();
if (lastBlockAt) {
// if we skipped a block we need to adjust the block time down to that level
const blockTime =
(currentTime - lastBlockAt) / newBlockNumbers.length;
blockTimesWindow.push(blockTime);
blockTimesWindow = blockTimesWindow.slice(-SLIDING_WINDOW_SIZE);
}
lastBlockAt = currentTime;
// for all new blockNumbers...
for (const b of newBlockNumbers) {
// ... call all current subscribers
for (const subscriberCallback of subscribers) {
subscriberCallback(b);
}
}
}
} catch (err: unknown) {
if (onError) {
onError(err as Error);
} else {
newBlockNumbers = [blockNumber];
}
lastBlockNumber = blockNumber;
const currentTime = new Date().getTime();
if (lastBlockAt) {
// if we skipped a block we need to adjust the block time down to that level
const blockTime = (currentTime - lastBlockAt) / newBlockNumbers.length;
blockTimesWindow.push(blockTime);
blockTimesWindow = blockTimesWindow.slice(-SLIDING_WINDOW_SIZE);
}
lastBlockAt = currentTime;
// for all new blockNumbers...
for (const b of newBlockNumbers) {
// ... call all current subscribers
for (const subscriberCallback of subscribers) {
subscriberCallback(b);
}
console.error(
`[watchBlockNumber]: Failed to poll for latest block number: ${err}`,
);
}
}
const currentApproximateBlockTime = getAverageBlockTime(blockTimesWindow);
// make sure we never poll faster than our minimum poll delay or slower than our maximum poll delay
@@ -141,6 +155,7 @@ export type WatchBlockNumberOptions = {
client: ThirdwebClient;
chain: Chain;
onNewBlockNumber: (blockNumber: bigint) => void;
onError?: (error: Error) => void;
overPollRatio?: number;
latestBlockNumber?: bigint;
};
@@ -157,7 +172,10 @@ export type WatchBlockNumberOptions = {
* chainId,
* onNewBlockNumber: (blockNumber) => {
* // do something with the block number
* },
* },
* onError: (err) => {
* // do something if getting the block number fails
* },
* });
*
* // later stop watching
@@ -166,14 +184,20 @@ export type WatchBlockNumberOptions = {
* @rpc
*/
export function watchBlockNumber(opts: WatchBlockNumberOptions) {
const { client, chain, onNewBlockNumber, overPollRatio, latestBlockNumber } =
opts;
const {
client,
chain,
onNewBlockNumber,
overPollRatio,
latestBlockNumber,
onError,
} = opts;
const chainId = chain.id;
// if we already have a poller for this chainId -> use it
// if we already have a poller for this chainId -> use it.
let poller = existingPollers.get(chainId);
// otherwise create a new poller
if (!poller) {
poller = createBlockNumberPoller(client, chain, overPollRatio);
poller = createBlockNumberPoller(client, chain, overPollRatio, onError);
// and store it for later use
existingPollers.set(chainId, poller);
}
@@ -8,7 +8,9 @@ import type { PreparedTransaction } from "../prepare-transaction.js";
export type EstimateGasOptions = Prettify<
{
transaction: PreparedTransaction;
// 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>;
} & (
| {
account: Account;
@@ -23,7 +25,10 @@ export type EstimateGasOptions = Prettify<
export type EstimateGasResult = bigint;
const cache = new WeakMap<PreparedTransaction, Promise<EstimateGasResult>>();
const cache = new WeakMap<
PreparedTransaction & { from: string | undefined },
Promise<EstimateGasResult>
>();
/**
* Estimates the gas required to execute a transaction. The gas is returned as a `bigint` and in gwei units.
@@ -41,9 +46,15 @@ const cache = new WeakMap<PreparedTransaction, Promise<EstimateGasResult>>();
export async function estimateGas(
options: EstimateGasOptions,
): Promise<EstimateGasResult> {
if (cache.has(options.transaction)) {
// from is:
// 1. the user specified from address
// 2. the passed in account address
// 3. the passed in wallet's account address
const from = options.from ?? options.account?.address ?? undefined;
const txWithFrom = { ...options.transaction, from };
if (cache.has(txWithFrom)) {
// biome-ignore lint/style/noNonNullAssertion: the `has` above ensures that this will always be set
return cache.get(options.transaction)!;
return cache.get(txWithFrom)!;
}
const { account } = options;
const promise = (async () => {
@@ -84,11 +95,6 @@ export async function estimateGas(
]);
const rpcRequest = getRpcClient(options.transaction);
// from is:
// 1. the user specified from address
// 2. the passed in account address
// 3. the passed in wallet's account address
const from = options.from ?? options.account?.address ?? undefined;
try {
let gas = await eth_estimateGas(
rpcRequest,
@@ -110,6 +116,6 @@ export async function estimateGas(
});
}
})();
cache.set(options.transaction, promise);
cache.set(txWithFrom, promise);
return promise;
}
@@ -26,6 +26,7 @@ export type SignTransactionOptions = {
* privateKey: "0x...",
* });
* ```
* @transaction
*/
export function signTransaction({
transaction,
@@ -0,0 +1,77 @@
import type { TransactionSerializable } from "viem";
import type { Address } from "../../../utils/address.js";
import type {
EIP721SerializedTransaction,
EIP721TransactionOptions,
} from "../../prepare-transaction.js";
export type EIP721TransactionSerializable = TransactionSerializable & {
from: Address;
} & EIP721TransactionOptions;
export const gasPerPubdataDefault = 50000n;
export const getEip712Domain = (transaction: EIP721TransactionSerializable) => {
const message = transactionToMessage(transaction);
return {
domain: {
name: "zkSync",
version: "2",
chainId: transaction.chainId,
},
types: {
Transaction: [
{ name: "txType", type: "uint256" },
{ name: "from", type: "uint256" },
{ name: "to", type: "uint256" },
{ name: "gasLimit", type: "uint256" },
{ name: "gasPerPubdataByteLimit", type: "uint256" },
{ name: "maxFeePerGas", type: "uint256" },
{ name: "maxPriorityFeePerGas", type: "uint256" },
{ name: "paymaster", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
{ name: "factoryDeps", type: "bytes32[]" },
{ name: "paymasterInput", type: "bytes" },
],
},
primaryType: "Transaction",
message: message,
};
};
function transactionToMessage(
transaction: EIP721TransactionSerializable,
): EIP721SerializedTransaction {
const {
gas,
nonce,
to,
from,
value,
maxFeePerGas,
maxPriorityFeePerGas,
paymaster,
paymasterInput,
gasPerPubdata,
data,
} = transaction;
return {
txType: 113n,
from: BigInt(from),
to: to ? BigInt(to) : 0n,
gasLimit: gas ?? 0n,
gasPerPubdataByteLimit: gasPerPubdata ?? gasPerPubdataDefault,
maxFeePerGas: maxFeePerGas ?? 0n,
maxPriorityFeePerGas: maxPriorityFeePerGas ?? 0n,
paymaster: paymaster ? BigInt(paymaster) : 0n,
nonce: nonce ? BigInt(nonce) : 0n,
value: value ?? 0n,
data: data ? data : "0x0",
// TODO suport factoryDeps
factoryDeps: [],
paymasterInput: paymasterInput ? paymasterInput : "0x",
};
}
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../test/src/test-wallets.js";
import { defineChain } from "../../../chains/utils.js";
import { prepareTransaction } from "../../prepare-transaction.js";
import { sendEip712Transaction } from "./send-eip712-transaction.js";
describe("sendEip712Transaction", () => {
// re-enable for testing, but disable for CI since it requires testnet funds
it.skip("should send eip712 transaction", async () => {
const transaction = prepareTransaction({
chain: defineChain(300),
client: TEST_CLIENT,
value: 0n,
to: TEST_ACCOUNT_B.address,
eip712: {
paymaster: "0xbA226d47Cbb2731CBAA67C916c57d68484AA269F",
paymasterInput:
"0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
},
});
const res = await sendEip712Transaction({
account: TEST_ACCOUNT_A,
transaction,
});
expect(res.transactionHash.length).toBe(66);
});
});
@@ -0,0 +1,200 @@
import { toRlp } from "viem";
import { eth_sendRawTransaction } from "../../../rpc/actions/eth_sendRawTransaction.js";
import { getRpcClient } from "../../../rpc/rpc.js";
import { toBigInt } from "../../../utils/bigint.js";
import { concatHex } from "../../../utils/encoding/helpers/concat-hex.js";
import { type Hex, numberToHex, toHex } from "../../../utils/encoding/hex.js";
import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../prepare-transaction.js";
import { encode } from "../encode.js";
import { toSerializableTransaction } from "../to-serializable-transaction.js";
import type { WaitForReceiptOptions } from "../wait-for-tx-receipt.js";
import {
type EIP721TransactionSerializable,
gasPerPubdataDefault,
getEip712Domain,
} from "./getEip721Domain.js";
export type SendEip712TransactionOptions = {
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>;
};
/**
* Sends a transaction using the provided wallet.
* @param options - The options for sending the transaction.
* @returns A promise that resolves to the transaction hash.
* @throws An error if the wallet is not connected.
* @transaction
* @example
* ```ts
* import { sendTransaction } from "thirdweb";
*
* const { transactionHash } = await sendTransaction({
* account,
* transaction
* });
* ```
*/
export async function sendEip712Transaction(
options: SendEip712TransactionOptions,
): Promise<WaitForReceiptOptions> {
const { account, transaction } = options;
const eip712Transaction = await populateEip712Transaction(options);
const hash = await signEip712Transaction({
account,
eip712Transaction,
chainId: transaction.chain.id,
});
const rpc = getRpcClient(transaction);
const result = await eth_sendRawTransaction(rpc, hash);
return {
transactionHash: result,
chain: transaction.chain,
client: transaction.client,
};
}
export async function signEip712Transaction(options: {
account: Account;
eip712Transaction: EIP721TransactionSerializable;
chainId: number;
}): Promise<Hex> {
const { account, eip712Transaction, chainId } = options;
// EIP712 signing of the serialized tx
const eip712Domain = getEip712Domain(eip712Transaction);
const customSignature = await account.signTypedData({
// biome-ignore lint/suspicious/noExplicitAny: TODO type properly
...(eip712Domain as any),
});
return serializeTransactionEIP712({
...eip712Transaction,
chainId,
customSignature,
});
}
export async function populateEip712Transaction(
options: SendEip712TransactionOptions,
): Promise<EIP721TransactionSerializable> {
const { account, transaction } = options;
let [
data,
to,
value,
gas,
maxFeePerGas,
maxPriorityFeePerGas,
gasPerPubdata,
] = await Promise.all([
encode(transaction),
resolvePromisedValue(transaction.to),
resolvePromisedValue(transaction.value),
resolvePromisedValue(transaction.gas),
resolvePromisedValue(transaction.maxFeePerGas),
resolvePromisedValue(transaction.maxPriorityFeePerGas),
resolvePromisedValue(transaction.eip712).then(
(eip712) => eip712?.gasPerPubdata,
),
]);
if (!gas || !maxFeePerGas || !maxPriorityFeePerGas) {
// fetch fees and gas
const rpc = getRpcClient(transaction);
const result = (await rpc({
// biome-ignore lint/suspicious/noExplicitAny: TODO add to RPC method types
method: "zks_estimateFee" as any,
params: [
{
from: account.address,
to,
data,
value: value ? numberToHex(value) : undefined,
// biome-ignore lint/suspicious/noExplicitAny: TODO add to RPC method types
} as any,
],
})) as {
gas_limit: string;
max_fee_per_gas: string;
max_priority_fee_per_gas: string;
gas_per_pubdata_limit: string;
};
gas = toBigInt(result.gas_limit);
const baseFee = toBigInt(result.max_fee_per_gas);
maxFeePerGas = baseFee * 2n; // bumping the base fee per gas to ensure fast inclusion
maxPriorityFeePerGas = toBigInt(result.max_priority_fee_per_gas) || 1n;
gasPerPubdata = toBigInt(result.gas_per_pubdata_limit);
}
// serialize the transaction (with fees, gas, nonce)
const serializableTransaction = await toSerializableTransaction({
transaction: {
...transaction,
gas,
maxFeePerGas,
maxPriorityFeePerGas,
},
from: account.address,
});
return {
...serializableTransaction,
...transaction.eip712,
gasPerPubdata,
from: account.address as Hex,
};
}
function serializeTransactionEIP712(
transaction: EIP721TransactionSerializable & {
chainId: number;
customSignature: Hex;
},
): Hex {
const {
chainId,
gas,
nonce,
to,
from,
value,
maxFeePerGas,
maxPriorityFeePerGas,
customSignature,
factoryDeps,
paymaster,
paymasterInput,
gasPerPubdata,
data,
} = transaction;
const serializedTransaction = [
nonce ? toHex(nonce) : "0x",
maxPriorityFeePerGas ? toHex(maxPriorityFeePerGas) : "0x",
maxFeePerGas ? toHex(maxFeePerGas) : "0x",
gas ? toHex(gas) : "0x",
to ?? "0x",
value ? toHex(value) : "0x",
data ?? "0x0",
toHex(chainId),
toHex(""),
toHex(""),
toHex(chainId),
from ?? "0x",
gasPerPubdata ? toHex(gasPerPubdata) : toHex(gasPerPubdataDefault),
factoryDeps ?? [],
customSignature ?? "0x", // EIP712 signature
paymaster && paymasterInput ? [paymaster, paymasterInput] : [],
];
// @ts-ignore - TODO: fix types
return concatHex(["0x71", toRlp(serializedTransaction)]);
}
@@ -18,11 +18,42 @@ export type StaticPrepareTransactionOptions = {
maxFeePerBlobGas?: bigint | undefined;
nonce?: number | undefined;
extraGas?: bigint | undefined;
// zksync specific
eip712?: EIP721TransactionOptions | undefined;
// tw specific
chain: Chain;
client: ThirdwebClient;
};
export type EIP721TransactionOptions = {
// constant or user input
gasPerPubdata?: bigint | undefined;
// optional signature, generated
customSignature?: Hex | undefined;
// optional, used to deploy contracts with the transaction
factoryDeps?: Hex[] | undefined;
// optional, paymaster contract address to invoke
paymaster?: Address | undefined;
// optional, paymaster contract input
paymasterInput?: Hex | undefined;
};
export type EIP721SerializedTransaction = {
txType: bigint;
from: bigint;
to: bigint;
gasLimit: bigint;
gasPerPubdataByteLimit: bigint;
maxFeePerGas: bigint;
maxPriorityFeePerGas: bigint;
nonce: bigint;
value: bigint;
data: Hex;
factoryDeps: Hex[];
paymaster: bigint;
paymasterInput: Hex;
};
export type PrepareTransactionOptions = {
chain: Chain;
client: ThirdwebClient;
@@ -32,6 +32,7 @@ import type { FetchDeployMetadataResult } from "./deploy-metadata.js";
* });
* ```
* @returns A promise that resolves to the predicted address of the contract.
* @extension DEPLOY
*/
export async function computePublishedContractAddress(args: {
client: ThirdwebClient;
+1
View File
@@ -15,6 +15,7 @@ export type ResolveArweaveSchemeOptions = {
* import { resolveArweaveScheme } from "thirdweb/storage";
* const url = resolveArweaveScheme({ uri: "ar://<fileId>" });
* ```
* @storage
*/
export function resolveArweaveScheme(options: ResolveArweaveSchemeOptions) {
if (options.uri.startsWith("ar://")) {
@@ -5,6 +5,7 @@ import type { Hex } from "./is-hex.js";
*
* @param values - An array of hexadecimal values to concatenate.
* @returns The concatenated hexadecimal value.
* @utils
*/
export function concatHex(values: readonly Hex[]): Hex {
return `0x${(values as Hex[]).reduce(
+1
View File
@@ -6,6 +6,7 @@
* const obj = { tokenId: 0n };
* const str = stringify(obj); // "{"tokenId":"0"}"
* ```
* @utils
*/
export const stringify: typeof JSON.stringify = (value, replacer, space) => {
const res = JSON.stringify(
@@ -28,6 +28,7 @@ export type SignMessageOptions = {
* privateKey: "0x...",
* });
* ```
* @utils
*/
export function signMessage({ message, privateKey }: SignMessageOptions): Hex {
const signature = sign({ hash: hashMessage(message), privateKey });
@@ -25,6 +25,7 @@ export type SignTypedDataOptions<
* ...typedData
* });
* ```
* @utils
*/
export function signTypedData<
const typedData extends TypedData | Record<string, unknown>,
@@ -26,6 +26,7 @@ export type SignOptions = {
* privateKey: "0x",
* });
* ```
* @utils
*/
export function sign({ hash, privateKey }: SignOptions): Signature {
const { r, s, recovery } = secp256k1.sign(hash.slice(2), privateKey.slice(2));
@@ -22,6 +22,8 @@ import { type Hex, hexToBigInt } from "../encoding/hex.js";
*
* console.log(hex);
* // "0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db81c"
* ```
* @utils
*/
export function signatureToHex(signature: Signature): Hex {
const { r, s, v } = signature;
@@ -150,7 +150,9 @@ export function createWallet<const ID extends WalletId>(
}
if (options && "client" in options) {
const { autoConnectWC } = await import("./wallet-connect/index.js");
const { autoConnectWC } = await import(
"./wallet-connect/controller.js"
);
const [
connectedAccount,
@@ -179,7 +181,9 @@ export function createWallet<const ID extends WalletId>(
},
connect: async (options) => {
async function wcConnect(wcOptions: WCConnectOptions) {
const { connectWC } = await import("./wallet-connect/index.js");
const { connectWC } = await import(
"./wallet-connect/controller.js"
);
const [
connectedAccount,
@@ -295,7 +299,6 @@ export function walletConnect() {
* import { smartWallet } from "thirdweb/wallets";
*
* const wallet = smartWallet({
* factoryAddress: "0x1234...",
* chain: sepolia,
* gasless: true,
* });
@@ -415,6 +418,7 @@ export function smartWallet(
* strategy: "google",
* });
* ```
*
* Enable smart accounts and sponsor gas for your users:
* ```ts
* import { inAppWallet } from "thirdweb/wallets";
@@ -439,6 +443,7 @@ export function smartWallet(
* },
* },
* });
* ```
*
* Hide the ability to export the private key within the Connect Modal
* ```ts
@@ -4,7 +4,7 @@ import { isInAppWallet } from "../in-app/core/wallet/index.js";
import { getInjectedProvider } from "../injected/index.js";
import type { Wallet } from "../interfaces/wallet.js";
import { isSmartWallet } from "../smart/index.js";
import { isWalletConnect } from "../wallet-connect/index.js";
import { isWalletConnect } from "../wallet-connect/controller.js";
import type { GetCallsStatusResponse, WalletSendCallsId } from "./types.js";
export type GetCallsStatusOptions = {
@@ -16,7 +16,7 @@ export type GetCallsStatusOptions = {
/**
* Get the status of an [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792) bundle.
*
* Note: This function is dependent on the wallet's support for EIP-5792 and could fail.
* @note This function is dependent on the wallet's support for EIP-5792 and could fail.
*
* @param {GetCallsStatusOptions} options
* @param {Wallet} options.wallet - The wallet that send the original calls.
@@ -39,7 +39,8 @@ export type GetCallsStatusOptions = {
* result = await getCallsStatus({ wallet, client, bundleId });
* }
* ```
* @wallets
* @extension EIP5792
* @extension EIP5792
*/
export async function getCallsStatus({
wallet,
@@ -4,7 +4,7 @@ import { isInAppWallet } from "../in-app/core/wallet/index.js";
import { getInjectedProvider } from "../injected/index.js";
import type { Wallet } from "../interfaces/wallet.js";
import { isSmartWallet } from "../smart/index.js";
import { isWalletConnect } from "../wallet-connect/index.js";
import { isWalletConnect } from "../wallet-connect/controller.js";
import type { WalletId } from "../wallet-types.js";
import type { WalletCapabilities, WalletCapabilitiesRecord } from "./types.js";
@@ -19,7 +19,7 @@ export type GetCapabilitiesResult = Prettify<
/**
* Get the capabilities of a wallet based on the [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792) specification.
*
* Note: This function is dependent on the wallet's support for EIP-5792, but will not throw.
* @note This function is dependent on the wallet's support for EIP-5792, but will not throw.
* **The returned object contains a `message` field detailing any issues with the wallet's support for EIP-5792.**
*
* @param {GetCapabilitiesOptions} options
@@ -34,7 +34,7 @@ export type GetCapabilitiesResult = Prettify<
* const capabilities = await getCapabilities({ wallet });
* ```
*
* @wallets
* @extension EIP5792
*/
export async function getCapabilities<const ID extends WalletId = WalletId>({
wallet,
@@ -13,7 +13,7 @@ import { isInAppWallet } from "../in-app/core/wallet/index.js";
import { getInjectedProvider } from "../injected/index.js";
import type { Wallet } from "../interfaces/wallet.js";
import { isSmartWallet } from "../smart/index.js";
import { isWalletConnect } from "../wallet-connect/index.js";
import { isWalletConnect } from "../wallet-connect/controller.js";
import type { WalletId } from "../wallet-types.js";
import type {
EIP5792Call,
@@ -56,7 +56,7 @@ export type SendCallsResult = WalletSendCallsId;
* This function works with all Thirdweb wallets (in-app and smart) and certain injected wallets that already support EIP-5792.
* Transactions will be bundled and sponsored when those capabilities are supported, otherwise they will be sent as individual transactions.
*
* Note: This function is dependent on the wallet's support for EIP-5792 and could fail.
* @note This function is dependent on the wallet's support for EIP-5792 and could fail.
*
* @param {SendCallsOptions} options
* @param {Wallet} options.wallet - The wallet to send the calls to.
@@ -106,8 +106,9 @@ export type SendCallsResult = WalletSendCallsId;
* }
* }
* });
* @note We recommend proxying any paymaster calls via an API route you setup and control.
* ```
* @wallets
* @extension EIP5792
*/
export async function sendCalls<const ID extends WalletId>(
options: SendCallsOptions<ID>,
@@ -3,7 +3,7 @@ import { isInAppWallet } from "../in-app/core/wallet/index.js";
import { getInjectedProvider } from "../injected/index.js";
import type { Wallet } from "../interfaces/wallet.js";
import { isSmartWallet } from "../smart/index.js";
import { isWalletConnect } from "../wallet-connect/index.js";
import { isWalletConnect } from "../wallet-connect/controller.js";
import type { WalletSendCallsId } from "./types.js";
export type ShowCallsStatusOptions = {
@@ -32,7 +32,7 @@ export type ShowCallsStatusOptions = {
* const bundleId = await sendCalls({ wallet, client, calls });
* await showCallsStatus({ wallet, bundleId });
* ```
* @wallets
* @extension EIP5792
* @internal
*/
export async function showCallsStatus({
@@ -0,0 +1,98 @@
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { ANVIL_CHAIN } from "../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
import { TEST_ACCOUNT_A } from "../../../test/src/test-wallets.js";
import * as watchBlockNumberExports from "../../rpc/watchBlockNumber.js";
import * as getCallsStatusExports from "../../wallets/eip5792/get-calls-status.js";
import { METAMASK } from "../constants.js";
import { createWallet } from "../create-wallet.js";
import type { GetCallsStatusResponse } from "./types.js";
import {
DEFAULT_MAX_BLOCKS_WAIT_TIME,
waitForBundle,
} from "./wait-for-bundle.js";
const MOCK_BUNDLE_ID = "0x1234567890abcdef";
const MOCK_SUCCESS_RECEIPT: GetCallsStatusResponse = {
status: "CONFIRMED",
receipts: [
{
logs: [],
status: "success",
blockHash:
"0xf19bbafd9fd0124ec110b848e8de4ab4f62bf60c189524e54213285e7f540d4a",
blockNumber: 12345n,
transactionHash:
"0x9b7bb827c2e5e3c1a0a44dc53e573aa0b3af3bd1f9f5ed03071b100bb039eaff",
gasUsed: 12345n,
},
],
};
const MOCK_PENDING_RECEIPT: GetCallsStatusResponse = {
status: "PENDING",
receipts: [],
};
const mockGetCallsStatus = vi.spyOn(getCallsStatusExports, "getCallsStatus");
const wallet = createWallet(METAMASK);
wallet.getAccount = () => TEST_ACCOUNT_A;
let emitBlockNumber: (blockNumber: bigint) => void;
vi.spyOn(watchBlockNumberExports, "watchBlockNumber").mockImplementation(
({ onNewBlockNumber }) => {
emitBlockNumber = (blockNumber: bigint) => {
onNewBlockNumber(blockNumber);
};
return () => {};
},
);
describe("waitForBundle", () => {
afterAll(() => {
vi.restoreAllMocks();
});
beforeEach(() => {
vi.clearAllMocks();
});
it("should resolve with transaction receipt when transactionHash is provided", async () => {
mockGetCallsStatus.mockResolvedValueOnce(MOCK_SUCCESS_RECEIPT);
// can't `await` here because we still need to be able to increment the block number below
const res = waitForBundle({
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
bundleId: MOCK_BUNDLE_ID,
wallet,
});
emitBlockNumber(1n);
await expect(res).resolves.toMatchObject(MOCK_SUCCESS_RECEIPT);
});
it("should reject with an error when bundle is not complete after waiting 10 blocks", async () => {
mockGetCallsStatus.mockRejectedValue(MOCK_PENDING_RECEIPT);
const result = waitForBundle({
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
bundleId: MOCK_BUNDLE_ID,
wallet,
});
for (let i = 1; i <= DEFAULT_MAX_BLOCKS_WAIT_TIME + 1; i++) {
emitBlockNumber(BigInt(i));
}
await expect(result).rejects.toThrow(
`Bundle not confirmed after ${DEFAULT_MAX_BLOCKS_WAIT_TIME} blocks`,
);
expect(mockGetCallsStatus).toHaveBeenCalledTimes(30);
});
});
@@ -0,0 +1,98 @@
import type { Chain } from "../../chains/types.js";
import type { ThirdwebClient } from "../../client/client.js";
import { watchBlockNumber } from "../../rpc/watchBlockNumber.js";
import type { Prettify } from "../../utils/type-utils.js";
import type { Wallet } from "../interfaces/wallet.js";
import { getCallsStatus } from "./get-calls-status.js";
import type { GetCallsStatusResponse, WalletSendCallsId } from "./types.js";
export const DEFAULT_MAX_BLOCKS_WAIT_TIME = 30;
const map = new Map<string, Promise<GetCallsStatusResponse>>();
export type WaitForBundleOptions = Prettify<{
bundleId: WalletSendCallsId;
wallet: Wallet;
client: ThirdwebClient;
chain: Chain;
maxBlocksWaitTime?: number;
}>;
/**
* Waits for the [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792) bundle to be confirmed.
*
* @note This function is dependent on the wallet's support for EIP-5792 and could fail.
*
* @param options - The options for waiting for the bundle.
* By default, the max wait time is 30 blocks.
* @returns A promise that resolves with the final {@link getCallsStatus} result.
* @throws an error if the wallet does not support EIP-5792.
* @beta
* @example
* ```ts
* import { waitForBundle } from "thirdweb/wallets/eip5792";
* const result = await waitForBundle({
* client,
* chain,
* wallet,
* bundleId: "0x123...",
* });
* ```
* @extension EIP5792
*/
export function waitForBundle(
options: WaitForBundleOptions,
): Promise<GetCallsStatusResponse> {
const { bundleId, chain, wallet, client } = options;
const chainId = chain.id;
const key = `${chainId}:calls_${bundleId}`;
const maxBlocksWaitTime =
options.maxBlocksWaitTime ?? DEFAULT_MAX_BLOCKS_WAIT_TIME;
if (map.has(key)) {
// biome-ignore lint/style/noNonNullAssertion: the `has` above ensures that this will always be set
return map.get(key)!;
}
const promise = new Promise<GetCallsStatusResponse>((resolve, reject) => {
// start at -1 because the first block doesn't count
let blocksWaited = -1;
const unwatch = watchBlockNumber({
client: client,
chain: chain,
onNewBlockNumber: async () => {
blocksWaited++;
if (blocksWaited >= maxBlocksWaitTime) {
unwatch();
reject(
new Error(`Bundle not confirmed after ${maxBlocksWaitTime} blocks`),
);
return;
}
try {
const result = await getCallsStatus({
wallet,
client,
bundleId,
});
if (result.status === "CONFIRMED") {
// stop the polling
unwatch();
// resolve the top level promise with the result
resolve(result);
}
} catch {
// noop, we'll try again on the next blocks
}
},
});
// remove the promise from the map when it's done (one way or the other)
}).finally(() => {
map.delete(key);
});
map.set(key, promise);
return promise;
}
@@ -69,6 +69,7 @@ export async function logoutAuthenticatedUser(
* console.log(user.walletAddress);
* }
* ```
* @wallet
*/
export async function getAuthenticatedUser(
options: GetAuthenticatedUserParams,
@@ -95,6 +96,7 @@ export async function getAuthenticatedUser(
* const email = await getUserEmail({ client });
* console.log(email);
* ```
* @wallet
*/
export async function getUserEmail(options: GetAuthenticatedUserParams) {
const user = await getAuthenticatedUser(options);
@@ -115,6 +117,7 @@ export async function getUserEmail(options: GetAuthenticatedUserParams) {
* const phoneNumber = await getUserPhoneNumber({ client });
* console.log(phoneNumber);
* ```
* @wallet
*/
export async function getUserPhoneNumber(options: GetAuthenticatedUserParams) {
const user = await getAuthenticatedUser(options);
@@ -139,6 +142,7 @@ export async function getUserPhoneNumber(options: GetAuthenticatedUserParams) {
* email: "[email protected]",
* });
* ```
* @wallet
*/
export async function preAuthenticate(args: PreAuthArgsType) {
const ewSDK = await getInAppWalletConnector(args.client);
@@ -160,6 +164,7 @@ export async function preAuthenticate(args: PreAuthArgsType) {
* verificationCode: "123456",
* });
* ```
* @wallet
*/
export async function authenticate(
args: AuthArgsType,
@@ -60,6 +60,12 @@ export const DEVICE_SHARE_LOCAL_STORAGE_NAME = (
userId: string,
) => `${DEVICE_SHARE_LOCAL_STORAGE_PREFIX}-${clientId}-${userId}`;
/**
* @internal
*/
export const WALLET_CONNECT_SESSIONS_LOCAL_STORAGE_NAME = (clientId: string) =>
`walletConnectSessions-${clientId}`;
/**
* @internal
*/
@@ -132,6 +132,7 @@ export async function loginWithPasskey(options: {
* Returns whether this device has a stored passkey ready to be used for sign-in
* @param client - the thirdweb client
* @returns whether the device has a stored passkey
* @wallet
*/
export async function hasStoredPasskey(client: ThirdwebClient) {
const storage = new LocalStorage({ clientId: client.clientId });

Some files were not shown because too many files have changed in this diff Show More