diff --git a/jest.config.js b/jest.config.js index 13160719..8e6ae184 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,4 @@ module.exports = { transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\](?!(axios|is-retry-allowed)).+\\.(js|jsx)$'], + testTimeout: 1000 * 30, }; diff --git a/src/client/mixin-client.ts b/src/client/mixin-client.ts index 61f41a08..15151105 100644 --- a/src/client/mixin-client.ts +++ b/src/client/mixin-client.ts @@ -1,5 +1,6 @@ import merge from 'lodash.merge'; import type { AxiosInstance } from 'axios'; +import { validate } from 'uuid'; import type Keystore from './types/keystore'; import type { HTTPConfig, RequestClient } from './types/client'; import { createAxiosClient, createRequestClient } from './utils/client'; @@ -52,6 +53,8 @@ export function MixinApi(config: HTTPConfig = {}): KeystoreClientReturnType & Re const requestClient = createRequestClient(axiosInstance); const { keystore } = config; + if (keystore && !keystore.user_id && keystore.client_id && validate(keystore.client_id)) keystore.user_id = keystore.client_id; + const keystoreClient = KeystoreClient(axiosInstance, keystore, config); return merge(keystoreClient, requestClient); diff --git a/src/client/types/client.ts b/src/client/types/client.ts index b6899076..11d24241 100644 --- a/src/client/types/client.ts +++ b/src/client/types/client.ts @@ -3,7 +3,7 @@ import Keystore from './keystore'; import { BlazeOptions } from './blaze'; export interface RequestConfig - extends Pick { + extends Partial> { responseCallback?: (rep: unknown) => void; retry?: number; } diff --git a/src/client/types/keystore.ts b/src/client/types/keystore.ts index 6634c687..586fb2d3 100644 --- a/src/client/types/keystore.ts +++ b/src/client/types/keystore.ts @@ -1,4 +1,5 @@ export interface Keystore { + client_id?: string; user_id: string; private_key: string; diff --git a/src/index.ts b/src/index.ts index 201ed2e3..3afd9d5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './client'; +export * from './mainnet'; export * from './mvm'; export * from './webview'; export * from './constant'; diff --git a/src/mainnet/client.ts b/src/mainnet/client.ts new file mode 100644 index 00000000..5745d36c --- /dev/null +++ b/src/mainnet/client.ts @@ -0,0 +1,97 @@ +import axios, { AxiosResponse } from 'axios'; +import axiosRetry, { isIdempotentRequestError } from 'axios-retry'; +import isRetryAllowed from 'is-retry-allowed'; +import type { RequestConfig } from '../client'; +import type { + NodeInfoRpcResponse, + SyncPoint, + SendRawTransactionRpcResponse, + TransactionRpcResponse, + UTXORpcResponse, + KeyRpcResponse, + SnapshotRpcResponse, + MintWorkRpcResponse, + MintDistributionRpcResponse, + NodeRpcResponse, + RoundRpcResponse, + RoundLinkRpcResponse, +} from './type'; + +axios.defaults.headers.post['Content-Type'] = 'application/json'; + +export const MixinMainnetRPC = 'https://rpc.mixin.dev'; + +export const MainnetRpcClient = (config: RequestConfig = {}) => { + const timeout = config?.timeout || 1000 * 10; + const retries = config?.retry || 5; + + const ins = axios.create({ + ...config, + timeout, + baseURL: MixinMainnetRPC, + }); + + ins.interceptors.response.use(async (res: AxiosResponse) => { + const { data, error } = res.data; + if (error) throw new Error(error); + return data; + }); + + ins.interceptors.response.use(undefined, async (e: any) => { + await config?.responseCallback?.(e); + + return Promise.reject(e); + }); + + axiosRetry(ins, { + retries, + shouldResetTimeout: true, + retryDelay: () => 500, + retryCondition: error => + (!error.response && + Boolean(error.code) && // Prevents retrying cancelled requests + isRetryAllowed(error)) || + isIdempotentRequestError(error), + }); + + return { + getInfo: (id = '1'): Promise => ins.post('/', { id, method: 'getinfo', params: [] }), + + dumpGraphHead: (id = '1'): Promise => ins.post('/', { id, method: 'dumpgraphhead', params: [] }), + + sendRawTransaction: (raw: string, id = '1'): Promise => + ins.post('/', { id, method: 'sendrawtransaction', params: [raw] }), + + getTransaction: (hash: string, id = '1'): Promise => ins.post('/', { id, method: 'gettransaction', params: [hash] }), + + getCacheTransaction: (hash: string, id = '1'): Promise => + ins.post('/', { id, method: 'getcachetransaction', params: [hash] }), + + getUTXO: (hash: string, index: string, id = '1'): Promise => ins.post('/', { id, method: 'getutxo', params: [hash, index] }), + + getKey: (key: string, id = '1'): Promise => ins.post('/', { id, method: 'getkey', params: [key] }), + + getSnapshot: (hash: string, id = '1'): Promise => ins.post('/', { id, method: 'getsnapshot', params: [hash] }), + + listSnapshots: (offset: string, count: string, sig: boolean, tx: boolean, id = '1'): Promise => + ins.post('/', { id, method: 'listsnapshots', params: [offset, count, sig, tx] }), + + listMintWorks: (offset: string, id = '1'): Promise => ins.post('/', { id, method: 'listmintworks', params: [offset] }), + + listMintDistributions: (offset: string, count: string, tx: boolean, id = '1'): Promise => + ins.post('/', { id, method: 'listmintdistributions', params: [offset, count, tx] }), + + listAllNodes: (threshold: string, state?: boolean, id = '1'): Promise => + ins.post('/', { id, method: 'listallnodes', params: [threshold, state] }), + + getRoundByNumber: (node: string, number: string, id = '1'): Promise => + ins.post('/', { id, method: 'getroundbynumber', params: [node, number] }), + + getRoundByHash: (hash: string, id = '1'): Promise => ins.post('/', { id, method: 'getroundbyhash', params: [hash] }), + + getRoundLink: (from: string, to: string, id = '1'): Promise => + ins.post('/', { id, method: 'getroundlink', params: [from, to] }), + }; +}; + +export default MainnetRpcClient; diff --git a/src/mainnet/index.ts b/src/mainnet/index.ts new file mode 100644 index 00000000..62c12a9d --- /dev/null +++ b/src/mainnet/index.ts @@ -0,0 +1,2 @@ +export * from './client'; +export * from './type'; diff --git a/src/mainnet/type.ts b/src/mainnet/type.ts new file mode 100644 index 00000000..4b1f4b71 --- /dev/null +++ b/src/mainnet/type.ts @@ -0,0 +1,190 @@ +import { Input, Output } from '../mvm/types/encoder'; + +type NodeState = 'PLEDGING' | 'ACCEPTED' | 'REMOVED' | 'CANCELLED'; + +type References = null | { + external: string; + self: string; +}; + +export interface NodeInfoRpcResponse { + network: string; + node: string; + version: string; + uptime: string; + epoch: string; + timestamp: string; + + mint?: Mint; + graph?: Gragh; + queue?: Queue; + metric?: Metric; +} + +export interface SyncPoint { + node: string; + round: number; + hash: string; + pool: { + count: number; + index: number; + }; +} + +export interface SendRawTransactionRpcResponse { + hash: string; +} + +export interface TransactionRpcResponse { + version: number; + asset: string; + inputs: Input[]; + outputs: Output[]; + extra: string; + hash: string; + hex: string; + snapshot?: string; +} + +export interface UTXORpcResponse { + type: string; + hash: string; + index: number; + amount: string; + + keys?: string[]; + script?: string; + mask?: string; + lock?: string; +} + +export interface KeyRpcResponse { + transaction: string; +} + +export interface SnapshotRpcResponse { + version: number; + node: string; + references: References; + round: number; + timestamp: number; + hash: string; + hex?: string; + topology?: number; + witness?: { + signature: string; + timestamp: number; + }; + + transaction: TransactionRpcResponse | string; + transactions?: TransactionRpcResponse[] | string[]; + + signature?: string; + signatures?: string[]; +} + +export interface MintWorkRpcResponse { + [key: string]: number[]; +} + +export interface MintDistributionRpcResponse { + batch: number; + group: string; + amount: string; + transaction: TransactionRpcResponse | string; +} + +export interface NodeRpcResponse { + id: string; + payee: string; + signer: string; + state: NodeState; + timestamp: number; + transaction: string; +} + +export interface RoundRpcResponse { + node: string; + hash: string; + start: number; + end: number; + number: number; + references: References; + snapshots: SnapshotRpcResponse[]; +} + +export interface RoundLinkRpcResponse { + link: number; +} + +interface Mint { + pool: string; + batch: number; + pledge: string; +} + +interface Gragh { + /** node state is 'PLEDGING' | 'ACCEPTED' */ + consensus: Node[]; + cache: { + [key: string]: CacheGraph; + }; + final: { + [key: string]: FinalGraph; + }; + sps: number; + topology: number; + tps: number; +} + +interface CacheGraph { + node: string; + round: number; + timestamp: number; + snapshots: SnapshotRpcResponse[]; + references: References; +} + +interface FinalGraph { + hash: string; + node: string; + round: number; + start: number; + end: number; +} + +interface Queue { + finals: number; + caches: number; + /** key is chain id */ + state: { + [key: string]: number[]; + }; +} + +interface Metric { + transport: { + received: MetricPool; + sent: MetricPool; + }; +} + +interface MetricPool { + ping: number; + authentication: number; + graph: number; + 'snapshot-confirm': number; + 'transaction-request': number; + transaction: number; + + 'snapshot-announcement': number; + 'snapshot-commitment': number; + 'transaciton-challenge': number; + 'snapshot-response': number; + 'snapshot-finalization': number; + commitments: number; + 'full-challenge': number; + + bundle: number; + 'gossip-neighbors': number; +} diff --git a/src/mvm/types/index.ts b/src/mvm/types/index.ts index 1666d6c4..85bf198d 100644 --- a/src/mvm/types/index.ts +++ b/src/mvm/types/index.ts @@ -3,5 +3,4 @@ export * from './encoder'; export * from './extra'; export * from './payment'; export * from './registry'; -export * from './transaction'; export * from './value'; diff --git a/src/mvm/types/transaction.ts b/src/mvm/types/transaction.ts deleted file mode 100644 index a56ac891..00000000 --- a/src/mvm/types/transaction.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Input, Output } from './encoder'; - -export interface Transaction { - hash?: string; - snapshot?: string; - signatures?: { - [key: number]: string; - }; - aggregated?: { - signers: number[]; - signature: string; - }; - - version?: number; - asset: string; - inputs?: Input[]; - outputs?: Output[]; - extra: string; -} diff --git a/test/mixin/rpc.test.ts b/test/mixin/rpc.test.ts new file mode 100644 index 00000000..3be72698 --- /dev/null +++ b/test/mixin/rpc.test.ts @@ -0,0 +1,31 @@ +import { MainnetRpcClient } from '../../src'; + +const client = MainnetRpcClient(); + +describe('Tests for rpc', () => { + test('Test for rpc', async () => { + const nodeInfo = await client.getInfo(); + expect(nodeInfo.node).toEqual('3bdca8b8a593d9c5882532735622170bce2a1452d47cdeee5f950800593ef0bb'); + + const utxo = await client.getUTXO('fb4f927f3f04bdf34fcd26056e84214edb7e8c9737ddf4c7e6829caaf8e54e27', '1'); + expect(utxo.hash).toEqual('fb4f927f3f04bdf34fcd26056e84214edb7e8c9737ddf4c7e6829caaf8e54e27'); + + const tx = await client.getTransaction('1453961d4f880775fa94c604b630a3662e99d83187b8f33e25e59d9ae4d8c4b6'); + expect(tx.hash).toEqual('1453961d4f880775fa94c604b630a3662e99d83187b8f33e25e59d9ae4d8c4b6'); + + const key = await client.getKey('29ecfd9fb1eca4d9375da58fd526ae91b22a244d6ee48b81f8f7915530239324'); + expect(key.transaction).toEqual('1453961d4f880775fa94c604b630a3662e99d83187b8f33e25e59d9ae4d8c4b6'); + + const snapshot = await client.getSnapshot('2714982c8c675f42f1198af2c8d1c5f7444b4f42abf75e4ec7c1e541802e47d7'); + expect(snapshot.hash).toEqual('2714982c8c675f42f1198af2c8d1c5f7444b4f42abf75e4ec7c1e541802e47d7'); + + const snapshots = await client.listSnapshots('0', '10', true, true); + expect(snapshots.length).toEqual(10); + + const roundByNumber = await client.getRoundByNumber('f3aa49c73bd64cec574aad27870968c8c3be40d0537040ff0252f35b8507de3f', '362153'); + expect(roundByNumber.number).toEqual(362153); + + const roundByHash = await client.getRoundByHash('5691c08eef98d40e2efb3a770c0257e1ffb45e480184a4349c9423ca2ac9fd30'); + expect(roundByHash.number).toEqual(362153); + }); +});