Skip to content

Commit

Permalink
Merge pull request #1153 from LimeChain/feat/implement-failoverRPCpro…
Browse files Browse the repository at this point in the history
…vider

feat: add failoverRPCprovider
  • Loading branch information
Pavel Ivanov authored Aug 2, 2024
2 parents 3054c66 + d97a969 commit 697e9e3
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 18 deletions.
40 changes: 38 additions & 2 deletions packages/core/src/lib/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getNetworkPreset, resolveNetwork } from "./options";
import type { NetworkId, Network } from "./options.types";

describe("getNetworkPreset", () => {
it("returns the correct config for 'mainnet'", () => {
it("returns the correct config for 'mainnet' without fallbackRpcUrls", () => {
const networkId: NetworkId = "mainnet";
const network = getNetworkPreset(networkId);

Expand All @@ -15,7 +15,25 @@ describe("getNetworkPreset", () => {
});
});

it("returns the correct config for 'testnet'", () => {
it("returns the correct config for 'mainnet' with fallbackRpcUrls", () => {
const networkId: NetworkId = "mainnet";
const fallbackRpcUrls: Array<string> = [
"https://rpc1.mainnet.near.org",
"https://rpc2.mainnet.near.org",
"https://rpc3.mainnet.near.org",
];
const network = getNetworkPreset(networkId, fallbackRpcUrls);

expect(network).toEqual({
networkId,
nodeUrl: "https://rpc1.mainnet.near.org",
helperUrl: "https://helper.mainnet.near.org",
explorerUrl: "https://nearblocks.io",
indexerUrl: "https://api.kitwallet.app",
});
});

it("returns the correct config for 'testnet' without fallbackRpcUrls", () => {
const networkId: NetworkId = "testnet";
const network = getNetworkPreset(networkId);

Expand All @@ -27,6 +45,24 @@ describe("getNetworkPreset", () => {
indexerUrl: "https://testnet-api.kitwallet.app",
});
});

it("returns the correct config for 'testnet' with fallbackRpcUrls", () => {
const networkId: NetworkId = "testnet";
const fallbackRpcUrls: Array<string> = [
"https://rpc1.testnet.near.org",
"https://rpc2.testnet.near.org",
"https://rpc3.testnet.near.org",
];
const network = getNetworkPreset(networkId, fallbackRpcUrls);

expect(network).toEqual({
networkId,
nodeUrl: "https://rpc1.testnet.near.org",
helperUrl: "https://helper.testnet.near.org",
explorerUrl: "https://testnet.nearblocks.io",
indexerUrl: "https://testnet-api.kitwallet.app",
});
});
});

describe("resolveNetwork", () => {
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ import type { WalletSelectorParams } from "./wallet-selector.types";
import type { Options, Network, NetworkId } from "./options.types";
import { WebStorageService } from "./services";

export const getNetworkPreset = (networkId: NetworkId): Network => {
export const getNetworkPreset = (
networkId: NetworkId,
fallbackRpcUrls?: Array<string>
): Network => {
switch (networkId) {
case "mainnet":
return {
networkId,
nodeUrl: "https://rpc.mainnet.near.org",
nodeUrl: fallbackRpcUrls?.[0] || "https://rpc.mainnet.near.org",
helperUrl: "https://helper.mainnet.near.org",
explorerUrl: "https://nearblocks.io",
indexerUrl: "https://api.kitwallet.app",
};
case "testnet":
return {
networkId,
nodeUrl: "https://rpc.testnet.near.org",
nodeUrl: fallbackRpcUrls?.[0] || "https://rpc.testnet.near.org",
helperUrl: "https://helper.testnet.near.org",
explorerUrl: "https://testnet.nearblocks.io",
indexerUrl: "https://testnet-api.kitwallet.app",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const setup = (url: string) => {

return {
provider,
service: new Provider(url),
service: new Provider([url]),
};
};

Expand Down
29 changes: 23 additions & 6 deletions packages/core/src/lib/services/provider/provider.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,34 @@ import type {
AccessKeyView,
BlockReference,
QueryResponseKind,
RpcQueryRequest,
} from "near-api-js/lib/providers/provider";
import type { SignedTransaction } from "near-api-js/lib/transaction";
import type {
ProviderService,
QueryParams,
ViewAccessKeyParams,
} from "./provider.service.types";
import { JsonRpcProvider } from "near-api-js/lib/providers";
import type { SignedTransaction } from "near-api-js/lib/transaction";

export class Provider implements ProviderService {
private provider: nearAPI.providers.JsonRpcProvider;
private provider: nearAPI.providers.FailoverRpcProvider;

constructor(url: string) {
this.provider = new nearAPI.providers.JsonRpcProvider({ url });
constructor(urls: Array<string>) {
this.provider = new nearAPI.providers.FailoverRpcProvider(
this.urlsToProviders(urls)
);
}

query<Response extends QueryResponseKind>(params: QueryParams) {
return this.provider.query<Response>(params);
query<Response extends QueryResponseKind>(
paramsOrPath: QueryParams | RpcQueryRequest | string,
data?: string
): Promise<Response> {
if (typeof paramsOrPath === "string" && data !== undefined) {
return this.provider.query<Response>(paramsOrPath, data);
} else {
return this.provider.query<Response>(paramsOrPath as RpcQueryRequest);
}
}

viewAccessKey({ accountId, publicKey }: ViewAccessKeyParams) {
Expand All @@ -38,4 +49,10 @@ export class Provider implements ProviderService {
sendTransaction(signedTransaction: SignedTransaction) {
return this.provider.sendTransaction(signedTransaction);
}

private urlsToProviders(urls: Array<string>) {
return urls && urls.length > 0
? urls.map((url) => new JsonRpcProvider({ url }))
: [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import {
PENDING_SELECTED_WALLET_ID,
} from "../../constants";
import { JsonStorage } from "../storage/json-storage.service";
import type { ProviderService } from "../provider/provider.service.types";
import type { SignMessageMethod } from "../../wallet";
import type { ProviderService } from "../provider/provider.service.types";

export class WalletModules {
private factories: Array<WalletModuleFactory>;
Expand Down
184 changes: 184 additions & 0 deletions packages/core/src/lib/wallet-selector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { setupWalletSelector } from "./wallet-selector";
import { FailoverRpcProvider } from "@near-js/providers";
import { getNetworkPreset } from "./options";
import { JsonRpcProvider } from "near-api-js/lib/providers";
import type { Network } from "./options.types";
import type { Store } from "./store.types";
import type { WalletModuleFactory } from "./wallet";

// Mock implementations for required modules
const _state: Record<string, string> = {};

global.localStorage = {
getItem: jest.fn((key) => _state[key] || null),
setItem: jest.fn((key, value) => {
_state[key] = value;
}),
removeItem: jest.fn((key) => {
delete _state[key];
}),
clear: jest.fn(() => {
for (const key in _state) {
delete _state[key];
}
}),
get length() {
return Object.keys(_state).length;
},
key: jest.fn((index) => Object.keys(_state)[index] || null),
};

jest.mock("./options", () => {
return {
...jest.requireActual("./options"),
getNetworkPreset: jest.fn().mockResolvedValue({
networkId: "testnet",
nodeUrl: "http://node.example.com",
helperUrl: "http://helper.example.com",
explorerUrl: "http://explorer.example.com",
indexerUrl: "http://indexer.example.com",
}),
};
});

jest.mock("./store", () => {
return {
...jest.requireActual("./store"),
createStore: jest.fn().mockResolvedValue({
toReadOnly: jest.fn().mockReturnValue({}),
getState: jest.fn().mockReturnValue({}),
dispatch: jest.fn(),
} as unknown as Store),
};
});

jest.mock("@near-js/providers", () => {
const originalModule = jest.requireActual("@near-js/providers");
return {
...originalModule,
FailoverRpcProvider: jest.fn(),
};
});

describe("setupWalletSelector", () => {
let params: {
network: Network;
fallbackRpcUrls: Array<string>;
modules: Array<WalletModuleFactory>;
};

beforeEach(() => {
jest.clearAllMocks();

params = {
network: {
networkId: "testnet",
nodeUrl: "http://node.example.com",
helperUrl: "http://helper.example.com",
explorerUrl: "http://explorer.example.com",
indexerUrl: "http://indexer.example.com",
},
fallbackRpcUrls: ["http://rpc1.example.com", "http://rpc2.example.com"],
modules: [],
};
});

it("should instantiate FailoverRpcProvider correctly with single URL", async () => {
const mockedRpcProvider = { setup: jest.fn() };
const mockedFailoverRpcProvider = FailoverRpcProvider as jest.MockedClass<
typeof FailoverRpcProvider
>;

mockedFailoverRpcProvider.mockImplementationOnce(
() => mockedRpcProvider as unknown as FailoverRpcProvider
);

const mockFallbackRpcUrl = "http://rpc1.example.com";

await setupWalletSelector({
...params,
fallbackRpcUrls: [mockFallbackRpcUrl],
});

const mockExpectedProviders = [
new JsonRpcProvider({ url: mockFallbackRpcUrl }),
];

expect(mockedFailoverRpcProvider).toHaveBeenCalledTimes(1);
expect(mockedFailoverRpcProvider).toHaveBeenCalledWith(
mockExpectedProviders
);
});

it("should instantiate FailoverRpcProvider correctly with multiple URLs", async () => {
const mockedRpcProvider = { setup: jest.fn() };
const mockedFailoverRpcProvider = FailoverRpcProvider as jest.MockedClass<
typeof FailoverRpcProvider
>;

mockedFailoverRpcProvider.mockImplementationOnce(
() => mockedRpcProvider as unknown as FailoverRpcProvider
);

const mockFallbackRpcUrls = [
"https://rpc1.example.com",
"https://rpc2.example.com",
];

await setupWalletSelector({
...params,
fallbackRpcUrls: mockFallbackRpcUrls,
});

const mockExpectedProviders = mockFallbackRpcUrls.map(
(url) => new JsonRpcProvider({ url })
);

expect(mockedFailoverRpcProvider).toHaveBeenCalledTimes(1);
expect(mockedFailoverRpcProvider).toHaveBeenCalledWith(
mockExpectedProviders
);
});

it("should instantiate FailoverRpcProvider correctly with default value when fallbackRpcUrls are empty", async () => {
const mockedRpcProvider = { setup: jest.fn() };
const mockedFailoverRpcProvider = FailoverRpcProvider as jest.MockedClass<
typeof FailoverRpcProvider
>;

mockedFailoverRpcProvider.mockImplementationOnce(
() => mockedRpcProvider as unknown as FailoverRpcProvider
);

const networkPreset = await getNetworkPreset("testnet", []);

await setupWalletSelector({
...params,
fallbackRpcUrls: [],
});

const mockExpectedProvider = [
new JsonRpcProvider({ url: networkPreset.nodeUrl }),
];

expect(mockedFailoverRpcProvider).toHaveBeenCalledTimes(1);
expect(mockedFailoverRpcProvider).toHaveBeenCalledWith(
mockExpectedProvider
);
});

it("should handle error during FailoverRpcProvider instantiation", async () => {
const mockedFailoverRpcProvider = FailoverRpcProvider as jest.MockedClass<
typeof FailoverRpcProvider
>;
mockedFailoverRpcProvider.mockImplementationOnce(() => {
throw new Error("Failed to instantiate FailoverRpcProvider");
});

await expect(setupWalletSelector(params)).rejects.toThrow(
"Failed to instantiate FailoverRpcProvider"
);

expect(mockedFailoverRpcProvider).toHaveBeenCalledTimes(1);
});
});
18 changes: 14 additions & 4 deletions packages/core/src/lib/wallet-selector.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { resolveOptions } from "./options";
import { getNetworkPreset, resolveOptions } from "./options";
import { createStore } from "./store";
import type {
WalletSelector,
WalletSelectorEvents,
WalletSelectorParams,
} from "./wallet-selector.types";
import { EventEmitter, Logger, Provider, WalletModules } from "./services";
import { EventEmitter, Logger, WalletModules, Provider } from "./services";
import type { Wallet } from "./wallet";
import type { Store } from "./store.types";
import type { Options } from "./options.types";
import type { NetworkId, Options } from "./options.types";

let walletSelectorInstance: WalletSelector | null = null;

Expand Down Expand Up @@ -76,13 +76,23 @@ export const setupWalletSelector = async (

const emitter = new EventEmitter<WalletSelectorEvents>();
const store = await createStore(storage);
const network = await getNetworkPreset(
options.network.networkId as NetworkId,
params.fallbackRpcUrls
);

const rpcProviderUrls =
params.fallbackRpcUrls && params.fallbackRpcUrls.length > 0
? params.fallbackRpcUrls
: [network.nodeUrl];

const walletModules = new WalletModules({
factories: params.modules,
storage,
options,
store,
emitter,
provider: new Provider(options.network.nodeUrl),
provider: new Provider(rpcProviderUrls),
});

await walletModules.setup();
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/lib/wallet-selector.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export interface WalletSelectorParams {
* The URL where DelegateActions are sent by meta transaction enabled wallet modules.
*/
relayerUrl?: string;
/**
* Whether multiple RPC URLs are included, used for the FailoverRpcProvider.
*/
fallbackRpcUrls?: Array<string>;
}

export type WalletSelectorStore = ReadOnlyStore;
Expand Down
Loading

0 comments on commit 697e9e3

Please sign in to comment.