Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add failoverRPCprovider #1153

Merged
merged 1 commit into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: add failoverRPCprovider
  • Loading branch information
dim-daskalov committed Jul 29, 2024
commit d97a9691204e593c12eb247bc1e41e0f1f4ec9ac
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
Loading