import { ArrayHelper , Blake2b } from "@iota/crypto.js" ; import { BigIntHelper , Converter , WriteStream } from "@iota/util.js" ; import bigInt , { BigInteger } from "big-integer" ; import { MAX_MESSAGE_LENGTH , serializeMessage } from "../binary/message" ; import type { IAddressOutputsResponse } from "../models/api/IAddressOutputsResponse" ; import type { IAddressResponse } from "../models/api/IAddressResponse" ; import type { IChildrenResponse } from "../models/api/IChildrenResponse" ; import type { IMessageIdResponse } from "../models/api/IMessageIdResponse" ; import type { IMessagesResponse } from "../models/api/IMessagesResponse" ; import type { IMilestoneResponse } from "../models/api/IMilestoneResponse" ; import type { IMilestoneUtxoChangesResponse } from "../models/api/IMilestoneUtxoChangesResponse" ; import type { IOutputResponse } from "../models/api/IOutputResponse" ; import type { IReceiptsResponse } from "../models/api/IReceiptsResponse" ; import type { IResponse } from "../models/api/IResponse" ; import type { ITipsResponse } from "../models/api/ITipsResponse" ; import type { IClient } from "../models/IClient" ; import type { IMessage } from "../models/IMessage" ; import type { IMessageMetadata } from "../models/IMessageMetadata" ; import type { INodeInfo } from "../models/INodeInfo" ; import type { IPeer } from "../models/IPeer" ; import type { IPowProvider } from "../models/IPowProvider" ; import type { ITreasury } from "../models/ITreasury" ; import { ClientError } from "./clientError" ; import type { SingleNodeClientOptions } from "./singleNodeClientOptions" ; /**
 * Client for API communication.
 */ export class SingleNodeClient implements IClient { internal private static readonly NONCE_ZERO : Uint8Array = new Uint8Array ( [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ) ; internal private readonly _endpoint : string ; internal private readonly _basePath : string ; internal private readonly _powProvider ? : IPowProvider ; internal private readonly _timeout ? : number ; internal private readonly _userName ? : string ; internal private readonly _password ? : string ; internal private readonly _headers ? : { [ id ] : string ; } ; param endpoint param options constructor ( endpoint , options ? ) { if ( ! endpoint ) { throw new Error ( "The endpoint can not be empty" ) ; } this . _endpoint = endpoint . replace ( /\/+$/ , "" ) ; this . _basePath = options ?. basePath ?? "/api/v1/" ; this . _powProvider = options ?. powProvider ; this . _timeout = options ?. timeout ; this . _userName = options ?. userName ; this . _password = options ?. password ; this . _headers = options ?. headers ; if ( this . _userName && this . _password && ! this . _endpoint . startsWith ( "https" ) ) { throw new Error ( "Basic authentication requires the endpoint to be https" ) ; } if ( this . _userName && this . _password && ( this . _headers ?. authorization || this . _headers ?. Authorization ) ) { throw new Error ( "You can not supply both user/pass and authorization header" ) ; } } returns public async health ( ) { const status = await this . fetchStatus ( "/health" ) ; if ( status === 200 ) { return true ; } else if ( status === 503 ) { return false ; } throw new ClientError ( "Unexpected response code" , "/health" , status ) ; } returns public async info ( ) { return this . fetchJson < never , INodeInfo > ( "get" , "info" ) ; } returns public async tips ( ) { return this . fetchJson < never , ITipsResponse > ( "get" , "tips" ) ; } param messageId returns public async message ( messageId ) { return this . fetchJson < never , IMessage > ( "get" , `messages/${ messageId }` ) ; } param messageId returns public async messageMetadata ( messageId ) { return this . fetchJson < never , IMessageMetadata > ( "get" , `messages/${ messageId }/metadata` ) ; } param messageId returns public async messageRaw ( messageId ) { return this . fetchBinary ( "get" , `messages/${ messageId }/raw` ) ; } param message returns public async messageSubmit ( message ) { let minPoWScore = 0 ; if ( this . _powProvider ) { const powInfo = await this . getPoWInfo ( ) ; minPoWScore = powInfo . minPoWScore ; if ( ! message . parentMessageIds || message . parentMessageIds . length === 0 ) { const tips = await this . tips ( ) ; message . parentMessageIds = tips . tipMessageIds ; } if ( ! message . networkId || message . networkId . length === 0 ) { message . networkId = powInfo . networkId . toString ( ) ; } } const writeStream = new WriteStream ( ) ; serializeMessage ( writeStream , message ) ; const messageBytes = writeStream . finalBytes ( ) ; if ( messageBytes . length > MAX_MESSAGE_LENGTH ) { throw new Error ( `The message length is ${ messageBytes . length }, which exceeds the maximum size of ${ MAX_MESSAGE_LENGTH }` ) ; } if ( this . _powProvider ) { const nonce = await this . _powProvider . pow ( messageBytes , minPoWScore ) ; message . nonce = nonce . toString ( ) ; } const response = await this . fetchJson < IMessage , IMessageIdResponse > ( "post" , "messages" , message ) ; return response . messageId ; } param message returns public async messageSubmitRaw ( message ) { if ( message . length > MAX_MESSAGE_LENGTH ) { throw new Error ( `The message length is ${ message . length }, which exceeds the maximum size of ${ MAX_MESSAGE_LENGTH }` ) ; } if ( this . _powProvider && ArrayHelper . equal ( message . slice ( - 8 ) , SingleNodeClient . NONCE_ZERO ) ) { const { networkId , minPoWScore } = await this . getPoWInfo ( ) ; BigIntHelper . write8 ( networkId , message , 0 ) ; const nonce = await this . _powProvider . pow ( message , minPoWScore ) ; BigIntHelper . write8 ( bigInt ( nonce ) , message , message . length - 8 ) ; } const response = await this . fetchBinary < IMessageIdResponse > ( "post" , "messages" , message ) ; return ( response as IMessageIdResponse ) . messageId ; } param indexationKey returns public async messagesFind ( indexationKey ) { return this . fetchJson < never , IMessagesResponse > ( "get" , `messages?index=${ typeof indexationKey === "string" ? Converter . utf8ToHex ( indexationKey ) : Converter . bytesToHex ( indexationKey ) }` ) ; } param messageId returns public async messageChildren ( messageId ) { return this . fetchJson < never , IChildrenResponse > ( "get" , `messages/${ messageId }/children` ) ; } param transactionId returns public async transactionIncludedMessage ( transactionId ) { return this . fetchJson < never , IMessage > ( "get" , `transactions/${ transactionId }/included-message` ) ; } param outputId returns public async output ( outputId ) { return this . fetchJson < never , IOutputResponse > ( "get" , `outputs/${ outputId }` ) ; } param addressBech32 returns public async address ( addressBech32 ) { return this . fetchJson < never , IAddressResponse > ( "get" , `addresses/${ addressBech32 }` ) ; } param addressBech32 param type param includeSpent returns public async addressOutputs ( addressBech32 , type ? , includeSpent ? ) { const queryParams = [ ] ; if ( type !== undefined ) { queryParams . push ( `type=${ type }` ) ; } if ( includeSpent !== undefined ) { queryParams . push ( `include-spent=${ includeSpent }` ) ; } return this . fetchJson < never , IAddressOutputsResponse > ( "get" , `addresses/${ addressBech32 }/outputs${ this . combineQueryParams ( queryParams ) }` ) ; } param addressEd25519 returns public async addressEd25519 ( addressEd25519 ) { if ( ! Converter . isHex ( addressEd25519 ) ) { throw new Error ( "The supplied address does not appear to be hex format" ) ; } return this . fetchJson < never , IAddressResponse > ( "get" , `addresses/ed25519/${ addressEd25519 }` ) ; } param addressEd25519 param type param includeSpent returns public async addressEd25519Outputs ( addressEd25519 , type ? , includeSpent ? ) { if ( ! Converter . isHex ( addressEd25519 ) ) { throw new Error ( "The supplied address does not appear to be hex format" ) ; } const queryParams = [ ] ; if ( type !== undefined ) { queryParams . push ( `type=${ type }` ) ; } if ( includeSpent !== undefined ) { queryParams . push ( `include-spent=${ includeSpent }` ) ; } return this . fetchJson < never , IAddressOutputsResponse > ( "get" , `addresses/ed25519/${ addressEd25519 }/outputs${ this . combineQueryParams ( queryParams ) }` ) ; } param index returns public async milestone ( index ) { return this . fetchJson < never , IMilestoneResponse > ( "get" , `milestones/${ index }` ) ; } param index returns public async milestoneUtxoChanges ( index ) { return this . fetchJson < never , IMilestoneUtxoChangesResponse > ( "get" , `milestones/${ index }/utxo-changes` ) ; } returns public async treasury ( ) { return this . fetchJson < never , ITreasury > ( "get" , "treasury" ) ; } param migratedAt returns public async receipts ( migratedAt ? ) { return this . fetchJson < never , IReceiptsResponse > ( "get" , `receipts${ migratedAt !== undefined ? `/${ migratedAt }` : "" }` ) ; } returns public async peers ( ) { return this . fetchJson < never , IPeer [ ] > ( "get" , "peers" ) ; } param multiAddress param alias returns public async peerAdd ( multiAddress , alias ? ) { return this . fetchJson < { multiAddress : string ; alias ? : string ; } , IPeer > ( "post" , "peers" , { multiAddress , alias } ) ; } param peerId returns public async peerDelete ( peerId ) { return this . fetchJson < never , void > ( "delete" , `peers/${ peerId }` ) ; } param peerId returns public async peer ( peerId ) { return this . fetchJson < never , IPeer > ( "get" , `peers/${ peerId }` ) ; } param route returns public async fetchStatus ( route ) { const response = await this . fetchWithTimeout ( "get" , route ) ; return response . status ; } param method param route param requestData returns public async fetchJson < T , U > ( method , route , requestData ? ) { const response = await this . fetchWithTimeout ( method , `${ this . _basePath }${ route }` , { "Content-Type" : "application/json" } , requestData ? JSON . stringify ( requestData ) : undefined ) ; let errorMessage ; let errorCode ; if ( response . ok ) { if ( response . status === 204 ) { return { } as U ; } try { const responseData = await response . json ( ) ; if ( responseData . error ) { errorMessage = responseData . error . message ; errorCode = responseData . error . code ; } else { return responseData . data ; } } catch { } } if ( ! errorMessage ) { try { const json = await response . json ( ) ; if ( json . error ) { errorMessage = json . error . message ; errorCode = json . error . code ; } } catch { } } if ( ! errorMessage ) { try { const text = await response . text ( ) ; if ( text . length > 0 ) { const match = /code=(\d+), message=(.*)/ . exec ( text ) ; if ( match ?. length === 3 ) { errorCode = match [ 1 ] ; errorMessage = match [ 2 ] ; } else { errorMessage = text ; } } } catch { } } throw new ClientError ( errorMessage ?? response . statusText , route , response . status , errorCode ?? response . status . toString ( ) ) ; } param method param route param requestData returns public async fetchBinary < T > ( method , route , requestData ? ) { const response = await this . fetchWithTimeout ( method , `${ this . _basePath }${ route }` , { "Content-Type" : "application/octet-stream" } , requestData ) ; let responseData ; if ( response . ok ) { if ( method === "get" ) { return new Uint8Array ( await response . arrayBuffer ( ) ) ; } responseData = await response . json ( ) ; if ( ! responseData ?. error ) { return responseData ?. data as T ; } } if ( ! responseData ) { responseData = await response . json ( ) ; } throw new ClientError ( responseData ?. error ?. message ?? response . statusText , route , response . status , responseData ?. error ?. code ) ; } param method param route param headers param requestData returns internal public async fetchWithTimeout ( method , route , headers ? , body ? ) { let controller ; let timerId ; if ( this . _timeout !== undefined ) { controller = new AbortController ( ) ; timerId = setTimeout ( ( ) => { if ( controller ) { controller . abort ( ) ; } } , this . _timeout ) ; } const finalHeaders = { } ; if ( this . _headers ) { for ( const header in this . _headers ) { finalHeaders [ header ] = this . _headers [ header ] ; } } if ( headers ) { for ( const header in headers ) { finalHeaders [ header ] = headers [ header ] ; } } if ( this . _userName && this . _password ) { const userPass = Converter . bytesToBase64 ( Converter . utf8ToBytes ( `${ this . _userName }:${ this . _password }` ) ) ; finalHeaders . Authorization = `Basic ${ userPass }` ; } try { const response = await fetch ( `${ this . _endpoint }${ route }` , { method , headers : finalHeaders , body , signal : controller ? controller . signal : undefined } ) ; return response ; } catch ( err ) { throw err instanceof Error && err . name === "AbortError" ? new Error ( "Timeout" ) : err ; } finally { if ( timerId ) { clearTimeout ( timerId ) ; } } } param queryParams returns public combineQueryParams ( queryParams ) { return queryParams . length > 0 ? `?${ queryParams . join ( "&" ) }` : "" ; } returns internal private async getPoWInfo ( ) { const nodeInfo = await this . info ( ) ; const networkIdBytes = Blake2b . sum256 ( Converter . utf8ToBytes ( nodeInfo . networkId ) ) ; return { networkId : BigIntHelper . read8 ( networkIdBytes , 0 ) , minPoWScore : nodeInfo . minPoWScore } ; } }