Merge pull request #86 from confio/search-non-existent

Improve implementation of CosmWasmConnection.getTx
This commit is contained in:
Simon Warta 2020-02-13 15:43:52 +01:00 committed by GitHub
commit 843bed9e04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 53 additions and 62 deletions

View File

@ -5,11 +5,11 @@ import {
ChainId,
isBlockInfoPending,
isConfirmedTransaction,
isFailedTransaction,
isSendTransaction,
PubkeyBytes,
SendTransaction,
TokenTicker,
TransactionId,
TransactionState,
} from "@iov/bcp";
import { Random, Secp256k1 } from "@iov/crypto";
@ -21,7 +21,7 @@ import { CosmWasmCodec } from "./cosmwasmcodec";
import { CosmWasmConnection, TokenConfiguration } from "./cosmwasmconnection";
import * as testdata from "./testdata.spec";
const { fromBase64, toHex } = Encoding;
const { fromBase64 } = Encoding;
function pendingWithoutCosmos(): void {
if (!process.env.COSMOS_ENABLED) {
@ -264,6 +264,21 @@ describe("CosmWasmConnection", () => {
});
});
describe("getTx", () => {
it("throws for non-existent transaction", async () => {
pendingWithoutCosmos();
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000" as TransactionId;
await connection.getTx(nonExistentId).then(
() => fail("this must not succeed"),
error => expect(error).toMatch(/transaction does not exist/i),
);
connection.disconnect();
});
});
describe("integration tests", () => {
it("can post and get a transaction", async () => {
pendingWithoutCosmos();
@ -295,35 +310,25 @@ describe("CosmWasmConnection", () => {
expect(blockInfo.state).toEqual(TransactionState.Succeeded);
const getResponse = await connection.getTx(transactionId);
expect(getResponse).toBeTruthy();
expect(getResponse.transactionId).toEqual(transactionId);
if (isFailedTransaction(getResponse)) {
throw new Error("Expected transaction to succeed");
}
assert(isConfirmedTransaction(getResponse), "Expected transaction to succeed");
assert(getResponse.log, "Log must be available");
// we get a json response in the log for each msg, multiple events is good (transfer succeeded)
const [firstLog] = JSON.parse(getResponse.log);
expect(firstLog.events.length).toEqual(2);
const { transaction, signatures } = getResponse;
if (!isSendTransaction(transaction)) {
throw new Error("Expected send transaction");
}
expect(transaction.kind).toEqual(unsigned.kind);
expect(transaction.sender).toEqual(unsigned.sender);
expect(transaction.recipient).toEqual(unsigned.recipient);
expect(transaction.memo).toEqual(unsigned.memo);
expect(transaction.amount).toEqual(unsigned.amount);
expect(transaction.chainId).toEqual(unsigned.chainId);
const { transaction, signatures } = getResponse;
assert(isSendTransaction(transaction), "Expected send transaction");
expect(transaction).toEqual(unsigned);
expect(signatures.length).toEqual(1);
expect(signatures[0].nonce).toEqual(signed.signatures[0].nonce);
expect(signatures[0].pubkey.algo).toEqual(signed.signatures[0].pubkey.algo);
expect(toHex(signatures[0].pubkey.data)).toEqual(
toHex(Secp256k1.compressPubkey(signed.signatures[0].pubkey.data)),
);
expect(toHex(signatures[0].signature)).toEqual(
toHex(Secp256k1.trimRecoveryByte(signed.signatures[0].signature)),
);
expect(signatures[0]).toEqual({
nonce: -1, // Unfortunately this information is unavailable as previous implementation attempt is broken. See https://github.com/iov-one/iov-core/pull/1390
pubkey: {
algo: signed.signatures[0].pubkey.algo,
data: Secp256k1.compressPubkey(signed.signatures[0].pubkey.data),
},
signature: Secp256k1.trimRecoveryByte(signed.signatures[0].signature),
});
connection.disconnect();
});

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/camelcase */
import { CosmosAddressBech32Prefix, CosmWasmClient, RestClient, TxsResponse, types } from "@cosmwasm/sdk";
import { CosmosAddressBech32Prefix, CosmWasmClient, RestClient, TxsResponse } from "@cosmwasm/sdk";
import {
Account,
AccountQuery,
@ -231,15 +231,14 @@ export class CosmWasmConnection implements BlockchainConnection {
public async getTx(
id: TransactionId,
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
try {
// tslint:disable-next-line: deprecation
const response = await this.restClient.txsById(id);
return this.parseAndPopulateTxResponseSigned(response);
} catch (error) {
if (error.response.status === 404) {
const results = await this.cosmWasmClient.searchTx({ id: id });
switch (results.length) {
case 0:
throw new Error("Transaction does not exist");
}
throw error;
case 1:
return this.parseAndPopulateTxResponseSigned(results[0]);
default:
throw new Error("Got unexpected amount of search results");
}
}
@ -365,30 +364,10 @@ export class CosmWasmConnection implements BlockchainConnection {
private async parseAndPopulateTxResponseSigned(
response: TxsResponse,
): 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?");
// needed to get the (account_number, sequence) for the primary signature
let primarySignerAddress: string;
if (types.isMsgSend(firstMsg)) {
primarySignerAddress = firstMsg.value.from_address;
} else if (
types.isMsgStoreCode(firstMsg) ||
types.isMsgInstantiateContract(firstMsg) ||
types.isMsgExecuteContract(firstMsg)
) {
primarySignerAddress = firstMsg.value.sender;
} else {
throw new Error(`Got unsupported type of message: ${firstMsg.type}`);
}
// tslint:disable-next-line: deprecation
const accountForHeight = await this.restClient.authAccounts(primarySignerAddress, response.height);
const accountNumber = accountForHeight.result.value.account_number;
// this is technically not the proper sequence. maybe this causes issues for sig validation?
// leaving for now unless it causes issues
const sequence = accountForHeight.result.value.sequence - 1;
const nonce = accountToNonce(accountNumber, sequence);
// There is no known way to get the nonce that was used for signing a transaction.
// This information is nesessary for signature validation.
// TODO: fix
const nonce = -1 as Nonce;
const chainId = this.chainId();
return parseTxsResponseSigned(chainId, parseInt(response.height, 10), nonce, response, this.bankTokens);

View File

@ -202,6 +202,14 @@ describe("CosmWasmClient", () => {
);
});
it("can search by ID (non existent)", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000";
const result = await client.searchTx({ id: nonExistentId });
expect(result.length).toEqual(0);
});
it("can search by height", async () => {
pendingWithoutCosmos();
assert(posted, "value must be set in beforeAll()");

View File

@ -164,7 +164,7 @@ export class CosmWasmClient {
}
if (isSearchByIdQuery(query)) {
return [await this.restClient.txsById(query.id)];
return (await this.restClient.txs(`tx.hash=${query.id}`)).txs;
} else if (isSearchByHeightQuery(query)) {
return (await this.restClient.txs(`tx.height=${query.height}`)).txs;
} else if (isSearchBySentFromOrToQuery(query)) {

View File

@ -228,9 +228,8 @@ export class RestClient {
return Encoding.fromBase64((responseData as EncodeTxResponse).tx);
}
public async authAccounts(address: string, height?: string): Promise<AuthAccountsResponse> {
const path =
height === undefined ? `/auth/accounts/${address}` : `/auth/accounts/${address}?tx.height=${height}`;
public async authAccounts(address: string): Promise<AuthAccountsResponse> {
const path = `/auth/accounts/${address}`;
const responseData = await this.get(path);
if ((responseData as any).result.type !== "cosmos-sdk/Account") {
throw new Error("Unexpected response data format");

View File

@ -93,7 +93,7 @@ export declare class RestClient {
blocks(height: number): Promise<BlocksResponse>;
/** returns the amino-encoding of the transaction performed by the server */
encodeTx(tx: CosmosSdkTx): Promise<Uint8Array>;
authAccounts(address: string, height?: string): Promise<AuthAccountsResponse>;
authAccounts(address: string): Promise<AuthAccountsResponse>;
txs(query: string): Promise<SearchTxsResponse>;
txsById(id: string): Promise<TxsResponse>;
postTx(tx: Uint8Array): Promise<PostTxsResponse>;