Skip to content

Commit 921449d

Browse files
joaquim-vergesd4mr
andauthored
Handle empty output in ABI for /write (#707)
* Handle empty output in ABI for /write * fix write tests * fix: sdk generation failing when using type.merge * minor test improvements --------- Co-authored-by: Prithvish Baidya <deformercoding@gmail.com>
1 parent e12dcaf commit 921449d

File tree

12 files changed

+126
-56
lines changed

12 files changed

+126
-56
lines changed

src/scripts/generate-sdk.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { execSync } from "child_process";
2-
import fs from "fs";
3-
import { kill } from "process";
1+
import { execSync } from "node:child_process";
2+
import fs from "node:fs";
3+
import { kill } from "node:process";
44

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

78
async function main() {
89
try {
@@ -22,7 +23,8 @@ async function main() {
2223

2324
const code = fs
2425
.readFileSync("./sdk/src/Engine.ts", "utf-8")
25-
.replace(`export class Engine`, `class EngineLogic`).concat(`
26+
.replace("export class Engine", "class EngineLogic")
27+
.concat(`
2628
export class Engine extends EngineLogic {
2729
constructor(config: { url: string; accessToken: string; }) {
2830
super({ BASE: config.url, TOKEN: config.accessToken });

src/server/middleware/error.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { FastifyInstance } from "fastify";
22
import { ReasonPhrases, StatusCodes } from "http-status-codes";
3+
import { stringify } from "thirdweb/utils";
34
import { ZodError } from "zod";
45
import { env } from "../../utils/env";
56
import { parseEthersError } from "../../utils/ethers";
@@ -22,6 +23,13 @@ export const createCustomError = (
2223
code,
2324
});
2425

26+
export function formatError(error: unknown) {
27+
if (error instanceof Error) {
28+
return error.message;
29+
}
30+
return stringify(error);
31+
}
32+
2533
export const customDateTimestampError = (date: string): CustomError =>
2634
createCustomError(
2735
`Invalid date: ${date}. Needs to new Date() / new Date().toISOstring() / new Date().getTime() / Unix Epoch`,

src/server/routes/contract/write/write.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Type, type Static } from "@sinclair/typebox";
1+
import { type Static, Type } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import { prepareContractCall, resolveMethod } from "thirdweb";
5-
import { stringify, type AbiFunction } from "thirdweb/utils";
5+
import type { AbiFunction } from "thirdweb/utils";
66
import { getContractV5 } from "../../../../utils/cache/getContractv5";
77
import { queueTransaction } from "../../../../utils/transaction/queueTransation";
8-
import { createCustomError } from "../../../middleware/error";
9-
import { abiSchema } from "../../../schemas/contract";
8+
import { createCustomError, formatError } from "../../../middleware/error";
9+
import { abiArraySchema } from "../../../schemas/contract";
1010
import {
1111
contractParamSchema,
1212
requestQuerystringSchema,
@@ -18,6 +18,7 @@ import {
1818
requiredAddress,
1919
walletWithAAHeaderSchema,
2020
} from "../../../schemas/wallet";
21+
import { sanitizeAbi } from "../../../utils/abi";
2122
import { getChainIdFromChain } from "../../../utils/chain";
2223
import { parseTransactionOverrides } from "../../../utils/transactionOverrides";
2324

@@ -30,7 +31,7 @@ const writeRequestBodySchema = Type.Object({
3031
description: "The arguments to call on the function",
3132
}),
3233
...txOverridesWithValueSchema.properties,
33-
abi: Type.Optional(Type.Array(abiSchema)),
34+
abi: Type.Optional(abiArraySchema),
3435
});
3536

3637
// LOGIC
@@ -71,7 +72,7 @@ export async function writeToContract(fastify: FastifyInstance) {
7172
const contract = await getContractV5({
7273
chainId,
7374
contractAddress,
74-
abi,
75+
abi: sanitizeAbi(abi),
7576
});
7677

7778
// 3 possible ways to get function from abi:
@@ -82,9 +83,9 @@ export async function writeToContract(fastify: FastifyInstance) {
8283
let method: AbiFunction;
8384
try {
8485
method = await resolveMethod(functionName)(contract);
85-
} catch (e: any) {
86+
} catch (e) {
8687
throw createCustomError(
87-
stringify(e),
88+
formatError(e),
8889
StatusCodes.BAD_REQUEST,
8990
"BAD_REQUEST",
9091
);

src/server/routes/transaction/blockchain/getLogs.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,29 +54,31 @@ const LogSchema = Type.Object({
5454
blockHash: Type.String(),
5555
logIndex: Type.Number(),
5656
removed: Type.Boolean(),
57-
});
5857

59-
const ParsedLogSchema = Type.Object({
60-
...LogSchema.properties,
61-
eventName: Type.String(),
62-
args: Type.Unknown({
63-
description: "Event arguments.",
64-
examples: [
65-
{
66-
from: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
67-
to: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
68-
value: "1000000000000000000n",
69-
},
70-
],
71-
}),
58+
// Additional properties only for parsed logs
59+
eventName: Type.Optional(
60+
Type.String({
61+
description: "Event name, only returned when `parseLogs` is true",
62+
}),
63+
),
64+
args: Type.Optional(
65+
Type.Unknown({
66+
description: "Event arguments. Only returned when `parseLogs` is true",
67+
examples: [
68+
{
69+
from: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
70+
to: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
71+
value: "1000000000000000000n",
72+
},
73+
],
74+
}),
75+
),
7276
});
7377

78+
// DO NOT USE type.union
79+
// this is known to cause issues with the generated types
7480
export const responseBodySchema = Type.Object({
75-
result: Type.Union([
76-
// ParsedLogSchema is listed before LogSchema because it is more specific.
77-
Type.Array(ParsedLogSchema),
78-
Type.Array(LogSchema),
79-
]),
81+
result: Type.Array(LogSchema),
8082
});
8183

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

222224
reply.status(StatusCodes.OK).send({
223225
result: superjson.serialize(parsedLogs).json as Static<
224-
typeof ParsedLogSchema
226+
typeof LogSchema
225227
>[],
226228
});
227229
},

src/server/schemas/contract/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Type, type Static } from "@sinclair/typebox";
1+
import { type Static, Type } from "@sinclair/typebox";
22
import type { contractSchemaTypes } from "../sharedApiSchemas";
33

44
/**
@@ -61,6 +61,9 @@ export const abiSchema = Type.Object({
6161
stateMutability: Type.Optional(Type.String()),
6262
});
6363

64+
export const abiArraySchema = Type.Array(abiSchema);
65+
export type AbiSchemaType = Static<typeof abiArraySchema>;
66+
6467
export const contractEventSchema = Type.Record(Type.String(), Type.Any());
6568

6669
export const rolesResponseSchema = Type.Object({

src/server/utils/abi.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Abi } from "thirdweb/utils";
2+
import type { AbiSchemaType } from "../schemas/contract";
3+
4+
export function sanitizeAbi(abi: AbiSchemaType | undefined): Abi | undefined {
5+
if (!abi) return undefined;
6+
return abi.map((item) => {
7+
if (item.type === "function") {
8+
return {
9+
...item,
10+
// older versions of engine allowed passing in empty inputs/outputs, but necesasry for abi validation
11+
inputs: item.inputs || [],
12+
outputs: item.outputs || [],
13+
};
14+
}
15+
return item;
16+
}) as Abi;
17+
}

src/utils/cache/getContractv5.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
import { getContract } from "thirdweb";
1+
import { type ThirdwebContract, getContract } from "thirdweb";
2+
import type { Abi } from "thirdweb/utils";
23
import { thirdwebClient } from "../../utils/sdk";
34
import { getChain } from "../chain";
45

56
interface GetContractParams {
67
chainId: number;
78
contractAddress: string;
8-
abi?: any;
9+
abi?: Abi;
910
}
1011

1112
// Using new v5 SDK
1213
export const getContractV5 = async ({
1314
chainId,
1415
contractAddress,
1516
abi,
16-
}: GetContractParams) => {
17+
}: GetContractParams): Promise<ThirdwebContract> => {
1718
const definedChain = await getChain(chainId);
1819

1920
// get a contract
@@ -25,5 +26,5 @@ export const getContractV5 = async ({
2526
// the chain the contract is deployed on
2627
chain: definedChain,
2728
abi,
28-
});
29+
}) as ThirdwebContract; // not using type inference here;
2930
};

src/utils/transaction/queueTransation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Static } from "@sinclair/typebox";
22
import { StatusCodes } from "http-status-codes";
33
import {
4-
encode,
54
type Address,
65
type Hex,
76
type PreparedTransaction,
7+
encode,
88
} from "thirdweb";
99
import { stringify } from "thirdweb/utils";
1010
import { createCustomError } from "../../server/middleware/error";

test/e2e/tests/smoke.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,13 @@ describe("Smoke Test", () => {
1212
backendWallet,
1313
{
1414
amount: "0",
15-
currencyAddress: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
1615
to: backendWallet,
1716
},
1817
);
1918

20-
expect(res.result.queueId).toBeDefined();
21-
2219
const transactionStatus = await pollTransactionStatus(
2320
engine,
24-
res.result.queueId!,
21+
res.result.queueId,
2522
true,
2623
);
2724

test/e2e/tests/write.test.ts

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { beforeAll, describe, expect, test } from "bun:test";
2+
import assert from "node:assert";
23
import type { Address } from "thirdweb";
34
import { zeroAddress } from "viem";
5+
import type { ApiError } from "../../../sdk/dist/thirdweb-dev-engine.cjs";
46
import { CONFIG } from "../config";
57
import type { setupEngine } from "../utils/engine";
68
import { pollTransactionStatus } from "../utils/transactions";
@@ -14,7 +16,7 @@ describe("Write Tests", () => {
1416
beforeAll(async () => {
1517
const { engine: _engine, backendWallet: _backendWallet } = await setup();
1618
engine = _engine;
17-
backendWallet = _backendWallet;
19+
backendWallet = _backendWallet as Address;
1820

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

3335
expect(res.result.queueId).toBeDefined();
36+
assert(res.result.queueId, "queueId must be defined");
3437
expect(res.result.deployedAddress).toBeDefined();
3538

3639
const transactionStatus = await pollTransactionStatus(
3740
engine,
38-
res.result.queueId!,
41+
res.result.queueId,
3942
true,
4043
);
4144

4245
expect(transactionStatus.minedAt).toBeDefined();
43-
tokenContractAddress = res.result.deployedAddress!;
46+
assert(res.result.deployedAddress, "deployedAddress must be defined");
47+
tokenContractAddress = res.result.deployedAddress;
4448
console.log("tokenContractAddress", tokenContractAddress);
4549
});
4650

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

6064
const writeTransactionStatus = await pollTransactionStatus(
6165
engine,
62-
writeRes.result.queueId!,
66+
writeRes.result.queueId,
6367
true,
6468
);
6569

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

8286
const writeTransactionStatus = await pollTransactionStatus(
8387
engine,
84-
writeRes.result.queueId!,
88+
writeRes.result.queueId,
8589
true,
8690
);
8791

@@ -107,7 +111,7 @@ describe("Write Tests", () => {
107111
name: "setContractURI",
108112
stateMutability: "nonpayable",
109113
type: "function",
110-
// outputs: [],
114+
outputs: [],
111115
},
112116
],
113117
},
@@ -117,14 +121,49 @@ describe("Write Tests", () => {
117121

118122
const writeTransactionStatus = await pollTransactionStatus(
119123
engine,
120-
writeRes.result.queueId!,
124+
writeRes.result.queueId,
121125
true,
122126
);
123127

124128
expect(writeTransactionStatus.minedAt).toBeDefined();
125129
});
126130

127-
test.only("Should throw error if function name is not found", async () => {
131+
test("Write to a contract with non-standard abi", async () => {
132+
const writeRes = await engine.contract.write(
133+
CONFIG.CHAIN.id.toString(),
134+
tokenContractAddress,
135+
backendWallet,
136+
{
137+
functionName: "setContractURI",
138+
args: ["https://abi-test.com"],
139+
abi: [
140+
{
141+
inputs: [
142+
{
143+
name: "uri",
144+
type: "string",
145+
},
146+
],
147+
name: "setContractURI",
148+
stateMutability: "nonpayable",
149+
type: "function",
150+
},
151+
],
152+
},
153+
);
154+
155+
expect(writeRes.result.queueId).toBeDefined();
156+
157+
const writeTransactionStatus = await pollTransactionStatus(
158+
engine,
159+
writeRes.result.queueId,
160+
true,
161+
);
162+
163+
expect(writeTransactionStatus.minedAt).toBeDefined();
164+
});
165+
166+
test("Should throw error if function name is not found", async () => {
128167
try {
129168
await engine.contract.write(
130169
CONFIG.CHAIN.id.toString(),
@@ -135,8 +174,8 @@ describe("Write Tests", () => {
135174
args: [""],
136175
},
137176
);
138-
} catch (e: any) {
139-
expect(e.message).toBe(
177+
} catch (e) {
178+
expect((e as ApiError).body?.error?.message).toBe(
140179
`could not find function with name "nonExistentFunction" in abi`,
141180
);
142181
}

test/e2e/utils/engine.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export const createChain = async (engine: Engine) => {
1616
const chains = await engine.configuration.getChainsConfiguration();
1717

1818
if (chains.result) {
19-
const parsedChains = JSON.parse(chains.result);
20-
if (parsedChains.find((chain: any) => chain.chainId === CONFIG.CHAIN.id)) {
19+
const parsedChains = chains.result;
20+
if (parsedChains.find((chain) => chain.chainId === CONFIG.CHAIN.id)) {
2121
console.log("Anvil chain already exists in engine");
2222
return;
2323
}

0 commit comments

Comments
 (0)