Skip to content

Commit b7a4bb7

Browse files
Refactor contract read route to use thirdweb v5 (#871)
1 parent 1fd02e1 commit b7a4bb7

File tree

6 files changed

+162
-37
lines changed

6 files changed

+162
-37
lines changed

sdk/src/services/BackendWalletService.ts

+13
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,17 @@ export class BackendWalletService {
495495
toAddress?: string;
496496
data: string;
497497
value: string;
498+
authorizationList?: Array<{
499+
/**
500+
* A contract or wallet address
501+
*/
502+
address: string;
503+
chainId: number;
504+
nonce: string;
505+
'r': string;
506+
's': string;
507+
yParity: number;
508+
}>;
498509
txOverrides?: {
499510
/**
500511
* Gas limit for the transaction
@@ -814,6 +825,8 @@ export class BackendWalletService {
814825
domain: Record<string, any>;
815826
types: Record<string, any>;
816827
value: Record<string, any>;
828+
primaryType?: string;
829+
chainId?: number;
817830
},
818831
xIdempotencyKey?: string,
819832
xTransactionMode?: 'sponsored',

sdk/src/services/ContractSubscriptionsService.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ export class ContractSubscriptionsService {
6868
*/
6969
contractAddress: string;
7070
/**
71-
* Webhook URL
71+
* The ID of an existing webhook to use for this contract subscription. Either `webhookId` or `webhookUrl` must be provided.
72+
*/
73+
webhookId?: number;
74+
/**
75+
* Creates a new webhook to call when new onchain data is detected. Either `webhookId` or `webhookUrl` must be provided.
7276
*/
7377
webhookUrl?: string;
7478
/**

src/server/routes/contract/read/read.ts

+34-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Type } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
4-
import { getContract } from "../../../../shared/utils/cache/get-contract";
4+
import type { AbiParameters } from "ox";
5+
import { readContract as readContractV5, resolveMethod } from "thirdweb";
6+
import { parseAbiParams } from "thirdweb/utils";
7+
import type { AbiFunction } from "thirdweb/utils";
8+
import { getContractV5 } from "../../../../shared/utils/cache/get-contractv5";
59
import { prettifyError } from "../../../../shared/utils/error";
610
import { createCustomError } from "../../../middleware/error";
711
import {
@@ -12,6 +16,8 @@ import {
1216
partialRouteSchema,
1317
standardResponseSchema,
1418
} from "../../../schemas/shared-api-schemas";
19+
import { sanitizeFunctionName } from "../../../utils/abi";
20+
import { sanitizeAbi } from "../../../utils/abi";
1521
import { getChainIdFromChain } from "../../../utils/chain";
1622
import { bigNumberReplacer } from "../../../utils/convertor";
1723

@@ -37,12 +43,13 @@ export async function readContract(fastify: FastifyInstance) {
3743
},
3844
handler: async (request, reply) => {
3945
const { chain, contractAddress } = request.params;
40-
const { functionName, args } = request.query;
46+
const { functionName, args, abi } = request.query;
4147

4248
const chainId = await getChainIdFromChain(chain);
43-
const contract = await getContract({
49+
const contract = await getContractV5({
4450
chainId,
4551
contractAddress,
52+
abi: sanitizeAbi(abi),
4653
});
4754

4855
let parsedArgs: unknown[] | undefined;
@@ -54,19 +61,33 @@ export async function readContract(fastify: FastifyInstance) {
5461
// fallback to string split
5562
}
5663

57-
parsedArgs ??= args?.split(",").map((arg) => {
58-
if (arg === "true") {
59-
return true;
60-
}
61-
if (arg === "false") {
62-
return false;
63-
}
64-
return arg;
65-
});
64+
parsedArgs ??= args?.split(",");
65+
66+
// 3 possible ways to get function from abi:
67+
// 1. functionName passed as solidity signature
68+
// 2. functionName passed as function name + passed in ABI
69+
// 3. functionName passed as function name + inferred ABI (fetched at encode time)
70+
// this is all handled inside the `resolveMethod` function
71+
let method: AbiFunction;
72+
let params: Array<string | bigint | boolean | object>;
73+
try {
74+
const functionNameOrSignature = sanitizeFunctionName(functionName);
75+
method = await resolveMethod(functionNameOrSignature)(contract);
76+
params = parseAbiParams(
77+
method.inputs.map((i: AbiParameters.Parameter) => i.type),
78+
parsedArgs ?? [],
79+
);
80+
} catch (e) {
81+
throw createCustomError(
82+
prettifyError(e),
83+
StatusCodes.BAD_REQUEST,
84+
"BAD_REQUEST",
85+
);
86+
}
6687

6788
let returnData: unknown;
6889
try {
69-
returnData = await contract.call(functionName, parsedArgs ?? []);
90+
returnData = await readContractV5({ contract, method, params });
7091
} catch (e) {
7192
throw createCustomError(
7293
prettifyError(e),

src/server/schemas/contract/index.ts

+26-21
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,7 @@
1-
import { Type, type Static } from "@sinclair/typebox";
1+
import { type Static, Type } from "@sinclair/typebox";
22
import { AddressSchema } from "../address";
33
import type { contractSchemaTypes } from "../shared-api-schemas";
44

5-
/**
6-
* Basic schema for all Request Query String
7-
*/
8-
export const readRequestQuerySchema = Type.Object({
9-
functionName: Type.String({
10-
description: "Name of the function to call on Contract",
11-
examples: ["balanceOf"],
12-
}),
13-
args: Type.Optional(
14-
Type.String({
15-
description: "Arguments for the function. Comma Separated",
16-
examples: [""],
17-
}),
18-
),
19-
});
20-
21-
export interface readSchema extends contractSchemaTypes {
22-
Querystring: Static<typeof readRequestQuerySchema>;
23-
}
24-
255
const abiTypeSchema = Type.Object({
266
type: Type.Optional(Type.String()),
277
name: Type.Optional(Type.String()),
@@ -65,6 +45,31 @@ export const abiSchema = Type.Object({
6545
export const abiArraySchema = Type.Array(abiSchema);
6646
export type AbiSchemaType = Static<typeof abiArraySchema>;
6747

48+
/**
49+
* Basic schema for all Request Query String
50+
*/
51+
export const readRequestQuerySchema = Type.Object({
52+
functionName: Type.String({
53+
description:
54+
"The function to call on the contract. It is highly recommended to provide a full function signature, such as 'function balanceOf(address owner) view returns (uint256)', to avoid ambiguity and to skip ABI resolution",
55+
examples: [
56+
"function balanceOf(address owner) view returns (uint256)",
57+
"balanceOf",
58+
],
59+
}),
60+
args: Type.Optional(
61+
Type.String({
62+
description: "Arguments for the function. Comma Separated",
63+
examples: [""],
64+
}),
65+
),
66+
abi: Type.Optional(abiArraySchema),
67+
});
68+
69+
export interface readSchema extends contractSchemaTypes {
70+
Querystring: Static<typeof readRequestQuerySchema>;
71+
}
72+
6873
export const contractEventSchema = Type.Record(Type.String(), Type.Any());
6974

7075
export const rolesResponseSchema = Type.Object({

src/server/utils/convertor.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { BigNumber } from "ethers";
33
const isHexBigNumber = (value: unknown) => {
44
const isNonNullObject = typeof value === "object" && value !== null;
55
const hasType = isNonNullObject && "type" in value;
6-
return hasType && value.type === "BigNumber" && "hex" in value
7-
}
6+
return hasType && value.type === "BigNumber" && "hex" in value;
7+
};
88
export const bigNumberReplacer = (value: unknown): unknown => {
99
// if we find a BigNumber then make it into a string (since that is safe)
1010
if (BigNumber.isBigNumber(value) || isHexBigNumber(value)) {
@@ -15,5 +15,9 @@ export const bigNumberReplacer = (value: unknown): unknown => {
1515
return value.map(bigNumberReplacer);
1616
}
1717

18+
if (typeof value === "bigint") {
19+
return value.toString();
20+
}
21+
1822
return value;
1923
};

tests/e2e/tests/routes/read.test.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { beforeAll, describe, expect, test } from "bun:test";
2+
import assert from "node:assert";
3+
import { ZERO_ADDRESS } from "thirdweb";
4+
import type { Address } from "thirdweb/utils";
5+
import { CONFIG } from "../../config";
6+
import type { setupEngine } from "../../utils/engine";
7+
import { pollTransactionStatus } from "../../utils/transactions";
8+
import { setup } from "../setup";
9+
10+
describe("readContractRoute", () => {
11+
let engine: ReturnType<typeof setupEngine>;
12+
let backendWallet: Address;
13+
let tokenContractAddress: string;
14+
15+
beforeAll(async () => {
16+
const { engine: _engine, backendWallet: _backendWallet } = await setup();
17+
engine = _engine;
18+
backendWallet = _backendWallet as Address;
19+
20+
const res = await engine.deploy.deployToken(
21+
CONFIG.CHAIN.id.toString(),
22+
backendWallet,
23+
{
24+
contractMetadata: {
25+
name: "test token",
26+
platform_fee_basis_points: 0,
27+
platform_fee_recipient: ZERO_ADDRESS,
28+
symbol: "TT",
29+
trusted_forwarders: [],
30+
},
31+
},
32+
);
33+
34+
expect(res.result.queueId).toBeDefined();
35+
assert(res.result.queueId, "queueId must be defined");
36+
expect(res.result.deployedAddress).toBeDefined();
37+
38+
const transactionStatus = await pollTransactionStatus(
39+
engine,
40+
res.result.queueId,
41+
true,
42+
);
43+
44+
expect(transactionStatus.minedAt).toBeDefined();
45+
assert(res.result.deployedAddress, "deployedAddress must be defined");
46+
tokenContractAddress = res.result.deployedAddress;
47+
});
48+
49+
test("readContract with function name", async () => {
50+
const res = await engine.contract.read(
51+
"name",
52+
CONFIG.CHAIN.id.toString(),
53+
tokenContractAddress,
54+
);
55+
56+
expect(res.result).toEqual("test token");
57+
});
58+
59+
test("readContract with function signature", async () => {
60+
const res = await engine.contract.read(
61+
"function symbol() public view returns (string memory)",
62+
CONFIG.CHAIN.id.toString(),
63+
tokenContractAddress,
64+
);
65+
66+
expect(res.result).toEqual("TT");
67+
});
68+
69+
test("readContract with function signature", async () => {
70+
const res = await engine.contract.read(
71+
"function totalSupply() public view returns (uint256)",
72+
CONFIG.CHAIN.id.toString(),
73+
tokenContractAddress,
74+
);
75+
76+
expect(res.result).toEqual("0");
77+
});
78+
});

0 commit comments

Comments
 (0)