diff --git a/.changeset/orange-crabs-double.md b/.changeset/orange-crabs-double.md new file mode 100644 index 000000000..cfd036da8 --- /dev/null +++ b/.changeset/orange-crabs-double.md @@ -0,0 +1,8 @@ +--- +"@farcaster/hub-nodejs": patch +"@farcaster/hub-web": patch +"@farcaster/core": patch +"@farcaster/hubble": patch +--- + +FIP-8 contract verifications diff --git a/apps/hubble/src/hubble.ts b/apps/hubble/src/hubble.ts index 5d0b7c06d..6c8fd0c16 100644 --- a/apps/hubble/src/hubble.ts +++ b/apps/hubble/src/hubble.ts @@ -55,7 +55,7 @@ import { L2EventsProvider, OptimismConstants } from "./eth/l2EventsProvider.js"; import { prettyPrintTable } from "./profile/profile.js"; import packageJson from "./package.json" assert { type: "json" }; import { createPublicClient, fallback, http } from "viem"; -import { mainnet } from "viem/chains"; +import { mainnet, optimism } from "viem/chains"; import { AddrInfo } from "@chainsafe/libp2p-gossipsub/types"; import { CheckIncomingPortsJobScheduler } from "./storage/jobs/checkIncomingPortsJob.js"; import { NetworkConfig, applyNetworkConfig, fetchNetworkConfig } from "./network/utils/networkConfig.js"; @@ -344,13 +344,20 @@ export class Hub implements HubInterface { lockTimeout: options.commitLockTimeout, }); + const opMainnetRpcUrls = options.l2RpcUrl.split(","); + const opTransports = opMainnetRpcUrls.map((url) => http(url, { retryCount: 2 })); + const opClient = createPublicClient({ + chain: optimism, + transport: fallback(opTransports, { rank: options.rankRpcs ?? false }), + }); + const ethMainnetRpcUrls = options.ethMainnetRpcUrl.split(","); const transports = ethMainnetRpcUrls.map((url) => http(url, { retryCount: 2 })); const mainnetClient = createPublicClient({ chain: mainnet, transport: fallback(transports, { rank: options.rankRpcs ?? false }), }); - this.engine = new Engine(this.rocksDB, options.network, eventHandler, mainnetClient); + this.engine = new Engine(this.rocksDB, options.network, eventHandler, mainnetClient, opClient); const profileSync = options.profileSync ?? false; this.syncEngine = new SyncEngine( diff --git a/apps/hubble/src/storage/engine/index.test.ts b/apps/hubble/src/storage/engine/index.test.ts index b468f4276..dc56f636b 100644 --- a/apps/hubble/src/storage/engine/index.test.ts +++ b/apps/hubble/src/storage/engine/index.test.ts @@ -204,9 +204,70 @@ describe("mergeMessage", () => { ); const result = await engine.mergeMessage(testnetVerificationAdd); // Signature will not match because we're attempting to recover the address based on the wrong network - expect(result).toEqual( - err(new HubError("bad_request.validation_failure", "ethSignature does not match address")), - ); + expect(result).toEqual(err(new HubError("bad_request.validation_failure", "invalid ethSignature"))); + }); + + describe("validateOrRevokeMessage", () => { + let mergedMessage: Message; + let verifications: VerificationAddEthAddressMessage[] = []; + + const getVerifications = async () => { + const verificationsResult = await engine.getVerificationsByFid(fid); + if (verificationsResult.isOk()) { + verifications = verificationsResult.value.messages; + } + }; + + const createVerification = async () => { + return await Factories.VerificationAddEthAddressMessage.create( + { + data: { + fid, + verificationAddEthAddressBody: Factories.VerificationAddEthAddressBody.build({ + chainId: 1, + verificationType: 1, + }), + }, + }, + { transient: { signer } }, + ); + }; + + beforeEach(async () => { + jest.replaceProperty(publicClient.chain, "id", 1); + jest.spyOn(publicClient, "verifyTypedData").mockResolvedValue(true); + mergedMessage = await createVerification(); + const result = await engine.mergeMessage(mergedMessage); + expect(result.isOk()).toBeTruthy(); + await getVerifications(); + expect(verifications.length).toBe(1); + }); + + afterEach(async () => { + jest.restoreAllMocks(); + }); + + test("revokes a contract verification when signature is no longer valid", async () => { + jest.spyOn(publicClient, "verifyTypedData").mockResolvedValue(false); + const result = await engine.validateOrRevokeMessage(mergedMessage); + expect(result.isOk()).toBeTruthy(); + + const verificationsResult = await engine.getVerificationsByFid(fid); + expect(verificationsResult.isOk()).toBeTruthy(); + + await getVerifications(); + expect(verifications.length).toBe(0); + }); + + test("does not revoke contract verifications when RPC call fails", async () => { + jest.spyOn(publicClient, "verifyTypedData").mockRejectedValue(new Error("verify failed")); + const result = await engine.validateOrRevokeMessage(mergedMessage); + expect(result._unsafeUnwrapErr().errCode).toEqual("unavailable.network_failure"); + expect(result._unsafeUnwrapErr().message).toMatch("verify failed"); + + await getVerifications(); + expect(verifications.length).toBe(1); + }); }); }); diff --git a/apps/hubble/src/storage/engine/index.ts b/apps/hubble/src/storage/engine/index.ts index 7d76ef20e..9b253c5fa 100644 --- a/apps/hubble/src/storage/engine/index.ts +++ b/apps/hubble/src/storage/engine/index.ts @@ -63,6 +63,7 @@ import { isRateLimitedByKey, consumeRateLimitByKey, getRateLimiterForTotalMessag import { nativeValidationMethods } from "../../rustfunctions.js"; import { RateLimiterAbstract } from "rate-limiter-flexible"; import { TypedEmitter } from "tiny-typed-emitter"; +import { ValidationWorkerData } from "./validation.worker.js"; const log = logger.child({ component: "Engine", @@ -79,6 +80,7 @@ class Engine extends TypedEmitter { private _db: RocksDB; private _network: FarcasterNetwork; private _publicClient: PublicClient | undefined; + private _l2PublicClient: PublicClient | undefined; private _linkStore: LinkStore; private _reactionStore: ReactionStore; @@ -98,11 +100,18 @@ class Engine extends TypedEmitter { private _totalPruneSize: number; - constructor(db: RocksDB, network: FarcasterNetwork, eventHandler?: StoreEventHandler, publicClient?: PublicClient) { + constructor( + db: RocksDB, + network: FarcasterNetwork, + eventHandler?: StoreEventHandler, + publicClient?: PublicClient, + l2PublicClient?: PublicClient, + ) { super(); this._db = db; this._network = network; this._publicClient = publicClient; + this._l2PublicClient = l2PublicClient; this.eventHandler = eventHandler ?? new StoreEventHandler(db); @@ -142,7 +151,7 @@ class Engine extends TypedEmitter { const workerPath = "./build/storage/engine/validation.worker.js"; try { if (fs.existsSync(workerPath)) { - this._validationWorker = new Worker(workerPath); + this._validationWorker = new Worker(workerPath, { workerData: this.getWorkerData() }); log.info({ workerPath }, "created validation worker thread"); this._validationWorker.on("message", (data) => { @@ -398,6 +407,10 @@ class Engine extends TypedEmitter { const isValid = await this.validateMessage(message); if (isValid.isErr() && message.data) { + if (isValid.error.errCode === "unavailable.network_failure") { + return err(isValid.error); + } + const setPostfix = typeToSetPostfix(message.data.type); switch (setPostfix) { @@ -417,11 +430,7 @@ class Engine extends TypedEmitter { return this._verificationStore.revoke(message); } case UserPostfix.UsernameProofMessage: { - if (isValid.error.errCode === "unavailable.network_failure") { - return err(isValid.error); - } else { - return this._usernameProofStore.revoke(message); - } + return this._usernameProofStore.revoke(message); } default: { return err(new HubError("bad_request.invalid_param", "invalid message type")); @@ -972,7 +981,7 @@ class Engine extends TypedEmitter { worker.postMessage({ id, message }); }); } else { - return validations.validateMessage(message, nativeValidationMethods); + return validations.validateMessage(message, nativeValidationMethods, this.getPublicClients()); } } @@ -1093,6 +1102,37 @@ class Engine extends TypedEmitter { return ok(undefined); } + + private getPublicClients(): { [chainId: number]: PublicClient } { + const clients: { [chainId: number]: PublicClient } = {}; + if (this._publicClient?.chain) { + clients[this._publicClient.chain.id] = this._publicClient; + } + if (this._l2PublicClient?.chain) { + clients[this._l2PublicClient.chain.id] = this._l2PublicClient; + } + return clients; + } + + private getWorkerData(): ValidationWorkerData { + const l1Transports: string[] = []; + this._publicClient?.transport["transports"].forEach((transport: { value?: { url: string } }) => { + if (transport?.value) { + l1Transports.push(transport.value["url"]); + } + }); + const l2Transports: string[] = []; + this._l2PublicClient?.transport["transports"].forEach((transport: { value?: { url: string } }) => { + if (transport?.value) { + l2Transports.push(transport.value["url"]); + } + }); + + return { + ethMainnetRpcUrl: l1Transports.join(","), + l2RpcUrl: l2Transports.join(","), + }; + } } export default Engine; diff --git a/apps/hubble/src/storage/engine/validation.worker.ts b/apps/hubble/src/storage/engine/validation.worker.ts index a3083ffe7..7a1d37d0d 100644 --- a/apps/hubble/src/storage/engine/validation.worker.ts +++ b/apps/hubble/src/storage/engine/validation.worker.ts @@ -1,12 +1,39 @@ import { validations } from "@farcaster/hub-nodejs"; import { nativeValidationMethods } from "../../rustfunctions.js"; -import { parentPort } from "worker_threads"; +import { workerData, parentPort } from "worker_threads"; +import { http, createPublicClient, fallback } from "viem"; +import { optimism, mainnet } from "viem/chains"; + +export interface ValidationWorkerData { + l2RpcUrl: string; + ethMainnetRpcUrl: string; +} + +const config = workerData as ValidationWorkerData; +const opMainnetRpcUrls = config.l2RpcUrl.split(","); +const opTransports = opMainnetRpcUrls.map((url) => http(url, { retryCount: 2 })); +const opClient = createPublicClient({ + chain: optimism, + transport: fallback(opTransports, { rank: false }), +}); + +const ethMainnetRpcUrls = config.ethMainnetRpcUrl.split(","); +const transports = ethMainnetRpcUrls.map((url) => http(url, { retryCount: 2 })); +const mainnetClient = createPublicClient({ + chain: mainnet, + transport: fallback(transports, { rank: false }), +}); + +const publicClients = { + [optimism.id]: opClient, + [mainnet.id]: mainnetClient, +}; // Wait for messages from the main thread and validate them, posting the result back parentPort?.on("message", (data) => { (async () => { const { id, message } = data; - const result = await validations.validateMessage(message, nativeValidationMethods); + const result = await validations.validateMessage(message, nativeValidationMethods, publicClients); if (result.isErr()) { parentPort?.postMessage({ id, errCode: result.error.errCode, errMessage: result.error.message }); diff --git a/apps/hubble/src/test/utils.ts b/apps/hubble/src/test/utils.ts index d6ba52ede..4d134af92 100644 --- a/apps/hubble/src/test/utils.ts +++ b/apps/hubble/src/test/utils.ts @@ -17,7 +17,7 @@ export const anvilChain = { http: [localHttpUrl], }, }, -} as const satisfies Chain; +} satisfies Chain; const provider = { // biome-ignore lint/suspicious/noExplicitAny: legacy code, avoid using ignore for new code diff --git a/packages/core/src/crypto/eip712.ts b/packages/core/src/crypto/eip712.ts index b64842be1..dd915e8ba 100644 --- a/packages/core/src/crypto/eip712.ts +++ b/packages/core/src/crypto/eip712.ts @@ -3,6 +3,8 @@ import { ResultAsync } from "neverthrow"; import { HubAsyncResult, HubError } from "../errors"; import { VerificationEthAddressClaim } from "../verifications"; import { UserNameProofClaim } from "../userNameProof"; +import { PublicClients, defaultPublicClients } from "../eth/clients"; +import { CHAIN_IDS } from "../eth/chains"; export const EIP_712_FARCASTER_DOMAIN = { name: "Farcaster Verify Ethereum Address", @@ -30,6 +32,8 @@ export const EIP_712_FARCASTER_VERIFICATION_CLAIM = [ }, ] as const; +export const EIP_712_FARCASTER_VERIFICATION_CLAIM_CHAIN_IDS = [...CHAIN_IDS, 0]; + export const EIP_712_FARCASTER_MESSAGE_DATA = [ { name: "hash", @@ -50,11 +54,18 @@ export const EIP_712_USERNAME_PROOF = [ { name: "owner", type: "address" }, ] as const; -export const verifyVerificationEthAddressClaimSignature = async ( +const verifyVerificationClaimEOASignature = async ( claim: VerificationEthAddressClaim, signature: Uint8Array, address: Uint8Array, + chainId: number, ): HubAsyncResult => { + if (chainId !== 0) { + return ResultAsync.fromPromise( + Promise.reject(), + () => new HubError("bad_request.invalid_param", "Invalid chain ID"), + ); + } const valid = await ResultAsync.fromPromise( verifyTypedData({ address: bytesToHex(address), @@ -66,10 +77,64 @@ export const verifyVerificationEthAddressClaimSignature = async ( }), (e) => new HubError("unknown", e as Error), ); + return valid; +}; +const verifyVerificationClaimContractSignature = async ( + claim: VerificationEthAddressClaim, + signature: Uint8Array, + address: Uint8Array, + chainId: number, + publicClients: PublicClients = defaultPublicClients, +): HubAsyncResult => { + const client = publicClients[chainId]; + if (!client) { + return ResultAsync.fromPromise( + Promise.reject(), + () => new HubError("bad_request.invalid_param", `RPC client not provided for chainId ${chainId}`), + ); + } + const valid = await ResultAsync.fromPromise( + client.verifyTypedData({ + address: bytesToHex(address), + domain: { ...EIP_712_FARCASTER_DOMAIN, chainId }, + types: { VerificationClaim: EIP_712_FARCASTER_VERIFICATION_CLAIM }, + primaryType: "VerificationClaim", + message: claim, + signature, + }), + (e) => new HubError("unavailable.network_failure", e as Error), + ); return valid; }; +export const verifyVerificationEthAddressClaimSignature = async ( + claim: VerificationEthAddressClaim, + signature: Uint8Array, + address: Uint8Array, + verificationType: number, + chainId: number, + publicClients: PublicClients = defaultPublicClients, +): HubAsyncResult => { + if (!EIP_712_FARCASTER_VERIFICATION_CLAIM_CHAIN_IDS.includes(chainId)) { + return ResultAsync.fromPromise( + Promise.reject(), + () => new HubError("bad_request.invalid_param", "Invalid chain ID"), + ); + } + + if (verificationType === 0) { + return verifyVerificationClaimEOASignature(claim, signature, address, chainId); + } else if (verificationType === 1) { + return verifyVerificationClaimContractSignature(claim, signature, address, chainId, publicClients); + } else { + return ResultAsync.fromPromise( + Promise.reject(), + () => new HubError("bad_request.invalid_param", "Invalid verification type"), + ); + } +}; + export const verifyUserNameProofClaim = async ( nameProof: UserNameProofClaim, signature: Uint8Array, diff --git a/packages/core/src/eth/chains.ts b/packages/core/src/eth/chains.ts new file mode 100644 index 000000000..e6c1fc01c --- /dev/null +++ b/packages/core/src/eth/chains.ts @@ -0,0 +1,3 @@ +import { mainnet, goerli, optimism, optimismGoerli } from "viem/chains"; + +export const CHAIN_IDS = [mainnet.id, goerli.id, optimism.id, optimismGoerli.id] as const; diff --git a/packages/core/src/eth/clients.ts b/packages/core/src/eth/clients.ts new file mode 100644 index 000000000..bf3212227 --- /dev/null +++ b/packages/core/src/eth/clients.ts @@ -0,0 +1,33 @@ +import { PublicClient, createPublicClient, http } from "viem"; +import { mainnet, goerli, optimism, optimismGoerli } from "viem/chains"; + +export type PublicClients = { + [chainId: number]: PublicClient; +}; + +export const defaultL1PublicClient: PublicClient = createPublicClient({ + chain: mainnet, + transport: http(), +}); + +export const defaultL2PublicClient: PublicClient = createPublicClient({ + chain: optimism, + transport: http(), +}); + +export const defaultL1PublicTestClient: PublicClient = createPublicClient({ + chain: goerli, + transport: http(), +}); + +export const defaultL2PublicTestClient: PublicClient = createPublicClient({ + chain: optimismGoerli, + transport: http(), +}); + +export const defaultPublicClients: PublicClients = { + [mainnet.id]: defaultL1PublicClient, + [optimism.id]: defaultL2PublicClient, + [goerli.id]: defaultL1PublicTestClient, + [optimismGoerli.id]: defaultL2PublicTestClient, +}; diff --git a/packages/core/src/eth/index.ts b/packages/core/src/eth/index.ts new file mode 100644 index 000000000..ebe666515 --- /dev/null +++ b/packages/core/src/eth/index.ts @@ -0,0 +1,2 @@ +export * as chains from "./chains"; +export * as clients from "./clients"; diff --git a/packages/core/src/factories.ts b/packages/core/src/factories.ts index 87e7997da..725696988 100644 --- a/packages/core/src/factories.ts +++ b/packages/core/src/factories.ts @@ -386,12 +386,19 @@ const VerificationEthAddressClaimFactory = Factory.define(({ onCreate, transientParams }) => { onCreate(async (body) => { const ethSigner = transientParams.signer ?? Eip712SignerFactory.build(); - body.address = (await ethSigner.getSignerKey())._unsafeUnwrap(); + if (!transientParams.contractSignature) { + body.address = (await ethSigner.getSignerKey())._unsafeUnwrap(); + } if (body.ethSignature.length === 0) { // Generate address and signature @@ -404,7 +411,7 @@ const VerificationAddEthAddressBodyFactory = Factory.define< blockHash: blockHash.isOk() ? blockHash.value : "0x", address: bytesToHexString(body.address)._unsafeUnwrap(), }); - body.ethSignature = (await ethSigner.signVerificationEthAddressClaim(claim))._unsafeUnwrap(); + body.ethSignature = (await ethSigner.signVerificationEthAddressClaim(claim, body.chainId))._unsafeUnwrap(); } return body; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ba07222cd..be05cfc6e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,6 +3,7 @@ export * from "./builders"; export * from "./bytes"; export * from "./crypto"; export * from "./errors"; +export * from "./eth"; export * from "./factories"; export * from "./signers"; export * from "./time"; diff --git a/packages/core/src/protobufs/generated/message.ts b/packages/core/src/protobufs/generated/message.ts index 5567b0250..0eb603d51 100644 --- a/packages/core/src/protobufs/generated/message.ts +++ b/packages/core/src/protobufs/generated/message.ts @@ -431,6 +431,10 @@ export interface VerificationAddEthAddressBody { ethSignature: Uint8Array; /** Hash of the latest Ethereum block when the signature was produced */ blockHash: Uint8Array; + /** Type of verification. 0 = EOA, 1 = contract */ + verificationType: number; + /** 0 for EOA verifications, 1 or 10 for contract verifications */ + chainId: number; } /** Removes a Verification of any type */ @@ -1382,7 +1386,13 @@ export const ReactionBody = { }; function createBaseVerificationAddEthAddressBody(): VerificationAddEthAddressBody { - return { address: new Uint8Array(), ethSignature: new Uint8Array(), blockHash: new Uint8Array() }; + return { + address: new Uint8Array(), + ethSignature: new Uint8Array(), + blockHash: new Uint8Array(), + verificationType: 0, + chainId: 0, + }; } export const VerificationAddEthAddressBody = { @@ -1396,6 +1406,12 @@ export const VerificationAddEthAddressBody = { if (message.blockHash.length !== 0) { writer.uint32(26).bytes(message.blockHash); } + if (message.verificationType !== 0) { + writer.uint32(32).uint32(message.verificationType); + } + if (message.chainId !== 0) { + writer.uint32(40).uint32(message.chainId); + } return writer; }, @@ -1427,6 +1443,20 @@ export const VerificationAddEthAddressBody = { message.blockHash = reader.bytes(); continue; + case 4: + if (tag != 32) { + break; + } + + message.verificationType = reader.uint32(); + continue; + case 5: + if (tag != 40) { + break; + } + + message.chainId = reader.uint32(); + continue; } if ((tag & 7) == 4 || tag == 0) { break; @@ -1441,6 +1471,8 @@ export const VerificationAddEthAddressBody = { address: isSet(object.address) ? bytesFromBase64(object.address) : new Uint8Array(), ethSignature: isSet(object.ethSignature) ? bytesFromBase64(object.ethSignature) : new Uint8Array(), blockHash: isSet(object.blockHash) ? bytesFromBase64(object.blockHash) : new Uint8Array(), + verificationType: isSet(object.verificationType) ? Number(object.verificationType) : 0, + chainId: isSet(object.chainId) ? Number(object.chainId) : 0, }; }, @@ -1454,6 +1486,8 @@ export const VerificationAddEthAddressBody = { )); message.blockHash !== undefined && (obj.blockHash = base64FromBytes(message.blockHash !== undefined ? message.blockHash : new Uint8Array())); + message.verificationType !== undefined && (obj.verificationType = Math.round(message.verificationType)); + message.chainId !== undefined && (obj.chainId = Math.round(message.chainId)); return obj; }, @@ -1468,6 +1502,8 @@ export const VerificationAddEthAddressBody = { message.address = object.address ?? new Uint8Array(); message.ethSignature = object.ethSignature ?? new Uint8Array(); message.blockHash = object.blockHash ?? new Uint8Array(); + message.verificationType = object.verificationType ?? 0; + message.chainId = object.chainId ?? 0; return message; }, }; diff --git a/packages/core/src/signers/eip712Signer.ts b/packages/core/src/signers/eip712Signer.ts index ddca7c890..6b31fb5b3 100644 --- a/packages/core/src/signers/eip712Signer.ts +++ b/packages/core/src/signers/eip712Signer.ts @@ -16,6 +16,9 @@ export abstract class Eip712Signer implements Signer { */ public abstract getSignerKey(): HubAsyncResult; public abstract signMessageHash(hash: Uint8Array): HubAsyncResult; - public abstract signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult; + public abstract signVerificationEthAddressClaim( + claim: VerificationEthAddressClaim, + chainId?: number, + ): HubAsyncResult; public abstract signUserNameProofClaim(claim: UserNameProofClaim): HubAsyncResult; } diff --git a/packages/core/src/signers/ethersEip712Signer.ts b/packages/core/src/signers/ethersEip712Signer.ts index 998f87d29..bc91c4db8 100644 --- a/packages/core/src/signers/ethersEip712Signer.ts +++ b/packages/core/src/signers/ethersEip712Signer.ts @@ -43,13 +43,13 @@ export class EthersEip712Signer extends Eip712Signer { return hexSignature.andThen((hex) => hexStringToBytes(hex)); } - public async signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult { + public async signVerificationEthAddressClaim( + claim: VerificationEthAddressClaim, + chainId = 0, + ): HubAsyncResult { + const domain = chainId === 0 ? EIP_712_FARCASTER_DOMAIN : { ...EIP_712_FARCASTER_DOMAIN, chainId }; const hexSignature = await ResultAsync.fromPromise( - this._ethersSigner.signTypedData( - EIP_712_FARCASTER_DOMAIN, - { VerificationClaim: [...EIP_712_FARCASTER_VERIFICATION_CLAIM] }, - claim, - ), + this._ethersSigner.signTypedData(domain, { VerificationClaim: [...EIP_712_FARCASTER_VERIFICATION_CLAIM] }, claim), (e) => new HubError("bad_request.invalid_param", e as Error), ); diff --git a/packages/core/src/signers/ethersV5Eip712Signer.ts b/packages/core/src/signers/ethersV5Eip712Signer.ts index dc4748b19..8eba34ca9 100644 --- a/packages/core/src/signers/ethersV5Eip712Signer.ts +++ b/packages/core/src/signers/ethersV5Eip712Signer.ts @@ -39,10 +39,14 @@ export class EthersV5Eip712Signer extends Eip712Signer { return hexSignature.andThen((hex) => hexStringToBytes(hex)); } - public async signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult { + public async signVerificationEthAddressClaim( + claim: VerificationEthAddressClaim, + chainId = 0, + ): HubAsyncResult { + const domain = chainId === 0 ? eip712.EIP_712_FARCASTER_DOMAIN : { ...eip712.EIP_712_FARCASTER_DOMAIN, chainId }; const hexSignature = await ResultAsync.fromPromise( this._typedDataSigner._signTypedData( - eip712.EIP_712_FARCASTER_DOMAIN, + domain, { VerificationClaim: [...eip712.EIP_712_FARCASTER_VERIFICATION_CLAIM] }, claim, ), diff --git a/packages/core/src/signers/testUtils.ts b/packages/core/src/signers/testUtils.ts index c783ff219..2a5957c46 100644 --- a/packages/core/src/signers/testUtils.ts +++ b/packages/core/src/signers/testUtils.ts @@ -44,7 +44,7 @@ export const testEip712Signer = async (signer: Eip712Signer) => { }); test("succeeds", async () => { - const valid = await eip712.verifyVerificationEthAddressClaimSignature(claim, signature, signerKey); + const valid = await eip712.verifyVerificationEthAddressClaimSignature(claim, signature, signerKey, 0, 0); expect(valid).toEqual(ok(true)); }); diff --git a/packages/core/src/signers/viemLocalEip712Signer.ts b/packages/core/src/signers/viemLocalEip712Signer.ts index ff5b69506..fc689b37b 100644 --- a/packages/core/src/signers/viemLocalEip712Signer.ts +++ b/packages/core/src/signers/viemLocalEip712Signer.ts @@ -44,10 +44,14 @@ export class ViemLocalEip712Signer extends Eip712Signer { return hexSignature.andThen((hex) => hexStringToBytes(hex)); } - public async signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult { + public async signVerificationEthAddressClaim( + claim: VerificationEthAddressClaim, + chainId = 0, + ): HubAsyncResult { + const domain = chainId === 0 ? EIP_712_FARCASTER_DOMAIN : { ...EIP_712_FARCASTER_DOMAIN, chainId }; const hexSignature = await ResultAsync.fromPromise( this._viemLocalAccount.signTypedData({ - domain: EIP_712_FARCASTER_DOMAIN, + domain, types: { VerificationClaim: EIP_712_FARCASTER_VERIFICATION_CLAIM }, primaryType: "VerificationClaim", message: claim, diff --git a/packages/core/src/validations.test.ts b/packages/core/src/validations.test.ts index 28b987d28..196ed3198 100644 --- a/packages/core/src/validations.test.ts +++ b/packages/core/src/validations.test.ts @@ -8,6 +8,9 @@ import { fromFarcasterTime, getFarcasterTime } from "./time"; import * as validations from "./validations"; import { makeVerificationEthAddressClaim } from "./verifications"; import { UserDataType, UserNameType } from "@farcaster/hub-nodejs"; +import { defaultL1PublicClient } from "./eth/clients"; +import { optimism } from "viem/chains"; +import { jest } from "@jest/globals"; const signer = Factories.Ed25519Signer.build(); const ethSigner = Factories.Eip712Signer.build(); @@ -17,6 +20,10 @@ beforeAll(async () => { ethSignerKey = (await ethSigner.getSignerKey())._unsafeUnwrap(); }); +afterEach(async () => { + jest.restoreAllMocks(); +}); + describe("validateFid", () => { test("succeeds", () => { const fid = Factories.Fid.build(); @@ -646,7 +653,7 @@ describe("validateVerificationAddEthAddressSignature", () => { const fid = Factories.Fid.build(); const network = Factories.FarcasterNetwork.build(); - test("succeeds", async () => { + test("succeeds for eoas", async () => { const body = await Factories.VerificationAddEthAddressBody.create({}, { transient: { fid, network } }); const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); expect(result.isOk()).toBeTruthy(); @@ -660,6 +667,81 @@ describe("validateVerificationAddEthAddressSignature", () => { expect(result).toEqual(err(new HubError("unknown", "Cannot convert 0x to a BigInt"))); }); + test("fails with invalid verificationType", async () => { + const body = await Factories.VerificationAddEthAddressBody.create({ + ethSignature: Factories.Bytes.build({}, { transient: { length: 1 } }), + verificationType: 2, + }); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); + expect(result).toEqual(err(new HubError("bad_request.invalid_param", "Invalid verification type"))); + }); + + test("fails with invalid chainId", async () => { + const body = await Factories.VerificationAddEthAddressBody.create({ + ethSignature: Factories.Bytes.build({}, { transient: { length: 1 } }), + chainId: 7, + verificationType: 1, + }); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); + expect(result).toEqual(err(new HubError("bad_request.invalid_param", "Invalid chain ID"))); + }); + + test("fails if client not provided for chainId", async () => { + const body = await Factories.VerificationAddEthAddressBody.create({ + ethSignature: Factories.Bytes.build({}, { transient: { length: 1 } }), + chainId: 1, + verificationType: 1, + }); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network, {}); + expect(result).toEqual(err(new HubError("bad_request.invalid_param", "RPC client not provided for chainId 1"))); + }); + + test("fails if ethSignature is > 256 bytes", async () => { + const body = await Factories.VerificationAddEthAddressBody.create({ + ethSignature: Factories.Bytes.build({}, { transient: { length: 257 } }), + }); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network, {}); + expect(result).toEqual(err(new HubError("bad_request.validation_failure", "ethSignature > 256 bytes"))); + }); + + test("succeeds for contract signatures", async () => { + jest.spyOn(defaultL1PublicClient, "verifyTypedData").mockImplementation(() => { + return Promise.resolve(true); + }); + const chainId = 1; + const publicClients = { + [chainId]: defaultL1PublicClient, + }; + const body = await Factories.VerificationAddEthAddressBody.create( + { + chainId, + verificationType: 1, + }, + { transient: { fid, network, contractSignature: true } }, + ); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network, publicClients); + expect(result.isOk()).toBeTruthy(); + }); + + test("fails with invalid contract signature", async () => { + jest.spyOn(defaultL1PublicClient, "verifyTypedData").mockImplementation(() => { + return Promise.resolve(false); + }); + const chainId = 1; + const publicClients = { + [chainId]: defaultL1PublicClient, + }; + const body = await Factories.VerificationAddEthAddressBody.create( + { + chainId, + verificationType: 1, + }, + { transient: { fid, network, contractSignature: true } }, + ); + const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network, publicClients); + expect(result).toEqual(err(new HubError("bad_request.validation_failure", "invalid ethSignature"))); + }); + test("fails with eth signature from different address", async () => { const blockHash = Factories.BlockHash.build(); const claim = makeVerificationEthAddressClaim(fid, ethSignerKey, network, blockHash)._unsafeUnwrap(); @@ -671,7 +753,7 @@ describe("validateVerificationAddEthAddressSignature", () => { address: Factories.EthAddress.build(), }); const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network); - expect(result).toEqual(err(new HubError("bad_request.validation_failure", "ethSignature does not match address"))); + expect(result).toEqual(err(new HubError("bad_request.validation_failure", "invalid ethSignature"))); }); }); diff --git a/packages/core/src/validations.ts b/packages/core/src/validations.ts index be2825662..f8bce5617 100644 --- a/packages/core/src/validations.ts +++ b/packages/core/src/validations.ts @@ -8,6 +8,7 @@ import { getFarcasterTime, toFarcasterTime } from "./time"; import { makeVerificationEthAddressClaim } from "./verifications"; import { UserNameType } from "./protobufs"; import { normalize } from "viem/ens"; +import { defaultPublicClients, PublicClients } from "./eth/clients"; /** Number of seconds (10 minutes) that is appropriate for clock skew */ export const ALLOWED_CLOCK_SKEW_SECONDS = 10 * 60; @@ -113,13 +114,14 @@ export const validateEd25519PublicKey = (publicKey?: Uint8Array | null): HubResu export const validateMessage = async ( message: protobufs.Message, validationMethods: ValidationMethods = pureJSValidationMethods, + publicClients: PublicClients = defaultPublicClients, ): HubAsyncResult => { // 1. Check the message data const data = message.data; if (!data) { return err(new HubError("bad_request.validation_failure", "data is missing")); } - const validData = await validateMessageData(data); + const validData = await validateMessageData(data, publicClients); if (validData.isErr()) { return err(validData.error); } @@ -166,7 +168,10 @@ export const validateMessage = async ( return ok(message); }; -export const validateMessageData = async (data: T): HubAsyncResult => { +export const validateMessageData = async ( + data: T, + publicClients: PublicClients = defaultPublicClients, +): HubAsyncResult => { // 1. Validate fid const validFid = validateFid(data.fid); if (validFid.isErr()) { @@ -226,6 +231,7 @@ export const validateMessageData = async (data: data.verificationAddEthAddressBody, validFid.value, validNetwork.value, + publicClients, ); } else if (validType.value === protobufs.MessageType.VERIFICATION_REMOVE && !!data.verificationRemoveBody) { bodyResult = validateVerificationRemoveBody(data.verificationRemoveBody); @@ -246,7 +252,12 @@ export const validateVerificationAddEthAddressSignature = async ( body: protobufs.VerificationAddEthAddressBody, fid: number, network: protobufs.FarcasterNetwork, + publicClients: PublicClients = defaultPublicClients, ): HubAsyncResult => { + if (body.ethSignature.length > 256) { + return err(new HubError("bad_request.validation_failure", "ethSignature > 256 bytes")); + } + const reconstructedClaim = makeVerificationEthAddressClaim(fid, body.address, network, body.blockHash); if (reconstructedClaim.isErr()) { return err(reconstructedClaim.error); @@ -256,6 +267,9 @@ export const validateVerificationAddEthAddressSignature = async ( reconstructedClaim.value, body.ethSignature, body.address, + body.verificationType, + body.chainId, + publicClients, ); if (verificationResult.isErr()) { @@ -263,7 +277,7 @@ export const validateVerificationAddEthAddressSignature = async ( } if (!verificationResult.value) { - return err(new HubError("bad_request.validation_failure", "ethSignature does not match address")); + return err(new HubError("bad_request.validation_failure", "invalid ethSignature")); } return ok(body.ethSignature); @@ -501,6 +515,7 @@ export const validateVerificationAddEthAddressBody = async ( body: protobufs.VerificationAddEthAddressBody, fid: number, network: protobufs.FarcasterNetwork, + publicClients: PublicClients, ): HubAsyncResult => { const validAddress = validateEthAddress(body.address); if (validAddress.isErr()) { @@ -512,7 +527,7 @@ export const validateVerificationAddEthAddressBody = async ( return err(validBlockHash.error); } - const validSignature = await validateVerificationAddEthAddressSignature(body, fid, network); + const validSignature = await validateVerificationAddEthAddressSignature(body, fid, network, publicClients); if (validSignature.isErr()) { return err(validSignature.error); } diff --git a/packages/hub-nodejs/src/generated/message.ts b/packages/hub-nodejs/src/generated/message.ts index 5567b0250..0eb603d51 100644 --- a/packages/hub-nodejs/src/generated/message.ts +++ b/packages/hub-nodejs/src/generated/message.ts @@ -431,6 +431,10 @@ export interface VerificationAddEthAddressBody { ethSignature: Uint8Array; /** Hash of the latest Ethereum block when the signature was produced */ blockHash: Uint8Array; + /** Type of verification. 0 = EOA, 1 = contract */ + verificationType: number; + /** 0 for EOA verifications, 1 or 10 for contract verifications */ + chainId: number; } /** Removes a Verification of any type */ @@ -1382,7 +1386,13 @@ export const ReactionBody = { }; function createBaseVerificationAddEthAddressBody(): VerificationAddEthAddressBody { - return { address: new Uint8Array(), ethSignature: new Uint8Array(), blockHash: new Uint8Array() }; + return { + address: new Uint8Array(), + ethSignature: new Uint8Array(), + blockHash: new Uint8Array(), + verificationType: 0, + chainId: 0, + }; } export const VerificationAddEthAddressBody = { @@ -1396,6 +1406,12 @@ export const VerificationAddEthAddressBody = { if (message.blockHash.length !== 0) { writer.uint32(26).bytes(message.blockHash); } + if (message.verificationType !== 0) { + writer.uint32(32).uint32(message.verificationType); + } + if (message.chainId !== 0) { + writer.uint32(40).uint32(message.chainId); + } return writer; }, @@ -1427,6 +1443,20 @@ export const VerificationAddEthAddressBody = { message.blockHash = reader.bytes(); continue; + case 4: + if (tag != 32) { + break; + } + + message.verificationType = reader.uint32(); + continue; + case 5: + if (tag != 40) { + break; + } + + message.chainId = reader.uint32(); + continue; } if ((tag & 7) == 4 || tag == 0) { break; @@ -1441,6 +1471,8 @@ export const VerificationAddEthAddressBody = { address: isSet(object.address) ? bytesFromBase64(object.address) : new Uint8Array(), ethSignature: isSet(object.ethSignature) ? bytesFromBase64(object.ethSignature) : new Uint8Array(), blockHash: isSet(object.blockHash) ? bytesFromBase64(object.blockHash) : new Uint8Array(), + verificationType: isSet(object.verificationType) ? Number(object.verificationType) : 0, + chainId: isSet(object.chainId) ? Number(object.chainId) : 0, }; }, @@ -1454,6 +1486,8 @@ export const VerificationAddEthAddressBody = { )); message.blockHash !== undefined && (obj.blockHash = base64FromBytes(message.blockHash !== undefined ? message.blockHash : new Uint8Array())); + message.verificationType !== undefined && (obj.verificationType = Math.round(message.verificationType)); + message.chainId !== undefined && (obj.chainId = Math.round(message.chainId)); return obj; }, @@ -1468,6 +1502,8 @@ export const VerificationAddEthAddressBody = { message.address = object.address ?? new Uint8Array(); message.ethSignature = object.ethSignature ?? new Uint8Array(); message.blockHash = object.blockHash ?? new Uint8Array(); + message.verificationType = object.verificationType ?? 0; + message.chainId = object.chainId ?? 0; return message; }, }; diff --git a/packages/hub-web/src/generated/message.ts b/packages/hub-web/src/generated/message.ts index 5567b0250..0eb603d51 100644 --- a/packages/hub-web/src/generated/message.ts +++ b/packages/hub-web/src/generated/message.ts @@ -431,6 +431,10 @@ export interface VerificationAddEthAddressBody { ethSignature: Uint8Array; /** Hash of the latest Ethereum block when the signature was produced */ blockHash: Uint8Array; + /** Type of verification. 0 = EOA, 1 = contract */ + verificationType: number; + /** 0 for EOA verifications, 1 or 10 for contract verifications */ + chainId: number; } /** Removes a Verification of any type */ @@ -1382,7 +1386,13 @@ export const ReactionBody = { }; function createBaseVerificationAddEthAddressBody(): VerificationAddEthAddressBody { - return { address: new Uint8Array(), ethSignature: new Uint8Array(), blockHash: new Uint8Array() }; + return { + address: new Uint8Array(), + ethSignature: new Uint8Array(), + blockHash: new Uint8Array(), + verificationType: 0, + chainId: 0, + }; } export const VerificationAddEthAddressBody = { @@ -1396,6 +1406,12 @@ export const VerificationAddEthAddressBody = { if (message.blockHash.length !== 0) { writer.uint32(26).bytes(message.blockHash); } + if (message.verificationType !== 0) { + writer.uint32(32).uint32(message.verificationType); + } + if (message.chainId !== 0) { + writer.uint32(40).uint32(message.chainId); + } return writer; }, @@ -1427,6 +1443,20 @@ export const VerificationAddEthAddressBody = { message.blockHash = reader.bytes(); continue; + case 4: + if (tag != 32) { + break; + } + + message.verificationType = reader.uint32(); + continue; + case 5: + if (tag != 40) { + break; + } + + message.chainId = reader.uint32(); + continue; } if ((tag & 7) == 4 || tag == 0) { break; @@ -1441,6 +1471,8 @@ export const VerificationAddEthAddressBody = { address: isSet(object.address) ? bytesFromBase64(object.address) : new Uint8Array(), ethSignature: isSet(object.ethSignature) ? bytesFromBase64(object.ethSignature) : new Uint8Array(), blockHash: isSet(object.blockHash) ? bytesFromBase64(object.blockHash) : new Uint8Array(), + verificationType: isSet(object.verificationType) ? Number(object.verificationType) : 0, + chainId: isSet(object.chainId) ? Number(object.chainId) : 0, }; }, @@ -1454,6 +1486,8 @@ export const VerificationAddEthAddressBody = { )); message.blockHash !== undefined && (obj.blockHash = base64FromBytes(message.blockHash !== undefined ? message.blockHash : new Uint8Array())); + message.verificationType !== undefined && (obj.verificationType = Math.round(message.verificationType)); + message.chainId !== undefined && (obj.chainId = Math.round(message.chainId)); return obj; }, @@ -1468,6 +1502,8 @@ export const VerificationAddEthAddressBody = { message.address = object.address ?? new Uint8Array(); message.ethSignature = object.ethSignature ?? new Uint8Array(); message.blockHash = object.blockHash ?? new Uint8Array(); + message.verificationType = object.verificationType ?? 0; + message.chainId = object.chainId ?? 0; return message; }, }; diff --git a/packages/hub-web/src/generated/rpc.ts b/packages/hub-web/src/generated/rpc.ts index 0335734ed..0257eae07 100644 --- a/packages/hub-web/src/generated/rpc.ts +++ b/packages/hub-web/src/generated/rpc.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { grpc } from "@improbable-eng/grpc-web"; +import grpcWeb from "@improbable-eng/grpc-web"; import { BrowserHeaders } from "browser-headers"; import { Observable } from "rxjs"; import { share } from "rxjs/operators"; @@ -43,75 +43,75 @@ import { UserNameProof } from "./username_proof"; export interface HubService { /** Submit Methods */ - submitMessage(request: DeepPartial, metadata?: grpc.Metadata): Promise; + submitMessage(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** Event Methods */ - subscribe(request: DeepPartial, metadata?: grpc.Metadata): Observable; - getEvent(request: DeepPartial, metadata?: grpc.Metadata): Promise; + subscribe(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Observable; + getEvent(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** Casts */ - getCast(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getCastsByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getCastsByParent(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getCastsByMention(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getCast(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getCastsByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getCastsByParent(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getCastsByMention(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** Reactions */ - getReaction(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getReactionsByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getReaction(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getReactionsByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** To be deprecated */ getReactionsByCast( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise; getReactionsByTarget( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise; /** User Data */ - getUserData(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getUserDataByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getUserData(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getUserDataByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** Username Proof */ - getUsernameProof(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getUserNameProofsByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getUsernameProof(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getUserNameProofsByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** Verifications */ - getVerification(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getVerificationsByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getVerification(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getVerificationsByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** OnChain Events */ - getOnChainSigner(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getOnChainSignersByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getOnChainEvents(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getIdRegistryOnChainEvent(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getOnChainSigner(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getOnChainSignersByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getOnChainEvents(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getIdRegistryOnChainEvent(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; getIdRegistryOnChainEventByAddress( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise; getCurrentStorageLimitsByFid( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise; - getFids(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getFids(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** Links */ - getLink(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getLinksByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getLinksByTarget(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getLink(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getLinksByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getLinksByTarget(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** Bulk Methods */ - getAllCastMessagesByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getAllReactionMessagesByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getAllCastMessagesByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getAllReactionMessagesByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; getAllVerificationMessagesByFid( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise; - getAllUserDataMessagesByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getAllLinkMessagesByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getAllUserDataMessagesByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getAllLinkMessagesByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; /** Sync Methods */ - getInfo(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getSyncStatus(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getAllSyncIdsByPrefix(request: DeepPartial, metadata?: grpc.Metadata): Promise; - getAllMessagesBySyncIds(request: DeepPartial, metadata?: grpc.Metadata): Promise; + getInfo(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getSyncStatus(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getAllSyncIdsByPrefix(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + getAllMessagesBySyncIds(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; getSyncMetadataByPrefix( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise; getSyncSnapshotByPrefix( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise; } @@ -160,99 +160,99 @@ export class HubServiceClientImpl implements HubService { this.getSyncSnapshotByPrefix = this.getSyncSnapshotByPrefix.bind(this); } - submitMessage(request: DeepPartial, metadata?: grpc.Metadata): Promise { + submitMessage(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceSubmitMessageDesc, Message.fromPartial(request), metadata); } - subscribe(request: DeepPartial, metadata?: grpc.Metadata): Observable { + subscribe(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Observable { return this.rpc.invoke(HubServiceSubscribeDesc, SubscribeRequest.fromPartial(request), metadata); } - getEvent(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getEvent(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetEventDesc, EventRequest.fromPartial(request), metadata); } - getCast(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getCast(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetCastDesc, CastId.fromPartial(request), metadata); } - getCastsByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getCastsByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetCastsByFidDesc, FidRequest.fromPartial(request), metadata); } - getCastsByParent(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getCastsByParent(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetCastsByParentDesc, CastsByParentRequest.fromPartial(request), metadata); } - getCastsByMention(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getCastsByMention(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetCastsByMentionDesc, FidRequest.fromPartial(request), metadata); } - getReaction(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getReaction(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetReactionDesc, ReactionRequest.fromPartial(request), metadata); } - getReactionsByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getReactionsByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetReactionsByFidDesc, ReactionsByFidRequest.fromPartial(request), metadata); } getReactionsByCast( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise { return this.rpc.unary(HubServiceGetReactionsByCastDesc, ReactionsByTargetRequest.fromPartial(request), metadata); } getReactionsByTarget( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise { return this.rpc.unary(HubServiceGetReactionsByTargetDesc, ReactionsByTargetRequest.fromPartial(request), metadata); } - getUserData(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getUserData(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetUserDataDesc, UserDataRequest.fromPartial(request), metadata); } - getUserDataByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getUserDataByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetUserDataByFidDesc, FidRequest.fromPartial(request), metadata); } - getUsernameProof(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getUsernameProof(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetUsernameProofDesc, UsernameProofRequest.fromPartial(request), metadata); } - getUserNameProofsByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getUserNameProofsByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetUserNameProofsByFidDesc, FidRequest.fromPartial(request), metadata); } - getVerification(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getVerification(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetVerificationDesc, VerificationRequest.fromPartial(request), metadata); } - getVerificationsByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getVerificationsByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetVerificationsByFidDesc, FidRequest.fromPartial(request), metadata); } - getOnChainSigner(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getOnChainSigner(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetOnChainSignerDesc, SignerRequest.fromPartial(request), metadata); } - getOnChainSignersByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getOnChainSignersByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetOnChainSignersByFidDesc, FidRequest.fromPartial(request), metadata); } - getOnChainEvents(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getOnChainEvents(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetOnChainEventsDesc, OnChainEventRequest.fromPartial(request), metadata); } - getIdRegistryOnChainEvent(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getIdRegistryOnChainEvent(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetIdRegistryOnChainEventDesc, FidRequest.fromPartial(request), metadata); } getIdRegistryOnChainEventByAddress( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise { return this.rpc.unary( HubServiceGetIdRegistryOnChainEventByAddressDesc, @@ -263,76 +263,76 @@ export class HubServiceClientImpl implements HubService { getCurrentStorageLimitsByFid( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise { return this.rpc.unary(HubServiceGetCurrentStorageLimitsByFidDesc, FidRequest.fromPartial(request), metadata); } - getFids(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getFids(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetFidsDesc, FidsRequest.fromPartial(request), metadata); } - getLink(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getLink(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetLinkDesc, LinkRequest.fromPartial(request), metadata); } - getLinksByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getLinksByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetLinksByFidDesc, LinksByFidRequest.fromPartial(request), metadata); } - getLinksByTarget(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getLinksByTarget(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetLinksByTargetDesc, LinksByTargetRequest.fromPartial(request), metadata); } - getAllCastMessagesByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getAllCastMessagesByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetAllCastMessagesByFidDesc, FidRequest.fromPartial(request), metadata); } - getAllReactionMessagesByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getAllReactionMessagesByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetAllReactionMessagesByFidDesc, FidRequest.fromPartial(request), metadata); } getAllVerificationMessagesByFid( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise { return this.rpc.unary(HubServiceGetAllVerificationMessagesByFidDesc, FidRequest.fromPartial(request), metadata); } - getAllUserDataMessagesByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getAllUserDataMessagesByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetAllUserDataMessagesByFidDesc, FidRequest.fromPartial(request), metadata); } - getAllLinkMessagesByFid(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getAllLinkMessagesByFid(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetAllLinkMessagesByFidDesc, FidRequest.fromPartial(request), metadata); } - getInfo(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getInfo(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetInfoDesc, HubInfoRequest.fromPartial(request), metadata); } - getSyncStatus(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getSyncStatus(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetSyncStatusDesc, SyncStatusRequest.fromPartial(request), metadata); } - getAllSyncIdsByPrefix(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getAllSyncIdsByPrefix(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetAllSyncIdsByPrefixDesc, TrieNodePrefix.fromPartial(request), metadata); } - getAllMessagesBySyncIds(request: DeepPartial, metadata?: grpc.Metadata): Promise { + getAllMessagesBySyncIds(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(HubServiceGetAllMessagesBySyncIdsDesc, SyncIds.fromPartial(request), metadata); } getSyncMetadataByPrefix( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise { return this.rpc.unary(HubServiceGetSyncMetadataByPrefixDesc, TrieNodePrefix.fromPartial(request), metadata); } getSyncSnapshotByPrefix( request: DeepPartial, - metadata?: grpc.Metadata, + metadata?: grpcWeb.grpc.Metadata, ): Promise { return this.rpc.unary(HubServiceGetSyncSnapshotByPrefixDesc, TrieNodePrefix.fromPartial(request), metadata); } @@ -1215,9 +1215,9 @@ export const HubServiceGetSyncSnapshotByPrefixDesc: UnaryMethodDefinitionish = { }; export interface AdminService { - rebuildSyncTrie(request: DeepPartial, metadata?: grpc.Metadata): Promise; - deleteAllMessagesFromDb(request: DeepPartial, metadata?: grpc.Metadata): Promise; - submitOnChainEvent(request: DeepPartial, metadata?: grpc.Metadata): Promise; + rebuildSyncTrie(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + deleteAllMessagesFromDb(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; + submitOnChainEvent(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise; } export class AdminServiceClientImpl implements AdminService { @@ -1230,15 +1230,15 @@ export class AdminServiceClientImpl implements AdminService { this.submitOnChainEvent = this.submitOnChainEvent.bind(this); } - rebuildSyncTrie(request: DeepPartial, metadata?: grpc.Metadata): Promise { + rebuildSyncTrie(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(AdminServiceRebuildSyncTrieDesc, Empty.fromPartial(request), metadata); } - deleteAllMessagesFromDb(request: DeepPartial, metadata?: grpc.Metadata): Promise { + deleteAllMessagesFromDb(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(AdminServiceDeleteAllMessagesFromDbDesc, Empty.fromPartial(request), metadata); } - submitOnChainEvent(request: DeepPartial, metadata?: grpc.Metadata): Promise { + submitOnChainEvent(request: DeepPartial, metadata?: grpcWeb.grpc.Metadata): Promise { return this.rpc.unary(AdminServiceSubmitOnChainEventDesc, OnChainEvent.fromPartial(request), metadata); } } @@ -1314,7 +1314,7 @@ export const AdminServiceSubmitOnChainEventDesc: UnaryMethodDefinitionish = { } as any, }; -interface UnaryMethodDefinitionishR extends grpc.UnaryMethodDefinition { +interface UnaryMethodDefinitionishR extends grpcWeb.grpc.UnaryMethodDefinition { requestStream: any; responseStream: any; } @@ -1325,32 +1325,32 @@ interface Rpc { unary( methodDesc: T, request: any, - metadata: grpc.Metadata | undefined, + metadata: grpcWeb.grpc.Metadata | undefined, ): Promise; invoke( methodDesc: T, request: any, - metadata: grpc.Metadata | undefined, + metadata: grpcWeb.grpc.Metadata | undefined, ): Observable; } export class GrpcWebImpl { private host: string; private options: { - transport?: grpc.TransportFactory; - streamingTransport?: grpc.TransportFactory; + transport?: grpcWeb.grpc.TransportFactory; + streamingTransport?: grpcWeb.grpc.TransportFactory; debug?: boolean; - metadata?: grpc.Metadata; + metadata?: grpcWeb.grpc.Metadata; upStreamRetryCodes?: number[]; }; constructor( host: string, options: { - transport?: grpc.TransportFactory; - streamingTransport?: grpc.TransportFactory; + transport?: grpcWeb.grpc.TransportFactory; + streamingTransport?: grpcWeb.grpc.TransportFactory; debug?: boolean; - metadata?: grpc.Metadata; + metadata?: grpcWeb.grpc.Metadata; upStreamRetryCodes?: number[]; }, ) { @@ -1361,21 +1361,21 @@ export class GrpcWebImpl { unary( methodDesc: T, _request: any, - metadata: grpc.Metadata | undefined, + metadata: grpcWeb.grpc.Metadata | undefined, ): Promise { const request = { ..._request, ...methodDesc.requestType }; const maybeCombinedMetadata = metadata && this.options.metadata ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap }) : metadata || this.options.metadata; return new Promise((resolve, reject) => { - grpc.unary(methodDesc, { + grpcWeb.grpc.unary(methodDesc, { request, host: this.host, metadata: maybeCombinedMetadata, transport: this.options.transport, debug: this.options.debug, onEnd: function (response) { - if (response.status === grpc.Code.OK) { + if (response.status === grpcWeb.grpc.Code.OK) { resolve(response.message!.toObject()); } else { const err = new GrpcWebError(response.statusMessage, response.status, response.trailers); @@ -1389,7 +1389,7 @@ export class GrpcWebImpl { invoke( methodDesc: T, _request: any, - metadata: grpc.Metadata | undefined, + metadata: grpcWeb.grpc.Metadata | undefined, ): Observable { const upStreamCodes = this.options.upStreamRetryCodes || []; const DEFAULT_TIMEOUT_TIME: number = 3_000; @@ -1399,14 +1399,14 @@ export class GrpcWebImpl { : metadata || this.options.metadata; return new Observable((observer) => { const upStream = (() => { - const client = grpc.invoke(methodDesc, { + const client = grpcWeb.grpc.invoke(methodDesc, { host: this.host, request, transport: this.options.streamingTransport || this.options.transport, metadata: maybeCombinedMetadata, debug: this.options.debug, onMessage: (next) => observer.next(next), - onEnd: (code: grpc.Code, message: string, trailers: grpc.Metadata) => { + onEnd: (code: grpcWeb.grpc.Code, message: string, trailers: grpcWeb.grpc.Metadata) => { if (code === 0) { observer.complete(); } else if (upStreamCodes.includes(code)) { @@ -1457,7 +1457,7 @@ type DeepPartial = T extends Builtin ? T : Partial; export class GrpcWebError extends tsProtoGlobalThis.Error { - constructor(message: string, public code: grpc.Code, public metadata: grpc.Metadata) { + constructor(message: string, public code: grpcWeb.grpc.Code, public metadata: grpcWeb.grpc.Metadata) { super(message); } } diff --git a/protobufs/schemas/message.proto b/protobufs/schemas/message.proto index 11569a5d3..b631f4120 100644 --- a/protobufs/schemas/message.proto +++ b/protobufs/schemas/message.proto @@ -1,9 +1,9 @@ syntax = "proto3"; import "username_proof.proto"; -/** - * A Message is a delta operation on the Farcaster network. The message protobuf is an envelope - * that wraps a MessageData object and contains a hash and signature which can verify its authenticity. +/** + * A Message is a delta operation on the Farcaster network. The message protobuf is an envelope + * that wraps a MessageData object and contains a hash and signature which can verify its authenticity. */ message Message { MessageData data = 1; // Contents of the message @@ -14,12 +14,12 @@ message Message { bytes signer = 6; // Public key or address of the key pair that produced the signature } - /** - * A MessageData object contains properties common to all messages and wraps a body object which + /** + * A MessageData object contains properties common to all messages and wraps a body object which * contains properties specific to the MessageType. */ message MessageData { - MessageType type = 1; // Type of message contained in the body + MessageType type = 1; // Type of message contained in the body uint64 fid = 2; // Farcaster ID of the user producing the message uint32 timestamp = 3; // Farcaster epoch timestamp in seconds FarcasterNetwork network = 4; // Farcaster network the message is intended for @@ -47,7 +47,7 @@ enum HashScheme { enum SignatureScheme { SIGNATURE_SCHEME_NONE = 0; SIGNATURE_SCHEME_ED25519 = 1; // Ed25519 signature (default) - SIGNATURE_SCHEME_EIP712 = 2; // ECDSA signature using EIP-712 scheme + SIGNATURE_SCHEME_EIP712 = 2; // ECDSA signature using EIP-712 scheme } /** Type of the MessageBody */ @@ -144,6 +144,8 @@ message VerificationAddEthAddressBody { bytes address = 1; // Ethereum address being verified bytes eth_signature = 2; // Signature produced by the user's Ethereum address bytes block_hash = 3; // Hash of the latest Ethereum block when the signature was produced + uint32 verification_type = 4; // Type of verification. 0 = EOA, 1 = contract + uint32 chain_id = 5; // 0 for EOA verifications, 1 or 10 for contract verifications } /** Removes a Verification of any type */