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

Add context.rpc #801

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions runner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"express": "^4.18.2",
"graphql": "^16.8.1",
"long": "^5.2.3",
"near-api-js": "^4.0.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be using @near-js packages instead, so for this I think that would mean @near-js/types and @near-js/providers.

near-api-js includes much more than we need, and @near-js is more likely to be maintained going forward.

"node-fetch": "^2.6.11",
"node-sql-parser": "^5.0.0",
"pg": "^8.11.1",
Expand Down
33 changes: 31 additions & 2 deletions runner/src/indexer/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,22 @@ import type IndexerConfig from '../indexer-config';
import { type PostgresConnectionParams } from '../pg-client';
import IndexerMeta, { IndexerStatus } from '../indexer-meta';
import { wrapSpan } from '../utility';
import { type IRpcClient } from '../rpc-client/rpc-client';
import { type CodeResult } from '@near-js/types/lib/provider/response';

interface Dependencies {
fetch: typeof fetch
provisioner: Provisioner
dmlHandler?: DmlHandler
indexerMeta?: IndexerMeta
parser: Parser
};
rpcClient?: IRpcClient
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rpcClient?: IRpcClient
rpcClient: IRpcClient

I'm not sure there is any reason not to include this? dmlHandler etc is only optional because we need to get DB credentials.

This would also tidy up the context generation since it would always be defined.

}

interface IRpcContext {
viewCallRaw: (contractId: string, methodName: string, args: Record<string, string | number | object>) => Promise<CodeResult>
viewCallJSON: (contractId: string, methodName: string, args: Record<string, string | number | object>) => Promise<any>
}

interface Context {
graphql: (operation: string, variables?: Record<string, any>) => Promise<any>
Expand All @@ -31,6 +39,7 @@ interface Context {
error: (message: string) => void
fetchFromSocialApi: (path: string, options?: any) => Promise<any>
db: Record<string, Record<string, (...args: any[]) => any>>
rpc: IRpcContext
}

export interface TableDefinitionNames {
Expand Down Expand Up @@ -200,7 +209,8 @@ export default class Indexer {
fetchFromSocialApi: async (path, options) => {
return await this.deps.fetch(`https://api.near.social${path}`, options);
},
db: this.buildDatabaseContext(blockHeight, logEntries)
db: this.buildDatabaseContext(blockHeight, logEntries),
rpc: this.buildRPCContext(blockHeight)
};
}

Expand Down Expand Up @@ -277,6 +287,25 @@ export default class Indexer {
return pascalCaseTableName;
}

buildRPCContext (
currentBlockHeight: number,
): IRpcContext {
const rpcClient = (): IRpcClient => {
if (!this.deps.rpcClient) {
throw new Error('RPC client is not configured');
}
return this.deps.rpcClient;
};
return {
viewCallRaw: async (contractId: string, methodName: string, args: Record<string, string | number | object> = {}, blockHeight = currentBlockHeight): Promise<CodeResult> => {
return await rpcClient().viewCallRaw(blockHeight, contractId, methodName, args);
},
viewCallJSON: async (contractId: string, methodName: string, args: Record<string, string | number | object> = {}, blockHeight = currentBlockHeight): Promise<any> => {
return await rpcClient().viewCallJSON(blockHeight, contractId, methodName, args);
}
};
}

buildDatabaseContext (
blockHeight: number,
logEntries: LogEntry[],
Expand Down
25 changes: 25 additions & 0 deletions runner/src/rpc-client/rpc-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import RpcClient from './rpc-client';

describe('RPCClient unit tests', () => {
const rpcClient = RpcClient.fromConfig({
networkId: 'mainnet',
nodeUrl: 'https://beta.rpc.mainnet.near.org',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add tests which don't rely on real endpoints? I don't want unnecessary CI failures due to network failures.

});
const testBlockHeight = 121_031_955;

it('Should make a get_total_staked_balance view call to pool.near', async () => {
const response = await rpcClient.viewCallJSON(testBlockHeight, 'epic.poolv1.near', 'get_total_staked_balance', {});
console.log(response);
expect(response).toBeDefined();
});

it('Should return non-empty dataplatform.near.list_by_account', async () => {
const response = await rpcClient.viewCallJSON(testBlockHeight, 'queryapi.dataplatform.near', 'list_by_account', { account_id: 'dataplatform.near' });
expect(Object.keys(response).length).toBeGreaterThanOrEqual(0);
}, 30_000);

it('Should get_contracts_metadata from sputnik-dao.near', async () => {
const response = await rpcClient.viewCallJSON(testBlockHeight, 'sputnik-dao.near', 'get_contracts_metadata', {});
expect(response.length).toBeGreaterThanOrEqual(3);
});
});
53 changes: 53 additions & 0 deletions runner/src/rpc-client/rpc-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { type NearConfig } from '@near-js/wallet-account/lib/near';
import { connect, type Near } from 'near-api-js';
import { type CodeResult } from '@near-js/types/lib/provider/response';

type RpcViewCallArgs = Record<string, string | number | object>;

export interface IRpcClient {
viewCallRaw: (blockHeight: number, contractId: string, methodName: string, args: RpcViewCallArgs) => Promise<CodeResult>
viewCallJSON: (blockHeight: number, contractId: string, methodName: string, args: RpcViewCallArgs) => Promise<any>
}

export default class RpcClient implements IRpcClient {
#near: Near | undefined;

private constructor (private readonly config: NearConfig) {}

async nearConnection (): Promise<Near> {
if (!this.#near) {
this.#near = await connect(this.config);
}
return this.#near;
}

async viewCallRaw (blockHeight: number, contractId: string, methodName: string, args: RpcViewCallArgs = {}): Promise<CodeResult> {
const near = await this.nearConnection();
return await near.connection.provider.query({
request_type: 'call_function',
blockId: blockHeight,
account_id: contractId,
method_name: methodName,
args_base64: Buffer.from(JSON.stringify(args)).toString('base64'),
});
}

async viewCallJSON (blockHeight: number, contractId: string, methodName: string, args: RpcViewCallArgs = {}): Promise<any> {
const response: CodeResult = await this.viewCallRaw(blockHeight, contractId, methodName, args);
return JSON.parse(Buffer.from(response.result).toString('ascii'));
}

static fromConfig (config: NearConfig): IRpcClient {
return new RpcClient(config);
}

static fromEnv (): IRpcClient {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I like it :)

if (!process.env.RPC_URL) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!process.env.RPC_URL) {
if (!process.env.NEAR_RPC_ENDPOINT) {

I've deployed under this environment variable, I didn't want it to be confused with the other RPC endpoints we use internally.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That actually makes sense. I was thinking to actually name it 'ARCHIVAL_RPC_ENDPOINT' – do you want me to make this change in this PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I've deployed NEAR_RPC_ENDPOINT, but can update.

throw new Error('Missing RPC_URL env var for RpcClient');
}
return RpcClient.fromConfig({
networkId: 'mainnet',
nodeUrl: process.env.RPC_URL,
});
}
}
5 changes: 4 additions & 1 deletion runner/src/stream-handler/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IndexerStatus } from '../indexer-meta/indexer-meta';
import IndexerConfig from '../indexer-config';
import parentLogger from '../logger';
import { wrapSpan } from '../utility';
import RpcClient from '../rpc-client/rpc-client';

if (isMainThread) {
throw new Error('Worker should not be run on main thread');
Expand Down Expand Up @@ -96,7 +97,9 @@ async function blockQueueProducer (workerContext: WorkerContext): Promise<void>
async function blockQueueConsumer (workerContext: WorkerContext): Promise<void> {
let previousError: string = '';
const indexerConfig: IndexerConfig = workerContext.indexerConfig;
const indexer = new Indexer(indexerConfig);
const indexer = new Indexer(indexerConfig, {
rpcClient: RpcClient.fromEnv()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically add defaults for injected dependencies to add convenience, could you do that please?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually proposing to not have defaults to make configuration of this class explicit with all dependencies – the number of places it is being configured in is very low. This will also be needed for the testing framework. Happy to discuss or change my opinion if defaults indeed make things more convenient.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What benefit would the explicitness provide? For me, defaults allow us to abstract the implementation away, so therefore don't need to think about it from the user side. I hadn't really considered otherwise, interested to see your side.

});
let streamMessageId = '';
let currBlockHeight = 0;

Expand Down
10 changes: 9 additions & 1 deletion runner/tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import block_115185108 from './blocks/00115185108/streamer_message.json';
import block_115185109 from './blocks/00115185109/streamer_message.json';
import { LogLevel } from '../src/indexer-meta/log-entry';
import IndexerConfig from '../src/indexer-config';
import RpcClient, { type IRpcClient } from '../src/rpc-client/rpc-client';

describe('Indexer integration', () => {
jest.setTimeout(300_000);
Expand All @@ -25,6 +26,7 @@ describe('Indexer integration', () => {
let postgresContainer: StartedPostgreSqlContainer;
let hasuraContainer: StartedHasuraGraphQLContainer;
let graphqlClient: GraphQLClient;
let rpcClient: IRpcClient;

beforeEach(async () => {
hasuraClient = new HasuraClient({}, {
Expand Down Expand Up @@ -56,6 +58,11 @@ describe('Indexer integration', () => {
pgBouncerPort: Number(postgresContainer.getPort()),
}
);

rpcClient = RpcClient.fromConfig({
networkId: 'mainnet',
nodeUrl: 'https://beta.rpc.mainnet.near.org',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment around adding tests which don't depend on real infrastructure. This isn't being used in integration tests so I would just remove it.

});
});

beforeAll(async () => {
Expand Down Expand Up @@ -117,7 +124,8 @@ describe('Indexer integration', () => {
const indexer = new Indexer(
indexerConfig,
{
provisioner
provisioner,
rpcClient
},
undefined,
{
Expand Down
Loading