Skip to content

Commit

Permalink
feat: return custody and verified addresses (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
horsefacts authored Jan 26, 2024
1 parent 8303558 commit cfd30e4
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 37 deletions.
8 changes: 8 additions & 0 deletions .changeset/itchy-stingrays-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@farcaster/auth-client": patch
"@farcaster/auth-kit": patch
"client-test": patch
"@farcaster/auth-relay": patch
---

Return custody address and verifications
2 changes: 2 additions & 0 deletions apps/relay/fastify.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { FastifyRequest as DefaultFastifyRequest } from "fastify";
import { RelaySession } from "./src/handlers";
import { ChannelStore } from "./src/channels";
import { AddressService } from "./src/addresses";

declare module "fastify" {
export interface FastifyRequest extends DefaultFastifyRequest {
channelToken: string;
channels: ChannelStore<RelaySession>;
addresses: AddressService;
}
}
6 changes: 4 additions & 2 deletions apps/relay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@
"node": ">=18"
},
"dependencies": {
"@farcaster/core": "^0.13.4",
"@fastify/cors": "^8.4.2",
"@fastify/rate-limit": "^9.0.1",
"axios": "^1.6.2",
"dotenv": "^16.3.1",
"ethers": "^6.0.8",
"fastify": "^4.24.3",
"ioredis": "^5.3.2",
"neverthrow": "^6.1.0",
"siwe": "^2.1.4",
"ethers": "^6.0.8"
"viem": "^2.5.0"
},
"devDependencies": {
"axios": "^1.6.2",
"jest": "^29.7.0"
}
}
76 changes: 76 additions & 0 deletions apps/relay/src/addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import axios, { AxiosInstance } from "axios";
import { ResultAsync, err, ok } from "neverthrow";
import { createPublicClient, http } from "viem";
import type { PublicClient, HttpTransport, Hex } from "viem";
import { optimism } from "viem/chains";
import { ID_REGISTRY_ADDRESS, idRegistryABI } from "@farcaster/core";

import { RelayAsyncResult, RelayError } from "./errors";
import { HUB_URL, OPTIMISM_RPC_URL } from "./env";

interface VerificationAddEthAddressBody {
address: Hex;
}

interface VerificationMessageData {
verificationAddEthAddressBody: VerificationAddEthAddressBody;
}

interface VerificationMessage {
data: VerificationMessageData;
}

interface VerificationsAPIResponse {
messages: VerificationMessage[];
}

export class AddressService {
private http: AxiosInstance;
private publicClient: PublicClient<HttpTransport, typeof optimism>;

constructor() {
this.http = axios.create();
this.publicClient = createPublicClient({
chain: optimism,
transport: http(OPTIMISM_RPC_URL),
});
}

async getAddresses(fid?: number): RelayAsyncResult<{ custody: Hex; verifications: Hex[] }> {
const custody = await this.custody(fid);
if (custody.isErr()) {
return err(custody.error);
}
const verifications = await this.verifications(fid);
if (verifications.isErr()) {
return err(verifications.error);
}
return ok({
custody: custody.value,
verifications: verifications.value,
});
}

async custody(fid?: number): RelayAsyncResult<Hex> {
return ResultAsync.fromPromise(
this.publicClient.readContract({
address: ID_REGISTRY_ADDRESS,
abi: idRegistryABI,
functionName: "custodyOf",
args: [BigInt(fid ?? 0)],
}),
(error) => {
return new RelayError("unknown", error as Error);
},
);
}

async verifications(fid?: number): RelayAsyncResult<Hex[]> {
const url = `${HUB_URL}/v1/verificationsByFid?fid=${fid}`;
return ResultAsync.fromPromise(this.http.get<VerificationsAPIResponse>(url), (error) => {
return new RelayError("unknown", error as Error);
}).andThen((res) => {
return ok(res.data.messages.map((message) => message.data.verificationAddEthAddressBody.address));
});
}
}
4 changes: 4 additions & 0 deletions apps/relay/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ export const RELAY_SERVER_HOST = process.env["RELAY_SERVER_HOST"] || "localhost"
export const URL_BASE =
process.env["URL_BASE"] || process.env["CONNECT_URI_BASE"] || "https://warpcast.com/~/sign-in-with-farcaster";

export const HUB_URL = process.env["HUB_URL"] || "https://nemes.farcaster.xyz:2281";

export const OPTIMISM_RPC_URL = process.env["OPTIMISM_RPC_URL"] || "https://mainnet.optimism.io";

export const AUTH_KEY = process.env["AUTH_KEY"];
47 changes: 28 additions & 19 deletions apps/relay/src/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FastifyError, FastifyReply, FastifyRequest } from "fastify";
import type { Hex } from "viem";
import { AUTH_KEY, URL_BASE } from "./env";
import { Logger } from "logger";
import { Logger } from "./logger";
import { generateNonce } from "siwe";

export type CreateChannelRequest = {
Expand Down Expand Up @@ -34,6 +35,8 @@ export type RelaySession = {
bio?: string;
displayName?: string;
pfpUrl?: string;
verifications?: Hex[];
custody?: Hex;
};

const constructUrl = (channelToken: string, nonce: string, extraParams: CreateChannelRequest): string => {
Expand Down Expand Up @@ -72,27 +75,33 @@ export async function authenticate(request: FastifyRequest<{ Body: AuthenticateR
const channelToken = request.channelToken;
const { message, signature, fid, username, displayName, bio, pfpUrl } = request.body;

const channel = await request.channels.read(channelToken);
if (channel.isOk()) {
const update = await request.channels.update(channelToken, {
...channel.value,
state: "completed",
message,
signature,
fid,
username,
displayName,
bio,
pfpUrl,
});
if (update.isOk()) {
reply.code(201).send(update.value);
const addrs = await request.addresses.getAddresses(fid);
if (addrs.isOk()) {
const channel = await request.channels.read(channelToken);
if (channel.isOk()) {
const update = await request.channels.update(channelToken, {
...channel.value,
state: "completed",
message,
signature,
fid,
username,
displayName,
bio,
pfpUrl,
...addrs.value,
});
if (update.isOk()) {
reply.code(201).send(update.value);
} else {
reply.code(500).send({ error: update.error.message });
}
} else {
reply.code(500).send({ error: update.error.message });
if (channel.error.errCode === "not_found") reply.code(401).send({ error: "Unauthorized " });
reply.code(500).send({ error: channel.error.message });
}
} else {
if (channel.error.errCode === "not_found") reply.code(401).send({ error: "Unauthorized " });
reply.code(500).send({ error: channel.error.message });
reply.code(500).send({ error: addrs.error.message });
}
}

Expand Down
5 changes: 4 additions & 1 deletion apps/relay/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,10 @@ describe("relay server", () => {
throw new Error("close error");
});
await http.post(getFullUrl("/v1/channel/authenticate"), authenticateParams, {
headers: { Authorization: `Bearer ${channelToken}` },
headers: {
Authorization: `Bearer ${channelToken}`,
"X-Farcaster-Auth-Relay-Key": "some-shared-secret",
},
});
const response = await http.get(getFullUrl("/v1/channel/status"), {
headers: { Authorization: `Bearer ${channelToken}` },
Expand Down
5 changes: 5 additions & 0 deletions apps/relay/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import rateLimit from "@fastify/rate-limit";
import { err, ok } from "neverthrow";
import { createChannelRequestSchema, authenticateRequestSchema } from "./schemas";
import { ChannelStore } from "./channels";
import { AddressService } from "./addresses";
import {
AuthenticateRequest,
CreateChannelRequest,
Expand All @@ -27,18 +28,22 @@ interface RelayServerConfig {
export class RelayServer {
app = fastify();
channels: ChannelStore<RelaySession>;
addresses: AddressService;

constructor({ redisUrl, ttl, corsOrigin }: RelayServerConfig) {
this.channels = new ChannelStore<RelaySession>({
redisUrl,
ttl,
});
this.addresses = new AddressService();
this.app.setErrorHandler(handleError.bind(this, log));

this.app.register(cors, { origin: [corsOrigin] });
this.app.decorateRequest("channels");
this.app.decorateRequest("addresses");
this.app.addHook("onRequest", async (request) => {
request.channels = this.channels;
request.addresses = this.addresses;
});
this.app.get("/healthcheck", async (_request, reply) => reply.send({ status: "OK" }));
this.app.addSchema(createChannelRequestSchema);
Expand Down
3 changes: 3 additions & 0 deletions packages/auth-client/src/actions/app/status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AsyncUnwrapped, unwrap } from "../../errors";
import { Client } from "../../clients/createClient";
import { get, HttpResponse } from "../../clients/transports/http";
import type { Hex } from "viem";

export interface StatusArgs {
channelToken: string;
Expand All @@ -19,6 +20,8 @@ export interface StatusAPIResponse {
bio?: string;
displayName?: string;
pfpUrl?: string;
verifications?: Hex[];
custody?: Hex;
}

const path = "channel/status";
Expand Down
14 changes: 1 addition & 13 deletions packages/auth-client/src/actions/app/watchStatus.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AsyncUnwrapped, unwrap } from "../../errors";
import { Client } from "../../clients/createClient";
import { poll, HttpResponse } from "../../clients/transports/http";
import { StatusAPIResponse } from "./status";

export interface WatchStatusArgs {
channelToken: string;
Expand All @@ -11,19 +12,6 @@ export interface WatchStatusArgs {

export type WatchStatusResponse = AsyncUnwrapped<HttpResponse<StatusAPIResponse>>;

interface StatusAPIResponse {
state: "pending" | "completed";
nonce: string;
url: string;
message?: string;
signature?: `0x${string}`;
fid?: number;
username?: string;
bio?: string;
displayName?: string;
pfpUrl?: string;
}

const path = "channel/status";

const voidCallback = () => {};
Expand Down
4 changes: 3 additions & 1 deletion test/client/src/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,15 @@ describe("clients", () => {
// 4. App client polls channel status
const {
response: completedStatusResponse,
data: { state: completedState, message, signature, nonce },
data: { state: completedState, message, signature, nonce, verifications, custody },
} = await appClient.status({ channelToken });
expect(completedStatusResponse.status).toBe(200);
expect(completedState).toBe("completed");
expect(message).toBe(messageString);
expect(signature).toBe(sig);
expect(nonce).toBe(nonce);
expect(custody).toBe("0x8773442740C17C9d0F0B87022c722F9a136206eD");
expect(verifications).toStrictEqual(["0x86924c37a93734e8611eb081238928a9d18a63c0"]);

// 5. Channel is now closed
const { response: channelClosedResponse } = await appClient.status({
Expand Down
Loading

0 comments on commit cfd30e4

Please sign in to comment.