Skip to content
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
12 changes: 7 additions & 5 deletions src/scripts/generate-sdk.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { execSync } from "child_process";
import fs from "fs";
import { kill } from "process";
import { execSync } from "node:child_process";
import fs from "node:fs";
import { kill } from "node:process";

const ENGINE_OPENAPI_URL = "https://demo.web3api.thirdweb.com/json";
// requires engine to be running locally
const ENGINE_OPENAPI_URL = "http://localhost:3005/json";

async function main() {
try {
Expand All @@ -22,7 +23,8 @@ async function main() {

const code = fs
.readFileSync("./sdk/src/Engine.ts", "utf-8")
.replace(`export class Engine`, `class EngineLogic`).concat(`
.replace("export class Engine", "class EngineLogic")
.concat(`
export class Engine extends EngineLogic {
constructor(config: { url: string; accessToken: string; }) {
super({ BASE: config.url, TOKEN: config.accessToken });
Expand Down
8 changes: 8 additions & 0 deletions src/server/middleware/error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FastifyInstance } from "fastify";
import { ReasonPhrases, StatusCodes } from "http-status-codes";
import { stringify } from "thirdweb/utils";
import { ZodError } from "zod";
import { env } from "../../utils/env";
import { parseEthersError } from "../../utils/ethers";
Expand All @@ -22,6 +23,13 @@ export const createCustomError = (
code,
});

export function formatError(error: unknown) {
Copy link
Contributor

@d4mr d4mr Oct 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe move to /utils/errors?

if (error instanceof Error) {
return error.message;
}
return stringify(error);
}

export const customDateTimestampError = (date: string): CustomError =>
createCustomError(
`Invalid date: ${date}. Needs to new Date() / new Date().toISOstring() / new Date().getTime() / Unix Epoch`,
Expand Down
17 changes: 9 additions & 8 deletions src/server/routes/contract/write/write.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Type, type Static } from "@sinclair/typebox";
import { type Static, Type } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { prepareContractCall, resolveMethod } from "thirdweb";
import { stringify, type AbiFunction } from "thirdweb/utils";
import type { AbiFunction } from "thirdweb/utils";
import { getContractV5 } from "../../../../utils/cache/getContractv5";
import { queueTransaction } from "../../../../utils/transaction/queueTransation";
import { createCustomError } from "../../../middleware/error";
import { abiSchema } from "../../../schemas/contract";
import { createCustomError, formatError } from "../../../middleware/error";
import { abiArraySchema } from "../../../schemas/contract";
import {
contractParamSchema,
requestQuerystringSchema,
Expand All @@ -18,6 +18,7 @@ import {
requiredAddress,
walletWithAAHeaderSchema,
} from "../../../schemas/wallet";
import { sanitizeAbi } from "../../../utils/abi";
import { getChainIdFromChain } from "../../../utils/chain";
import { parseTransactionOverrides } from "../../../utils/transactionOverrides";

Expand All @@ -30,7 +31,7 @@ const writeRequestBodySchema = Type.Object({
description: "The arguments to call on the function",
}),
...txOverridesWithValueSchema.properties,
abi: Type.Optional(Type.Array(abiSchema)),
abi: Type.Optional(abiArraySchema),
});

// LOGIC
Expand Down Expand Up @@ -71,7 +72,7 @@ export async function writeToContract(fastify: FastifyInstance) {
const contract = await getContractV5({
chainId,
contractAddress,
abi,
abi: sanitizeAbi(abi),
});

// 3 possible ways to get function from abi:
Expand All @@ -82,9 +83,9 @@ export async function writeToContract(fastify: FastifyInstance) {
let method: AbiFunction;
try {
method = await resolveMethod(functionName)(contract);
} catch (e: any) {
} catch (e) {
throw createCustomError(
stringify(e),
formatError(e),
StatusCodes.BAD_REQUEST,
"BAD_REQUEST",
);
Expand Down
42 changes: 22 additions & 20 deletions src/server/routes/transaction/blockchain/getLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,29 +54,31 @@ const LogSchema = Type.Object({
blockHash: Type.String(),
logIndex: Type.Number(),
removed: Type.Boolean(),
});

const ParsedLogSchema = Type.Object({
...LogSchema.properties,
eventName: Type.String(),
args: Type.Unknown({
description: "Event arguments.",
examples: [
{
from: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
to: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
value: "1000000000000000000n",
},
],
}),
// Additional properties only for parsed logs
eventName: Type.Optional(
Type.String({
description: "Event name, only returned when `parseLogs` is true",
}),
),
args: Type.Optional(
Type.Unknown({
description: "Event arguments. Only returned when `parseLogs` is true",
examples: [
{
from: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
to: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
value: "1000000000000000000n",
},
],
}),
),
});

// DO NOT USE type.union
// this is known to cause issues with the generated types
export const responseBodySchema = Type.Object({
result: Type.Union([
// ParsedLogSchema is listed before LogSchema because it is more specific.
Type.Array(ParsedLogSchema),
Type.Array(LogSchema),
]),
result: Type.Array(LogSchema),
});

responseBodySchema.example = {
Expand Down Expand Up @@ -221,7 +223,7 @@ export async function getTransactionLogs(fastify: FastifyInstance) {

reply.status(StatusCodes.OK).send({
result: superjson.serialize(parsedLogs).json as Static<
typeof ParsedLogSchema
typeof LogSchema
>[],
});
},
Expand Down
5 changes: 4 additions & 1 deletion src/server/schemas/contract/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type, type Static } from "@sinclair/typebox";
import { type Static, Type } from "@sinclair/typebox";
import type { contractSchemaTypes } from "../sharedApiSchemas";

/**
Expand Down Expand Up @@ -61,6 +61,9 @@ export const abiSchema = Type.Object({
stateMutability: Type.Optional(Type.String()),
});

export const abiArraySchema = Type.Array(abiSchema);
export type AbiSchemaType = Static<typeof abiArraySchema>;

export const contractEventSchema = Type.Record(Type.String(), Type.Any());

export const rolesResponseSchema = Type.Object({
Expand Down
17 changes: 17 additions & 0 deletions src/server/utils/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Abi } from "thirdweb/utils";
import type { AbiSchemaType } from "../schemas/contract";

export function sanitizeAbi(abi: AbiSchemaType | undefined): Abi | undefined {
if (!abi) return undefined;
return abi.map((item) => {
if (item.type === "function") {
return {
...item,
// older versions of engine allowed passing in empty inputs/outputs, but necesasry for abi validation
inputs: item.inputs || [],
outputs: item.outputs || [],
};
}
return item;
}) as Abi;
}
9 changes: 5 additions & 4 deletions src/utils/cache/getContractv5.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { getContract } from "thirdweb";
import { type ThirdwebContract, getContract } from "thirdweb";
import type { Abi } from "thirdweb/utils";
import { thirdwebClient } from "../../utils/sdk";
import { getChain } from "../chain";

interface GetContractParams {
chainId: number;
contractAddress: string;
abi?: any;
abi?: Abi;
}

// Using new v5 SDK
export const getContractV5 = async ({
chainId,
contractAddress,
abi,
}: GetContractParams) => {
}: GetContractParams): Promise<ThirdwebContract> => {
const definedChain = await getChain(chainId);

// get a contract
Expand All @@ -25,5 +26,5 @@ export const getContractV5 = async ({
// the chain the contract is deployed on
chain: definedChain,
abi,
});
}) as ThirdwebContract; // not using type inference here;
};
2 changes: 1 addition & 1 deletion src/utils/transaction/queueTransation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Static } from "@sinclair/typebox";
import { StatusCodes } from "http-status-codes";
import {
encode,
type Address,
type Hex,
type PreparedTransaction,
encode,
} from "thirdweb";
import { stringify } from "thirdweb/utils";
import { createCustomError } from "../../server/middleware/error";
Expand Down
5 changes: 1 addition & 4 deletions test/e2e/tests/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,13 @@ describe("Smoke Test", () => {
backendWallet,
{
amount: "0",
currencyAddress: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
to: backendWallet,
},
);

expect(res.result.queueId).toBeDefined();

const transactionStatus = await pollTransactionStatus(
engine,
res.result.queueId!,
res.result.queueId,
true,
);

Expand Down
59 changes: 49 additions & 10 deletions test/e2e/tests/write.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { beforeAll, describe, expect, test } from "bun:test";
import assert from "node:assert";
import type { Address } from "thirdweb";
import { zeroAddress } from "viem";
import type { ApiError } from "../../../sdk/dist/thirdweb-dev-engine.cjs";
import { CONFIG } from "../config";
import type { setupEngine } from "../utils/engine";
import { pollTransactionStatus } from "../utils/transactions";
Expand All @@ -14,7 +16,7 @@ describe("Write Tests", () => {
beforeAll(async () => {
const { engine: _engine, backendWallet: _backendWallet } = await setup();
engine = _engine;
backendWallet = _backendWallet;
backendWallet = _backendWallet as Address;

const res = await engine.deploy.deployToken(
CONFIG.CHAIN.id.toString(),
Expand All @@ -31,16 +33,18 @@ describe("Write Tests", () => {
);

expect(res.result.queueId).toBeDefined();
assert(res.result.queueId, "queueId must be defined");
expect(res.result.deployedAddress).toBeDefined();

const transactionStatus = await pollTransactionStatus(
engine,
res.result.queueId!,
res.result.queueId,
true,
);

expect(transactionStatus.minedAt).toBeDefined();
tokenContractAddress = res.result.deployedAddress!;
assert(res.result.deployedAddress, "deployedAddress must be defined");
tokenContractAddress = res.result.deployedAddress;
console.log("tokenContractAddress", tokenContractAddress);
});

Expand All @@ -59,7 +63,7 @@ describe("Write Tests", () => {

const writeTransactionStatus = await pollTransactionStatus(
engine,
writeRes.result.queueId!,
writeRes.result.queueId,
true,
);

Expand All @@ -81,7 +85,7 @@ describe("Write Tests", () => {

const writeTransactionStatus = await pollTransactionStatus(
engine,
writeRes.result.queueId!,
writeRes.result.queueId,
true,
);

Expand All @@ -107,7 +111,7 @@ describe("Write Tests", () => {
name: "setContractURI",
stateMutability: "nonpayable",
type: "function",
// outputs: [],
outputs: [],
},
],
},
Expand All @@ -117,14 +121,49 @@ describe("Write Tests", () => {

const writeTransactionStatus = await pollTransactionStatus(
engine,
writeRes.result.queueId!,
writeRes.result.queueId,
true,
);

expect(writeTransactionStatus.minedAt).toBeDefined();
});

test.only("Should throw error if function name is not found", async () => {
test("Write to a contract with non-standard abi", async () => {
const writeRes = await engine.contract.write(
CONFIG.CHAIN.id.toString(),
tokenContractAddress,
backendWallet,
{
functionName: "setContractURI",
args: ["https://abi-test.com"],
abi: [
{
inputs: [
{
name: "uri",
type: "string",
},
],
name: "setContractURI",
stateMutability: "nonpayable",
type: "function",
},
],
},
);

expect(writeRes.result.queueId).toBeDefined();

const writeTransactionStatus = await pollTransactionStatus(
engine,
writeRes.result.queueId,
true,
);

expect(writeTransactionStatus.minedAt).toBeDefined();
});

test("Should throw error if function name is not found", async () => {
try {
await engine.contract.write(
CONFIG.CHAIN.id.toString(),
Expand All @@ -135,8 +174,8 @@ describe("Write Tests", () => {
args: [""],
},
);
} catch (e: any) {
expect(e.message).toBe(
} catch (e) {
expect((e as ApiError).body?.error?.message).toBe(
`could not find function with name "nonExistentFunction" in abi`,
);
}
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/utils/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const createChain = async (engine: Engine) => {
const chains = await engine.configuration.getChainsConfiguration();

if (chains.result) {
const parsedChains = JSON.parse(chains.result);
if (parsedChains.find((chain: any) => chain.chainId === CONFIG.CHAIN.id)) {
const parsedChains = chains.result;
if (parsedChains.find((chain) => chain.chainId === CONFIG.CHAIN.id)) {
console.log("Anvil chain already exists in engine");
return;
}
Expand Down
Loading
Loading