Skip to content

[Engine] Add FAILED status to execution result and update error handling #7065

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/grumpy-areas-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Return timestamps in Engine.getTransactionStatus()
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type ExecutionResult4337Serialized =
| {
status: "QUEUED";
}
| {
status: "FAILED";
error: string;
}
| {
status: "SUBMITTED";
monitoringStatus: "WILL_MONITOR" | "CANNOT_MONITOR";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ export async function createManagementAccessToken(props: {
});
if (res.success) {
const data = res.data;
// store the management access token in the project
await updateProjectClient(
{
projectId: props.project.id,
Expand All @@ -354,8 +353,9 @@ export async function createManagementAccessToken(props: {
],
},
);
return res;
}
return res;
throw new Error(`Failed to create management access token: ${res.error}`);
}

export function maskSecret(secret: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,17 @@ export function TransactionDetailsUI({
transactionHash,
confirmedAt,
createdAt,
errorMessage,
executionParams,
executionResult,
} = transaction;

const status = executionResult?.status as keyof typeof statusDetails;
const errorMessage =
executionResult && "error" in executionResult
? executionResult.error
: executionResult && "revertData" in executionResult
? executionResult.revertData?.revertReason
: null;

const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined;
const explorer = chain?.explorers?.[0];
Expand Down
1 change: 0 additions & 1 deletion apps/playground-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"@radix-ui/react-switch": "^1.2.2",
"@radix-ui/react-tooltip": "1.2.3",
"@tanstack/react-query": "5.74.4",
"@thirdweb-dev/engine": "0.0.19",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "4.1.0",
Expand Down
68 changes: 58 additions & 10 deletions apps/playground-web/src/app/engine/_hooks/useEngineTxStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,64 @@ export function useEngineTxStatus(queueId: string | undefined) {
if (!queueId) throw new Error("No queue ID provided");
const res = await get_engine_tx_status(queueId);

const txStatus: EngineTxStatus = {
queueId: queueId,
status: res.result.status,
chainId: res.result.chainId,
transactionHash: res.result.transactionHash,
queuedAt: res.result.queuedAt,
sentAt: res.result.sentAt,
minedAt: res.result.minedAt,
cancelledAt: res.result.cancelledAt,
};
let txStatus: EngineTxStatus;
switch (res.status) {
case "QUEUED": {
txStatus = {
queueId: queueId,
status: "queued",
chainId: res.chain.id.toString(),
transactionHash: null,
queuedAt: res.createdAt,
sentAt: null,
minedAt: null,
cancelledAt: null,
};
break;
}
case "SUBMITTED": {
txStatus = {
queueId: queueId,
status: "sent",
chainId: res.chain.id.toString(),
transactionHash: null,
queuedAt: res.createdAt,
sentAt: res.createdAt,
minedAt: null,
cancelledAt: null,
};
break;
}
case "CONFIRMED": {
txStatus = {
queueId: queueId,
status: "mined",
chainId: res.chain.id.toString(),
transactionHash: res.transactionHash,
queuedAt: res.createdAt,
sentAt: res.confirmedAt,
minedAt: res.confirmedAt,
cancelledAt: null,
};
break;
}
case "FAILED": {
txStatus = {
queueId: queueId,
status: "errored",
chainId: res.chain.id.toString(),
transactionHash: null,
queuedAt: res.createdAt,
sentAt: null,
minedAt: null,
cancelledAt: res.cancelledAt,
};
break;
}
default: {
throw new Error(`Unknown engine tx status: ${res}`);
}
}

return txStatus;
},
Expand Down
96 changes: 61 additions & 35 deletions apps/playground-web/src/app/engine/actions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
"use server";

import { Engine } from "@thirdweb-dev/engine";

import { Engine, defineChain, encode, getContract } from "thirdweb";
import { multicall } from "thirdweb/extensions/common";
import * as ERC20 from "thirdweb/extensions/erc20";
import * as ERC1155 from "thirdweb/extensions/erc1155";
import { THIRDWEB_CLIENT } from "../../lib/client";
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
const ENGINE_VAULT_ACCESS_TOKEN = process.env
.ENGINE_VAULT_ACCESS_TOKEN as string;

const engine = new Engine({
url: process.env.ENGINE_URL as string,
accessToken: process.env.ENGINE_ACCESS_TOKEN as string,
const serverWallet = Engine.serverWallet({
address: BACKEND_WALLET_ADDRESS,
client: THIRDWEB_CLIENT,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
});

export async function airdrop_tokens_with_engine(params: {
Expand All @@ -17,20 +23,37 @@ export async function airdrop_tokens_with_engine(params: {
amount: string;
}[];
}) {
const res = await engine.erc20.mintBatchTo(
params.chainId.toString(),
params.contractAddress,
BACKEND_WALLET_ADDRESS,
{
data: params.receivers,
},
const contract = getContract({
address: params.contractAddress,
chain: defineChain(params.chainId),
client: THIRDWEB_CLIENT,
});
const data = await Promise.all(
params.receivers.map((receiver) =>
encode(
ERC20.mintTo({
contract,
to: receiver.toAddress,
amount: receiver.amount,
}),
),
),
);
const tx = multicall({
contract,
data,
});

const res = await serverWallet.enqueueTransaction({ transaction: tx });

return res.result;
return res.transactionId;
}

export async function get_engine_tx_status(queueId: string) {
const status = await engine.transaction.status(queueId);
const status = await Engine.getTransactionStatus({
client: THIRDWEB_CLIENT,
transactionId: queueId,
});
return status;
}

Expand All @@ -49,17 +72,19 @@ type MintNFTParams = {
};

export async function mint_erc1155_nft_with_engine(params: MintNFTParams) {
const res = await engine.erc1155.mintTo(
params.chainId.toString(),
params.contractAddress,
BACKEND_WALLET_ADDRESS,
{
receiver: params.toAddress,
metadataWithSupply: params.metadataWithSupply,
},
);
const tx = ERC1155.mintTo({
contract: getContract({
address: params.contractAddress,
chain: defineChain(params.chainId),
client: THIRDWEB_CLIENT,
}),
nft: params.metadataWithSupply.metadata,
to: params.toAddress,
supply: BigInt(params.metadataWithSupply.supply),
});
const res = await serverWallet.enqueueTransaction({ transaction: tx });

return res.result;
return res.transactionId;
}

type ClaimNFTParams = {
Expand All @@ -71,16 +96,17 @@ type ClaimNFTParams = {
};

export async function claim_erc1155_nft_with_engine(params: ClaimNFTParams) {
const res = await engine.erc1155.claimTo(
params.chainId.toString(),
params.contractAddress,
BACKEND_WALLET_ADDRESS,
{
receiver: params.receiverAddress,
quantity: params.quantity.toString(),
tokenId: params.tokenId,
},
);
const tx = ERC1155.claimTo({
contract: getContract({
address: params.contractAddress,
chain: defineChain(params.chainId),
client: THIRDWEB_CLIENT,
}),
to: params.receiverAddress,
tokenId: BigInt(params.tokenId),
quantity: BigInt(params.quantity),
});
const res = await serverWallet.enqueueTransaction({ transaction: tx });

return res.result;
return res.transactionId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,49 +34,49 @@ export function AirdropCode() {
}

const engineAirdropSendCode = `\
const chainId = ${airdropExample.chainId};
const contractAddress = "${airdropExample.contractAddress}";
const addresses = ${JSON.stringify(
airdropExample.receivers.map((x) => ({
address: x.toAddress,
quantity: x.amount,
recipient: x.toAddress,
amount: x.amount,
})),
null,
2,
)};

const url = \`\${YOUR_ENGINE_URL}\/contract/\${chainId}/\${contractAddress}\/erc1155\/airdrop\`;
const contract = getContract({
address: ${airdropExample.contractAddress},
chain: defineChain(${airdropExample.chainId}),
client: THIRDWEB_CLIENT,
});

const transaction = airdropERC20({
contract,
tokenAddress: ${airdropExample.contractAddress},
contents: addresses,
});

const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_SECRET_TOKEN",
"Content-Type": "application/json",
"X-Backend-Wallet-Address": "YOUR_BACKEND_WALLET_ADDRESS",
},
body: JSON.stringify({ addresses }),
const serverWallet = Engine.serverWallet({
address: BACKEND_WALLET_ADDRESS,
client: THIRDWEB_CLIENT,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
});

const data = await response.json();
const queueId = data.queueId;
const { transactionId } = await serverWallet.enqueueTransaction({ transaction });
`;

const engineAirdropGetStatus = `\
function getEngineTxStatus(queueId: string) {
const url = \`\${YOUR_ENGINE_URL}\/transaction/\${queueId}\`;
const response = await fetch(url, {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_SECRET_TOKEN",
},
});
const result = await Engine.getTransactionStatus({
client: THIRDWEB_CLIENT,
transactionId: transactionId,
});

const data = await response.json();
return data.result;
}
console.log(result.status);

// you can keep polling for the status until you get a status of either "mined" or "errored" or "cancelled"
const result = await getEngineTxStatus(queueId);
// or wait for the transaction to be mined (polls status until it's mined)
const result = await Engine.waitForTransactionHash({
client: THIRDWEB_CLIENT,
transactionId: transactionId,
});

console.log(result.status);
console.log(result.transactionHash);
`;
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ export function EngineAirdropPreview() {
const res = await airdropMutation.mutateAsync();
updateEngineTxStatus({
chainId: airdropExample.chainId,
queueId: res.queueId,
queueId: res,
});

setQueueId(res.queueId);
setQueueId(res);
};

return (
Expand Down
2 changes: 1 addition & 1 deletion apps/playground-web/src/app/engine/airdrop/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const airdropExample = {
contractAddress: "0xcB30dB8FB977e8b27ae34c86aF16C4F5E428c0bE",
contractAddress: "0x6E238275023A2575136CF60f655B6B2C0C58b4ac",
chainId: 84532,
chainName: "Base Sepolia",
chainExplorer: "https://base-sepolia.blockscout.com",
Expand Down
Loading
Loading