-
Notifications
You must be signed in to change notification settings - Fork 411
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Nebula AI chat and transaction execution API (#5948)
- Loading branch information
1 parent
d1c03b0
commit b10f306
Showing
12 changed files
with
458 additions
and
174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.