Skip to content

Latest commit

 

History

History
253 lines (178 loc) · 10 KB

CONTRIBUTING.md

File metadata and controls

253 lines (178 loc) · 10 KB

Contributing

  1. How to Contribute
  2. Setting up your development environment
    1. Installing Dependencies
    2. Signing Commits
  3. Proposing Changes
    1. Writing Tests
    2. Writing Docs
    3. Handling Errors
    4. Creating the PR

1. How to Contribute

Thanks for your interest in improving the Farcaster Hub!

No contribution is too small and we welcome to your help. There's always something to work on, no matter how experienced you are. If you're looking for ideas, start with the good first issue or help wanted sections in the issues. You can help make Farcaster better by:

  • Opening issues or adding details to existing issues
  • Fixing bugs in the code
  • Making tests or the ci builds faster
  • Improving the documentation
  • Keeping packages up-to-date
  • Proposing and implementing new features

Before you get down to coding, take a minute to consider this:

  • If your proposal modifies the farcaster protocol, open an issue there first.
  • If your proposal is a non-trivial change, consider opening an issue first to get buy-in.
  • If your issue is a small bugfix or improvement, you can simply make the changes and open the PR.

2. Setting up your development environment

2.1 Installing Dependencies

First, ensure that the following are installed globally on your machine:

Then, run:

  • yarn install to install dependencies
  • yarn test to ensure that the test suite runs correctly
  • yarn start to boot up the Hub

This will start an instance of the Hub that you can send messages to. Hubs do not (yet) peer automatically, this will be added closer to the v2 release in Q4 2022.

2.2. Signing Commits

All commits need to be signed with a GPG key. This adds a second factor of authentication that proves that it came from you, and not someone who managed to compromise your GitHub account. You can enable signing by following these steps:

  1. Generate GPG Keys and upload them to your Github account, GPG Suite is recommended for OSX

  2. Use gpg-agent to remember your password locally

vi ~/.gnupg/gpg-agent.conf

default-cache-ttl 100000000
max-cache-ttl 100000000
  1. Configure Git to use your keys when signing.

  2. Configure Git to always sign commits by running git config --global commit.gpgsign true

  3. Commit all changes with your usual git commands and you should see a Verified badge near your commits

3. Proposing Changes

When proposing a change, make sure that you've followed all of these steps before you ask for a review.

3.1. Writing Tests

All changes that involve features or bugfixes should be accompanied by tests, and remember that:

  • Unit tests should live side-by-side with code as foo.test.ts
  • Tests that span multiple files should live in src/test/ under the appropriate subfolder
  • Tests should use factories instead of stubs wherever possible.
  • Critical code paths should have 100% test coverage, which you can check in the Coveralls CI.

3.2 Writing Docs

All changes should have supporting documentation that makes reviewing and understand the code easy. You should:

  • Update high-level changes in the contract docs.
  • Always use TSDoc style comments for functions, variables, constants, events and params.
  • Prefer single-line comments /** The comment */ when the TSDoc comment fits on a single line.
  • Always use regular comments // for inline commentary on code.
  • Comments explaining the 'why' when code is not obvious.
  • Do not comment obvious changes (e.g. starts the db before the line db.start())
  • Add a Safety: .. comment explaining every use of as.
  • Prefer active, present-tense doing form (Gets the connection) over other forms (Connection is obtained, Get the connection, We get the connection, will get a connection)

3.4. Handling Errors

Errors are not handled using throw and try / catch as is common with Javascript programs. This pattern makes it hard for people to reason about whether methods are safe which leads to incomplete test coverage, unexpected errors and less safety. Instead we use a more functional approach to dealing with errors. See this issue for the rationale behind this approach.

All errors must be constructed using the HubError class which extends extends Error. It is stricter than error and requires a Hub Error Code (e.g. unavailable.database_error) and some additional context. Codes are used a replacement for error subclassing since they can be easily serialized over network calls. Codes also have multiple levels (e.g. database_error is a type of unavailable) which help with making decisions about error handling.

Functions that can fail should always return HubResult which is a type that can either be the desired value or an error. Callers of the function should inspect the value and handle the success and failure case explicitly. The HubResult is an alias over neverthrow's Result. If you have never used a language where this is common (like Rust) you may want to start with the API docs. This pattern ensures that:

  1. Readers can immediately tell whether a function is safe or unsafe
  2. Readers know the type of error that may get thrown
  3. Authors can never accidentally ignore an error.

We also enforce the following rules during code reviews:


Always return HubResult<T> instead of throwing if the function can fail

// incorrect usage
const parseMessage = (message: string): Uint8Array => {
  if (message == '') throw new HubError(...);
  return message;
};

// correct usage
const parseMessage = (message: string): HubResult<Uint8Array> => {
  if (message == '') return new HubError(...)
  return ok(message);
};

Always wrap external calls with Result.fromThrowable or ResultAsync.fromPromise and wrap external an Error into a HubError.

// incorrect usage
const parseMessage = (message: string): string => {
  try {
    return JSON.parse(message);
  } catch (err) {
    return err as Error;
  }
};

// correct usage: wrap the external call for safety
const parseMessage = (message: string): HubResult<string> => {
  return Result.fromThrowable(
    () => JSON.parse(message),
    (err) => new HubError('bad_request.parse_failure', err as Error)
  )();
};

// correct usage: build a convenience method so you can call it easily
const safeJsonStringify = Result.fromThrowable(
  JSON.stringify,
  () => new HubError('bad_request', 'json stringify failure')
);

const result = safeJsonStringify(json);

Prefer result.match to handle HubResult since it is explicit about how all branches are handled

const result = computationThatMightFail().match(
  (str) => str.toUpperCase(),
  (error) => err(error)
);

Only use isErr() in cases where you want to short-circuit early on failure and refactoring is unwieldy or not performant

public something(): HubResult<number> {
  const result = computationThatMightFail();
  if (result.isErr()) return err(new HubError('unavailable', 'down'));

   // do a lot of things that would be unweidly to put in a match
   // ...
   // ...
   return ok(200);
}

Use _unsafeUnwrap() and _unsafeUnwrapErr() in tests to assert results

// when expecting an error
const error = foo()._unsafeUnwrapErr();
expect(error.errCode).toEqual('bad_request');
expect(error.message).toMatch('invalid AddressInfo family');

Prefer combine and combineWithAllErrors when operating on multiple results

const results = await Promise.all(things.map((thing) => foo(thing)));

// 1. Only fail if all failed
const combinedResults = Result.combineWithAllErrors(results) as Result<void[], HubError[]>;
if (combinedResults.isErr() && combinedResults.error.length == things.length) {
  return err(new HubError('unavailable', 'could not connect to any bootstrap nodes'));
}

// 2. Fail if at least one failed
const combinedResults = Result.combine(results);
if (combinedResults.isErr()) {
  return err(new HubError('unavailable', 'could not connect to any bootstrap nodes'));
}

3.4. Creating the PR

All submissions must be opened as a Pull Request and reviewed and approved by a project member. The CI build process will ensure that all tests pass and that all linters have been run correctly. In addition, you should ensure that:

As an example, a good PR title would look like this:

fix(signers): validate signatures correctly

While a good commit message might look like this:

fix(signers): validate signatures correctly

Called Signer.verify with the correct parameter to ensure that older signature
types would not pass verification in our Signer Sets

4. FAQ

Updating Flatbuffer Schemas

If you update the message.fbs file you'll also need to generate new TS classes, which can be done by running: flatc --ts --ts-flat-files --gen-object-api -o src/utils/generated src/utils/schemas/message.fbs

To regerenrate all the TS classes, yarn flatc