Skip to content

Commit 0355ab6

Browse files
Merge pull request #386 from bmgalego/client-farcaster
feat: Farcaster Client
2 parents 3fbad4b + 6db8eac commit 0355ab6

14 files changed

+1200
-0
lines changed

.env.example

+7
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ STARKNET_ADDRESS=
9191
STARKNET_PRIVATE_KEY=
9292
STARKNET_RPC_URL=
9393

94+
95+
# Farcaster
96+
FARCASTER_HUB_URL=
97+
FARCASTER_FID=
98+
FARCASTER_PRIVATE_KEY=
99+
94100
# Coinbase
95101
COINBASE_COMMERCE_KEY= # from coinbase developer portal
96102
COINBASE_API_KEY= # from coinbase developer portal
@@ -112,6 +118,7 @@ ZEROG_EVM_RPC=
112118
ZEROG_PRIVATE_KEY=
113119
ZEROG_FLOW_ADDRESS=
114120

121+
115122
# Coinbase Commerce
116123
COINBASE_COMMERCE_KEY=
117124

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@ai16z/client-farcaster",
3+
"version": "0.0.1",
4+
"main": "dist/index.js",
5+
"type": "module",
6+
"types": "dist/index.d.ts",
7+
"dependencies": {
8+
"@ai16z/eliza": "workspace:*",
9+
"@farcaster/hub-nodejs": "^0.12.7",
10+
"viem": "^2.21.47"
11+
},
12+
"devDependencies": {
13+
"tsup": "^8.3.5"
14+
},
15+
"scripts": {
16+
"build": "tsup --format esm --dts",
17+
"dev": "tsup --watch"
18+
},
19+
"peerDependencies": {}
20+
}
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { CastId, FarcasterNetwork, Signer } from "@farcaster/hub-nodejs";
2+
import { CastType, makeCastAdd } from "@farcaster/hub-nodejs";
3+
import type { FarcasterClient } from "./client";
4+
import type { Content, IAgentRuntime, Memory, UUID } from "@ai16z/eliza";
5+
import type { Cast, Profile } from "./types";
6+
import { createCastMemory } from "./memory";
7+
import { splitPostContent } from "./utils";
8+
9+
export async function sendCast({
10+
client,
11+
runtime,
12+
content,
13+
roomId,
14+
inReplyTo,
15+
signer,
16+
profile,
17+
}: {
18+
profile: Profile;
19+
client: FarcasterClient;
20+
runtime: IAgentRuntime;
21+
content: Content;
22+
roomId: UUID;
23+
signer: Signer;
24+
inReplyTo?: CastId;
25+
}): Promise<{ memory: Memory; cast: Cast }[]> {
26+
const chunks = splitPostContent(content.text);
27+
const sent: Cast[] = [];
28+
let parentCastId = inReplyTo;
29+
30+
for (const chunk of chunks) {
31+
const castAddMessageResult = await makeCastAdd(
32+
{
33+
text: chunk,
34+
embeds: [],
35+
embedsDeprecated: [],
36+
mentions: [],
37+
mentionsPositions: [],
38+
type: CastType.CAST, // TODO: check CastType.LONG_CAST
39+
parentCastId,
40+
},
41+
{
42+
fid: profile.fid,
43+
network: FarcasterNetwork.MAINNET,
44+
},
45+
signer
46+
);
47+
48+
if (castAddMessageResult.isErr()) {
49+
throw castAddMessageResult.error;
50+
}
51+
52+
await client.submitMessage(castAddMessageResult.value);
53+
54+
const cast = await client.loadCastFromMessage(
55+
castAddMessageResult.value
56+
);
57+
58+
sent.push(cast);
59+
60+
parentCastId = {
61+
fid: cast.profile.fid,
62+
hash: cast.message.hash,
63+
};
64+
65+
// TODO: check rate limiting
66+
// Wait a bit between tweets to avoid rate limiting issues
67+
// await wait(1000, 2000);
68+
}
69+
70+
return sent.map((cast) => ({
71+
cast,
72+
memory: createCastMemory({
73+
roomId,
74+
agentId: runtime.agentId,
75+
userId: runtime.agentId,
76+
cast,
77+
}),
78+
}));
79+
}
+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { IAgentRuntime } from "@ai16z/eliza";
2+
import {
3+
CastAddMessage,
4+
CastId,
5+
FidRequest,
6+
getInsecureHubRpcClient,
7+
getSSLHubRpcClient,
8+
HubRpcClient,
9+
isCastAddMessage,
10+
isUserDataAddMessage,
11+
Message,
12+
MessagesResponse,
13+
} from "@farcaster/hub-nodejs";
14+
import { Cast, Profile } from "./types";
15+
import { toHex } from "viem";
16+
import { populateMentions } from "./utils";
17+
18+
export class FarcasterClient {
19+
runtime: IAgentRuntime;
20+
farcaster: HubRpcClient;
21+
22+
cache: Map<string, any>;
23+
24+
constructor(opts: {
25+
runtime: IAgentRuntime;
26+
url: string;
27+
ssl: boolean;
28+
cache: Map<string, any>;
29+
}) {
30+
this.cache = opts.cache;
31+
this.runtime = opts.runtime;
32+
this.farcaster = opts.ssl
33+
? getSSLHubRpcClient(opts.url)
34+
: getInsecureHubRpcClient(opts.url);
35+
}
36+
37+
async submitMessage(cast: Message, retryTimes?: number): Promise<void> {
38+
const result = await this.farcaster.submitMessage(cast);
39+
40+
if (result.isErr()) {
41+
throw result.error;
42+
}
43+
}
44+
45+
async loadCastFromMessage(message: CastAddMessage): Promise<Cast> {
46+
const profileMap = {};
47+
48+
const profile = await this.getProfile(message.data.fid);
49+
50+
profileMap[message.data.fid] = profile;
51+
52+
for (const mentionId of message.data.castAddBody.mentions) {
53+
if (profileMap[mentionId]) continue;
54+
profileMap[mentionId] = await this.getProfile(mentionId);
55+
}
56+
57+
const text = populateMentions(
58+
message.data.castAddBody.text,
59+
message.data.castAddBody.mentions,
60+
message.data.castAddBody.mentionsPositions,
61+
profileMap
62+
);
63+
64+
return {
65+
id: toHex(message.hash),
66+
message,
67+
text,
68+
profile,
69+
};
70+
}
71+
72+
async getCast(castId: CastId): Promise<Message> {
73+
const castHash = toHex(castId.hash);
74+
75+
if (this.cache.has(`farcaster/cast/${castHash}`)) {
76+
return this.cache.get(`farcaster/cast/${castHash}`);
77+
}
78+
79+
const cast = await this.farcaster.getCast(castId);
80+
81+
if (cast.isErr()) {
82+
throw cast.error;
83+
}
84+
85+
this.cache.set(`farcaster/cast/${castHash}`, cast);
86+
87+
return cast.value;
88+
}
89+
90+
async getCastsByFid(request: FidRequest): Promise<MessagesResponse> {
91+
const cast = await this.farcaster.getCastsByFid(request);
92+
if (cast.isErr()) {
93+
throw cast.error;
94+
}
95+
96+
cast.value.messages.map((cast) => {
97+
this.cache.set(`farcaster/cast/${toHex(cast.hash)}`, cast);
98+
});
99+
100+
return cast.value;
101+
}
102+
103+
async getMentions(request: FidRequest): Promise<MessagesResponse> {
104+
const cast = await this.farcaster.getCastsByMention(request);
105+
if (cast.isErr()) {
106+
throw cast.error;
107+
}
108+
109+
cast.value.messages.map((cast) => {
110+
this.cache.set(`farcaster/cast/${toHex(cast.hash)}`, cast);
111+
});
112+
113+
return cast.value;
114+
}
115+
116+
async getProfile(fid: number): Promise<Profile> {
117+
if (this.cache.has(`farcaster/profile/${fid}`)) {
118+
return this.cache.get(`farcaster/profile/${fid}`) as Profile;
119+
}
120+
121+
const result = await this.farcaster.getUserDataByFid({
122+
fid: fid,
123+
reverse: true,
124+
});
125+
126+
if (result.isErr()) {
127+
throw result.error;
128+
}
129+
130+
const profile: Profile = {
131+
fid,
132+
name: "",
133+
signer: "0x",
134+
username: "",
135+
};
136+
137+
const userDataBodyType = {
138+
1: "pfp",
139+
2: "name",
140+
3: "bio",
141+
5: "url",
142+
6: "username",
143+
// 7: "location",
144+
// 8: "twitter",
145+
// 9: "github",
146+
} as const;
147+
148+
for (const message of result.value.messages) {
149+
if (isUserDataAddMessage(message)) {
150+
if (message.data.userDataBody.type in userDataBodyType) {
151+
const prop =
152+
userDataBodyType[message.data.userDataBody.type];
153+
profile[prop] = message.data.userDataBody.value;
154+
}
155+
}
156+
}
157+
158+
const [lastMessage] = result.value.messages;
159+
160+
if (lastMessage) {
161+
profile.signer = toHex(lastMessage.signer);
162+
}
163+
164+
this.cache.set(`farcaster/profile/${fid}`, profile);
165+
166+
return profile;
167+
}
168+
169+
async getTimeline(request: FidRequest): Promise<{
170+
timeline: Cast[];
171+
nextPageToken?: Uint8Array<ArrayBufferLike> | undefined;
172+
}> {
173+
const timeline: Cast[] = [];
174+
175+
const results = await this.getCastsByFid(request);
176+
177+
for (const message of results.messages) {
178+
if (isCastAddMessage(message)) {
179+
this.cache.set(
180+
`farcaster/cast/${toHex(message.hash)}`,
181+
message
182+
);
183+
184+
timeline.push(await this.loadCastFromMessage(message));
185+
}
186+
}
187+
188+
return {
189+
timeline,
190+
nextPageToken: results.nextPageToken,
191+
};
192+
}
193+
}
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Client, IAgentRuntime } from "@ai16z/eliza";
2+
import { Signer, NobleEd25519Signer } from "@farcaster/hub-nodejs";
3+
import { Hex, hexToBytes } from "viem";
4+
import { FarcasterClient } from "./client";
5+
import { FarcasterPostManager } from "./post";
6+
import { FarcasterInteractionManager } from "./interactions";
7+
8+
export class FarcasterAgentClient implements Client {
9+
client: FarcasterClient;
10+
posts: FarcasterPostManager;
11+
interactions: FarcasterInteractionManager;
12+
13+
private signer: Signer;
14+
15+
constructor(
16+
public runtime: IAgentRuntime,
17+
client?: FarcasterClient
18+
) {
19+
const cache = new Map<string, any>();
20+
21+
this.signer = new NobleEd25519Signer(
22+
hexToBytes(runtime.getSetting("FARCASTER_PRIVATE_KEY")! as Hex)
23+
);
24+
25+
this.client =
26+
client ??
27+
new FarcasterClient({
28+
runtime,
29+
ssl: true,
30+
url:
31+
runtime.getSetting("FARCASTER_HUB_URL") ??
32+
"hub.pinata.cloud",
33+
cache,
34+
});
35+
36+
this.posts = new FarcasterPostManager(
37+
this.client,
38+
this.runtime,
39+
this.signer,
40+
cache
41+
);
42+
43+
this.interactions = new FarcasterInteractionManager(
44+
this.client,
45+
this.runtime,
46+
this.signer,
47+
cache
48+
);
49+
}
50+
51+
async start() {
52+
await Promise.all([this.posts.start(), this.interactions.start()]);
53+
}
54+
55+
async stop() {
56+
await Promise.all([this.posts.stop(), this.interactions.stop()]);
57+
}
58+
}

0 commit comments

Comments
 (0)