Skip to content

Commit 7fbba2b

Browse files
mPaellayoussefea
authored andcommitted
plugin: tensor (goat-sdk#38)
* save * package * tweak * lint
1 parent 194c213 commit 7fbba2b

12 files changed

+277
-2
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@goat-sdk/plugin-tensor",
3+
"version": "0.0.1",
4+
"files": ["dist/**/*", "README.md", "package.json"],
5+
"scripts": {
6+
"build": "tsup",
7+
"clean": "rm -rf dist",
8+
"test": "vitest run --passWithNoTests"
9+
},
10+
"sideEffects": false,
11+
"main": "./dist/index.js",
12+
"module": "./dist/index.mjs",
13+
"types": "./dist/index.d.ts",
14+
"dependencies": {
15+
"@goat-sdk/core": "workspace:*",
16+
"@solana/web3.js": "catalog:",
17+
"zod": "catalog:"
18+
},
19+
"peerDependencies": {
20+
"@goat-sdk/core": "workspace:*"
21+
},
22+
"homepage": "https://ohmygoat.dev",
23+
"repository": {
24+
"type": "git",
25+
"url": "git+https://github.com/goat-sdk/goat.git"
26+
},
27+
"license": "MIT",
28+
"bugs": {
29+
"url": "https://github.com/goat-sdk/goat/issues"
30+
},
31+
"keywords": ["ai", "agents", "web3"]
32+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./plugin";
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { SolanaWalletClient } from "@goat-sdk/core";
2+
import type { Connection } from "@solana/web3.js";
3+
import type { z } from "zod";
4+
import type { getBuyListingTransactionResponseSchema } from "../parameters";
5+
import { deserializeTxResponseToInstructions } from "../utils/deserializeTxResponseToInstructions";
6+
import { getNftInfo } from "./getNftInfo";
7+
8+
export async function getBuyListingTransaction({
9+
walletClient,
10+
mintHash,
11+
apiKey,
12+
connection,
13+
}: { walletClient: SolanaWalletClient; mintHash: string; apiKey: string; connection: Connection }) {
14+
const nftInfo = await getNftInfo({ mintHash, apiKey });
15+
16+
const price = nftInfo.listing?.price;
17+
const owner = nftInfo.owner;
18+
19+
if (!price || !owner) {
20+
throw new Error(`No listing found for ${mintHash}`);
21+
}
22+
23+
const queryParams = new URLSearchParams({
24+
buyer: walletClient.getAddress(),
25+
mint: mintHash,
26+
owner,
27+
maxPrice: price,
28+
blockhash: "11111111111111111111111111111111",
29+
});
30+
31+
let data: z.infer<typeof getBuyListingTransactionResponseSchema>;
32+
try {
33+
const response = await fetch(`https://api.mainnet.tensordev.io/api/v1/tx/buy?${queryParams.toString()}`, {
34+
headers: {
35+
"Content-Type": "application/json",
36+
"x-tensor-api-key": apiKey,
37+
},
38+
});
39+
40+
data = (await response.json()) as z.infer<typeof getBuyListingTransactionResponseSchema>;
41+
console.log(data);
42+
} catch (error) {
43+
throw new Error(`Failed to get buy listing transaction: ${error}`);
44+
}
45+
46+
const { versionedTransaction, instructions } = await deserializeTxResponseToInstructions(connection, data);
47+
const lookupTableAddresses = versionedTransaction.message.addressTableLookups.map((lookup) => lookup.accountKey);
48+
return { versionedTransaction, instructions, lookupTableAddresses };
49+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { z } from "zod";
2+
import type { getNftInfoResponseSchema } from "../parameters";
3+
4+
export async function getNftInfo({ mintHash, apiKey }: { mintHash: string; apiKey: string }) {
5+
let nftInfo: z.infer<typeof getNftInfoResponseSchema>;
6+
try {
7+
const response = await fetch(`https://api.mainnet.tensordev.io/api/v1/mint?mints=${mintHash}`, {
8+
headers: {
9+
"Content-Type": "application/json",
10+
"x-tensor-api-key": apiKey,
11+
},
12+
});
13+
14+
nftInfo = (await response.json()) as z.infer<typeof getNftInfoResponseSchema>;
15+
} catch (error) {
16+
throw new Error(`Failed to get NFT info: ${error}`);
17+
}
18+
19+
return nftInfo[0];
20+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { z } from "zod";
2+
3+
export const getNftInfoParametersSchema = z.object({
4+
mintHash: z.string(),
5+
});
6+
7+
export const getNftInfoResponseSchema = z.array(
8+
z.object({
9+
onchainId: z.string(),
10+
attributes: z.array(z.any()),
11+
imageUri: z.string().optional(),
12+
lastSale: z
13+
.object({
14+
price: z.string(),
15+
priceUnit: z.string(),
16+
})
17+
.optional(),
18+
metadataUri: z.string().optional(),
19+
name: z.string().optional(),
20+
rarityRankTT: z.number().optional(),
21+
rarityRankTTStat: z.number().optional(),
22+
rarityRankHrtt: z.number().optional(),
23+
rarityRankStat: z.number().optional(),
24+
sellRoyaltyFeeBPS: z.number().optional(),
25+
tokenEdition: z.string().optional(),
26+
tokenStandard: z.string().optional(),
27+
hidden: z.boolean().optional(),
28+
compressed: z.boolean().optional(),
29+
verifiedCollection: z.string().optional(),
30+
owner: z.string().optional(),
31+
inscription: z.string().optional(),
32+
tokenProgram: z.string().optional(),
33+
metadataProgram: z.string().optional(),
34+
transferHookProgram: z.string().optional(),
35+
listingNormalizedPrice: z.string().optional(),
36+
hybridAmount: z.string().optional(),
37+
listing: z
38+
.object({
39+
price: z.string(),
40+
txId: z.string(),
41+
seller: z.string(),
42+
source: z.string(),
43+
})
44+
.optional(),
45+
slugDisplay: z.string().optional(),
46+
collId: z.string().optional(),
47+
collName: z.string().optional(),
48+
numMints: z.number().optional(),
49+
}),
50+
);
51+
52+
export const getBuyListingTransactionResponseSchema = z.object({
53+
txs: z.array(
54+
z.object({
55+
tx: z.object({
56+
type: z.string(),
57+
data: z.array(z.number()),
58+
}),
59+
txV0: z.object({
60+
type: z.string(),
61+
data: z.array(z.number()),
62+
}),
63+
}),
64+
),
65+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Plugin, SolanaWalletClient } from "@goat-sdk/core";
2+
import type { Connection } from "@solana/web3.js";
3+
import { getTools } from "./tools";
4+
5+
export function tensor(params: { connection: Connection; apiKey: string }): Plugin<SolanaWalletClient> {
6+
return {
7+
name: "tensor",
8+
supportsSmartWallets: () => false,
9+
supportsChain: (chain) => chain.type === "solana",
10+
getTools: async () => getTools(params),
11+
};
12+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { SolanaWalletClient } from "@goat-sdk/core";
2+
3+
import type { DeferredTool } from "@goat-sdk/core";
4+
import type { Connection } from "@solana/web3.js";
5+
import { getBuyListingTransaction } from "./methods/getBuyListingTransaction";
6+
import { getNftInfo } from "./methods/getNftInfo";
7+
import { getNftInfoParametersSchema } from "./parameters";
8+
9+
export function getTools({
10+
apiKey,
11+
connection,
12+
}: { apiKey: string; connection: Connection }): DeferredTool<SolanaWalletClient>[] {
13+
const getNftInfoTool: DeferredTool<SolanaWalletClient> = {
14+
name: "get_nft_info",
15+
description: "Gets information about a Solana NFT, from the Tensor API",
16+
parameters: getNftInfoParametersSchema,
17+
method: async (walletClient, parameters) => getNftInfo({ mintHash: parameters.mintHash, apiKey }),
18+
};
19+
20+
const buyListingTool: DeferredTool<SolanaWalletClient> = {
21+
name: "get_buy_listing_transaction",
22+
description: "Gets a transaction to buy an NFT from a listing from the Tensor API",
23+
parameters: getNftInfoParametersSchema,
24+
method: async (walletClient, parameters) =>
25+
getBuyListingTransaction({ walletClient, mintHash: parameters.mintHash, apiKey, connection }),
26+
};
27+
28+
return [getNftInfoTool, buyListingTool];
29+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type DecompileArgs, TransactionMessage } from "@solana/web3.js";
2+
3+
import type { Connection, VersionedTransaction } from "@solana/web3.js";
4+
5+
export async function decompileVersionedTransactionToInstructions(
6+
connection: Connection,
7+
versionedTransaction: VersionedTransaction,
8+
) {
9+
const lookupTableAddresses = versionedTransaction.message.addressTableLookups.map((lookup) => lookup.accountKey);
10+
const addressLookupTableAccounts = await Promise.all(
11+
lookupTableAddresses.map((address) =>
12+
connection.getAddressLookupTable(address).then((lookupTable) => lookupTable.value),
13+
),
14+
);
15+
const nonNullAddressLookupTableAccounts = addressLookupTableAccounts.filter((lookupTable) => lookupTable != null);
16+
const decompileArgs: DecompileArgs = {
17+
addressLookupTableAccounts: nonNullAddressLookupTableAccounts,
18+
};
19+
return TransactionMessage.decompile(versionedTransaction.message, decompileArgs).instructions;
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { type Connection, VersionedTransaction } from "@solana/web3.js";
2+
import type { z } from "zod";
3+
import type { getBuyListingTransactionResponseSchema } from "../parameters";
4+
import { decompileVersionedTransactionToInstructions } from "./decompileVersionedTransactionToInstructions";
5+
6+
export async function deserializeTxResponseToInstructions(
7+
connection: Connection,
8+
txResponse: z.infer<typeof getBuyListingTransactionResponseSchema>,
9+
) {
10+
const firstTransaction = txResponse.txs[0];
11+
if (firstTransaction == null) {
12+
throw new Error("No transaction in response");
13+
}
14+
const txV0 = firstTransaction.txV0;
15+
if (txV0 == null) {
16+
throw new Error("No txV0 in response");
17+
}
18+
const versionedTransaction = VersionedTransaction.deserialize(Buffer.from(txV0.data));
19+
const instructions = await decompileVersionedTransactionToInstructions(connection, versionedTransaction);
20+
return { versionedTransaction, instructions };
21+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "https://json.schemastore.org/tsconfig",
3+
"extends": "../../../tsconfig.base.json",
4+
"include": ["src/**/*"],
5+
"exclude": ["node_modules", "dist"]
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineConfig } from "tsup";
2+
import { treeShakableConfig } from "../../../tsup.config.base";
3+
4+
export default defineConfig({
5+
...treeShakableConfig,
6+
});

typescript/pnpm-lock.yaml

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)