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
5 changes: 5 additions & 0 deletions .changeset/great-flowers-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/api": patch
---

Init api wrapper
42 changes: 42 additions & 0 deletions packages/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# thirdweb API OpenAPI TypeScript wrapper

This package is a thin OpenAPI wrapper for the thirdweb API

## Configuration

```ts
import { configure } from "@thirdweb-dev/api";

// call this once at the startup of your application
configure({
secretKey: "<PROJECT_SECRET_KEY>",
});
```

## Example Usage

```ts
import { writeContract } from "@thirdweb-dev/api";

const result = await writeContract({
headers: {
"x-secret-key": "<PROJECT-SECRET-KEY>",
},
body: {
from: "0x1234567891234567891234567891234567891234",
chainId: "1",
calls: [
{
contractAddress: "0x1234567890123456789012345678901234567890",
method: "function transfer(address to, uint256 amount)",
params: [
"0x1234567890123456789012345678901234567890",
"1000000000000000000",
],
},
],
},
});
```

This package was autogenerated from the [thirdweb openAPI spec](https://api.thirdweb.com/reference) using [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
28 changes: 28 additions & 0 deletions packages/api/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"linter": {
"rules": {
"correctness": {
"useImportExtensions": {
"fix": "safe",
"level": "error",
"options": {
"forceJsExtensions": true
}
}
}
}
},
"overrides": [
{
"assist": {
"actions": {
"source": {
"useSortedKeys": "off"
}
}
},
"includes": ["package.json"]
}
]
}
6 changes: 6 additions & 0 deletions packages/api/openapi-ts.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from "@hey-api/openapi-ts";

export default defineConfig({
input: "https://api.thirdweb.com/openapi.json",
output: { format: "biome", lint: "biome", path: "src/client" },
});
61 changes: 61 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@thirdweb-dev/api",
"version": "0.0.1",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
},
"author": "thirdweb eng <[email protected]>",
"type": "module",
"main": "./dist/cjs/exports/thirdweb.js",
"module": "./dist/esm/exports/thirdweb.js",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/thirdweb-dev/js/issues"
},
"dependencies": {
"@hey-api/client-fetch": "0.10.0"
},
"engines": {
"node": ">=18"
},
"exports": {
".": {
"types": "./dist/types/exports/thirdweb.d.ts",
"import": "./dist/esm/exports/thirdweb.js",
"default": "./dist/cjs/exports/thirdweb.js"
},
"./package.json": "./package.json"
},
"files": [
"dist/*",
"src/*"
],
"devDependencies": {
"@biomejs/biome": "2.0.6",
"@hey-api/openapi-ts": "0.76.0",
"rimraf": "6.0.1",
"tslib": "^2.8.1"
},
"peerDependencies": {
"typescript": ">=5.0.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"scripts": {
"build": "pnpm clean && pnpm build:cjs && pnpm build:esm && pnpm build:types",
"build:cjs": "tsc --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
"build:esm": "tsc --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
"build:generate": "openapi-ts && pnpm format && pnpm fix",
"build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
"clean": "rimraf dist",
"fix": "biome check --write ./src",
"format": "biome format --write ./src",
"lint": "biome check ./src"
}
}
28 changes: 28 additions & 0 deletions packages/api/src/client/client.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// This file is auto-generated by @hey-api/openapi-ts

import {
type Config,
createClient,
createConfig,
type ClientOptions as DefaultClientOptions,
} from "./client/index.js";
import type { ClientOptions } from "./types.gen.js";

/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
(
override?: Config<DefaultClientOptions & T>,
) => Config<Required<DefaultClientOptions> & T>;

export const client = createClient(
createConfig<ClientOptions>({
baseUrl: "https://api.thirdweb.com",
}),
);
189 changes: 189 additions & 0 deletions packages/api/src/client/client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import type { Client, Config, RequestOptions } from "./types.js";
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from "./utils.js";

type ReqInit = Omit<RequestInit, "body" | "headers"> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};

export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);

const getConfig = (): Config => ({ ..._config });

const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};

const interceptors = createInterceptors<
Request,
Response,
unknown,
RequestOptions
>();

const request: Client["request"] = async (options) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
};

if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}

if (opts.body && opts.bodySerializer) {
opts.body = opts.bodySerializer(opts.body);
}

// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.body === "") {
opts.headers.delete("Content-Type");
}

const url = buildUrl(opts);
const requestInit: ReqInit = {
redirect: "follow",
...opts,
};

let request = new Request(url, requestInit);

for (const fn of interceptors.request._fns) {
if (fn) {
request = await fn(request, opts);
}
}

// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response = await _fetch(request);
Comment on lines +72 to +75
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid non-null assertion operator for better runtime safety.

The non-null assertion on line 74 could lead to runtime errors if the fallback chain fails. Consider adding a runtime check instead.

-		// fetch must be assigned here, otherwise it would throw the error:
-		// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
-		const _fetch = opts.fetch!;
-		let response = await _fetch(request);
+		// fetch must be assigned here, otherwise it would throw the error:
+		// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
+		const _fetch = opts.fetch;
+		if (!_fetch) {
+			throw new Error('No fetch implementation available');
+		}
+		let response = await _fetch(request);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response = await _fetch(request);
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch;
if (!_fetch) {
throw new Error('No fetch implementation available');
}
let response = await _fetch(request);
🤖 Prompt for AI Agents
In packages/api/src/client/client/client.ts around lines 72 to 75, avoid using
the non-null assertion operator on opts.fetch as it can cause runtime errors if
fetch is undefined. Instead, add a runtime check to verify that opts.fetch is
defined before assigning it to _fetch. If it is undefined, handle the error
gracefully, for example by throwing a descriptive error or providing a fallback
implementation, to ensure safer execution.


for (const fn of interceptors.response._fns) {
if (fn) {
response = await fn(response, request, opts);
}
}

const result = {
request,
response,
};

if (response.ok) {
if (
response.status === 204 ||
response.headers.get("Content-Length") === "0"
) {
return opts.responseStyle === "data"
? {}
: {
data: {},
...result,
};
}

const parseAs =
(opts.parseAs === "auto"
? getParseAs(response.headers.get("Content-Type"))
: opts.parseAs) ?? "json";

let data: any;
switch (parseAs) {
case "arrayBuffer":
case "blob":
case "formData":
case "json":
case "text":
data = await response[parseAs]();
break;
case "stream":
return opts.responseStyle === "data"
? response.body
: {
data: response.body,
...result,
};
}

if (parseAs === "json") {
if (opts.responseValidator) {
await opts.responseValidator(data);
}

if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}

return opts.responseStyle === "data"
? data
: {
data,
...result,
};
}

let error = await response.text();

try {
error = JSON.parse(error);
} catch {
// noop
}
Comment on lines +144 to +148
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle JSON parsing errors explicitly.

The empty catch block silently ignores JSON parsing errors. Consider logging or handling the error explicitly.

 		try {
 			error = JSON.parse(error);
-		} catch {
-			// noop
+		} catch (e) {
+			// Keep the original text error if JSON parsing fails
+			// This is expected for non-JSON error responses
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
error = JSON.parse(error);
} catch {
// noop
}
try {
error = JSON.parse(error);
} catch (e) {
// Keep the original text error if JSON parsing fails
// This is expected for non-JSON error responses
}
🤖 Prompt for AI Agents
In packages/api/src/client/client/client.ts around lines 144 to 148, the empty
catch block for JSON.parse error handling silently ignores parsing errors.
Modify the catch block to explicitly handle or log the JSON parsing error, such
as logging the error message or taking appropriate action, instead of leaving it
empty.


let finalError = error;

for (const fn of interceptors.error._fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}

finalError = finalError || ({} as string);

if (opts.throwOnError) {
throw finalError;
}

// TODO: we probably want to return error and improve types
return opts.responseStyle === "data"
? undefined
: {
error: finalError,
...result,
};
};

return {
buildUrl,
connect: (options) => request({ ...options, method: "CONNECT" }),
delete: (options) => request({ ...options, method: "DELETE" }),
get: (options) => request({ ...options, method: "GET" }),
getConfig,
head: (options) => request({ ...options, method: "HEAD" }),
interceptors,
options: (options) => request({ ...options, method: "OPTIONS" }),
patch: (options) => request({ ...options, method: "PATCH" }),
post: (options) => request({ ...options, method: "POST" }),
put: (options) => request({ ...options, method: "PUT" }),
request,
setConfig,
trace: (options) => request({ ...options, method: "TRACE" }),
};
};
22 changes: 22 additions & 0 deletions packages/api/src/client/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type { Auth } from "../core/auth.js";
export type { QuerySerializerOptions } from "../core/bodySerializer.js";
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from "../core/bodySerializer.js";
export { buildClientParams } from "../core/params.js";
export { createClient } from "./client.js";
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
OptionsLegacyParser,
RequestOptions,
RequestResult,
ResponseStyle,
TDataShape,
} from "./types.js";
export { createConfig, mergeHeaders } from "./utils.js";
Loading
Loading