Skip to content

Commit

Permalink
feat: add standardised common Provider (#184)
Browse files Browse the repository at this point in the history
* feat: add standardised fallBackProvider

* feat: updated the functions with test cases.

* feat: update the provider generator function and read me

* feat: update according to PR

* feat: updated types and getDefaultProvider

* feat: updated defaultBuilderOption to include process.env.PROVIDER_NETWORK

* feat: update providerType for getDefaultProvider

* feat: refactor getDefaultProvider,  test case and integration testing

* feat: added back INFURA_API_KE when using getDefaultProvider

* feat: update test case to store and restore process.env varables
  • Loading branch information
isaackps authored Aug 17, 2021
1 parent 98527b4 commit 59581d1
Show file tree
Hide file tree
Showing 11 changed files with 1,077 additions and 42 deletions.
69 changes: 60 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,18 @@ console.log(isValid(fragments)); // display true
"type": "ISSUER_IDENTITY"
}
]

```

## Advanced usage

### Environment Variables

- `INFURA_API_KEY`: let you provide your own `INFURA` API key.
- `PROVIDER_API_KEY`: let you provide your own PROVIDER API key.
- `PROVIDER_ENDPOINT_URL`: let you provide your preferred JSON-RPC HTTP API URL.
- `PROVIDER_NETWORK`: let you specify the network to use, i.e. "homestead", "mainnet", "ropsten", "rinkeby".
- `PROVIDER_ENDPOINT_TYPE`: let you specify the provider to use, i.e. "infura", "alchemy", "jsonrpc".

_Provider that is supported: Infura, EtherScan, Alchemy, JSON-RPC_

### Switching network

Expand All @@ -119,20 +123,17 @@ const verify = verificationBuilder(openAttestationVerifiers, { network: "ropsten
`oa-verify` exposes a method, called `createResolver` that allows you to easily create custom resolvers, to resolve DIDs:

```ts
import {
createResolver,
verificationBuilder,
openAttestationVerifiers
} from '@govtechsg/oa-verify';
import { createResolver, verificationBuilder, openAttestationVerifiers } from "@govtechsg/oa-verify";

const resolver = createResolver({
networks: [{ name: 'my-network', rpcUrl: 'https://my-private-chain/besu', registry: '0xaE5a9b9...' }],
networks: [{ name: "my-network", rpcUrl: "https://my-private-chain/besu", registry: "0xaE5a9b9..." }],
});

const verify = verificationBuilder(openAttestationVerifiers, { resolver });
```

At the moment, oa-verify supports two did resolvers:

- [web-did-resolver](https://github.com/decentralized-identity/web-did-resolver#readme)
- [ethd-did-resolver](https://github.com/decentralized-identity/ethr-did-resolver)

Expand Down Expand Up @@ -193,6 +194,56 @@ isValid(fragments); // display false because ISSUER_IDENTITY is INVALID
isValid(fragments, ["DOCUMENT_INTEGRITY", "DOCUMENT_STATUS"]); // display true because those types are VALID
```

## Provider

You may generate a provider using the provider generator, it supports `INFURA`, `ALCHEMY`, `ETHERSCAN` and `JsonRPC` provider.

It requires a set of options:

- `network`: The _network_ may be specified as a **string** for a common network name, i.e. "homestead", "mainnet", "ropsten", "rinkeby".
- `provider`: The _provider_ may be specified as a **string**, i.e. "infura", "alchemy" or "jsonrpc".
- `url`: The _url_ may be specified as a **string** in which is being used to connect to a JSON-RPC HTTP API
- `apiKey`: The _apiKey_ may be specified as a **string** for use together with the provider. If no apiKey is provided, a default shared API key will be used, which may result in reduced performance and throttled requests.

### Example

The most basic way to use:

```
import { utils } from "@govtechsg/oa-verify";
const provider = utils.generateProvider();
// This will generate an infura provider using the default values.
```

Alternate way 1 (with environment variables):

```
// environment file
PROVIDER_NETWORK="ropsten"
PROVIDER_ENDPOINT_TYPE="infura"
PROVIDER_ENDPOINT_URL="http://jsonrpc.com"
PROVIDER_API_KEY="ajdh1j23"
// provider file
import { utils } from "@govtechsg/oa-verify";
const provider = utils.generateProvider();
// This will use the environment variables declared in the files automatically.
```

Alternate way 2 (passing values in as parameters):

```
import { utils } from "@govtechsg/oa-verify";
const providerOptions = {
network: "ropsten",
providerType: "infura",
apiKey: "abdfddsfe23232"
};
const provider = utils.generateProvider(providerOptions);
// This will generate a provider based on the options provided.
// NOTE: by using this way, it will override all environment variables and default values.
```

## Utils and types

### Overview
Expand Down Expand Up @@ -230,6 +281,7 @@ if(utils.isValidFragment(fragment)) {
Note that in the example above, using `utils.isValidFragment` might be unnecessary. It's possible to use directly `ValidTokenRegistryDataV2.guard` over the data.

### List of utilities

- `getOpenAttestationHashFragment`
- `getOpenAttestationDidSignedDocumentStatusFragment`
- `getOpenAttestationEthereumDocumentStoreStatusFragment`
Expand All @@ -245,7 +297,6 @@ Note that in the example above, using `utils.isValidFragment` might be unnecessa
- `isErrorFragment`: type guard to filter only `ERROR` fragment type
- `isSkippedFragment`: type guard to filter only `SKIPPED` fragment type


## Development

For generating of test documents (for v3) you may use the script at `scripts/generate.v3.ts` by running `npm run generate:v3`.
Expand Down
147 changes: 147 additions & 0 deletions src/common/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
getOpenAttestationEthereumDocumentStoreStatusFragment,
getOpenAttestationEthereumTokenRegistryStatusFragment,
getOpenAttestationHashFragment,
generateProvider,
} from "./utils";
import { AllVerificationFragment } from "..";
import { ProviderDetails } from "../types/core";

const fragments: AllVerificationFragment[] = [
{
Expand Down Expand Up @@ -250,3 +252,148 @@ describe("getFragmentsByType", () => {
`);
});
});

describe("generateProvider", () => {
beforeEach(() => {
jest.resetModules();
process.env = {
PROVIDER_NETWORK: "",
PROVIDER_API_KEY: "",
PROVIDER_ENDPOINT_TYPE: "",
PROVIDER_ENDPOINT_URL: "",
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
jest.spyOn(console, "warn").mockImplementation(() => {});
});

afterEach(() => {
jest.spyOn(console, "warn").mockRestore();
});

it("should use the details provided as top priority", () => {
const options = {
network: "ropsten",
providerType: "infura",
apiKey: "bb46da3f80e040e8ab73c0a9ff365d18",
} as ProviderDetails;
const provider = generateProvider(options) as any;

expect(provider?._network?.name).toEqual("ropsten");
expect(provider?.apiKey).toEqual("bb46da3f80e040e8ab73c0a9ff365d18");
expect(provider?.connection?.url).toMatch(/(infura)/i);
});

it("should use the default values to generate the provider if user did not specify any provider details", () => {
const provider = generateProvider() as any;
expect(provider?._network?.name).toEqual("homestead");
expect(provider?.apiKey).toEqual("84842078b09946638c03157f83405213");
expect(provider?.connection?.url).toMatch(/(infura)/i);
});

it("should use the default alchemy apiKey if no apiKey specified", () => {
const options = {
network: "ropsten",
providerType: "alchemy",
} as ProviderDetails;
const provider = generateProvider(options) as any;
expect(provider?.connection?.url).toMatch(/(alchemy)/i);
expect(provider?.apiKey).toEqual("_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC");
});

it("should use the default jsonrpc url which is localhost:8545", () => {
const options = {
network: "ropsten",
providerType: "jsonrpc",
} as ProviderDetails;
const provider = generateProvider(options) as any;
expect(provider?.connection?.url).toMatch(/(localhost:8545)/i);
});

it("should still generate a provider even if only one option (network) is provided", () => {
const options = { network: "ropsten" } as ProviderDetails;
const provider = generateProvider(options) as any;
expect(provider?._network?.name).toEqual("ropsten");
expect(provider?.apiKey).toEqual("84842078b09946638c03157f83405213");
expect(provider?.connection?.url).toMatch(/(infura)/i);
});

it("should still generate a provider even if only one option (provider) is provided", () => {
const options = { providerType: "infura" } as ProviderDetails;
const provider = generateProvider(options) as any;
expect(provider?._network?.name).toEqual("homestead");
expect(provider?.apiKey).toEqual("84842078b09946638c03157f83405213");
expect(provider?.connection?.url).toMatch(/(infura)/i);
});

it("should still generate a provider even if only one option (url) is provided", () => {
const options = { url: "www.123.com" } as ProviderDetails;
const provider = generateProvider(options) as any;
expect(provider?.connection?.url).toMatch(/(www.123.com)/i);
});

it("should throw an error and not generate a provider when only one option (apikey) is provided", () => {
const options = { apiKey: "abc123" } as ProviderDetails;
expect(() => {
generateProvider(options);
}).toThrowError(
"We could not link the apiKey provided to a provider, please state the provider to use in the parameter."
);
});

it("should throw an error when if process.env is using the wrong value for PROVIDER", () => {
process.env.PROVIDER_ENDPOINT_TYPE = "ABC";
expect(() => generateProvider()).toThrowError(
"The provider provided is not on the list of providers. Please use one of the following: infura, alchemy or jsonrpc."
);
});

it("should use the process.env values if there is one, should not use the default values, for infura test case", () => {
process.env.PROVIDER_NETWORK = "rinkeby";
process.env.PROVIDER_API_KEY = "env123123";

const provider = generateProvider() as any;
expect(provider?._network?.name).toEqual("rinkeby");
expect(provider?._network?.name).not.toEqual("mainnet");
expect(provider?.apiKey).toEqual("env123123");
expect(provider?.apiKey).not.toEqual("bb46da3f80e040e8ab73c0a9ff365d18");
expect(provider?.connection?.url).toMatch(/(infura)/i);
});

it("should use the process.env values if there is one, should not use the default values, for alchemy test case", () => {
process.env.PROVIDER_NETWORK = "rinkeby";
process.env.PROVIDER_API_KEY = "env789789";

const options = {
providerType: "alchemy",
} as ProviderDetails;
const provider = generateProvider(options) as any;
expect(provider?._network?.name).toEqual("rinkeby");
expect(provider?._network?.name).not.toEqual("mainnet");
expect(provider?.apiKey).toEqual("env789789");
expect(provider?.apiKey).not.toEqual("OlOgD-8qs5l3pQm-B_fcrMAmHTmAwkGj");
expect(provider?.connection?.url).toMatch(/(alchemy)/i);
});

it("should use the process.env values if there is one, should not use the default values, for Json RPC test case", () => {
process.env.PROVIDER_ENDPOINT_URL = "www.1234.com";

const options = {
providerType: "jsonrpc",
} as ProviderDetails;
const provider = generateProvider(options) as any;
expect(provider?.connection?.url).toMatch(/(www.1234.com)/i);
});

it("should override the process.env value with the function parameter value", () => {
process.env.PROVIDER_NETWORK = "rinkeby";
process.env.PROVIDER_API_KEY = "env789789";

const options = { network: "ropsten", providerType: "alchemy", apiKey: "abc123" } as ProviderDetails;
const provider = generateProvider(options) as any;
expect(provider?._network?.name).toEqual("ropsten");
expect(provider?._network?.name).not.toEqual("rinkeby");
expect(provider?.apiKey).toEqual("abc123");
expect(provider?.apiKey).not.toEqual("env789789");
expect(provider?.connection?.url).toMatch(/(alchemy)/i);
});
});
62 changes: 59 additions & 3 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { providers } from "ethers";
import { INFURA_API_KEY } from "../config";
import {
VerificationBuilderOptions,
VerificationBuilderOptionsWithNetwork,
VerificationFragment,
VerificationFragmentType,
ProviderDetails,
providerType,
} from "../types/core";
import { INFURA_API_KEY } from "../config";
import { OpenAttestationHashVerificationFragment } from "../verifiers/documentIntegrity/hash/openAttestationHash.type";
import { OpenAttestationDidSignedDocumentStatusVerificationFragment } from "../verifiers/documentStatus/didSigned/didSignedDocumentStatus.type";
import { OpenAttestationEthereumDocumentStoreStatusFragment } from "../verifiers/documentStatus/documentStore/ethereumDocumentStoreStatus.type";
Expand All @@ -15,20 +17,74 @@ import { OpenAttestationDnsDidIdentityProofVerificationFragment } from "../verif
import { OpenAttestationDnsTxtIdentityProofVerificationFragment } from "../verifiers/issuerIdentity/dnsTxt/openAttestationDnsTxt.type";

export const getDefaultProvider = (options: VerificationBuilderOptionsWithNetwork): providers.Provider => {
const network = options.network || process.env.PROVIDER_NETWORK || "homestead";
const providerType = (process.env.PROVIDER_ENDPOINT_TYPE as providerType) || "infura";
const apiKey = process.env.PROVIDER_API_KEY || (providerType === "infura" && INFURA_API_KEY) || "";
// create infura provider to get connection information
// we then use StaticJsonRpcProvider so that we can set our own custom limit
const uselessProvider = new providers.InfuraProvider(options.network, INFURA_API_KEY);
const uselessProvider = generateProvider({
providerType,
network,
apiKey,
}) as providers.UrlJsonRpcProvider;
const connection = {
...uselessProvider.connection,
throttleLimit: 3, // default is 12 which may retry 12 times for 2 minutes on 429 failures
};
return new providers.StaticJsonRpcProvider(connection, options.network);
return new providers.StaticJsonRpcProvider(connection, network);
};

// getProvider is a function to get an existing provider or to get a Default provider, when given the options
export const getProvider = (options: VerificationBuilderOptions): providers.Provider => {
return options.provider ?? getDefaultProvider(options);
};

/**
* Generate Provider generates a provider based on the defined options or your env var, if no options or env var was detected, it will generate a provider based on the default values.
* Generate Provider using the following options: (if no option is specified it will use the default values)
* @param {Object} ProviderDetails - Details to use for the function to successfully generate a provider.
* @param {string} ProviderDetails.network - The network in which the provider is connected to, i.e. "homestead", "mainnet", "ropsten", "rinkeby"
* @param {string} ProviderDetails.providerType - Specify which provider to use: "infura", "alchemy" or "jsonrpc"
* @param {string} ProviderDetails.url - Specify which url for JsonRPC to connect to, if not specified will connect to localhost:8545
* @param {string} ProviderDetails.apiKey - If no apiKey is provided, a default shared API key will be used, which may result in reduced performance and throttled requests.
*/
export const generateProvider = (options?: ProviderDetails): providers.Provider => {
if (!!options && Object.keys(options).length === 1 && options.apiKey) {
throw new Error(
"We could not link the apiKey provided to a provider, please state the provider to use in the parameter."
);
}

const network = options?.network || process.env.PROVIDER_NETWORK || "homestead";
const provider = options?.providerType || process.env.PROVIDER_ENDPOINT_TYPE || "infura";
const url = options?.url || process.env.PROVIDER_ENDPOINT_URL || "";
const apiKey =
options?.apiKey || (provider === "infura" && process.env.INFURA_API_KEY) || process.env.PROVIDER_API_KEY || "";
!apiKey &&
console.warn(
"You are using oa-verify default configuration for provider, which is not suitable for production environment. Please make sure that you configured the library correctly."
);

if (!!options && Object.keys(options).length === 1 && url) {
return new providers.JsonRpcProvider(url);
}
switch (provider) {
case "infura":
return apiKey ? new providers.InfuraProvider(network, apiKey) : new providers.InfuraProvider(network);

case "alchemy":
return apiKey ? new providers.AlchemyProvider(network, apiKey) : new providers.AlchemyProvider(network);

case "jsonrpc":
return new providers.JsonRpcProvider(url);

default:
throw new Error(
"The provider provided is not on the list of providers. Please use one of the following: infura, alchemy or jsonrpc."
);
}
};

/**
* Simple typed utility to return a fragment depending on the name
* @param name
Expand Down
Loading

0 comments on commit 59581d1

Please sign in to comment.