diff --git a/packages/libs/chainweb-node-client/etc/chainweb-node-client.api.md b/packages/libs/chainweb-node-client/etc/chainweb-node-client.api.md index 19b3fe9406..89de06536e 100644 --- a/packages/libs/chainweb-node-client/etc/chainweb-node-client.api.md +++ b/packages/libs/chainweb-node-client/etc/chainweb-node-client.api.md @@ -23,6 +23,9 @@ export type ChainwebChainId = (typeof CHAINS)[number]; // @alpha export type ChainwebNetworkId = 'mainnet01' | 'testnet04' | 'testnet05' | 'development'; +// @alpha (undocumented) +export type ClientRequestInit = Omit; + // @alpha (undocumented) export function convertIUnsignedTransactionToNoSig(transaction: IUnsignedCommand): ICommand; @@ -86,9 +89,7 @@ export interface ILocalCommandResult { } // @alpha (undocumented) -export interface ILocalOptions { - // (undocumented) - headers?: Record; +export interface ILocalOptions extends ClientRequestInit { // (undocumented) preflight?: boolean; // (undocumented) @@ -155,9 +156,7 @@ export interface ISPVRequestBody { } // @alpha -export function listen(requestBody: IListenRequestBody, apiHost: string, { headers }?: { - headers?: Record; -}): Promise; +export function listen(requestBody: IListenRequestBody, apiHost: string, requestInit?: ClientRequestInit): Promise; // @alpha (undocumented) export type ListenResponse = ICommandResult; @@ -166,10 +165,9 @@ export type ListenResponse = ICommandResult; export function local(requestBody: LocalRequestBody, apiHost: string, options?: T): Promise>; // @alpha -export function localRaw(requestBody: LocalRequestBody, apiHost: string, { preflight, signatureVerification, headers, }: { +export function localRaw(requestBody: LocalRequestBody, apiHost: string, { preflight, signatureVerification, ...requestInit }: ILocalOptions & { signatureVerification: boolean; preflight: boolean; - headers?: Record; }): Promise; // @alpha (undocumented) @@ -196,33 +194,36 @@ export function parseResponse(response: Response): Promise; export function parseResponseTEXT(response: Response): Promise; // @alpha -export function poll(requestBody: IPollRequestBody, apiHost: string, confirmationDepth?: number, { headers }?: { - headers?: Record; -}): Promise; +export function poll(requestBody: IPollRequestBody, apiHost: string, confirmationDepth?: number, requestInit?: ClientRequestInit): Promise; // @alpha -export function send(requestBody: ISendRequestBody, apiHost: string, { headers }?: { - headers?: Record; -}): Promise; +export function send(requestBody: ISendRequestBody, apiHost: string, requestInit?: ClientRequestInit): Promise; // @alpha export type SendResponse = IRequestKeys; // @alpha -export function spv(requestBody: ISPVRequestBody, apiHost: string, { headers }?: { - headers?: Record; -}): Promise; +export function spv(requestBody: ISPVRequestBody, apiHost: string, requestInit?: ClientRequestInit): Promise; // @alpha export type SPVResponse = SPVProof; // @alpha -export function stringifyAndMakePOSTRequest(body: T, headers?: Record): { - headers: { - 'Content-Type': string; - }; +export function stringifyAndMakePOSTRequest(body: T, requestInit?: ClientRequestInit): { method: string; body: string; + cache?: RequestCache | undefined; + credentials?: RequestCredentials | undefined; + headers?: HeadersInit | undefined; + integrity?: string | undefined; + keepalive?: boolean | undefined; + mode?: RequestMode | undefined; + priority?: RequestPriority | undefined; + redirect?: RequestRedirect | undefined; + referrer?: string | undefined; + referrerPolicy?: ReferrerPolicy | undefined; + signal?: AbortSignal | null | undefined; + window?: null | undefined; }; // (No @packageDocumentation comment for this package) diff --git a/packages/libs/chainweb-node-client/src/listen.ts b/packages/libs/chainweb-node-client/src/listen.ts index ab911615bf..35d53ac48a 100644 --- a/packages/libs/chainweb-node-client/src/listen.ts +++ b/packages/libs/chainweb-node-client/src/listen.ts @@ -1,4 +1,5 @@ import type { ICommandResult, IListenRequestBody } from './interfaces/PactAPI'; +import type { ClientRequestInit } from './local'; import { parseResponse } from './parseResponse'; import { stringifyAndMakePOSTRequest } from './stringifyAndMakePOSTRequest'; import { fetch } from './utils/fetch'; @@ -13,9 +14,9 @@ import { fetch } from './utils/fetch'; export async function listen( requestBody: IListenRequestBody, apiHost: string, - { headers }: { headers?: Record } = {}, + requestInit?: ClientRequestInit, ): Promise { - const request = stringifyAndMakePOSTRequest(requestBody, headers); + const request = stringifyAndMakePOSTRequest(requestBody, requestInit); const listenUrl = new URL(`${apiHost}/api/v1/listen`); const response = await fetch(listenUrl.toString(), request); diff --git a/packages/libs/chainweb-node-client/src/local.ts b/packages/libs/chainweb-node-client/src/local.ts index ace884eb88..6ca7a72789 100644 --- a/packages/libs/chainweb-node-client/src/local.ts +++ b/packages/libs/chainweb-node-client/src/local.ts @@ -13,10 +13,14 @@ import { fetch } from './utils/fetch'; /** * @alpha */ -export interface ILocalOptions { +export type ClientRequestInit = Omit; + +/** + * @alpha + */ +export interface ILocalOptions extends ClientRequestInit { preflight?: boolean; signatureVerification?: boolean; - headers?: Record; } /** @@ -45,7 +49,7 @@ export async function local( const { signatureVerification = true, preflight = true, - headers = {}, + ...requestInit } = options ?? {}; if (!signatureVerification) { @@ -56,7 +60,7 @@ export async function local( const result = await localRaw(body, apiHost, { preflight, signatureVerification, - headers, + ...requestInit, }); return parsePreflight(result); @@ -78,14 +82,13 @@ export async function localRaw( { preflight, signatureVerification, - headers = {}, - }: { + ...requestInit + }: ILocalOptions & { signatureVerification: boolean; preflight: boolean; - headers?: Record; }, ): Promise { - const request = stringifyAndMakePOSTRequest(requestBody, headers); + const request = stringifyAndMakePOSTRequest(requestBody, requestInit); const localUrlWithQueries = new URL(`${apiHost}/api/v1/local`); localUrlWithQueries.searchParams.append('preflight', preflight.toString()); diff --git a/packages/libs/chainweb-node-client/src/poll.ts b/packages/libs/chainweb-node-client/src/poll.ts index 5e501f0c06..a22662a552 100644 --- a/packages/libs/chainweb-node-client/src/poll.ts +++ b/packages/libs/chainweb-node-client/src/poll.ts @@ -1,4 +1,5 @@ import type { IPollRequestBody, IPollResponse } from './interfaces/PactAPI'; +import type { ClientRequestInit } from './local'; import { parseResponse } from './parseResponse'; import { stringifyAndMakePOSTRequest } from './stringifyAndMakePOSTRequest'; import { fetch } from './utils/fetch'; @@ -18,9 +19,9 @@ export async function poll( requestBody: IPollRequestBody, apiHost: string, confirmationDepth = 0, - { headers }: { headers?: Record } = {}, + requestInit?: ClientRequestInit, ): Promise { - const request = stringifyAndMakePOSTRequest(requestBody, headers); + const request = stringifyAndMakePOSTRequest(requestBody, requestInit); const pollUrl = new URL(`${apiHost}/api/v1/poll`); if (confirmationDepth > 0) { pollUrl.searchParams.append( diff --git a/packages/libs/chainweb-node-client/src/send.ts b/packages/libs/chainweb-node-client/src/send.ts index 6e15519d71..4d24e2844a 100644 --- a/packages/libs/chainweb-node-client/src/send.ts +++ b/packages/libs/chainweb-node-client/src/send.ts @@ -1,4 +1,5 @@ import type { ISendRequestBody, SendResponse } from './interfaces/PactAPI'; +import type { ClientRequestInit } from './local'; import { parseResponse } from './parseResponse'; import { stringifyAndMakePOSTRequest } from './stringifyAndMakePOSTRequest'; import { fetch } from './utils/fetch'; @@ -16,9 +17,9 @@ import { fetch } from './utils/fetch'; export async function send( requestBody: ISendRequestBody, apiHost: string, - { headers }: { headers?: Record } = {}, + requestInit?: ClientRequestInit, ): Promise { - const request = stringifyAndMakePOSTRequest(requestBody, headers); + const request = stringifyAndMakePOSTRequest(requestBody, requestInit); const sendUrl = new URL(`${apiHost}/api/v1/send`); const response = await fetch(sendUrl.toString(), request); diff --git a/packages/libs/chainweb-node-client/src/spv.ts b/packages/libs/chainweb-node-client/src/spv.ts index 6392af5754..c469aa648c 100644 --- a/packages/libs/chainweb-node-client/src/spv.ts +++ b/packages/libs/chainweb-node-client/src/spv.ts @@ -1,4 +1,5 @@ import type { ISPVRequestBody, SPVResponse } from './interfaces/PactAPI'; +import type { ClientRequestInit } from './local'; import { parseResponseTEXT } from './parseResponseTEXT'; import { stringifyAndMakePOSTRequest } from './stringifyAndMakePOSTRequest'; import { fetch } from './utils/fetch'; @@ -14,9 +15,9 @@ import { fetch } from './utils/fetch'; export async function spv( requestBody: ISPVRequestBody, apiHost: string, - { headers }: { headers?: Record } = {}, + requestInit?: ClientRequestInit, ): Promise { - const request = stringifyAndMakePOSTRequest(requestBody, headers); + const request = stringifyAndMakePOSTRequest(requestBody, requestInit); const spvUrl = new URL(`${apiHost}/spv`); const response = await fetch(spvUrl.toString(), request); diff --git a/packages/libs/chainweb-node-client/src/stringifyAndMakePOSTRequest.ts b/packages/libs/chainweb-node-client/src/stringifyAndMakePOSTRequest.ts index 442df48513..9f2422efc2 100644 --- a/packages/libs/chainweb-node-client/src/stringifyAndMakePOSTRequest.ts +++ b/packages/libs/chainweb-node-client/src/stringifyAndMakePOSTRequest.ts @@ -1,3 +1,5 @@ +import type { ClientRequestInit } from './local'; + /** * Formats API request body to use with `fetch` function. * @@ -7,13 +9,10 @@ */ export function stringifyAndMakePOSTRequest( body: T, - headers: Record = {}, + requestInit?: ClientRequestInit, ) { return { - headers: { - 'Content-Type': 'application/json', - ...headers, - }, + ...requestInit, method: 'POST', body: JSON.stringify(body), }; diff --git a/packages/libs/client/etc/client.api.md b/packages/libs/client/etc/client.api.md index 7bce33bf60..f95952972d 100644 --- a/packages/libs/client/etc/client.api.md +++ b/packages/libs/client/etc/client.api.md @@ -6,6 +6,7 @@ import { ChainId } from '@kadena/types'; import type Client from '@walletconnect/sign-client'; +import type { ClientRequestInit } from '@kadena/chainweb-node-client'; import { ICap } from '@kadena/types'; import { ICommand } from '@kadena/types'; import { ICommandResult } from '@kadena/chainweb-node-client'; @@ -82,9 +83,9 @@ export const getHostUrl: (hostBaseUrl: string) => ({ networkId, chainId }: INetw // @public (undocumented) export interface IBaseClient { - createSpv: (transactionDescriptor: ITransactionDescriptor, targetChainId: ChainId) => Promise; - getStatus: (transactionDescriptors: ITransactionDescriptor[] | ITransactionDescriptor) => Promise; - listen: (transactionDescriptor: ITransactionDescriptor) => Promise; + createSpv: (transactionDescriptor: ITransactionDescriptor, targetChainId: ChainId, options?: ClientRequestInit) => Promise; + getStatus: (transactionDescriptors: ITransactionDescriptor[] | ITransactionDescriptor, options?: ClientRequestInit) => Promise; + listen: (transactionDescriptor: ITransactionDescriptor, options?: ClientRequestInit) => Promise; local: (transaction: LocalRequestBody, options?: T) => Promise>; pollCreateSpv: (transactionDescriptor: ITransactionDescriptor, targetChainId: ChainId, options?: IPollOptions) => Promise; pollStatus: (transactionDescriptors: ITransactionDescriptor[] | ITransactionDescriptor, options?: IPollOptions) => IPollRequestPromise; @@ -118,16 +119,16 @@ export type ICapabilityItem = ICap; // @public export interface IClient extends IBaseClient { - dirtyRead: (transaction: IUnsignedCommand) => Promise; + dirtyRead: (transaction: IUnsignedCommand, options?: ClientRequestInit) => Promise; // @deprecated - getPoll: (transactionDescriptors: ITransactionDescriptor[] | ITransactionDescriptor) => Promise; + getPoll: (transactionDescriptors: ITransactionDescriptor[] | ITransactionDescriptor, options?: ClientRequestInit) => Promise; pollOne: (transactionDescriptor: ITransactionDescriptor, options?: IPollOptions) => Promise; - preflight: (transaction: ICommand | IUnsignedCommand) => Promise; - runPact: (code: string, data: Record, option: INetworkOptions) => Promise; + preflight: (transaction: ICommand | IUnsignedCommand, options?: ClientRequestInit) => Promise; + runPact: (code: string, data: Record, option: ClientRequestInit & INetworkOptions) => Promise; // @deprecated send: ISubmit; - signatureVerification: (transaction: ICommand) => Promise; - submitOne: (transaction: ICommand) => Promise; + signatureVerification: (transaction: ICommand, options?: ClientRequestInit) => Promise; + submitOne: (transaction: ICommand, options?: ClientRequestInit) => Promise; } export { ICommand } @@ -168,8 +169,8 @@ export interface ICreateClient { networkId: string; type?: 'local' | 'send' | 'poll' | 'listen' | 'spv'; }) => string | { - host: string; - headers: Record; + hostUrl: string; + requestInit: ClientRequestInit; }, defaults?: { confirmationDepth?: number; }): IClient; @@ -281,17 +282,17 @@ export interface IPartialPactCommand extends AllPartial { } // @public -export interface IPollOptions { +export interface IPollOptions extends ClientRequestInit { // (undocumented) confirmationDepth?: number; - // (undocumented) - headers?: Record; // Warning: (ae-incompatible-release-tags) The symbol "interval" is marked as @public, but its signature references "Milliseconds" which is marked as @alpha // // (undocumented) interval?: Milliseconds; // (undocumented) - onPoll?: (id: string) => void; + onPoll?: (id: string | undefined, error: any) => void; + // (undocumented) + onResult?: (requestKey: string, result: ICommandResult) => void; // Warning: (ae-incompatible-release-tags) The symbol "timeout" is marked as @public, but its signature references "Milliseconds" which is marked as @alpha // // (undocumented) @@ -441,8 +442,8 @@ export function isSignedTransaction(command: IUnsignedCommand | ICommand): comma // @public (undocumented) export interface ISubmit { - (transaction: ICommand): Promise; - (transactionList: ICommand[]): Promise; + (transaction: ICommand, options?: ClientRequestInit): Promise; + (transactionList: ICommand[], options?: ClientRequestInit): Promise; } // @public (undocumented) diff --git a/packages/libs/client/src/client/api/runPact.ts b/packages/libs/client/src/client/api/runPact.ts index ed71c4fdf0..2ce8bcef5e 100644 --- a/packages/libs/client/src/client/api/runPact.ts +++ b/packages/libs/client/src/client/api/runPact.ts @@ -1,4 +1,7 @@ -import type { ICommandResult } from '@kadena/chainweb-node-client'; +import type { + ClientRequestInit, + ICommandResult, +} from '@kadena/chainweb-node-client'; import { local } from '@kadena/chainweb-node-client'; import { hash as blackHash } from '@kadena/cryptography-utils'; import { composePactCommand, execution } from '../../composePactCommand'; @@ -7,7 +10,7 @@ export function runPact( hostUrl: string, code: string, data: Record = {}, - requestOptions: { headers?: Record } = {}, + requestInit?: ClientRequestInit, ): Promise { const pactCommand = composePactCommand(execution(code), { payload: { exec: { data } }, @@ -24,7 +27,7 @@ export function runPact( { preflight: false, signatureVerification: false, - ...requestOptions, + ...requestInit, }, ); } diff --git a/packages/libs/client/src/client/api/spv.ts b/packages/libs/client/src/client/api/spv.ts index d19a2b390d..ad313afa52 100644 --- a/packages/libs/client/src/client/api/spv.ts +++ b/packages/libs/client/src/client/api/spv.ts @@ -1,4 +1,7 @@ -import type { SPVResponse } from '@kadena/chainweb-node-client'; +import type { + ClientRequestInit, + SPVResponse, +} from '@kadena/chainweb-node-client'; import { spv } from '@kadena/chainweb-node-client'; import type { ChainId } from '@kadena/types'; import type { IPollOptions } from '../interfaces/interfaces'; @@ -8,9 +11,9 @@ export async function getSpv( host: string, requestKey: string, targetChainId: ChainId, - requestOptions: { headers?: Record } = {}, + requestInit: ClientRequestInit = {}, ): Promise { - const proof = await spv({ requestKey, targetChainId }, host, requestOptions); + const proof = await spv({ requestKey, targetChainId }, host, requestInit); if (typeof proof !== 'string') throw new Error('PROOF_IS_NOT_AVAILABLE'); return proof; } @@ -22,7 +25,7 @@ export const pollSpv = ( pollingOptions?: IPollOptions, ): Promise => { const task = async (): Promise => - getSpv(host, requestKey, targetChainId); + getSpv(host, requestKey, targetChainId, pollingOptions); const retrySpv = retry(task); diff --git a/packages/libs/client/src/client/api/status.ts b/packages/libs/client/src/client/api/status.ts index 874fb8d5af..ec8d044c84 100644 --- a/packages/libs/client/src/client/api/status.ts +++ b/packages/libs/client/src/client/api/status.ts @@ -29,8 +29,10 @@ export const pollStatus: IPollStatus = ( timeout, interval, confirmationDepth = 0, - headers = {}, + onResult = () => {}, + ...requestInit } = options ?? {}; + const signal = requestInit.signal ?? undefined; let requestKeys = [...requestIds]; const prs: Record> = requestKeys.reduce( (acc, requestKey) => ({ @@ -40,19 +42,28 @@ export const pollStatus: IPollStatus = ( {}, ); const task = async (): Promise => { - requestKeys.forEach(onPoll); - const pollResponse = await poll({ requestKeys }, host, confirmationDepth, { - headers, - }); - Object.values(pollResponse).forEach((item) => { - prs[item.reqKey].resolve(item); - requestKeys = requestKeys.filter((key) => key !== item.reqKey); - }); + try { + requestKeys.forEach(onPoll); + const pollResponse = await poll( + { requestKeys }, + host, + confirmationDepth, + requestInit, + ); + Object.values(pollResponse).forEach((item) => { + prs[item.reqKey].resolve(item); + onResult(item.reqKey, item); + requestKeys = requestKeys.filter((key) => key !== item.reqKey); + }); + } catch (error) { + onPoll(undefined, error); + throw error; + } if (requestKeys.length > 0) { return Promise.reject(new Error('NOT_COMPLETED')); } }; - const retryStatus = retry(task); + const retryStatus = retry(task, signal); retryStatus({ interval, timeout }).catch((err) => { Object.values(prs).forEach((pr) => { diff --git a/packages/libs/client/src/client/client.ts b/packages/libs/client/src/client/client.ts index 6452d005e9..eda0fcf7fd 100644 --- a/packages/libs/client/src/client/client.ts +++ b/packages/libs/client/src/client/client.ts @@ -1,4 +1,5 @@ import type { + ClientRequestInit, ICommandResult, ILocalCommandResult, ILocalOptions, @@ -8,6 +9,7 @@ import type { } from '@kadena/chainweb-node-client'; import { listen, local, poll, send } from '@kadena/chainweb-node-client'; import type { ChainId, ICommand, IUnsignedCommand } from '@kadena/types'; +import { head } from '../integration-tests/helpers/fp-helpers'; import type { IPactCommand } from '../interfaces/IPactCommand'; import { runPact } from './api/runPact'; import { getSpv, pollSpv } from './api/spv'; @@ -17,6 +19,7 @@ import type { IPollOptions, IPollRequestPromise, } from './interfaces/interfaces'; +import { mergeOptions } from './utils/mergeOptions'; import { groupByHost, kadenaHostGenerator, @@ -49,7 +52,10 @@ export interface ISubmit { * @param transaction - The transaction to be submitted. * @returns A promise that resolves the transactionDescriptor {@link ITransactionDescriptor} */ - (transaction: ICommand): Promise; + ( + transaction: ICommand, + options?: ClientRequestInit, + ): Promise; /** * Submits one or more public (unencrypted) signed commands to the blockchain for execution. @@ -60,7 +66,10 @@ export interface ISubmit { * @param transactionList - The list of transactions to be submitted. * @returns A promise that resolves the transactionDescriptor {@link ITransactionDescriptor} */ - (transactionList: ICommand[]): Promise; + ( + transactionList: ICommand[], + options?: ClientRequestInit, + ): Promise; } /** @@ -118,6 +127,7 @@ export interface IBaseClient { */ getStatus: ( transactionDescriptors: ITransactionDescriptor[] | ITransactionDescriptor, + options?: ClientRequestInit, ) => Promise; /** @@ -130,6 +140,7 @@ export interface IBaseClient { */ listen: ( transactionDescriptor: ITransactionDescriptor, + options?: ClientRequestInit, ) => Promise; /** @@ -160,6 +171,7 @@ export interface IBaseClient { createSpv: ( transactionDescriptor: ITransactionDescriptor, targetChainId: ChainId, + options?: ClientRequestInit, ) => Promise; } @@ -174,6 +186,7 @@ export interface IClient extends IBaseClient { */ preflight: ( transaction: ICommand | IUnsignedCommand, + options?: ClientRequestInit, ) => Promise; /** @@ -182,7 +195,10 @@ export interface IClient extends IBaseClient { * @remarks * @see {@link IBaseClient.local | local() function} */ - signatureVerification: (transaction: ICommand) => Promise; + signatureVerification: ( + transaction: ICommand, + options?: ClientRequestInit, + ) => Promise; /** * An alias for `local` when both preflight and signatureVerification are `false`. @@ -191,7 +207,10 @@ export interface IClient extends IBaseClient { * @remarks * @see {@link IBaseClient.local | local() function} */ - dirtyRead: (transaction: IUnsignedCommand) => Promise; + dirtyRead: ( + transaction: IUnsignedCommand, + options?: ClientRequestInit, + ) => Promise; /** * Generates a command from the code and data, then sends it to the '/local' endpoint. @@ -201,7 +220,7 @@ export interface IClient extends IBaseClient { runPact: ( code: string, data: Record, - option: INetworkOptions, + option: ClientRequestInit & INetworkOptions, ) => Promise; /** @@ -216,7 +235,10 @@ export interface IClient extends IBaseClient { * Alias for `submit` that accepts only one transaction. useful when you want more precise type checking. * {@link IBaseClient.submit | submit() function} */ - submitOne: (transaction: ICommand) => Promise; + submitOne: ( + transaction: ICommand, + options?: ClientRequestInit, + ) => Promise; /** * Use {@link IBaseClient.getStatus | getStatus() function} @@ -226,6 +248,7 @@ export interface IClient extends IBaseClient { */ getPoll: ( transactionDescriptors: ITransactionDescriptor[] | ITransactionDescriptor, + options?: ClientRequestInit, ) => Promise; /** @@ -268,18 +291,19 @@ export interface ICreateClient { chainId: ChainId; networkId: string; type?: 'local' | 'send' | 'poll' | 'listen' | 'spv'; - }) => string | { hostUrl: string; headers: Record }, + }) => string | { hostUrl: string; requestInit: ClientRequestInit }, defaults?: { confirmationDepth?: number }, ): IClient; } const getHostData = ( - hostObject: string | { hostUrl: string; headers: Record }, + hostObject: string | { hostUrl: string; requestInit: ClientRequestInit }, ) => { const hostUrl = typeof hostObject === 'string' ? hostObject : hostObject.hostUrl; - const headers = typeof hostObject === 'object' ? hostObject.headers : {}; - return { hostUrl, headers }; + const requestInit = + typeof hostObject === 'object' ? hostObject.requestInit : {}; + return { hostUrl, requestInit }; }; /** @@ -300,13 +324,10 @@ export const createClient: ICreateClient = ( chainId: cmd.meta.chainId, networkId: cmd.networkId, }); - const { hostUrl, headers } = getHostData(hostObject); - return local(body, hostUrl, { - ...options, - headers: headers, - }); + const { hostUrl, requestInit } = getHostData(hostObject); + return local(body, hostUrl, mergeOptions(requestInit, options)); }, - submit: (async (body) => { + submit: (async (body, options) => { const isList = Array.isArray(body); const commands = isList ? body : [body]; const [first] = commands; @@ -319,11 +340,13 @@ export const createClient: ICreateClient = ( networkId: cmd.networkId, }); - const { hostUrl, headers } = getHostData(hostObject); + const { hostUrl, requestInit } = getHostData(hostObject); - const { requestKeys } = await send({ cmds: commands }, hostUrl, { - headers, - }); + const { requestKeys } = await send( + { cmds: commands }, + hostUrl, + mergeOptions(requestInit, options), + ); const transactionDescriptors = requestKeys.map((key) => ({ requestKey: key, @@ -343,20 +366,23 @@ export const createClient: ICreateClient = ( const results = groupByHost( requestsList.map(({ requestKey, chainId, networkId }) => { const hostObject = getHost({ chainId, networkId, type: 'poll' }); - const { hostUrl, headers } = getHostData(hostObject); - const host = JSON.stringify({ host: hostUrl, headers }); + const { hostUrl, requestInit } = getHostData(hostObject); return { requestKey, - host, + host: hostUrl, + requestInit, }; }), ).map(([host, requestKeys]) => { - const { hostUrl, headers } = JSON.parse(host); - return pollStatus(hostUrl, requestKeys, { - confirmationDepth, - ...options, - headers, - }); + const requestInit = requestKeys[0].requestInit; + return pollStatus( + host, + requestKeys.map((r) => r.requestKey), + { + confirmationDepth, + ...mergeOptions(requestInit, options), + }, + ); }); // merge all of the result in one object @@ -364,7 +390,7 @@ export const createClient: ICreateClient = ( return mergedPollRequestPromises; }, - async getStatus(transactionDescriptors) { + async getStatus(transactionDescriptors, options?: ClientRequestInit) { const requestsList = Array.isArray(transactionDescriptors) ? transactionDescriptors : [transactionDescriptors]; @@ -373,14 +399,23 @@ export const createClient: ICreateClient = ( groupByHost( requestsList.map(({ requestKey, chainId, networkId }) => { const hostObject = getHost({ chainId, networkId, type: 'poll' }); - const { hostUrl, headers } = getHostData(hostObject); - const host = JSON.stringify({ host: hostUrl, headers }); + const { hostUrl, requestInit } = getHostData(hostObject); return { requestKey, - host, + host: hostUrl, + requestInit, }; }), - ).map(([hostUrl, requestKeys]) => poll({ requestKeys }, hostUrl)), + ).map(([hostUrl, requestKeys]) => { + const requestInit = requestKeys[0].requestInit; + + return poll( + { requestKeys: requestKeys.map((r) => r.requestKey) }, + hostUrl, + undefined, + mergeOptions(requestInit, options), + ); + }), ); // merge all of the result in one object @@ -389,57 +424,75 @@ export const createClient: ICreateClient = ( return mergedResults; }, - async listen({ requestKey, chainId, networkId }) { + async listen({ requestKey, chainId, networkId }, options) { const hostObject = getHost({ chainId, networkId, type: 'listen' }); - const { hostUrl, headers } = getHostData(hostObject); - const result = await listen({ listen: requestKey }, hostUrl, { headers }); + const { hostUrl, requestInit } = getHostData(hostObject); + const result = await listen( + { listen: requestKey }, + hostUrl, + mergeOptions(requestInit, options), + ); return result; }, pollCreateSpv({ requestKey, chainId, networkId }, targetChainId, options) { const hostObject = getHost({ chainId, networkId, type: 'spv' }); - const { hostUrl, headers } = getHostData(hostObject); - return pollSpv(hostUrl, requestKey, targetChainId, { - ...options, - headers, - }); + const { hostUrl, requestInit } = getHostData(hostObject); + return pollSpv( + hostUrl, + requestKey, + targetChainId, + mergeOptions(requestInit, options), + ); }, - async createSpv({ requestKey, chainId, networkId }, targetChainId) { + async createSpv( + { requestKey, chainId, networkId }, + targetChainId, + options, + ) { const hostObject = getHost({ chainId, networkId, type: 'spv' }); - const { hostUrl, headers } = getHostData(hostObject); - return getSpv(hostUrl, requestKey, targetChainId, { headers }); + const { hostUrl, requestInit } = getHostData(hostObject); + return getSpv( + hostUrl, + requestKey, + targetChainId, + mergeOptions(requestInit, options), + ); }, }; return { ...client, submitOne: client.submit, - preflight(body) { + preflight(body, options) { return client.local(body, { + ...options, preflight: true, signatureVerification: true, }); }, - signatureVerification(body) { + signatureVerification(body, options) { return client.local(body, { + ...options, preflight: false, signatureVerification: true, }); }, - dirtyRead(body) { + dirtyRead(body, options) { return client.local(body, { + ...options, preflight: false, signatureVerification: false, }); }, runPact: (code, data, options) => { const hostObject = getHost(options); - const { hostUrl, headers } = getHostData(hostObject); + const { hostUrl, requestInit } = getHostData(hostObject); if (hostUrl === '') throw new Error('NO_HOST_URL'); - return runPact(hostUrl, code, data, { headers }); + return runPact(hostUrl, code, data, mergeOptions(requestInit, options)); }, send: client.submit, getPoll: client.getStatus, diff --git a/packages/libs/client/src/client/interfaces/interfaces.ts b/packages/libs/client/src/client/interfaces/interfaces.ts index 6d79b5d01a..822c7502d6 100644 --- a/packages/libs/client/src/client/interfaces/interfaces.ts +++ b/packages/libs/client/src/client/interfaces/interfaces.ts @@ -1,4 +1,9 @@ +import type { + ClientRequestInit, + ICommandResult, +} from '@kadena/chainweb-node-client'; import type { ChainId } from '@kadena/types'; +import type { ITransactionDescriptor } from '../client'; /** * @public @@ -17,17 +22,20 @@ export type Milliseconds = number & { _brand?: 'milliseconds' }; * Options for any polling action on {@link IClient} * @public */ -export interface IPollOptions { - onPoll?: (id: string) => void; +export interface IPollOptions extends ClientRequestInit { + onPoll?: (id: string | undefined, error: any) => void; + onResult?: (requestKey: string, result: ICommandResult) => void; timeout?: Milliseconds; interval?: Milliseconds; confirmationDepth?: number; - headers?: Record; } /** * @public */ export type IPollRequestPromise = Promise> & { + /** + * @deprecated pass callback to {@link IPollOptions.onResult} instead + */ requests: Record>; }; diff --git a/packages/libs/client/src/client/utils/mergeOptions.ts b/packages/libs/client/src/client/utils/mergeOptions.ts new file mode 100644 index 0000000000..377e812f0a --- /dev/null +++ b/packages/libs/client/src/client/utils/mergeOptions.ts @@ -0,0 +1,33 @@ +export function mergeOptions | undefined>( + first: T, + second: T, +): T { + if (!first) return second; + if (!second) return first; + const merged: T = { ...second }; + Object.entries(first).forEach(([key, value]) => { + if (merged[key] === undefined) { + merged[key] = value; + return; + } + if (Array.isArray(merged[key])) { + merged[key] = [ + ...(Array.isArray(value) ? value : [value]), + ...(merged[key] as Array), + ]; + return; + } + if ( + value !== null && + typeof merged[key] === 'object' && + typeof value === 'object' + ) { + merged[key] = mergeOptions( + value as Record, + merged[key] as Record, + ); + return; + } + }); + return merged; +} diff --git a/packages/libs/client/src/client/utils/retry.ts b/packages/libs/client/src/client/utils/retry.ts index 7ea325f60e..d70c607394 100644 --- a/packages/libs/client/src/client/utils/retry.ts +++ b/packages/libs/client/src/client/utils/retry.ts @@ -20,16 +20,22 @@ const rejectAfter = ( export const retry = ( task: () => Promise, + signal?: AbortSignal, ) => async function runTask(options?: IPollOptions, count = 0): Promise { const startTime = Date.now(); const { timeout = 1000 * 60 * 3, interval = 5000 } = options ?? {}; - const rejectTimer = rejectAfter(timeout); try { const result = await Promise.race([ + new Promise((resolve, reject) => { + if (signal?.aborted === true) { + reject(new Error('ABORTED')); + } + signal?.addEventListener('abort', () => reject(new Error('ABORTED'))); + }), rejectTimer.promise, // sleep for 1ms to let the timeout promise reject first. sleep(1) @@ -41,7 +47,10 @@ export const retry = ( ]); return result as T; } catch (error) { - if (error !== undefined && error.message === 'TIME_OUT_REJECT') { + if ( + error !== undefined && + (error.message === 'TIME_OUT_REJECT' || error.message === 'ABORTED') + ) { throw error; } diff --git a/packages/libs/client/src/client/utils/utils.ts b/packages/libs/client/src/client/utils/utils.ts index 5e35c2f7d0..a5628e9680 100644 --- a/packages/libs/client/src/client/utils/utils.ts +++ b/packages/libs/client/src/client/utils/utils.ts @@ -1,3 +1,4 @@ +import type { ClientRequestInit } from '@kadena/chainweb-node-client'; import type { INetworkOptions, IPollRequestPromise, @@ -156,12 +157,16 @@ export const groupByHost = ( items: Array<{ requestKey: string; host: string; + requestInit?: ClientRequestInit; }>, -): [string, string[]][] => { - const byHost = new Map(); - items.forEach(({ host: hostUrl, requestKey }) => { +): [string, { requestInit?: ClientRequestInit; requestKey: string }[]][] => { + const byHost = new Map< + string, + { requestInit?: ClientRequestInit; requestKey: string }[] + >(); + items.forEach(({ host: hostUrl, requestKey, requestInit }) => { const prev = byHost.get(hostUrl) ?? []; - byHost.set(hostUrl, [...prev, requestKey]); + byHost.set(hostUrl, [...prev, { requestInit, requestKey }]); }); return [...byHost.entries()]; };