diff --git a/sdk/src/services/BackendWalletService.ts b/sdk/src/services/BackendWalletService.ts index 9be834083..de88eba66 100644 --- a/sdk/src/services/BackendWalletService.ts +++ b/sdk/src/services/BackendWalletService.ts @@ -495,6 +495,17 @@ export class BackendWalletService { toAddress?: string; data: string; value: string; + authorizationList?: Array<{ + /** + * A contract or wallet address + */ + address: string; + chainId: number; + nonce: string; + 'r': string; + 's': string; + yParity: number; + }>; txOverrides?: { /** * Gas limit for the transaction @@ -814,6 +825,8 @@ export class BackendWalletService { domain: Record; types: Record; value: Record; + primaryType?: string; + chainId?: number; }, xIdempotencyKey?: string, xTransactionMode?: 'sponsored', diff --git a/sdk/src/services/ContractSubscriptionsService.ts b/sdk/src/services/ContractSubscriptionsService.ts index 2bfbfcca1..5b1c2e9ef 100644 --- a/sdk/src/services/ContractSubscriptionsService.ts +++ b/sdk/src/services/ContractSubscriptionsService.ts @@ -68,7 +68,11 @@ export class ContractSubscriptionsService { */ contractAddress: string; /** - * Webhook URL + * The ID of an existing webhook to use for this contract subscription. Either `webhookId` or `webhookUrl` must be provided. + */ + webhookId?: number; + /** + * Creates a new webhook to call when new onchain data is detected. Either `webhookId` or `webhookUrl` must be provided. */ webhookUrl?: string; /** diff --git a/src/server/routes/contract/read/read.ts b/src/server/routes/contract/read/read.ts index c84610df8..c8239d7c0 100644 --- a/src/server/routes/contract/read/read.ts +++ b/src/server/routes/contract/read/read.ts @@ -1,7 +1,11 @@ import { Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; -import { getContract } from "../../../../shared/utils/cache/get-contract"; +import type { AbiParameters } from "ox"; +import { readContract as readContractV5, resolveMethod } from "thirdweb"; +import { parseAbiParams } from "thirdweb/utils"; +import type { AbiFunction } from "thirdweb/utils"; +import { getContractV5 } from "../../../../shared/utils/cache/get-contractv5"; import { prettifyError } from "../../../../shared/utils/error"; import { createCustomError } from "../../../middleware/error"; import { @@ -12,6 +16,8 @@ import { partialRouteSchema, standardResponseSchema, } from "../../../schemas/shared-api-schemas"; +import { sanitizeFunctionName } from "../../../utils/abi"; +import { sanitizeAbi } from "../../../utils/abi"; import { getChainIdFromChain } from "../../../utils/chain"; import { bigNumberReplacer } from "../../../utils/convertor"; @@ -37,12 +43,13 @@ export async function readContract(fastify: FastifyInstance) { }, handler: async (request, reply) => { const { chain, contractAddress } = request.params; - const { functionName, args } = request.query; + const { functionName, args, abi } = request.query; const chainId = await getChainIdFromChain(chain); - const contract = await getContract({ + const contract = await getContractV5({ chainId, contractAddress, + abi: sanitizeAbi(abi), }); let parsedArgs: unknown[] | undefined; @@ -54,19 +61,33 @@ export async function readContract(fastify: FastifyInstance) { // fallback to string split } - parsedArgs ??= args?.split(",").map((arg) => { - if (arg === "true") { - return true; - } - if (arg === "false") { - return false; - } - return arg; - }); + parsedArgs ??= args?.split(","); + + // 3 possible ways to get function from abi: + // 1. functionName passed as solidity signature + // 2. functionName passed as function name + passed in ABI + // 3. functionName passed as function name + inferred ABI (fetched at encode time) + // this is all handled inside the `resolveMethod` function + let method: AbiFunction; + let params: Array; + try { + const functionNameOrSignature = sanitizeFunctionName(functionName); + method = await resolveMethod(functionNameOrSignature)(contract); + params = parseAbiParams( + method.inputs.map((i: AbiParameters.Parameter) => i.type), + parsedArgs ?? [], + ); + } catch (e) { + throw createCustomError( + prettifyError(e), + StatusCodes.BAD_REQUEST, + "BAD_REQUEST", + ); + } let returnData: unknown; try { - returnData = await contract.call(functionName, parsedArgs ?? []); + returnData = await readContractV5({ contract, method, params }); } catch (e) { throw createCustomError( prettifyError(e), diff --git a/src/server/schemas/contract/index.ts b/src/server/schemas/contract/index.ts index dea203627..8c525c3f5 100644 --- a/src/server/schemas/contract/index.ts +++ b/src/server/schemas/contract/index.ts @@ -1,27 +1,7 @@ -import { Type, type Static } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { AddressSchema } from "../address"; import type { contractSchemaTypes } from "../shared-api-schemas"; -/** - * Basic schema for all Request Query String - */ -export const readRequestQuerySchema = Type.Object({ - functionName: Type.String({ - description: "Name of the function to call on Contract", - examples: ["balanceOf"], - }), - args: Type.Optional( - Type.String({ - description: "Arguments for the function. Comma Separated", - examples: [""], - }), - ), -}); - -export interface readSchema extends contractSchemaTypes { - Querystring: Static; -} - const abiTypeSchema = Type.Object({ type: Type.Optional(Type.String()), name: Type.Optional(Type.String()), @@ -65,6 +45,31 @@ export const abiSchema = Type.Object({ export const abiArraySchema = Type.Array(abiSchema); export type AbiSchemaType = Static; +/** + * Basic schema for all Request Query String + */ +export const readRequestQuerySchema = Type.Object({ + functionName: Type.String({ + description: + "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", + examples: [ + "function balanceOf(address owner) view returns (uint256)", + "balanceOf", + ], + }), + args: Type.Optional( + Type.String({ + description: "Arguments for the function. Comma Separated", + examples: [""], + }), + ), + abi: Type.Optional(abiArraySchema), +}); + +export interface readSchema extends contractSchemaTypes { + Querystring: Static; +} + export const contractEventSchema = Type.Record(Type.String(), Type.Any()); export const rolesResponseSchema = Type.Object({ diff --git a/src/server/utils/convertor.ts b/src/server/utils/convertor.ts index 5f0334679..6348d387b 100644 --- a/src/server/utils/convertor.ts +++ b/src/server/utils/convertor.ts @@ -3,8 +3,8 @@ import { BigNumber } from "ethers"; const isHexBigNumber = (value: unknown) => { const isNonNullObject = typeof value === "object" && value !== null; const hasType = isNonNullObject && "type" in value; - return hasType && value.type === "BigNumber" && "hex" in value -} + return hasType && value.type === "BigNumber" && "hex" in value; +}; export const bigNumberReplacer = (value: unknown): unknown => { // if we find a BigNumber then make it into a string (since that is safe) if (BigNumber.isBigNumber(value) || isHexBigNumber(value)) { @@ -15,5 +15,9 @@ export const bigNumberReplacer = (value: unknown): unknown => { return value.map(bigNumberReplacer); } + if (typeof value === "bigint") { + return value.toString(); + } + return value; }; diff --git a/tests/e2e/tests/routes/read.test.ts b/tests/e2e/tests/routes/read.test.ts new file mode 100644 index 000000000..a0e52317e --- /dev/null +++ b/tests/e2e/tests/routes/read.test.ts @@ -0,0 +1,78 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import assert from "node:assert"; +import { ZERO_ADDRESS } from "thirdweb"; +import type { Address } from "thirdweb/utils"; +import { CONFIG } from "../../config"; +import type { setupEngine } from "../../utils/engine"; +import { pollTransactionStatus } from "../../utils/transactions"; +import { setup } from "../setup"; + +describe("readContractRoute", () => { + let engine: ReturnType; + let backendWallet: Address; + let tokenContractAddress: string; + + beforeAll(async () => { + const { engine: _engine, backendWallet: _backendWallet } = await setup(); + engine = _engine; + backendWallet = _backendWallet as Address; + + const res = await engine.deploy.deployToken( + CONFIG.CHAIN.id.toString(), + backendWallet, + { + contractMetadata: { + name: "test token", + platform_fee_basis_points: 0, + platform_fee_recipient: ZERO_ADDRESS, + symbol: "TT", + trusted_forwarders: [], + }, + }, + ); + + 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, + true, + ); + + expect(transactionStatus.minedAt).toBeDefined(); + assert(res.result.deployedAddress, "deployedAddress must be defined"); + tokenContractAddress = res.result.deployedAddress; + }); + + test("readContract with function name", async () => { + const res = await engine.contract.read( + "name", + CONFIG.CHAIN.id.toString(), + tokenContractAddress, + ); + + expect(res.result).toEqual("test token"); + }); + + test("readContract with function signature", async () => { + const res = await engine.contract.read( + "function symbol() public view returns (string memory)", + CONFIG.CHAIN.id.toString(), + tokenContractAddress, + ); + + expect(res.result).toEqual("TT"); + }); + + test("readContract with function signature", async () => { + const res = await engine.contract.read( + "function totalSupply() public view returns (uint256)", + CONFIG.CHAIN.id.toString(), + tokenContractAddress, + ); + + expect(res.result).toEqual("0"); + }); +});