Skip to content

Commit

Permalink
Merge pull request #1372 from near/feat/client
Browse files Browse the repository at this point in the history
feat: client
  • Loading branch information
andy-haynes authored Oct 9, 2024
2 parents bc78d21 + 773f50d commit 4661484
Show file tree
Hide file tree
Showing 84 changed files with 2,574 additions and 740 deletions.
19 changes: 19 additions & 0 deletions packages/client/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
env:
es6: true
node: true
extends:
- 'eslint:recommended'
- 'plugin:@typescript-eslint/eslint-recommended'
- 'plugin:@typescript-eslint/recommended'
parser: '@typescript-eslint/parser'
rules:
no-inner-declarations: off
indent:
- error
- 2
- SwitchCase: 1
'@typescript-eslint/no-explicit-any': off

parserOptions:
ecmaVersion: 2018
sourceType: module
192 changes: 192 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# @near-js/client

This package provides a simple interface for interacting with the Near blockchain. As a modern, tree-shakeable package,
it is intended to replace usage of `near-api-js`.

### Installation
```shell
# npm
npm i -s @near-js/client

# pnpm
pnpm add @near-js/client
```

### Usage


### Dependency Initialization
This package uses a common interface for specifying dependencies for RPC providers and cryptographic signing, allowing
a more flexible approach to composing functionality.

#### RPC provider
RPC providers are required for any blockchain interaction, e.g. block queries, transaction publishing.

This interface makes use of the `FallbackRpcProvider`, making it configurable with multiple RPC endpoint URLs
which can be cycled through when the current URL returns an error.

Specifying by URL endpoints:
```ts
import { getProviderByEndpoints } from '@near-js/client';

const rpcProvider = getProviderByEndpoints('https://rpc.tld', 'https://fallback-rpc.tld');
```

Specifying by network (uses default RPC endpoints specified in code):
```ts
import { getProviderByNetwork } from '@near-js/client';

const rpcProvider = getProviderByNetwork('testnet');
```

Shortcut methods:
```ts
import { getTestnetRpcProvider } from '@near-js/client';

// equivalent to getProviderByNetwork('testnet');
const rpcProvider = getTestnetRpcProvider();
```

#### Message Signer
Implementers of the `MessageSigner` interface can be initialized from the existing `KeyStore` classes or using private keys.

Using an existing keystore:
```ts
import { getSignerFromKeystore } from '@near-js/client';
import { BrowserLocalStorageKeyStore } from '@near-js/keystores-browser';

const keystore = new BrowserLocalStorageKeyStore();
const signer = getSignerFromKeystore('account.near', 'mainnet', keystore);
```


Using a private key string:
```ts
import { getSignerFromKeystore } from '@near-js/client';
import { BrowserLocalStorageKeyStore } from '@near-js/keystores-browser';

const signer = getSignerFromPrivateKey('ed25519:...');
```

An access key-based signer is also available. Initialized using a `MessageSigner` implementer, this signer caches the
access key record and maintains a local, auto-incremented value for its nonce:
```ts
import { getAccessKeySigner, getMainnetRpcProvider, getSignerFromPrivateKey } from '@near-js/client';
import { BrowserLocalStorageKeyStore } from '@near-js/keystores-browser';

const keystore = new BrowserLocalStorageKeyStore();
const signer = getSignerFromKeystore('account.near', 'mainnet', keystore);
const accessKeySigner = getAccessKeySigner({
accout: 'account.near',
deps: {
signer,
rpcProvider: getMainnetRpcProvider(),
},
});
```

### View Methods
Several functions are provided for working with builtin account methods and smart contract view methods.

Executing a view method on a smart contract and return the entire response:
```ts
import { callViewMethod, getTestnetRpcProvider } from '@near-js/client';

const response = await callViewMethod({
account: 'guest-book.testnet',
method: 'getMessages',
deps: { rpcProvider: getTestnetRpcProvider() },
});
```

Shorthand function to call the method and return only the parsed value:
```ts
import { getTestnetRpcProvider, view } from '@near-js/client';

interface GuestBookMessage {
premium: boolean;
sender: string;
text: string;
}

// parse the returned buffer and parse as JSON
const data = await view<GuestBookMessage[]>({
account: 'guest-book.testnet',
method: 'getMessages',
deps: { rpcProvider: getTestnetRpcProvider() },
});
```

Client method for requesting access keys:
```ts
import { getAccessKeys, getTestnetRpcProvider } from '@near-js/client';

const { fullAccessKeys, functionCallAccessKeys } = await getAccessKeys({
account: 'account.testnet',
deps: { rpcProvider: getTestnetRpcProvider() },
});
```

### Transactions
New `*Composer` classes facilitate transaction building and signing:
```ts
import {
getSignerFromKeystore,
getTestnetRpcProvider,
SignedTransactionComposer,
} from '@near-js/client';
import { KeyPairEd25519 } from '@near-js/crypto';
import { BrowserLocalStorageKeyStore } from '@near-js/keystores-browser';

const keystore = new BrowserLocalStorageKeyStore();
const signer = getSignerFromKeystore('account.testnet', 'testnet', keystore);

const oldPublicKey = await signer.getPublicKey();
const newKeyPair = KeyPairEd25519.fromRandom();

const composer = SignedTransactionComposer.init({
sender: 'account.testnet',
receiver: 'receiver.testnet',
deps: {
signer,
rpcProvider: getMainnetRpcProvider(),
}
});

// add new key and remove old key in single transaction
await composer
.addFullAccessKey(newKeyPair.publicKey)
.deleteKey(oldPublicKey.toString())
.signAndSend();

keystore.setKey('testnet', 'account.testnet', newKeyPair);
```

For convenience, there are also functions wrapping individual actions, e.g. `transfer`:
```ts
import {
getSignerFromKeystore,
getTestnetRpcProvider,
transfer,
} from '@near-js/client';
import { KeyPairEd25519 } from '@near-js/crypto';
import { BrowserLocalStorageKeyStore } from '@near-js/keystores-browser';

const keystore = new BrowserLocalStorageKeyStore();
const signer = getSignerFromKeystore('account.testnet', 'testnet', keystore);

await transfer({
sender: 'account.testnet',
receiver: 'receiver.testnet',
amount: 1000n, // in yoctoNear
deps: {
rpcProvider: getTestnetRpcProvider(),
signer,
}
});
```

# License

This repository is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
See [LICENSE](https://github.com/near/near-api-js/blob/master/LICENSE) and [LICENSE-APACHE](https://github.com/near/near-api-js/blob/master/LICENSE-APACHE) for details.
13 changes: 13 additions & 0 deletions packages/client/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default {
preset: 'ts-jest',
collectCoverage: true,
testEnvironment: 'node',
testRegex: "(/tests/.*|(\\.|/)(test|spec))\\.[jt]sx?$",
transform: {
'^.+\\.[tj]s$': ['ts-jest', {
tsconfig: {
allowJs: true,
},
}],
},
};
41 changes: 41 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@near-js/client",
"version": "0.0.1",
"description": "",
"main": "lib/esm/index.js",
"type": "module",
"scripts": {
"build": "pnpm compile:esm && pnpm compile:cjs",
"compile:esm": "tsc -p tsconfig.json",
"compile:cjs": "tsc -p tsconfig.cjs.json && cjsify ./lib/commonjs",
"lint": "eslint -c .eslintrc.yml src/**/*.ts --no-eslintrc --no-error-on-unmatched-pattern",
"lint:fix": "eslint -c .eslintrc.yml src/**/*.ts --no-eslintrc --no-error-on-unmatched-pattern --fix"
},
"dependencies": {
"@near-js/crypto": "workspace:*",
"@near-js/keystores": "workspace:*",
"@near-js/providers": "workspace:*",
"@near-js/signers": "workspace:*",
"@near-js/transactions": "workspace:*",
"@near-js/types": "workspace:*",
"@near-js/utils": "workspace:*",
"@noble/hashes": "1.3.3",
"isomorphic-fetch": "3.0.0"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "20.0.0",
"build": "workspace:*",
"tsconfig": "workspace:*",
"typescript": "5.4.5"
},
"files": [
"lib"
],
"exports": {
"require": "./lib/commonjs/index.cjs",
"import": "./lib/esm/index.js"
}
}
22 changes: 22 additions & 0 deletions packages/client/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const DEFAULT_META_TRANSACTION_BLOCK_HEIGHT_TTL = 100n;
export const MAX_GAS = 300000000000000n;

export const PAGODA_RPC_ENDPOINTS_MAINNET = [
'https://rpc.near.org',
'https://rpc.mainnet.pagoda.co',
];

export const PAGODA_RPC_ARCHIVAL_ENDPOINTS_MAINNET = [
'https://archival-rpc.near.org',
];

export const PAGODA_RPC_ENDPOINTS_TESTNET = [
'https://rpc.testnet.near.org',
'https://rpc.testnet.pagoda.co',
];

export const PAGODA_RPC_ARCHIVAL_ENDPOINTS_TESTNET = [
'https://archival-rpc.testnet.near.org',
];

export const KITWALLET_FUNDED_TESTNET_ACCOUNT_ENDPOINT = 'https://testnet-api.kitwallet.app/account';
17 changes: 17 additions & 0 deletions packages/client/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CurveType, KeyPair, KeyPairString } from '@near-js/crypto';

/**
* Generate a random key pair for the specified elliptic curve
* @param curve elliptic curve (e.g. `ed25519`)
*/
export function generateRandomKeyPair(curve: CurveType) {
return KeyPair.fromRandom(curve);
}

/**
* Parse a signing key pair from a private key string
* @param privateKey private key string
*/
export function parseKeyPair(privateKey: KeyPairString) {
return KeyPair.fromString(privateKey);
}
38 changes: 38 additions & 0 deletions packages/client/src/funded_account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { FinalExecutionOutcome } from '@near-js/types';
import fetch from 'isomorphic-fetch';

import { KITWALLET_FUNDED_TESTNET_ACCOUNT_ENDPOINT } from './constants';
import { NewAccountParams } from './interfaces';

interface CreateFundedTestnetAccountParams extends NewAccountParams {
endpointUrl?: string;
}

/**
* Create a new funded testnet account via faucet REST endpoint
* (e.g. create `new.testnet` with a preset amount of Near)
* @param endpointUrl REST endpoint for the funded testnet account creation (defaults to the current Near Contract Helper endpoint)
* @param newAccount name of the created account
* @param newPublicKey public key for the created account's initial full access key
*/
export async function createFundedTestnetAccount({
newAccount,
newPublicKey,
endpointUrl = KITWALLET_FUNDED_TESTNET_ACCOUNT_ENDPOINT,
}: CreateFundedTestnetAccountParams) {
const res = await fetch(endpointUrl, {
method: 'POST',
body: JSON.stringify({
newAccountId: newAccount,
newAccountPublicKey: newPublicKey,
}),
headers: { 'Content-Type': 'application/json' },
});

const { ok, status } = res;
if (!ok) {
throw new Error(`Failed to create account on ${endpointUrl}: ${status}`);
}

return await res.json() as FinalExecutionOutcome;
}
10 changes: 10 additions & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { formatNearAmount } from '@near-js/utils';

export * from './constants';
export * from './crypto';
export * from './funded_account';
export * from './interfaces';
export * from './providers';
export * from './signing';
export * from './transactions';
export * from './view';
33 changes: 33 additions & 0 deletions packages/client/src/interfaces/dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { PublicKey } from '@near-js/crypto';

import type { RpcQueryProvider } from './providers';
import { FullAccessKey, FunctionCallAccessKey } from './view';

interface Dependent<T> {
deps: T;
}

export interface MessageSigner {
getPublicKey(): Promise<PublicKey>;
signMessage(m: Uint8Array): Promise<Uint8Array>;
}

export interface AccessKeySigner extends MessageSigner {
getAccessKey(ignoreCache?: boolean): Promise<FullAccessKey | FunctionCallAccessKey>;
getNonce(ignoreCache?: boolean): Promise<bigint>;
getSigningAccount(): string;
}

interface RpcProviderDependent {
rpcProvider: RpcQueryProvider;
}

interface SignerDependent {
signer: MessageSigner;
}

export interface RpcProviderDependency extends Dependent<RpcProviderDependent> {}

export interface SignerDependency extends Dependent<SignerDependent> {}

export interface SignAndSendTransactionDependency extends Dependent<RpcProviderDependent & SignerDependent> {}
4 changes: 4 additions & 0 deletions packages/client/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './dependencies';
export * from './providers';
export * from './transactions';
export * from './view';
Loading

0 comments on commit 4661484

Please sign in to comment.