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
44 changes: 44 additions & 0 deletions .changeset/warm-dodos-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"thirdweb": minor
---

Introducing Nebula API

You can now chat with Nebula and ask it to execute transactions with your wallet.

Ask questions about real time blockchain data.

```ts
import { Nebula } from "thirdweb/ai";

const response = await Nebula.chat({
client: TEST_CLIENT,
prompt:
"What's the symbol of this contract: 0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8",
context: {
chains: [sepolia],
},
});

console.log("chat response:", response.message);
```

Ask it to execute transactions with your wallet.

```ts
import { Nebula } from "thirdweb/ai";

const wallet = createWallet("io.metamask");
const account = await wallet.connect({ client });

const result = await Nebula.execute({
client,
prompt: "send 0.0001 ETH to vitalik.eth",
account,
context: {
chains: [sepolia],
},
});

console.log("executed transaction:", result.transactionHash);
```
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const tagsToGroup = {
"@modules": "Modules",
"@client": "Client",
"@account": "Account",
"@nebula": "Nebula",
} as const;

type TagKey = keyof typeof tagsToGroup;
Expand Down Expand Up @@ -81,6 +82,7 @@ const sidebarGroupOrder: TagKey[] = [
"@utils",
"@others",
"@account",
"@nebula",
];

function findTag(
Expand Down
78 changes: 24 additions & 54 deletions packages/thirdweb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,64 +123,34 @@
"import": "./dist/esm/exports/social.js",
"default": "./dist/cjs/exports/social.js"
},
"./ai": {
"types": "./dist/types/exports/ai.d.ts",
"import": "./dist/esm/exports/ai.js",
"default": "./dist/cjs/exports/ai.js"
},
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"adapters/*": [
"./dist/types/exports/adapters/*.d.ts"
],
"auth": [
"./dist/types/exports/auth.d.ts"
],
"chains": [
"./dist/types/exports/chains.d.ts"
],
"contract": [
"./dist/types/exports/contract.d.ts"
],
"deploys": [
"./dist/types/exports/deploys.d.ts"
],
"event": [
"./dist/types/exports/event.d.ts"
],
"extensions/*": [
"./dist/types/exports/extensions/*.d.ts"
],
"pay": [
"./dist/types/exports/pay.d.ts"
],
"react": [
"./dist/types/exports/react.d.ts"
],
"react-native": [
"./dist/types/exports/react-native.d.ts"
],
"rpc": [
"./dist/types/exports/rpc.d.ts"
],
"storage": [
"./dist/types/exports/storage.d.ts"
],
"transaction": [
"./dist/types/exports/transaction.d.ts"
],
"utils": [
"./dist/types/exports/utils.d.ts"
],
"wallets": [
"./dist/types/exports/wallets.d.ts"
],
"wallets/*": [
"./dist/types/exports/wallets/*.d.ts"
],
"modules": [
"./dist/types/exports/modules.d.ts"
],
"social": [
"./dist/types/exports/social.d.ts"
]
"adapters/*": ["./dist/types/exports/adapters/*.d.ts"],
"auth": ["./dist/types/exports/auth.d.ts"],
"chains": ["./dist/types/exports/chains.d.ts"],
"contract": ["./dist/types/exports/contract.d.ts"],
"deploys": ["./dist/types/exports/deploys.d.ts"],
"event": ["./dist/types/exports/event.d.ts"],
"extensions/*": ["./dist/types/exports/extensions/*.d.ts"],
"pay": ["./dist/types/exports/pay.d.ts"],
"react": ["./dist/types/exports/react.d.ts"],
"react-native": ["./dist/types/exports/react-native.d.ts"],
"rpc": ["./dist/types/exports/rpc.d.ts"],
"storage": ["./dist/types/exports/storage.d.ts"],
"transaction": ["./dist/types/exports/transaction.d.ts"],
"utils": ["./dist/types/exports/utils.d.ts"],
"wallets": ["./dist/types/exports/wallets.d.ts"],
"wallets/*": ["./dist/types/exports/wallets/*.d.ts"],
"modules": ["./dist/types/exports/modules.d.ts"],
"social": ["./dist/types/exports/social.d.ts"],
"ai": ["./dist/types/exports/ai.d.ts"]
}
},
"browser": {
Expand Down
1 change: 1 addition & 0 deletions packages/thirdweb/scripts/typedoc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const app = await Application.bootstrapWithPlugins({
"src/extensions/modules/**/index.ts",
"src/adapters/eip1193/index.ts",
"src/wallets/smart/presets/index.ts",
"src/ai/index.ts",
],
exclude: [
"src/exports/*.native.ts",
Expand Down
31 changes: 31 additions & 0 deletions packages/thirdweb/src/ai/chat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "../../test/src/test-clients.js";
import { TEST_ACCOUNT_A, TEST_ACCOUNT_B } from "../../test/src/test-wallets.js";
import { sepolia } from "../chains/chain-definitions/sepolia.js";
import * as Nebula from "./index.js";

describe.runIf(process.env.TW_SECRET_KEY)("chat", () => {
it("should respond with a message", async () => {
const response = await Nebula.chat({
client: TEST_CLIENT,
prompt: `What's the symbol of this contract: 0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8`,
context: {
chains: [sepolia],
},
});
expect(response.message).toContain("CAT");
});

it("should respond with a transaction", async () => {
const response = await Nebula.chat({
client: TEST_CLIENT,
prompt: `send 0.0001 ETH on sepolia to ${TEST_ACCOUNT_B.address}`,
account: TEST_ACCOUNT_A,
context: {
chains: [sepolia],
walletAddresses: [TEST_ACCOUNT_A.address],
},
});
expect(response.transactions.length).toBe(1);
});
});
26 changes: 26 additions & 0 deletions packages/thirdweb/src/ai/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type Input, type Output, nebulaFetch } from "./common.js";

/**
* Chat with Nebula.
*
* @param input - The input for the chat.
* @returns The chat response.
* @beta
* @nebula
*
* @example
* ```ts
* import { Nebula } from "thirdweb/ai";
*
* const response = await Nebula.chat({
* client: TEST_CLIENT,
* prompt: "What's the symbol of this contract: 0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8",
* context: {
* chains: [sepolia],
* },
* });
* ```
*/
export async function chat(input: Input): Promise<Output> {
return nebulaFetch("chat", input);
}
114 changes: 114 additions & 0 deletions packages/thirdweb/src/ai/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Chain } from "../chains/types.js";
import { getCachedChain } from "../chains/utils.js";
import type { ThirdwebClient } from "../client/client.js";
import {
type PreparedTransaction,
prepareTransaction,
} from "../transaction/prepare-transaction.js";
import type { Address } from "../utils/address.js";
import { toBigInt } from "../utils/bigint.js";
import type { Hex } from "../utils/encoding/hex.js";
import { getClientFetch } from "../utils/fetch.js";
import type { Account } from "../wallets/interfaces/wallet.js";

const NEBULA_API_URL = "https://nebula-api.thirdweb.com";

export type Input = {
client: ThirdwebClient;
prompt: string | string[];
account?: Account;
context?: {
chains?: Chain[];
walletAddresses?: string[];
contractAddresses?: string[];
};
sessionId?: string;
};

export type Output = {
message: string;
sessionId: string;
transactions: PreparedTransaction[];
};

type ApiResponse = {
message: string;
session_id: string;
actions?: {
type: "init" | "presence" | "sign_transaction";
source: string;
data: string;
}[];
};

export async function nebulaFetch(
mode: "execute" | "chat",
input: Input,
): Promise<Output> {
const fetch = getClientFetch(input.client);
const response = await fetch(`${NEBULA_API_URL}/${mode}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: input.prompt, // TODO: support array of messages
session_id: input.sessionId,
...(input.account
? {
execute_config: {
mode: "client",
signer_wallet_address: input.account.address,
},
}
: {}),
...(input.context
? {
context_filter: {
chain_ids:
input.context.chains?.map((c) => c.id.toString()) || [],
signer_wallet_address: input.context.walletAddresses || [],
contract_addresses: input.context.contractAddresses || [],
},
}
: {}),
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Nebula API error: ${error}`);
}
const data = (await response.json()) as ApiResponse;

// parse transactions if present
let transactions: PreparedTransaction[] = [];
if (data.actions) {
transactions = data.actions
.map((action) => {
// only parse sign_transaction actions
if (action.type === "sign_transaction") {
const tx = JSON.parse(action.data) as {
chainId: number;
to: Address | undefined;
value: Hex;
data: Hex;
};
return prepareTransaction({
chain: getCachedChain(tx.chainId),
client: input.client,
to: tx.to,
value: tx.value ? toBigInt(tx.value) : undefined,
data: tx.data,
});
}
return undefined;
})
.filter((tx) => tx !== undefined);
}

return {
message: data.message,
sessionId: data.session_id,
transactions,
};
}
43 changes: 43 additions & 0 deletions packages/thirdweb/src/ai/execute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "../../test/src/test-clients.js";
import { TEST_ACCOUNT_A, TEST_ACCOUNT_B } from "../../test/src/test-wallets.js";
import { sepolia } from "../chains/chain-definitions/sepolia.js";
import { getContract } from "../contract/contract.js";
import * as Nebula from "./index.js";

describe("execute", () => {
it("should execute a tx", async () => {
await expect(
Nebula.execute({
client: TEST_CLIENT,
prompt: `send 0.0001 ETH to ${TEST_ACCOUNT_B.address}`,
account: TEST_ACCOUNT_A,
context: {
chains: [sepolia],
walletAddresses: [TEST_ACCOUNT_A.address],
},
}),
).rejects.toThrow(/insufficient funds for gas/); // shows that the tx was sent
});

// TODO make this work reliably
it.skip("should execute a contract call", async () => {
const nftContract = getContract({
client: TEST_CLIENT,
chain: sepolia,
address: "0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8",
});

const response = await Nebula.execute({
client: TEST_CLIENT,
prompt: `approve 1 token of token id 0 to ${TEST_ACCOUNT_B.address} using the approve function`,
account: TEST_ACCOUNT_A,
context: {
chains: [nftContract.chain],
walletAddresses: [TEST_ACCOUNT_A.address],
contractAddresses: [nftContract.address],
},
});
expect(response.transactionHash).toBeDefined();
});
});
Loading
Loading