Skip to content

Commit

Permalink
feat: txClient
Browse files Browse the repository at this point in the history
  • Loading branch information
bangjelkoski committed Jun 13, 2022
1 parent 3d18d29 commit efab6c8
Show file tree
Hide file tree
Showing 14 changed files with 579 additions and 36 deletions.
16 changes: 16 additions & 0 deletions packages/sdk-ts/src/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SHA256 } from 'jscrypto/SHA256'
import { RIPEMD160 } from 'jscrypto/RIPEMD160'
import { Base64 } from 'jscrypto/Base64'
import { Word32Array } from 'jscrypto'

export function hashToHex(data: string): string {
return SHA256.hash(Base64.parse(data)).toString().toUpperCase()
}

export function sha256(data: Uint8Array): Uint8Array {
return SHA256.hash(new Word32Array(data)).toUint8Array()
}

export function ripemd160(data: Uint8Array): Uint8Array {
return RIPEMD160.hash(new Word32Array(data)).toUint8Array()
}
1 change: 1 addition & 0 deletions packages/sdk-ts/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './numbers'
export * from './pagination'
export * from './address'
export * from './utf8'
export * from './crypto'
1 change: 1 addition & 0 deletions packages/tx-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@injectivelabs/chain-api": "^1.8.0-rc2",
"@injectivelabs/utils": "^1.0.2",
"google-protobuf": "^3.20.1",
"jscrypto": "^1.0.3",
"link-module-alias": "^1.2.0",
"sha3": "^2.1.4",
"shx": "^0.3.2"
Expand Down
30 changes: 30 additions & 0 deletions packages/tx-ts/src/client/TxClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { TxRaw } from '@injectivelabs/chain-api/cosmos/tx/v1beta1/tx_pb'
import { hashToHex } from '../utils/crypto'

export class TxClient {
/**
* Encode a transaction to base64-encoded protobuf
* @param tx transaction to encode
*/
public static encode(tx: TxRaw): string {
return Buffer.from(tx.serializeBinary()).toString('base64')
}

/**
* Decode a transaction from base64-encoded protobuf
* @param tx transaction string to decode
*/
public static decode(encodedTx: string): TxRaw {
return TxRaw.deserializeBinary(Buffer.from(encodedTx, 'base64'))
}

/**
* Get the transaction's hash
* @param tx transaction to hash
*/
public static async hash(tx: TxRaw): Promise<string> {
const txBytes = await TxClient.encode(tx)

return hashToHex(txBytes)
}
}
File renamed without changes.
281 changes: 281 additions & 0 deletions packages/tx-ts/src/client/TxRestClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
/* eslint-disable no-param-reassign */
/* eslint-disable camelcase */
import { TxRaw } from '@injectivelabs/chain-api/cosmos/tx/v1beta1/tx_pb'
import { HttpClient } from '@injectivelabs/utils'
import {
Wait,
Block,
Sync,
TxSuccess,
TxBroadcastResult,
TxError,
TxInfo,
TxResult,
BroadcastMode,
SimulationResponse,
WaitTxBroadcastResult,
SyncTxBroadcastResult,
BlockTxBroadcastResult,
TxSearchResult,
} from '../types/tx-rest-client'
import { APIParams, TxSearchOptions } from '../types/rest-client'
import { BlockInfo } from '../types/block'
import { TxClient } from './TxClient'
import { hashToHex } from '../utils/crypto'

export function isTxError<
C extends TxSuccess | TxError | {},
B extends Wait | Block | Sync,
T extends TxBroadcastResult<B, C>,
>(x: T): x is T & TxBroadcastResult<B, TxError> {
return (
(x as T & TxError).code !== undefined &&
(x as T & TxError).code !== 0 &&
(x as T & TxError).code !== '0'
)
}

export class TxRestClient {
public httpClient: HttpClient

constructor(endpoint: string) {
this.httpClient = new HttpClient(endpoint)
}

public async simulate(txRaw: TxRaw): Promise<any> {
try {
return await this.httpClient.post('cosmos/tx/v1beta1/simulate', {
tx_bytes: Buffer.from(txRaw.serializeBinary()).toString('base64'),
})
} catch (e: any) {
throw new Error(e.message)
}
}

public async txInfo(txHash: string, params: APIParams = {}): Promise<TxInfo> {
try {
const response = await this.getRaw<TxResult>(
`/cosmos/tx/v1beta1/txs/${txHash}`,
params,
)

return response.tx
} catch (e) {
throw new Error((e as any).message)
}
}

public async txInfosByHeight(height: number | undefined): Promise<TxInfo[]> {
const endpoint =
height !== undefined
? `/cosmos/base/tendermint/v1beta1/blocks/${height}`
: `/cosmos/base/tendermint/v1beta1/blocks/latest`

const blockInfo = await this.getRaw<BlockInfo>(endpoint)
const { txs } = blockInfo.block.data

if (!txs) {
return []
}

const txHashes = txs.map((txData) => hashToHex(txData))
const txInfos: TxInfo[] = []

for (const txhash of txHashes) {
txInfos.push(await this.txInfo(txhash))
}

return txInfos
}

public async simulateTx(txRaw: TxRaw): Promise<SimulationResponse> {
try {
txRaw.clearSignaturesList()

const response = await this.postRaw<SimulationResponse>(
'/cosmos/tx/v1beta1/simulate',
{
tx_bytes: TxClient.encode(txRaw),
},
)

return response
} catch (e: any) {
throw new Error(e.message)
}
}

private async broadcastTx<T>(
txRaw: TxRaw,
mode: BroadcastMode = BroadcastMode.Sync,
): Promise<T> {
try {
const response = await this.postRaw<T>('cosmos/tx/v1beta1/txs', {
tx_bytes: TxClient.encode(txRaw),
mode,
})

return response
} catch (e: any) {
throw new Error(e.message)
}
}

public async broadcast(
tx: TxRaw,
timeout = 30000,
): Promise<WaitTxBroadcastResult> {
const POLL_INTERVAL = 500
const { tx_response: txResponse } = await this.broadcastTx<{
tx_response: SyncTxBroadcastResult
}>(tx, BroadcastMode.Sync)

if ((txResponse as TxError).code !== 0) {
const result: WaitTxBroadcastResult = {
height: txResponse.height,
txhash: txResponse.txhash,
raw_log: txResponse.raw_log,
code: (txResponse as TxError).code,
codespace: (txResponse as TxError).codespace,
gas_used: 0,
gas_wanted: 0,
timestamp: '',
logs: [],
}

return result
}

for (let i = 0; i <= timeout / POLL_INTERVAL; i += 1) {
try {
const txInfo = await this.txInfo(txResponse.txhash)

return {
txhash: txInfo.txhash,
raw_log: txInfo.raw_log,
gas_wanted: parseInt(txInfo.gas_wanted, 10),
gas_used: parseInt(txInfo.gas_used, 10),
height: parseInt(txInfo.height, 10),
logs: txInfo.logs,
code: txInfo.code,
codespace: txInfo.codespace,
timestamp: txInfo.timestamp,
}
} catch (error) {
// Errors when transaction is not found.
}

await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL))
}

throw new Error(
`Transaction was not included in a block before timeout of ${timeout}ms`,
)
}

/**
* Broadcast the transaction using the "block" mode, waiting for its inclusion in the blockchain.
* @param tx transaction to broadcast
*/
public async broadcastBlock(tx: TxRaw): Promise<BlockTxBroadcastResult> {
const response = await this.broadcastTx<{
tx_response: BlockTxBroadcastResult
}>(tx, BroadcastMode.Block)

const { tx_response: txResponse } = response

return {
txhash: txResponse.txhash,
raw_log: txResponse.raw_log,
gas_wanted: txResponse.gas_wanted,
gas_used: txResponse.gas_used,
height: txResponse.height,
logs: txResponse.logs,
code: (txResponse as TxError).code,
codespace: (txResponse as TxError).codespace,
data: txResponse.data,
info: txResponse.info,
timestamp: txResponse.timestamp,
}
}

/**
* NOTE: This is not a synchronous function and is unconventionally named. This function
* can be await as it returns a `Promise`.
*
* Broadcast the transaction using the "sync" mode, returning after CheckTx() is performed.
* @param tx transaction to broadcast
*/
public async broadcastSync(tx: TxRaw): Promise<SyncTxBroadcastResult> {
const response = await this.broadcastTx<{
tx_response: BlockTxBroadcastResult
}>(tx, BroadcastMode.Sync)

const { tx_response: txResponse } = response

const blockResult: any = {
height: txResponse.height,
txhash: txResponse.txhash,
raw_log: txResponse.raw_log,
}

if ((txResponse as TxError).code) {
blockResult.code = (txResponse as TxError).code
}

if ((txResponse as TxError).codespace) {
blockResult.codespace = (txResponse as TxError).codespace
}

return blockResult
}

/**
* Search for transactions based on event attributes.
* @param options
*/
public async search(
options: Partial<TxSearchOptions>,
): Promise<TxSearchResult> {
const params = new URLSearchParams()

// build search params
options.events?.forEach((v) =>
params.append(
'events',
v.key === 'tx.height' ? `${v.key}=${v.value}` : `${v.key}='${v.value}'`,
),
)

delete options.events

Object.entries(options).forEach((v) => {
params.append(v[0], v[1] as string)
})

const response = await this.getRaw<TxSearchResult>(
`cosmos/tx/v1beta1/txs`,
params,
)

return response
}

private async postRaw<T>(
endpoint: string,
params: URLSearchParams | APIParams = {},
): Promise<T> {
return this.httpClient
.post<URLSearchParams | APIParams, { data: T }>(endpoint, params)
.then((d) => d.data)
}

private async getRaw<T>(
endpoint: string,
params: URLSearchParams | APIParams = {},
): Promise<T> {
return this.httpClient
.get<URLSearchParams | APIParams, { data: T }>(endpoint, params)
.then((d) => d.data)
}
}
5 changes: 3 additions & 2 deletions packages/tx-ts/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './grpc'
export * from './rest'
export * from './TxGrpcClient'
export * from './TxRestClient'
export * from './TxClient'
33 changes: 0 additions & 33 deletions packages/tx-ts/src/client/rest.ts

This file was deleted.

Loading

0 comments on commit efab6c8

Please sign in to comment.