Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0299034fc1 Version Packages (#7207)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-29 13:30:44 -07:00
Joaquim VergesandGitHub 861e623a1b [SDK] Engine search transactions and batch transaction support (#7190) 2025-05-29 13:19:26 -07:00
MananTank 6a335bfa77 Fix requests to thirdwebstorage-dev.com not set with secret-key (#7205)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `fetch.ts` file in the `thirdweb` package to include a new URL for the thirdweb storage service.

### Detailed summary
- Added `".thirdwebstorage-dev.com"` to the list of URLs.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Chores**
  - Updated the list of recognized thirdweb service domains to include ".thirdwebstorage-dev.com".

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 20:09:50 +00:00
AmineAfia cb994dcb8f Re-enable webhooks sidebar link in project dashboard (#7196)
## [Dashboard] Feature: Add Webhooks to Project Sidebar

## Notes for the reviewer

This PR adds the Webhooks option to the project sidebar navigation. The option was previously commented out due to a scrolling issue, which has now been resolved.

## How to test

Navigate to a project page and verify that the Webhooks option appears in the sidebar with a "New" badge.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refining the handling of webhook payloads and enhancing the `ProjectSidebarLayout` component by reintroducing the webhooks link. It also updates the API fetch calls to ensure they consistently use HTTPS.

### Detailed summary
- Changed `params` type in `webhookPayloadUtils.ts` from `Record<string, unknown> | unknown[]` to `Record<string, unknown>`.
- Updated `params` in the `buildTransactionWebhookPayload` function to use an empty object instead of an empty array.
- Reintroduced the webhooks link in `ProjectSidebarLayout.tsx` with appropriate properties.
- Updated fetch calls in `webhooks.ts` to use HTTPS for the API domain in multiple functions (`getWebhooks`, `deleteWebhook`, `testWebhook`).

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Re-enabled the "Webhooks" link in the project sidebar, now visible with a "New" badge.

- **Bug Fixes**
  - Improved API reliability by ensuring all webhook-related network requests use the correct HTTPS scheme.

- **Refactor**
  - Updated internal data structures for webhook payloads to improve consistency and type safety.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 19:30:57 +00:00
Joaquim VergesandGitHub 4d38a7e8fe Update page.mdx (#7204)
Signed-off-by: Joaquim Verges <[email protected]>
2025-05-29 11:32:36 -07:00
25 changed files with 872 additions and 124 deletions
+22 -16
View File
@@ -73,15 +73,18 @@ export async function createWebhook(
): Promise<WebhookSingleResponse> {
try {
const authToken = await getAuthToken();
const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
const response = await fetch(
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(payload),
},
body: JSON.stringify(payload),
});
);
if (!response.ok) {
const errorText = await response.text();
@@ -105,13 +108,16 @@ export async function getWebhooks(
): Promise<WebhooksListResponse> {
try {
const authToken = await getAuthToken();
const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, {
method: "GET",
headers: {
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
const response = await fetch(
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`,
{
method: "GET",
headers: {
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
},
},
});
);
if (!response.ok) {
const errorText = await response.text();
@@ -137,7 +143,7 @@ export async function deleteWebhook(
try {
const authToken = await getAuthToken();
const response = await fetch(
`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${encodeURIComponent(webhookId)}`,
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${encodeURIComponent(webhookId)}`,
{
method: "DELETE",
headers: {
@@ -171,7 +177,7 @@ export async function testWebhook(
try {
const authToken = await getAuthToken();
const response = await fetch(
`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`,
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`,
{
method: "POST",
headers: {
@@ -2,6 +2,7 @@
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
import { Badge } from "@/components/ui/badge";
import {
BellIcon,
BookTextIcon,
BoxIcon,
CoinsIcon,
@@ -94,17 +95,16 @@ export function ProjectSidebarLayout(props: {
icon: NebulaIcon,
tracking: tracking("nebula"),
},
// Commented until we solve the scrolling issue
// {
// href: `${layoutPath}/webhooks`,
// label: (
// <span className="flex items-center gap-2">
// Webhooks <Badge>New</Badge>
// </span>
// ),
// icon: BellIcon,
// tracking: tracking("webhooks"),
// },
{
href: `${layoutPath}/webhooks`,
label: (
<span className="flex items-center gap-2">
Webhooks <Badge>New</Badge>
</span>
),
icon: BellIcon,
tracking: tracking("webhooks"),
},
]}
footerSidebarLinks={[
{
@@ -20,7 +20,7 @@ export interface WebhookPayload {
signatures?: Array<{
sig_hash: string;
abi?: string;
params: Record<string, unknown> | unknown[];
params: Record<string, unknown>;
}>;
}
>;
@@ -59,7 +59,7 @@ export function buildTransactionWebhookPayload(
txSignatures.push({
sig_hash: data.sigHash,
abi: data.sigHashAbi,
params: [],
params: {},
});
}
return {
@@ -27,7 +27,7 @@ You can manage ecosystem partners in the Permissions tab of your Ecosystem Walle
### Which login providers can display my Ecosystem Wallet?
thirdweb Connect, with WalletConnect and any wagmi- or viem-based login providers coming soon.
thirdweb Connect, WalletConnect and any wagmi- or viem-based login providers.
### How can Partners integrate my Ecosystem Wallet?
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/insight
## 3.0.3
### Patch Changes
- [#7190](https://github.com/thirdweb-dev/js/pull/7190) [`861e623`](https://github.com/thirdweb-dev/js/commit/861e623a1b7519bcac09c0c6d975cad2c0c5be4f) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Updated to latest API
## 3.0.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/engine",
"version": "3.0.2",
"version": "3.0.3",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
+54
View File
@@ -7,6 +7,8 @@ import type {
} from "@hey-api/client-fetch";
import { client as _heyApiClient } from "./client.gen.js";
import type {
CreateAccountData,
CreateAccountResponse,
EncodeFunctionDataData,
EncodeFunctionDataResponse,
GetNativeBalanceData,
@@ -15,6 +17,8 @@ import type {
GetTransactionAnalyticsResponse,
GetTransactionAnalyticsSummaryData,
GetTransactionAnalyticsSummaryResponse,
ListAccountsData,
ListAccountsResponse,
ReadContractData,
ReadContractResponse,
SearchTransactionsData,
@@ -48,6 +52,56 @@ export type Options<
meta?: Record<string, unknown>;
};
/**
* List Server Wallets
* List all engine server wallets for the current project. Returns an array of EOA addresses with their corresponding predicted smart account addresses.
*/
export const listAccounts = <ThrowOnError extends boolean = false>(
options?: Options<ListAccountsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
ListAccountsResponse,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/accounts",
...options,
});
};
/**
* Create Server Wallet
* Create a new engine server wallet. This is a helper route for creating a new EOA with your KMS provider, provided as a convenient alternative to creating an EOA directly with your KMS provider. Your KMS credentials are not stored, and usage of created accounts require your KMS credentials to be sent with requests.
*/
export const createAccount = <ThrowOnError extends boolean = false>(
options?: Options<CreateAccountData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
CreateAccountResponse,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/accounts",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
/**
* Write Contract
* Call a write function on a contract.
+65 -1
View File
@@ -9,7 +9,7 @@ export type TransactionsFilterValue = {
| "smartAccountAddress"
| "chainId";
values: Array<string>;
operation: "AND" | "OR";
operation?: "AND" | "OR";
};
export type TransactionsFilterNested = {
@@ -92,6 +92,70 @@ export type AaZksyncExecutionOptions = {
chainId: string;
};
export type ListAccountsData = {
body?: never;
path?: never;
query?: never;
url: "/v1/accounts";
};
export type ListAccountsResponses = {
/**
* Accounts retrieved successfully
*/
200: {
result: Array<{
/**
* EVM address in hex format
*/
address: string;
/**
* The predicted smart account address for use with the default thirdweb v0.7 AccountFactory
*/
smartAccountAddress?: string;
}>;
};
};
export type ListAccountsResponse =
ListAccountsResponses[keyof ListAccountsResponses];
export type CreateAccountData = {
body?: {
label: string;
};
headers?: {
/**
* Vault Access Token used to access your EOA
*/
"x-vault-access-token"?: string;
};
path?: never;
query?: never;
url: "/v1/accounts";
};
export type CreateAccountResponses = {
/**
* Account created successfully
*/
201: {
result: {
/**
* EVM address in hex format
*/
address: string;
/**
* The predicted smart account address for use with the default thirdweb v0.7 AccountFactory
*/
smartAccountAddress?: string;
};
};
};
export type CreateAccountResponse =
CreateAccountResponses[keyof CreateAccountResponses];
export type WriteContractData = {
body?: {
/**
+101
View File
@@ -1,5 +1,106 @@
# thirdweb
## 5.102.0
### Minor Changes
- [#7190](https://github.com/thirdweb-dev/js/pull/7190) [`861e623`](https://github.com/thirdweb-dev/js/commit/861e623a1b7519bcac09c0c6d975cad2c0c5be4f) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Enhanced Engine functionality with server wallet management, search transactions and batch transaction support:
- Added `Engine.createServerWallet()` to create a new server wallet with a custom label
```ts
import { Engine } from "thirdweb";
const serverWallet = await Engine.createServerWallet({
client,
label: "My Server Wallet",
});
console.log(serverWallet.address);
console.log(serverWallet.smartAccountAddress);
```
- Added `Engine.getServerWallets()` to list all existing server wallets
```ts
import { Engine } from "thirdweb";
const serverWallets = await Engine.getServerWallets({
client,
});
console.log(serverWallets);
```
- Added `Engine.searchTransactions()` to search for transactions by various filters (id, chainId, from address, etc.)
```ts
// Search by transaction IDs
const transactions = await Engine.searchTransactions({
client,
filters: [
{
field: "id",
values: ["1", "2", "3"],
},
],
});
// Search by chain ID and sender address
const transactions = await Engine.searchTransactions({
client,
filters: [
{
filters: [
{
field: "from",
values: ["0x1234567890123456789012345678901234567890"],
},
{
field: "chainId",
values: ["8453"],
},
],
operation: "AND",
},
],
pageSize: 100,
page: 0,
});
```
- Added `serverWallet.enqueueBatchTransaction()` to enqueue multiple transactions in a single batch
```ts
// Prepare multiple transactions
const transaction1 = claimTo({
contract,
to: firstRecipient,
quantity: 1n,
});
const transaction2 = claimTo({
contract,
to: secondRecipient,
quantity: 1n,
});
// Enqueue as a batch
const { transactionId } = await serverWallet.enqueueBatchTransaction({
transactions: [transaction1, transaction2],
});
// Wait for batch completion
const { transactionHash } = await Engine.waitForTransactionHash({
client,
transactionId,
});
```
- Improved server wallet transaction handling with better error reporting
### Patch Changes
- Updated dependencies [[`861e623`](https://github.com/thirdweb-dev/js/commit/861e623a1b7519bcac09c0c6d975cad2c0c5be4f)]:
- @thirdweb-dev/engine@3.0.3
## 5.101.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
"version": "5.101.2",
"version": "5.102.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "~test/test-clients.js";
import { chains } from "./Chains.js";
describe("chains", () => {
describe.runIf(process.env.TW_SECRET_KEY)("chains", () => {
it("should fetch chains", async () => {
// Setup
const client = TEST_CLIENT;
@@ -0,0 +1,57 @@
import { createAccount } from "@thirdweb-dev/engine";
import { stringify } from "viem";
import type { ThirdwebClient } from "../client/client.js";
import { getThirdwebBaseUrl } from "../utils/domains.js";
import { getClientFetch } from "../utils/fetch.js";
export type CreateServerWalletArgs = {
client: ThirdwebClient;
label: string;
};
/**
* Create a server wallet.
* @param params - The parameters for the server wallet.
* @param params.client - The thirdweb client to use.
* @param params.label - The label for the server wallet.
* @returns The server wallet signer address and the predicted smart account address.
* @engine
* @example
* ```ts
* import { Engine } from "thirdweb";
*
* const serverWallet = await Engine.createServerWallet({
* client,
* label: "My Server Wallet",
* });
* console.log(serverWallet.address);
* console.log(serverWallet.smartAccountAddress);
* ```
*/
export async function createServerWallet(params: CreateServerWalletArgs) {
const { client, label } = params;
const result = await createAccount({
baseUrl: getThirdwebBaseUrl("engineCloud"),
bodySerializer: stringify,
fetch: getClientFetch(client),
body: {
label,
},
});
if (result.error) {
throw new Error(
`Error creating server wallet with label ${label}: ${stringify(
result.error,
)}`,
);
}
const data = result.data?.result;
if (!data) {
throw new Error(`No server wallet created with label ${label}`);
}
return data;
}
@@ -2,7 +2,6 @@ import { searchTransactions } from "@thirdweb-dev/engine";
import type { Chain } from "../chains/types.js";
import { getCachedChain } from "../chains/utils.js";
import type { ThirdwebClient } from "../client/client.js";
import type { WaitForReceiptOptions } from "../transaction/actions/wait-for-tx-receipt.js";
import { getThirdwebBaseUrl } from "../utils/domains.js";
import type { Hex } from "../utils/encoding/hex.js";
import { getClientFetch } from "../utils/fetch.js";
@@ -117,71 +116,3 @@ export async function getTransactionStatus(args: {
id: data.id,
};
}
/**
* Wait for a transaction to be submitted onchain and return the transaction hash.
* @param args - The arguments for the transaction.
* @param args.client - The thirdweb client to use.
* @param args.transactionId - The id of the transaction to wait for.
* @param args.timeoutInSeconds - The timeout in seconds.
* @engine
* @example
* ```ts
* import { Engine } from "thirdweb";
*
* const { transactionHash } = await Engine.waitForTransactionHash({
* client,
* transactionId, // the transaction id returned from enqueueTransaction
* });
* ```
*/
export async function waitForTransactionHash(args: {
client: ThirdwebClient;
transactionId: string;
timeoutInSeconds?: number;
}): Promise<WaitForReceiptOptions> {
const startTime = Date.now();
const TIMEOUT_IN_MS = args.timeoutInSeconds
? args.timeoutInSeconds * 1000
: 5 * 60 * 1000; // 5 minutes in milliseconds
while (Date.now() - startTime < TIMEOUT_IN_MS) {
const executionResult = await getTransactionStatus(args);
const status = executionResult.status;
switch (status) {
case "FAILED": {
throw new Error(
`Transaction failed: ${executionResult.error || "Unknown error"}`,
);
}
case "CONFIRMED": {
const onchainStatus =
executionResult && "onchainStatus" in executionResult
? executionResult.onchainStatus
: null;
if (onchainStatus === "REVERTED") {
const revertData =
"revertData" in executionResult
? executionResult.revertData
: undefined;
throw new Error(
`Transaction reverted: ${revertData?.errorName || ""} ${revertData?.errorArgs ? stringify(revertData.errorArgs) : ""}`,
);
}
return {
transactionHash: executionResult.transactionHash as Hex,
client: args.client,
chain: executionResult.chain,
};
}
default: {
// wait for the transaction to be confirmed
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
throw new Error(
`Transaction timed out after ${TIMEOUT_IN_MS / 1000} seconds`,
);
}
+13 -1
View File
@@ -5,7 +5,19 @@ export {
} from "./server-wallet.js";
export {
getTransactionStatus,
waitForTransactionHash,
type ExecutionResult,
type RevertData,
} from "./get-status.js";
export { waitForTransactionHash } from "./wait-for-tx-hash.js";
export {
searchTransactions,
type SearchTransactionsArgs,
} from "./search-transactions.js";
export {
createServerWallet,
type CreateServerWalletArgs,
} from "./create-server-wallet.js";
export {
getServerWallets,
type GetServerWalletsArgs,
} from "./list-server-wallets.js";
@@ -0,0 +1,46 @@
import { listAccounts } from "@thirdweb-dev/engine";
import { stringify } from "viem";
import type { ThirdwebClient } from "../client/client.js";
import { getThirdwebBaseUrl } from "../utils/domains.js";
import { getClientFetch } from "../utils/fetch.js";
export type GetServerWalletsArgs = {
client: ThirdwebClient;
};
/**
* List all server wallets.
* @param params - The parameters for the server wallet.
* @param params.client - The thirdweb client to use.
* @returns an array of server wallets with their signer address and predicted smart account address.
* @engine
* @example
* ```ts
* import { Engine } from "thirdweb";
*
* const serverWallets = await Engine.getServerWallets({
* client,
* });
* console.log(serverWallets);
* ```
*/
export async function getServerWallets(params: GetServerWalletsArgs) {
const { client } = params;
const result = await listAccounts({
baseUrl: getThirdwebBaseUrl("engineCloud"),
bodySerializer: stringify,
fetch: getClientFetch(client),
});
if (result.error) {
throw new Error(`Error listing server wallets: ${stringify(result.error)}`);
}
const data = result.data?.result;
if (!data) {
throw new Error("No server wallets found");
}
return data;
}
@@ -0,0 +1,132 @@
import {
type TransactionsFilterNested,
type TransactionsFilterValue,
searchTransactions as engineSearchTransactions,
} from "@thirdweb-dev/engine";
import type { ThirdwebClient } from "../client/client.js";
import { getThirdwebBaseUrl } from "../utils/domains.js";
import { getClientFetch } from "../utils/fetch.js";
import { stringify } from "../utils/json.js";
export type SearchTransactionsArgs = {
client: ThirdwebClient;
filters?: (TransactionsFilterValue | TransactionsFilterNested)[];
pageSize?: number;
page?: number;
};
/**
* Search for transactions by their ids.
* @param args - The arguments for the search.
* @param args.client - The thirdweb client to use.
* @param args.transactionIds - The ids of the transactions to search for.
* @engine
* @example
* ## Search for transactions by their ids
*
* ```ts
* import { Engine } from "thirdweb";
*
* const transactions = await Engine.searchTransactions({
* client,
* filters: [
* {
* field: "id",
* values: ["1", "2", "3"],
* },
* ],
* });
* console.log(transactions);
* ```
*
* ## Search for transactions by chain id
*
* ```ts
* import { Engine } from "thirdweb";
*
* const transactions = await Engine.searchTransactions({
* client,
* filters: [
* {
* field: "chainId",
* values: ["1", "137"],
* },
* ],
* });
* console.log(transactions);
* ```
*
* ## Search for transactions by sender wallet address
*
* ```ts
* import { Engine } from "thirdweb";
*
* const transactions = await Engine.searchTransactions({
* client,
* filters: [
* {
* field: "from",
* values: ["0x1234567890123456789012345678901234567890"],
* },
* ],
* });
* console.log(transactions);
* ```
*
* ## Combined search
*
* ```ts
* import { Engine } from "thirdweb";
*
* const transactions = await Engine.searchTransactions({
* client,
* filters: [
* {
* filters: [
* {
* field: "from",
* values: ["0x1234567890123456789012345678901234567890"],
* },
* {
* field: "chainId",
* values: ["8453"],
* },
* ],
* operation: "AND",
* },
* ],
* pageSize: 100,
* page: 0,
* });
* console.log(transactions);
* ```
*/
export async function searchTransactions(args: SearchTransactionsArgs) {
const { client, filters, pageSize = 100, page = 1 } = args;
const searchResult = await engineSearchTransactions({
baseUrl: getThirdwebBaseUrl("engineCloud"),
bodySerializer: stringify,
fetch: getClientFetch(client),
body: {
filters,
limit: pageSize,
page,
},
});
if (searchResult.error) {
throw new Error(
`Error searching for transaction with filters ${stringify(filters)}: ${stringify(
searchResult.error,
)}`,
);
}
const data = searchResult.data?.result;
if (!data) {
throw new Error(`No transactions found with filters ${stringify(filters)}`);
}
return data;
}
@@ -47,6 +47,23 @@ describe.runIf(
});
});
it("should create a server wallet", async () => {
const serverWallet = await Engine.createServerWallet({
client: TEST_CLIENT,
label: "My Server Wallet",
});
expect(serverWallet).toBeDefined();
const serverWallets = await Engine.getServerWallets({
client: TEST_CLIENT,
});
expect(serverWallets).toBeDefined();
expect(serverWallets.length).toBeGreaterThan(0);
expect(
serverWallets.find((s) => s.address === serverWallet.address),
).toBeDefined();
});
it("should sign a message", async () => {
const signature = await serverWallet.signMessage({
message: "hello",
@@ -95,6 +112,16 @@ describe.runIf(
transactionId: result.transactionId,
});
expect(txHash.transactionHash).toBeDefined();
const res = await Engine.searchTransactions({
client: TEST_CLIENT,
filters: [
{ field: "id", values: [result.transactionId], operation: "OR" },
],
});
expect(res).toBeDefined();
expect(res.transactions.length).toBe(1);
expect(res.transactions[0]?.id).toBe(result.transactionId);
});
it("should send a extension tx", async () => {
@@ -115,6 +142,33 @@ describe.runIf(
expect(tx).toBeDefined();
});
it("should enqueue a batch of txs", async () => {
const tokenContract = getContract({
client: TEST_CLIENT,
chain: baseSepolia,
address: "0x87C52295891f208459F334975a3beE198fE75244",
});
const claimTx1 = mintTo({
contract: tokenContract,
to: serverWallet.address,
amount: "0.001",
});
const claimTx2 = mintTo({
contract: tokenContract,
to: serverWallet.address,
amount: "0.002",
});
const tx = await serverWallet.enqueueBatchTransaction({
transactions: [claimTx1, claimTx2],
});
expect(tx.transactionId).toBeDefined();
const txHash = await Engine.waitForTransactionHash({
client: TEST_CLIENT,
transactionId: tx.transactionId,
});
expect(txHash.transactionHash).toBeDefined();
});
it("should get revert reason", async () => {
const nftContract = getContract({
client: TEST_CLIENT,
+104 -17
View File
@@ -19,7 +19,7 @@ import type {
Account,
SendTransactionOption,
} from "../wallets/interfaces/wallet.js";
import { waitForTransactionHash } from "./get-status.js";
import { waitForTransactionHash } from "./wait-for-tx-hash.js";
/**
* Options for creating an server wallet.
@@ -54,6 +54,9 @@ export type ServerWallet = Account & {
transaction: PreparedTransaction;
simulate?: boolean;
}) => Promise<{ transactionId: string }>;
enqueueBatchTransaction: (args: {
transactions: PreparedTransaction[];
}) => Promise<{ transactionId: string }>;
};
/**
@@ -102,6 +105,37 @@ export type ServerWallet = Account & {
* console.log("Transaction sent:", transactionHash);
* ```
*
* ### Sending a batch of transactions
* ```ts
* // prepare the transactions
* const transaction1 = claimTo({
* contract,
* to: firstRecipient,
* quantity: 1n,
* });
* const transaction2 = claimTo({
* contract,
* to: secondRecipient,
* quantity: 1n,
* });
*
*
* // enqueue the transactions in a batch
* const { transactionId } = await myServerWallet.enqueueBatchTransaction({
* transactions: [transaction1, transaction2],
* });
* ```
*
* ### Polling for the batch of transactions to be submitted onchain
* ```ts
* // optionally poll for the transaction to be submitted onchain
* const { transactionHash } = await Engine.waitForTransactionHash({
* client,
* transactionId,
* });
* console.log("Transaction sent:", transactionHash);
* ```
*
* ### Getting the execution status of a transaction
* ```ts
* const executionResult = await Engine.getTransactionStatus({
@@ -130,16 +164,30 @@ export function serverWallet(options: ServerWalletOptions): ServerWallet {
};
};
const enqueueTx = async (transaction: SendTransactionOption) => {
const enqueueTx = async (transaction: SendTransactionOption[]) => {
if (transaction.length === 0) {
throw new Error("No transactions to enqueue");
}
const firstTransaction = transaction[0];
if (!firstTransaction) {
throw new Error("No transactions to enqueue");
}
const chainId = firstTransaction.chainId;
// Validate all transactions are on the same chain
for (let i = 1; i < transaction.length; i++) {
if (transaction[i]?.chainId !== chainId) {
throw new Error(
`All transactions in batch must be on the same chain. Expected ${chainId}, got ${transaction[i]?.chainId} at index ${i}`,
);
}
}
const body = {
executionOptions: getExecutionOptions(transaction.chainId),
params: [
{
to: transaction.to ?? undefined,
data: transaction.data,
value: transaction.value?.toString(),
},
],
executionOptions: getExecutionOptions(chainId),
params: transaction.map((t) => ({
to: t.to ?? undefined,
data: t.data,
value: t.value?.toString(),
})),
};
const result = await sendTransaction({
@@ -158,11 +206,7 @@ export function serverWallet(options: ServerWalletOptions): ServerWallet {
if (!data) {
throw new Error("No data returned from engine");
}
const transactionId = data.transactions?.[0]?.id;
if (!transactionId) {
throw new Error("No transactionId returned from engine");
}
return transactionId;
return data.transactions.map((t) => t.id);
};
return {
@@ -193,11 +237,54 @@ export function serverWallet(options: ServerWalletOptions): ServerWallet {
value: value ?? undefined,
};
}
const transactionId = await enqueueTx(serializedTransaction);
const transactionIds = await enqueueTx([serializedTransaction]);
const transactionId = transactionIds[0];
if (!transactionId) {
throw new Error("No transactionId returned from engine");
}
return { transactionId };
},
enqueueBatchTransaction: async (args: {
transactions: PreparedTransaction[];
}) => {
const serializedTransactions: SendTransactionOption[] = [];
for (const transaction of args.transactions) {
const [to, data, value] = await Promise.all([
transaction.to ? resolvePromisedValue(transaction.to) : null,
encode(transaction),
transaction.value ? resolvePromisedValue(transaction.value) : null,
]);
serializedTransactions.push({
chainId: transaction.chain.id,
data,
to: to ?? undefined,
value: value ?? undefined,
});
}
const transactionIds = await enqueueTx(serializedTransactions);
const transactionId = transactionIds[0];
if (!transactionId) {
throw new Error("No transactionId returned from engine");
}
return { transactionId };
},
sendTransaction: async (transaction: SendTransactionOption) => {
const transactionId = await enqueueTx(transaction);
const transactionIds = await enqueueTx([transaction]);
const transactionId = transactionIds[0];
if (!transactionId) {
throw new Error("No transactionId returned from engine");
}
return waitForTransactionHash({
client,
transactionId,
});
},
sendBatchTransaction: async (transactions: SendTransactionOption[]) => {
const transactionIds = await enqueueTx(transactions);
const transactionId = transactionIds[0];
if (!transactionId) {
throw new Error("No transactionId returned from engine");
}
return waitForTransactionHash({
client,
transactionId,
@@ -0,0 +1,75 @@
import { stringify } from "viem";
import type { Hex } from "../utils/encoding/hex.js";
import type { ThirdwebClient } from "../client/client.js";
import type { WaitForReceiptOptions } from "../transaction/actions/wait-for-tx-receipt.js";
import { getTransactionStatus } from "./get-status.js";
/**
* Wait for a transaction to be submitted onchain and return the transaction hash.
* @param args - The arguments for the transaction.
* @param args.client - The thirdweb client to use.
* @param args.transactionId - The id of the transaction to wait for.
* @param args.timeoutInSeconds - The timeout in seconds.
* @engine
* @example
* ```ts
* import { Engine } from "thirdweb";
*
* const { transactionHash } = await Engine.waitForTransactionHash({
* client,
* transactionId, // the transaction id returned from enqueueTransaction
* });
* ```
*/
export async function waitForTransactionHash(args: {
client: ThirdwebClient;
transactionId: string;
timeoutInSeconds?: number;
}): Promise<WaitForReceiptOptions> {
const startTime = Date.now();
const TIMEOUT_IN_MS = args.timeoutInSeconds
? args.timeoutInSeconds * 1000
: 5 * 60 * 1000; // 5 minutes in milliseconds
while (Date.now() - startTime < TIMEOUT_IN_MS) {
const executionResult = await getTransactionStatus(args);
const status = executionResult.status;
switch (status) {
case "FAILED": {
throw new Error(
`Transaction failed: ${executionResult.error || "Unknown error"}`,
);
}
case "CONFIRMED": {
const onchainStatus =
executionResult && "onchainStatus" in executionResult
? executionResult.onchainStatus
: null;
if (onchainStatus === "REVERTED") {
const revertData =
"revertData" in executionResult
? executionResult.revertData
: undefined;
throw new Error(
`Transaction reverted: ${revertData?.errorName || "unknown error"} ${revertData?.errorArgs ? stringify(revertData.errorArgs) : ""} - ${executionResult.transactionHash ? executionResult.transactionHash : ""}`,
);
}
return {
transactionHash: executionResult.transactionHash as Hex,
client: args.client,
chain: executionResult.chain,
};
}
default: {
// wait for the transaction to be confirmed
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
throw new Error(
`Transaction timed out after ${TIMEOUT_IN_MS / 1000} seconds`,
);
}
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { DROP1155_CONTRACT } from "~test/test-contracts.js";
import { getOwnedNFTs } from "./getOwnedNFTs.js";
describe.runIf(process.env.TW_SECRET_KEY)("erc1155.getOwnedNFTs", () => {
it("with indexer", async () => {
const nfts = await getOwnedNFTs({
contract: DROP1155_CONTRACT,
address: "0x00d4da27dedce60f859471d8f595fdb4ae861557",
});
expect(nfts.length).toBe(3);
expect(nfts.find((nft) => nft.id === 4n)?.quantityOwned).toBe(411n);
});
it("without indexer", async () => {
const nfts = await getOwnedNFTs({
contract: DROP1155_CONTRACT,
address: "0x00d4da27dedce60f859471d8f595fdb4ae861557",
useIndexer: false,
});
expect(nfts.length).toBe(3);
expect(nfts.find((nft) => nft.id === 4n)?.quantityOwned).toBe(411n);
});
});
@@ -11,7 +11,7 @@ import {
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
import { toEther } from "../../utils/units.js";
import { toEther, toWei } from "../../utils/units.js";
import { name } from "../common/read/name.js";
import { deployERC20Contract } from "../prebuilts/deploy-erc20.js";
import { canClaim } from "./drops/read/canClaim.js";
@@ -20,6 +20,8 @@ import { claimTo } from "./drops/write/claimTo.js";
import { resetClaimEligibility } from "./drops/write/resetClaimEligibility.js";
import { setClaimConditions } from "./drops/write/setClaimConditions.js";
import { getBalance } from "./read/getBalance.js";
import { getApprovalForTransaction } from "./write/getApprovalForTransaction.js";
import { mintTo } from "./write/mintTo.js";
describe.runIf(process.env.TW_SECRET_KEY)(
"DropERC20",
@@ -135,6 +137,94 @@ describe.runIf(process.env.TW_SECRET_KEY)(
).toBe("2");
});
it("should allow to claim tokens with erc20 price", async () => {
expect(
(await getBalance({ contract, address: TEST_ACCOUNT_C.address }))
.displayValue,
).toBe("2");
const erc20ContractAddres = await deployERC20Contract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
type: "TokenERC20",
params: {
name: "Test DropERC20",
},
});
const erc20Contract = getContract({
address: erc20ContractAddres,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
const mintToTx = mintTo({
contract: erc20Contract,
to: TEST_ACCOUNT_C.address,
amount: "0.02",
});
await sendAndConfirmTransaction({
transaction: mintToTx,
account: TEST_ACCOUNT_A,
});
expect(
(
await getBalance({
contract: erc20Contract,
address: TEST_ACCOUNT_C.address,
})
).displayValue,
).toBe("0.02");
// set cc with price
await sendAndConfirmTransaction({
transaction: setClaimConditions({
contract,
phases: [
{
price: "0.01",
currencyAddress: erc20ContractAddres,
},
],
}),
account: TEST_ACCOUNT_A,
});
const claimTx = claimTo({
contract,
to: TEST_ACCOUNT_C.address,
quantity: "2",
});
// assert value is set correctly
const value = await resolvePromisedValue(claimTx.erc20Value);
expect(value).toBeDefined();
if (!value) throw new Error("value is undefined");
expect(value.amountWei).toBe(toWei("0.02"));
const approve = await getApprovalForTransaction({
transaction: claimTx,
account: TEST_ACCOUNT_C,
});
if (approve) {
await sendAndConfirmTransaction({
transaction: approve,
account: TEST_ACCOUNT_C,
});
}
// claim
await sendAndConfirmTransaction({
transaction: claimTx,
account: TEST_ACCOUNT_C,
});
expect(
(await getBalance({ contract, address: TEST_ACCOUNT_C.address }))
.displayValue,
).toBe("4");
expect(
(
await getBalance({
contract: erc20Contract,
address: TEST_ACCOUNT_C.address,
})
).displayValue,
).toBe("0");
});
describe("Allowlists", () => {
it("should allow to claim tokens with an allowlist", async () => {
await sendAndConfirmTransaction({
+6 -1
View File
@@ -319,7 +319,12 @@ async function transformNFTModel(
});
}
return parsedNft;
return {
...parsedNft,
...(contract?.type === "erc1155"
? { quantityOwned: balance ? BigInt(balance) : undefined }
: {}),
};
}),
);
}
+1
View File
@@ -127,6 +127,7 @@ const THIRDWEB_DOMAINS = [
// dev domains
".thirdweb.dev",
".thirdweb-dev.com",
".thirdwebstorage-dev.com",
] as const;
export const IS_THIRDWEB_URL_CACHE = new LruMap<boolean>(4096);
+2
View File
@@ -1,5 +1,7 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.86
## 0.2.85
## 0.2.84
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wagmi-adapter",
"version": "0.2.85",
"version": "0.2.86",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"