Create higher level IndexedTx

This commit is contained in:
Simon Warta 2020-03-02 13:46:35 +01:00
parent 01758aadb2
commit 5bf3a191b3
9 changed files with 106 additions and 71 deletions

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/camelcase */
import { CosmWasmClient, findSequenceForSignedTx, SearchTxFilter, TxsResponse, types } from "@cosmwasm/sdk";
import { CosmWasmClient, findSequenceForSignedTx, IndexedTx, SearchTxFilter, types } from "@cosmwasm/sdk";
import {
Account,
AccountQuery,
@ -71,11 +70,11 @@ function deduplicate<T>(input: ReadonlyArray<T>, comparator: (a: T, b: T) => num
}
/** Compares transaxtion by height. If the height is equal, compare by hash to ensure deterministic order */
function compareByHeightAndHash(a: TxsResponse, b: TxsResponse): number {
function compareByHeightAndHash(a: IndexedTx, b: IndexedTx): number {
if (a.height === b.height) {
return a.txhash.localeCompare(b.txhash);
return a.hash.localeCompare(b.hash);
} else {
return parseInt(a.height, 10) - parseInt(b.height, 10);
return a.height - b.height;
}
}
@ -248,9 +247,10 @@ export class CosmWasmConnection implements BlockchainConnection {
}
public async getBlockHeader(height: number): Promise<BlockHeader> {
const { block_id, block } = await this.cosmWasmClient.getBlock(height);
// eslint-disable-next-line @typescript-eslint/camelcase
const { block_id: blockId, block } = await this.cosmWasmClient.getBlock(height);
return {
id: block_id.hash as BlockId,
id: blockId.hash as BlockId,
height: parseInt(block.header.height, 10),
time: new ReadonlyDate(block.header.time),
transactionCount: block.data.txs?.length || 0,
@ -337,13 +337,13 @@ export class CosmWasmConnection implements BlockchainConnection {
const filter: SearchTxFilter = { minHeight: minHeight, maxHeight: maxHeight };
let txs: readonly TxsResponse[];
let txs: readonly IndexedTx[];
if (id) {
txs = await this.cosmWasmClient.searchTx({ id: id }, filter);
} else if (height) {
txs = await this.cosmWasmClient.searchTx({ height: height }, filter);
} else if (sentFromOrTo) {
const pendingRequests = new Array<Promise<readonly TxsResponse[]>>();
const pendingRequests = new Array<Promise<readonly IndexedTx[]>>();
pendingRequests.push(this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter));
for (const contract of this.erc20Tokens.map(token => token.contractAddress)) {
const searchBySender = [
@ -371,7 +371,7 @@ export class CosmWasmConnection implements BlockchainConnection {
}
const responses = await Promise.all(pendingRequests);
const allResults = responses.reduce((accumulator, results) => accumulator.concat(results), []);
txs = deduplicate(allResults, (a, b) => a.txhash.localeCompare(b.txhash)).sort(compareByHeightAndHash);
txs = deduplicate(allResults, (a, b) => a.hash.localeCompare(b.hash)).sort(compareByHeightAndHash);
} else {
throw new Error("Unsupported query");
}
@ -460,11 +460,11 @@ export class CosmWasmConnection implements BlockchainConnection {
}
private parseAndPopulateTxResponseUnsigned(
response: TxsResponse,
response: IndexedTx,
): ConfirmedTransaction<UnsignedTransaction> | FailedTransaction {
return parseTxsResponseUnsigned(
this.chainId,
parseInt(response.height, 10),
response.height,
response,
this.bankTokens,
this.erc20Tokens,
@ -472,7 +472,7 @@ export class CosmWasmConnection implements BlockchainConnection {
}
private async parseAndPopulateTxResponseSigned(
response: TxsResponse,
response: IndexedTx,
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
const firstMsg = response.tx.value.msg.find(() => true);
if (!firstMsg) throw new Error("Got transaction without a first message. What is going on here?");
@ -503,7 +503,7 @@ export class CosmWasmConnection implements BlockchainConnection {
return parseTxsResponseSigned(
this.chainId,
parseInt(response.height, 10),
response.height,
nonce,
response,
this.bankTokens,

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/camelcase */
import { types } from "@cosmwasm/sdk";
import { IndexedTx, types } from "@cosmwasm/sdk";
import { Address, Algorithm, isSendTransaction, SendTransaction, TokenTicker } from "@iov/bcp";
import { Encoding } from "@iov/encoding";
import { assert } from "@iov/utils";
@ -281,10 +281,11 @@ describe("decode", () => {
describe("parseTxsResponseUnsigned", () => {
it("works", () => {
const currentHeight = 2923;
const txsResponse = {
height: "2823",
txhash: testdata.txId,
raw_log: '[{"msg_index":0,"success":true,"log":""}]',
const txsResponse: IndexedTx = {
height: 2823,
hash: testdata.txId,
rawLog: '[{"msg_index":0,"success":true,"log":""}]',
logs: [],
tx: cosmoshub.tx,
timestamp: "2020-02-14T11:35:41Z",
};
@ -310,10 +311,11 @@ describe("decode", () => {
describe("parseTxsResponseSigned", () => {
it("works", () => {
const currentHeight = 2923;
const txsResponse = {
height: "2823",
txhash: testdata.txId,
raw_log: '[{"msg_index":0,"success":true,"log":""}]',
const txsResponse: IndexedTx = {
height: 2823,
hash: testdata.txId,
rawLog: '[{"msg_index":0,"success":true,"log":""}]',
logs: [],
tx: cosmoshub.tx,
timestamp: "2020-02-14T11:35:41Z",
};

View File

@ -1,4 +1,4 @@
import { TxsResponse, types } from "@cosmwasm/sdk";
import { IndexedTx, types } from "@cosmwasm/sdk";
import {
Address,
Algorithm,
@ -180,17 +180,16 @@ export function parseSignedTx(
export function parseTxsResponseUnsigned(
chainId: ChainId,
currentHeight: number,
response: TxsResponse,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedTransaction<UnsignedTransaction> {
const height = parseInt(response.height, 10);
return {
transaction: parseUnsignedTx(response.tx.value, chainId, tokens, erc20Tokens),
height: height,
confirmations: currentHeight - height + 1,
transactionId: response.txhash as TransactionId,
log: response.raw_log,
height: response.height,
confirmations: currentHeight - response.height + 1,
transactionId: response.hash as TransactionId,
log: response.rawLog,
};
}
@ -198,16 +197,15 @@ export function parseTxsResponseSigned(
chainId: ChainId,
currentHeight: number,
nonce: Nonce,
response: TxsResponse,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedAndSignedTransaction<UnsignedTransaction> {
const height = parseInt(response.height, 10);
return {
...parseSignedTx(response.tx.value, chainId, nonce, tokens, erc20Tokens),
height: height,
confirmations: currentHeight - height + 1,
transactionId: response.txhash as TransactionId,
log: response.raw_log,
height: response.height,
confirmations: currentHeight - response.height + 1,
transactionId: response.hash as TransactionId,
log: response.rawLog,
};
}

View File

@ -1,4 +1,4 @@
import { TxsResponse, types } from "@cosmwasm/sdk";
import { IndexedTx, types } from "@cosmwasm/sdk";
import {
Amount,
ChainId,
@ -43,7 +43,7 @@ export declare function parseSignedTx(
export declare function parseTxsResponseUnsigned(
chainId: ChainId,
currentHeight: number,
response: TxsResponse,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedTransaction<UnsignedTransaction>;
@ -51,7 +51,7 @@ export declare function parseTxsResponseSigned(
chainId: ChainId,
currentHeight: number,
nonce: Nonce,
response: TxsResponse,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedAndSignedTransaction<UnsignedTransaction>;

View File

@ -92,8 +92,8 @@ describe("CosmWasmClient.searchTx", () => {
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -144,8 +144,8 @@ describe("CosmWasmClient.searchTx", () => {
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -163,7 +163,7 @@ describe("CosmWasmClient.searchTx", () => {
// Check basic structure of all results
for (const result of results) {
const msg = fromOneElementArray(result.tx.value.msg);
assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`);
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
expect(
msg.value.to_address === postedSend.sender || msg.value.from_address == postedSend.sender,
).toEqual(true);
@ -172,8 +172,8 @@ describe("CosmWasmClient.searchTx", () => {
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -189,7 +189,7 @@ describe("CosmWasmClient.searchTx", () => {
// Check basic structure of all results
for (const result of results) {
const msg = fromOneElementArray(result.tx.value.msg);
assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`);
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
expect(
msg.value.to_address === postedSend.recipient || msg.value.from_address == postedSend.recipient,
).toEqual(true);
@ -198,8 +198,8 @@ describe("CosmWasmClient.searchTx", () => {
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -273,15 +273,15 @@ describe("CosmWasmClient.searchTx", () => {
// Check basic structure of all results
for (const result of results) {
const msg = fromOneElementArray(result.tx.value.msg);
assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`);
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
expect(msg.value.to_address).toEqual(postedSend.recipient);
}
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -301,7 +301,7 @@ describe("CosmWasmClient.searchTx", () => {
const msg = fromOneElementArray(result.tx.value.msg);
assert(
isMsgExecuteContract(msg) || isMsgInstantiateContract(msg),
`${result.txhash} (at ${result.height}) not an execute or instantiate msg`,
`${result.hash} (at ${result.height}) not an execute or instantiate msg`,
);
}
@ -322,8 +322,8 @@ describe("CosmWasmClient.searchTx", () => {
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedExecute.height.toString(),
txhash: postedExecute.hash,
height: postedExecute.height,
hash: postedExecute.hash,
tx: postedExecute.tx,
}),
);
@ -344,15 +344,15 @@ describe("CosmWasmClient.searchTx", () => {
// Check basic structure of all results
for (const result of results) {
const msg = fromOneElementArray(result.tx.value.msg);
assert(isMsgExecuteContract(msg), `${result.txhash} (at ${result.height}) not an execute msg`);
assert(isMsgExecuteContract(msg), `${result.hash} (at ${result.height}) not an execute msg`);
expect(msg.value.contract).toEqual(postedExecute.contract);
}
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedExecute.height.toString(),
txhash: postedExecute.hash,
height: postedExecute.height,
hash: postedExecute.hash,
tx: postedExecute.tx,
}),
);

View File

@ -2,7 +2,7 @@ import { Sha256 } from "@iov/crypto";
import { Encoding } from "@iov/encoding";
import { Log, parseLogs } from "./logs";
import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient";
import { BlockResponse, BroadcastMode, RestClient } from "./restclient";
import { CosmosSdkAccount, CosmosSdkTx, StdTx } from "./types";
export interface GetNonceResult {
@ -92,6 +92,20 @@ export interface ContractDetails extends Contract {
readonly initMsg: object;
}
/** A transaction that is indexed as part of the transaction history */
export interface IndexedTx {
readonly height: number;
readonly hash: string;
readonly rawLog: string;
readonly logs: readonly Log[];
readonly tx: CosmosSdkTx;
/** The gas limit as set by the user */
readonly gasWanted?: number;
/** The gas used by the execution */
readonly gasUsed?: number;
readonly timestamp: string;
}
export class CosmWasmClient {
protected readonly restClient: RestClient;
@ -153,7 +167,7 @@ export class CosmWasmClient {
}
}
public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise<readonly TxsResponse[]> {
public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise<readonly IndexedTx[]> {
const minHeight = filter.minHeight || 0;
const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER;
@ -163,7 +177,7 @@ export class CosmWasmClient {
return `${originalQuery}&tx.minheight=${minHeight}&tx.maxheight=${maxHeight}`;
}
let txs: readonly TxsResponse[];
let txs: readonly IndexedTx[];
if (isSearchByIdQuery(query)) {
txs = await this.txsQuery(`tx.hash=${query.id}`);
} else if (isSearchByHeightQuery(query)) {
@ -180,8 +194,8 @@ export class CosmWasmClient {
const sent = await this.txsQuery(sentQuery);
const received = await this.txsQuery(receivedQuery);
const sentHashes = sent.map(t => t.txhash);
txs = [...sent, ...received.filter(t => !sentHashes.includes(t.txhash))];
const sentHashes = sent.map(t => t.hash);
txs = [...sent, ...received.filter(t => !sentHashes.includes(t.hash))];
} else if (isSearchByTagsQuery(query)) {
const rawQuery = withFilters(query.tags.map(t => `${t.key}=${t.value}`).join("&"));
txs = await this.txsQuery(rawQuery);
@ -190,10 +204,7 @@ export class CosmWasmClient {
}
// backend sometimes messes up with min/max height filtering
const filtered = txs.filter(tx => {
const txHeight = parseInt(tx.height, 10);
return txHeight >= minHeight && txHeight <= maxHeight;
});
const filtered = txs.filter(tx => tx.height >= minHeight && tx.height <= maxHeight);
return filtered;
}
@ -303,7 +314,7 @@ export class CosmWasmClient {
}
}
private async txsQuery(query: string): Promise<readonly TxsResponse[]> {
private async txsQuery(query: string): Promise<readonly IndexedTx[]> {
// TODO: we need proper pagination support
const limit = 100;
const result = await this.restClient.txsQuery(`${query}&limit=${limit}`);
@ -313,6 +324,15 @@ export class CosmWasmClient {
`Found more results on the backend than we can process currently. Results: ${result.total_count}, supported: ${limit}`,
);
}
return result.txs;
return result.txs.map(
(restItem): IndexedTx => ({
height: parseInt(restItem.height, 10),
hash: restItem.txhash,
rawLog: restItem.raw_log,
logs: parseLogs(restItem.logs || []),
tx: restItem.tx,
timestamp: restItem.timestamp,
}),
);
}
}

View File

@ -12,6 +12,7 @@ export {
Contract,
CosmWasmClient,
GetNonceResult,
IndexedTx,
PostTxResult,
SearchByHeightQuery,
SearchByIdQuery,

View File

@ -1,5 +1,5 @@
import { Log } from "./logs";
import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient";
import { BlockResponse, BroadcastMode, RestClient } from "./restclient";
import { CosmosSdkAccount, CosmosSdkTx, StdTx } from "./types";
export interface GetNonceResult {
readonly accountNumber: number;
@ -63,6 +63,19 @@ export interface ContractDetails extends Contract {
/** Argument passed on initialization of the contract */
readonly initMsg: object;
}
/** A transaction that is indexed as part of the transaction history */
export interface IndexedTx {
readonly height: number;
readonly hash: string;
readonly rawLog: string;
readonly logs: readonly Log[];
readonly tx: CosmosSdkTx;
/** The gas limit as set by the user */
readonly gasWanted?: number;
/** The gas used by the execution */
readonly gasUsed?: number;
readonly timestamp: string;
}
export declare class CosmWasmClient {
protected readonly restClient: RestClient;
constructor(url: string, broadcastMode?: BroadcastMode);
@ -86,7 +99,7 @@ export declare class CosmWasmClient {
* @param height The height of the block. If undefined, the latest height is used.
*/
getBlock(height?: number): Promise<BlockResponse>;
searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise<readonly TxsResponse[]>;
searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise<readonly IndexedTx[]>;
postTx(tx: StdTx): Promise<PostTxResult>;
getCodes(): Promise<readonly Code[]>;
getCodeDetails(codeId: number): Promise<CodeDetails>;

View File

@ -11,6 +11,7 @@ export {
Contract,
CosmWasmClient,
GetNonceResult,
IndexedTx,
PostTxResult,
SearchByHeightQuery,
SearchByIdQuery,