Merge pull request #89 from confio/findSequenceForSignedTx
Find sequence for signed tx by bruteforce
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { CosmosAddressBech32Prefix } from "@cosmwasm/sdk";
|
import { CosmosAddressBech32Prefix, decodeSignature } from "@cosmwasm/sdk";
|
||||||
import {
|
import {
|
||||||
Address,
|
Address,
|
||||||
Algorithm,
|
Algorithm,
|
||||||
ChainId,
|
ChainId,
|
||||||
isBlockInfoPending,
|
isBlockInfoPending,
|
||||||
|
isBlockInfoSucceeded,
|
||||||
isConfirmedTransaction,
|
isConfirmedTransaction,
|
||||||
isSendTransaction,
|
isSendTransaction,
|
||||||
PubkeyBytes,
|
PubkeyBytes,
|
||||||
@@ -12,12 +13,13 @@ import {
|
|||||||
TransactionId,
|
TransactionId,
|
||||||
TransactionState,
|
TransactionState,
|
||||||
} from "@iov/bcp";
|
} from "@iov/bcp";
|
||||||
import { Random, Secp256k1 } from "@iov/crypto";
|
import { Random, Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto";
|
||||||
import { Bech32, Encoding } from "@iov/encoding";
|
import { Bech32, Encoding } from "@iov/encoding";
|
||||||
import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol";
|
import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol";
|
||||||
import { assert } from "@iov/utils";
|
import { assert } from "@iov/utils";
|
||||||
|
|
||||||
import { CosmWasmConnection, TokenConfiguration } from "./cosmwasmconnection";
|
import { CosmWasmConnection, TokenConfiguration } from "./cosmwasmconnection";
|
||||||
|
import { encodeFullSignature } from "./encode";
|
||||||
import * as testdata from "./testdata.spec";
|
import * as testdata from "./testdata.spec";
|
||||||
|
|
||||||
const { fromBase64 } = Encoding;
|
const { fromBase64 } = Encoding;
|
||||||
@@ -34,22 +36,24 @@ function makeRandomAddress(): Address {
|
|||||||
return Bech32.encode(defaultPrefix, Random.getBytes(20)) as Address;
|
return Bech32.encode(defaultPrefix, Random.getBytes(20)) as Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const faucet = {
|
||||||
|
mnemonic:
|
||||||
|
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone",
|
||||||
|
path: HdPaths.cosmos(0),
|
||||||
|
pubkey: {
|
||||||
|
algo: Algorithm.Secp256k1,
|
||||||
|
data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ") as PubkeyBytes,
|
||||||
|
},
|
||||||
|
address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" as Address,
|
||||||
|
};
|
||||||
|
|
||||||
describe("CosmWasmConnection", () => {
|
describe("CosmWasmConnection", () => {
|
||||||
const cosm = "COSM" as TokenTicker;
|
const cosm = "COSM" as TokenTicker;
|
||||||
const httpUrl = "http://localhost:1317";
|
const httpUrl = "http://localhost:1317";
|
||||||
const defaultChainId = "cosmos:testing" as ChainId;
|
const defaultChainId = "cosmos:testing" as ChainId;
|
||||||
const defaultEmptyAddress = "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address;
|
const defaultEmptyAddress = "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address;
|
||||||
const faucetMnemonic =
|
|
||||||
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone";
|
|
||||||
const faucetPath = HdPaths.cosmos(0);
|
|
||||||
const defaultRecipient = "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" as Address;
|
const defaultRecipient = "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" as Address;
|
||||||
const faucetAccount = {
|
|
||||||
pubkey: {
|
|
||||||
algo: Algorithm.Secp256k1,
|
|
||||||
data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ") as PubkeyBytes,
|
|
||||||
},
|
|
||||||
address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" as Address,
|
|
||||||
};
|
|
||||||
const unusedAccount = {
|
const unusedAccount = {
|
||||||
pubkey: {
|
pubkey: {
|
||||||
algo: Algorithm.Secp256k1,
|
algo: Algorithm.Secp256k1,
|
||||||
@@ -256,13 +260,105 @@ describe("CosmWasmConnection", () => {
|
|||||||
it("has a pubkey when getting account with transactions", async () => {
|
it("has a pubkey when getting account with transactions", async () => {
|
||||||
pendingWithoutCosmos();
|
pendingWithoutCosmos();
|
||||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||||
const account = await connection.getAccount({ address: faucetAccount.address });
|
const account = await connection.getAccount({ address: faucet.address });
|
||||||
expect(account?.pubkey).toEqual(faucetAccount.pubkey);
|
expect(account?.pubkey).toEqual(faucet.pubkey);
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getTx", () => {
|
describe("getTx", () => {
|
||||||
|
it("can get a recently posted transaction", async () => {
|
||||||
|
pendingWithoutCosmos();
|
||||||
|
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||||
|
const profile = new UserProfile();
|
||||||
|
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic));
|
||||||
|
const senderIdentity = await profile.createIdentity(wallet.id, defaultChainId, faucet.path);
|
||||||
|
const senderAddress = connection.codec.identityToAddress(senderIdentity);
|
||||||
|
|
||||||
|
const unsigned = await connection.withDefaultFee<SendTransaction>({
|
||||||
|
kind: "bcp/send",
|
||||||
|
chainId: defaultChainId,
|
||||||
|
sender: senderAddress,
|
||||||
|
recipient: defaultRecipient,
|
||||||
|
memo: "My first payment",
|
||||||
|
amount: {
|
||||||
|
quantity: "75000",
|
||||||
|
fractionalDigits: 6,
|
||||||
|
tokenTicker: cosm,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const nonce = await connection.getNonce({ address: senderAddress });
|
||||||
|
const signed = await profile.signTransaction(senderIdentity, unsigned, connection.codec, nonce);
|
||||||
|
const postableBytes = connection.codec.bytesToPost(signed);
|
||||||
|
const response = await connection.postTx(postableBytes);
|
||||||
|
const { transactionId } = response;
|
||||||
|
await response.blockInfo.waitFor(info => isBlockInfoSucceeded(info));
|
||||||
|
|
||||||
|
const getResponse = await connection.getTx(transactionId);
|
||||||
|
expect(getResponse.transactionId).toEqual(transactionId);
|
||||||
|
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;
|
||||||
|
assert(isSendTransaction(transaction), "Expected send transaction");
|
||||||
|
expect(transaction).toEqual(unsigned);
|
||||||
|
expect(signatures.length).toEqual(1);
|
||||||
|
expect(signatures[0]).toEqual({
|
||||||
|
nonce: signed.signatures[0].nonce,
|
||||||
|
pubkey: {
|
||||||
|
algo: signed.signatures[0].pubkey.algo,
|
||||||
|
data: Secp256k1.compressPubkey(signed.signatures[0].pubkey.data),
|
||||||
|
},
|
||||||
|
signature: Secp256k1.trimRecoveryByte(signed.signatures[0].signature),
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can get an old transaction", async () => {
|
||||||
|
pendingWithoutCosmos();
|
||||||
|
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||||
|
|
||||||
|
const results = await connection.searchTx({ sentFromOrTo: faucet.address });
|
||||||
|
const firstSearchResult = results.find(() => true);
|
||||||
|
assert(firstSearchResult, "At least one transaction sent by the faucet must be available.");
|
||||||
|
assert(isConfirmedTransaction(firstSearchResult), "Transaction must be confirmed.");
|
||||||
|
const {
|
||||||
|
transaction: searchedTransaction,
|
||||||
|
transactionId: searchedTransactionId,
|
||||||
|
height: searchedHeight,
|
||||||
|
} = firstSearchResult;
|
||||||
|
|
||||||
|
const getResponse = await connection.getTx(searchedTransactionId);
|
||||||
|
assert(isConfirmedTransaction(getResponse), "Expected transaction to succeed");
|
||||||
|
const { height, transactionId, log, transaction, signatures } = getResponse;
|
||||||
|
|
||||||
|
// Test properties of getTx result: height, transactionId, log, transaction
|
||||||
|
expect(height).toEqual(searchedHeight);
|
||||||
|
expect(transactionId).toEqual(searchedTransactionId);
|
||||||
|
assert(log, "Log must be available");
|
||||||
|
const [firstLog] = JSON.parse(log);
|
||||||
|
expect(firstLog.events.length).toEqual(2);
|
||||||
|
expect(transaction).toEqual(searchedTransaction);
|
||||||
|
|
||||||
|
// Signature test ensures the nonce is correct
|
||||||
|
expect(signatures.length).toEqual(1);
|
||||||
|
const signBytes = connection.codec.bytesToSign(getResponse.transaction, signatures[0].nonce).bytes;
|
||||||
|
const { pubkey, signature } = decodeSignature(encodeFullSignature(signatures[0]));
|
||||||
|
const prehashed = new Sha256(signBytes).digest();
|
||||||
|
const valid = await Secp256k1.verifySignature(
|
||||||
|
new Secp256k1Signature(signature.slice(0, 32), signature.slice(32, 64)),
|
||||||
|
prehashed,
|
||||||
|
pubkey,
|
||||||
|
);
|
||||||
|
expect(valid).toEqual(true);
|
||||||
|
|
||||||
|
connection.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
it("throws for non-existent transaction", async () => {
|
it("throws for non-existent transaction", async () => {
|
||||||
pendingWithoutCosmos();
|
pendingWithoutCosmos();
|
||||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||||
@@ -278,70 +374,18 @@ describe("CosmWasmConnection", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("integration tests", () => {
|
describe("integration tests", () => {
|
||||||
it("can post and get a transaction", async () => {
|
|
||||||
pendingWithoutCosmos();
|
|
||||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
|
||||||
const profile = new UserProfile();
|
|
||||||
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic));
|
|
||||||
const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath);
|
|
||||||
const faucetAddress = connection.codec.identityToAddress(faucet);
|
|
||||||
|
|
||||||
const unsigned = await connection.withDefaultFee<SendTransaction>({
|
|
||||||
kind: "bcp/send",
|
|
||||||
chainId: defaultChainId,
|
|
||||||
sender: faucetAddress,
|
|
||||||
recipient: defaultRecipient,
|
|
||||||
memo: "My first payment",
|
|
||||||
amount: {
|
|
||||||
quantity: "75000",
|
|
||||||
fractionalDigits: 6,
|
|
||||||
tokenTicker: cosm,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const nonce = await connection.getNonce({ address: faucetAddress });
|
|
||||||
const signed = await profile.signTransaction(faucet, unsigned, connection.codec, nonce);
|
|
||||||
const postableBytes = connection.codec.bytesToPost(signed);
|
|
||||||
const response = await connection.postTx(postableBytes);
|
|
||||||
const { transactionId } = response;
|
|
||||||
const blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info));
|
|
||||||
expect(blockInfo.state).toEqual(TransactionState.Succeeded);
|
|
||||||
|
|
||||||
const getResponse = await connection.getTx(transactionId);
|
|
||||||
expect(getResponse.transactionId).toEqual(transactionId);
|
|
||||||
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;
|
|
||||||
assert(isSendTransaction(transaction), "Expected send transaction");
|
|
||||||
expect(transaction).toEqual(unsigned);
|
|
||||||
expect(signatures.length).toEqual(1);
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can post and search for a transaction", async () => {
|
it("can post and search for a transaction", async () => {
|
||||||
pendingWithoutCosmos();
|
pendingWithoutCosmos();
|
||||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||||
const profile = new UserProfile();
|
const profile = new UserProfile();
|
||||||
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic));
|
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic));
|
||||||
const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath);
|
const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path);
|
||||||
const faucetAddress = connection.codec.identityToAddress(faucet);
|
const senderAddress = connection.codec.identityToAddress(sender);
|
||||||
|
|
||||||
const unsigned = await connection.withDefaultFee<SendTransaction>({
|
const unsigned = await connection.withDefaultFee<SendTransaction>({
|
||||||
kind: "bcp/send",
|
kind: "bcp/send",
|
||||||
chainId: defaultChainId,
|
chainId: defaultChainId,
|
||||||
sender: faucetAddress,
|
sender: senderAddress,
|
||||||
recipient: defaultRecipient,
|
recipient: defaultRecipient,
|
||||||
memo: "My first payment",
|
memo: "My first payment",
|
||||||
amount: {
|
amount: {
|
||||||
@@ -350,8 +394,8 @@ describe("CosmWasmConnection", () => {
|
|||||||
tokenTicker: cosm,
|
tokenTicker: cosm,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const nonce = await connection.getNonce({ address: faucetAddress });
|
const nonce = await connection.getNonce({ address: senderAddress });
|
||||||
const signed = await profile.signTransaction(faucet, unsigned, connection.codec, nonce);
|
const signed = await profile.signTransaction(sender, unsigned, connection.codec, nonce);
|
||||||
const postableBytes = connection.codec.bytesToPost(signed);
|
const postableBytes = connection.codec.bytesToPost(signed);
|
||||||
const response = await connection.postTx(postableBytes);
|
const response = await connection.postTx(postableBytes);
|
||||||
const { transactionId } = response;
|
const { transactionId } = response;
|
||||||
@@ -374,7 +418,7 @@ describe("CosmWasmConnection", () => {
|
|||||||
expect(byIdTransaction).toEqual(unsigned);
|
expect(byIdTransaction).toEqual(unsigned);
|
||||||
|
|
||||||
// search by sender address
|
// search by sender address
|
||||||
const bySenderResults = await connection.searchTx({ sentFromOrTo: faucetAddress });
|
const bySenderResults = await connection.searchTx({ sentFromOrTo: senderAddress });
|
||||||
expect(bySenderResults).toBeTruthy();
|
expect(bySenderResults).toBeTruthy();
|
||||||
expect(bySenderResults.length).toBeGreaterThanOrEqual(1);
|
expect(bySenderResults.length).toBeGreaterThanOrEqual(1);
|
||||||
const bySenderResult = bySenderResults[bySenderResults.length - 1];
|
const bySenderResult = bySenderResults[bySenderResults.length - 1];
|
||||||
@@ -424,15 +468,15 @@ describe("CosmWasmConnection", () => {
|
|||||||
pendingWithoutCosmos();
|
pendingWithoutCosmos();
|
||||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||||
const profile = new UserProfile();
|
const profile = new UserProfile();
|
||||||
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic));
|
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic));
|
||||||
const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath);
|
const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path);
|
||||||
const faucetAddress = connection.codec.identityToAddress(faucet);
|
const senderAddress = connection.codec.identityToAddress(sender);
|
||||||
const recipient = makeRandomAddress();
|
const recipient = makeRandomAddress();
|
||||||
|
|
||||||
const unsigned = await connection.withDefaultFee<SendTransaction>({
|
const unsigned = await connection.withDefaultFee<SendTransaction>({
|
||||||
kind: "bcp/send",
|
kind: "bcp/send",
|
||||||
chainId: defaultChainId,
|
chainId: defaultChainId,
|
||||||
sender: faucetAddress,
|
sender: senderAddress,
|
||||||
recipient: recipient,
|
recipient: recipient,
|
||||||
memo: "My first payment",
|
memo: "My first payment",
|
||||||
amount: {
|
amount: {
|
||||||
@@ -441,8 +485,8 @@ describe("CosmWasmConnection", () => {
|
|||||||
tokenTicker: "BASH" as TokenTicker,
|
tokenTicker: "BASH" as TokenTicker,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const nonce = await connection.getNonce({ address: faucetAddress });
|
const nonce = await connection.getNonce({ address: senderAddress });
|
||||||
const signed = await profile.signTransaction(faucet, unsigned, connection.codec, nonce);
|
const signed = await profile.signTransaction(sender, unsigned, connection.codec, nonce);
|
||||||
const postableBytes = connection.codec.bytesToPost(signed);
|
const postableBytes = connection.codec.bytesToPost(signed);
|
||||||
const response = await connection.postTx(postableBytes);
|
const response = await connection.postTx(postableBytes);
|
||||||
const blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info));
|
const blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info));
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
/* eslint-disable @typescript-eslint/camelcase */
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
import { CosmosAddressBech32Prefix, CosmWasmClient, RestClient, TxsResponse } from "@cosmwasm/sdk";
|
import {
|
||||||
|
CosmosAddressBech32Prefix,
|
||||||
|
CosmWasmClient,
|
||||||
|
findSequenceForSignedTx,
|
||||||
|
RestClient,
|
||||||
|
TxsResponse,
|
||||||
|
types,
|
||||||
|
} from "@cosmwasm/sdk";
|
||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
AccountQuery,
|
AccountQuery,
|
||||||
@@ -359,10 +366,32 @@ export class CosmWasmConnection implements BlockchainConnection {
|
|||||||
private async parseAndPopulateTxResponseSigned(
|
private async parseAndPopulateTxResponseSigned(
|
||||||
response: TxsResponse,
|
response: TxsResponse,
|
||||||
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
|
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
|
||||||
// There is no known way to get the nonce that was used for signing a transaction.
|
const firstMsg = response.tx.value.msg.find(() => true);
|
||||||
// This information is nesessary for signature validation.
|
if (!firstMsg) throw new Error("Got transaction without a first message. What is going on here?");
|
||||||
// TODO: fix
|
|
||||||
const nonce = -1 as Nonce;
|
let senderAddress: string;
|
||||||
|
if (types.isMsgSend(firstMsg)) {
|
||||||
|
senderAddress = firstMsg.value.from_address;
|
||||||
|
} else if (
|
||||||
|
types.isMsgStoreCode(firstMsg) ||
|
||||||
|
types.isMsgInstantiateContract(firstMsg) ||
|
||||||
|
types.isMsgExecuteContract(firstMsg)
|
||||||
|
) {
|
||||||
|
senderAddress = firstMsg.value.sender;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Got unsupported type of message: ${firstMsg.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accountNumber, sequence: currentSequence } = await this.cosmWasmClient.getNonce(senderAddress);
|
||||||
|
const sequenceForTx = await findSequenceForSignedTx(
|
||||||
|
response.tx,
|
||||||
|
Caip5.decode(this.chainId),
|
||||||
|
accountNumber,
|
||||||
|
currentSequence,
|
||||||
|
);
|
||||||
|
if (!sequenceForTx) throw new Error("Cound not find matching sequence for this transaction");
|
||||||
|
|
||||||
|
const nonce = accountToNonce(accountNumber, sequenceForTx);
|
||||||
|
|
||||||
return parseTxsResponseSigned(
|
return parseTxsResponseSigned(
|
||||||
this.chainId,
|
this.chainId,
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ describe("decode", () => {
|
|||||||
txhash: testdata.txId,
|
txhash: testdata.txId,
|
||||||
raw_log: '[{"msg_index":0,"success":true,"log":""}]',
|
raw_log: '[{"msg_index":0,"success":true,"log":""}]',
|
||||||
tx: cosmoshub.tx,
|
tx: cosmoshub.tx,
|
||||||
|
timestamp: "2020-02-14T11:35:41Z",
|
||||||
};
|
};
|
||||||
const expected = {
|
const expected = {
|
||||||
transaction: testdata.sendTxJson,
|
transaction: testdata.sendTxJson,
|
||||||
@@ -205,6 +206,7 @@ describe("decode", () => {
|
|||||||
txhash: testdata.txId,
|
txhash: testdata.txId,
|
||||||
raw_log: '[{"msg_index":0,"success":true,"log":""}]',
|
raw_log: '[{"msg_index":0,"success":true,"log":""}]',
|
||||||
tx: cosmoshub.tx,
|
tx: cosmoshub.tx,
|
||||||
|
timestamp: "2020-02-14T11:35:41Z",
|
||||||
};
|
};
|
||||||
const expected = {
|
const expected = {
|
||||||
...testdata.signedTxJson,
|
...testdata.signedTxJson,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
import { Bech32, Encoding } from "@iov/encoding";
|
import { Bech32, Encoding } from "@iov/encoding";
|
||||||
import { assert } from "@iov/utils";
|
import { assert, sleep } from "@iov/utils";
|
||||||
|
|
||||||
import { CosmWasmClient } from "./cosmwasmclient";
|
import { CosmWasmClient } from "./cosmwasmclient";
|
||||||
import { makeSignBytes, marshalTx } from "./encoding";
|
import { makeSignBytes, marshalTx } from "./encoding";
|
||||||
@@ -91,9 +92,7 @@ describe("CosmWasmClient", () => {
|
|||||||
const sendMsg: MsgSend = {
|
const sendMsg: MsgSend = {
|
||||||
type: "cosmos-sdk/MsgSend",
|
type: "cosmos-sdk/MsgSend",
|
||||||
value: {
|
value: {
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
from_address: faucet.address,
|
from_address: faucet.address,
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
to_address: makeRandomAddress(),
|
to_address: makeRandomAddress(),
|
||||||
amount: [
|
amount: [
|
||||||
{
|
{
|
||||||
@@ -144,7 +143,6 @@ describe("CosmWasmClient", () => {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
if (cosmosEnabled()) {
|
if (cosmosEnabled()) {
|
||||||
pendingWithoutCosmos();
|
|
||||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||||
const client = CosmWasmClient.makeReadOnly(httpUrl);
|
const client = CosmWasmClient.makeReadOnly(httpUrl);
|
||||||
|
|
||||||
@@ -152,9 +150,7 @@ describe("CosmWasmClient", () => {
|
|||||||
const sendMsg: MsgSend = {
|
const sendMsg: MsgSend = {
|
||||||
type: "cosmos-sdk/MsgSend",
|
type: "cosmos-sdk/MsgSend",
|
||||||
value: {
|
value: {
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
from_address: faucet.address,
|
from_address: faucet.address,
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
to_address: makeRandomAddress(),
|
to_address: makeRandomAddress(),
|
||||||
amount: [
|
amount: [
|
||||||
{
|
{
|
||||||
@@ -187,6 +183,7 @@ describe("CosmWasmClient", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await client.postTx(marshalTx(signedTx));
|
const result = await client.postTx(marshalTx(signedTx));
|
||||||
|
await sleep(50); // wait until tx is indexed
|
||||||
const txDetails = await new RestClient(httpUrl).txsById(result.transactionHash);
|
const txDetails = await new RestClient(httpUrl).txsById(result.transactionHash);
|
||||||
posted = {
|
posted = {
|
||||||
sender: sendMsg.value.from_address,
|
sender: sendMsg.value.from_address,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr
|
|||||||
export { unmarshalTx } from "./decoding";
|
export { unmarshalTx } from "./decoding";
|
||||||
export { makeSignBytes, marshalTx } from "./encoding";
|
export { makeSignBytes, marshalTx } from "./encoding";
|
||||||
export { RestClient, TxsResponse } from "./restclient";
|
export { RestClient, TxsResponse } from "./restclient";
|
||||||
export { encodeSecp256k1Signature } from "./signature";
|
|
||||||
export {
|
export {
|
||||||
CosmWasmClient,
|
CosmWasmClient,
|
||||||
ExecuteResult,
|
ExecuteResult,
|
||||||
@@ -24,3 +23,5 @@ export {
|
|||||||
encodeBech32Pubkey,
|
encodeBech32Pubkey,
|
||||||
encodeSecp256k1Pubkey,
|
encodeSecp256k1Pubkey,
|
||||||
} from "./pubkey";
|
} from "./pubkey";
|
||||||
|
export { findSequenceForSignedTx } from "./sequence";
|
||||||
|
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
|
||||||
|
|||||||
@@ -69,7 +69,13 @@ export interface TxsResponse {
|
|||||||
readonly height: string;
|
readonly height: string;
|
||||||
readonly txhash: string;
|
readonly txhash: string;
|
||||||
readonly raw_log: string;
|
readonly raw_log: string;
|
||||||
|
readonly logs?: object;
|
||||||
readonly tx: CosmosSdkTx;
|
readonly tx: CosmosSdkTx;
|
||||||
|
/** The gas limit as set by the user */
|
||||||
|
readonly gas_wanted?: string;
|
||||||
|
/** The gas used by the execution */
|
||||||
|
readonly gas_used?: string;
|
||||||
|
readonly timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SearchTxsResponse {
|
interface SearchTxsResponse {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { findSequenceForSignedTx } from "./sequence";
|
||||||
|
import response1 from "./testdata/txresponse1.json";
|
||||||
|
import response2 from "./testdata/txresponse2.json";
|
||||||
|
import response3 from "./testdata/txresponse3.json";
|
||||||
|
|
||||||
|
// Those values must match ./testdata/txresponse*.json
|
||||||
|
const chainId = "testing";
|
||||||
|
const accountNumber = 4;
|
||||||
|
|
||||||
|
describe("sequence", () => {
|
||||||
|
describe("findSequenceForSignedTx", () => {
|
||||||
|
it("works", async () => {
|
||||||
|
const current = 100; // what we get from GET /auth/accounts/{address}
|
||||||
|
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, current)).toEqual(10);
|
||||||
|
// We know response3.height > response1.height, so the sequence must be at least 10+1
|
||||||
|
expect(await findSequenceForSignedTx(response3.tx, chainId, accountNumber, current, 11)).toEqual(19);
|
||||||
|
// We know response3.height > response2.height > response1.height, so the sequence must be at least 10+1 and smaller than 19
|
||||||
|
expect(await findSequenceForSignedTx(response2.tx, chainId, accountNumber, 19, 11)).toEqual(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when sequence is not in range", async () => {
|
||||||
|
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 5)).toBeUndefined();
|
||||||
|
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 20, 11)).toBeUndefined();
|
||||||
|
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 20, 50)).toBeUndefined();
|
||||||
|
|
||||||
|
// upper bound is not included in the possible results
|
||||||
|
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 10)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto";
|
||||||
|
|
||||||
|
import { makeSignBytes } from "./encoding";
|
||||||
|
import { decodeSignature } from "./signature";
|
||||||
|
import { CosmosSdkTx } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serach for sequence s with `min` <= `s` < `upperBound` to find the sequence that was used to sign the transaction
|
||||||
|
*
|
||||||
|
* @param tx The signed transaction
|
||||||
|
* @param chainId The chain ID for which this transaction was signed
|
||||||
|
* @param accountNumber The account number for which this transaction was signed
|
||||||
|
* @param upperBound The upper bound for the testing, i.e. sequence must be lower than this value
|
||||||
|
* @param min The lowest sequence that is tested
|
||||||
|
*
|
||||||
|
* @returns the sequence if a match was found and undefined otherwise
|
||||||
|
*/
|
||||||
|
export async function findSequenceForSignedTx(
|
||||||
|
tx: CosmosSdkTx,
|
||||||
|
chainId: string,
|
||||||
|
accountNumber: number,
|
||||||
|
upperBound: number,
|
||||||
|
min = 1,
|
||||||
|
): Promise<number | undefined> {
|
||||||
|
const firstSignature = tx.value.signatures.find(() => true);
|
||||||
|
if (!firstSignature) throw new Error("Signature missing in tx");
|
||||||
|
|
||||||
|
const { pubkey, signature } = decodeSignature(firstSignature);
|
||||||
|
const secp256keSignature = new Secp256k1Signature(signature.slice(0, 32), signature.slice(32, 64));
|
||||||
|
|
||||||
|
for (let s = min; s < upperBound; s++) {
|
||||||
|
// console.log(`Trying sequence ${s}`);
|
||||||
|
const signBytes = makeSignBytes(
|
||||||
|
tx.value.msg,
|
||||||
|
tx.value.fee,
|
||||||
|
chainId,
|
||||||
|
tx.value.memo || "",
|
||||||
|
accountNumber,
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
const prehashed = new Sha256(signBytes).digest();
|
||||||
|
const valid = await Secp256k1.verifySignature(secp256keSignature, prehashed, pubkey);
|
||||||
|
if (valid) return s;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"height": "15888",
|
||||||
|
"txhash": "672DEDE8EF4DE8B5818959F417CCA357079D4D7A19C4B65443C7FBF8176AABF9",
|
||||||
|
"raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]",
|
||||||
|
"logs": [
|
||||||
|
{
|
||||||
|
"msg_index": 0,
|
||||||
|
"log": "",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"attributes": [
|
||||||
|
{ "key": "action", "value": "send" },
|
||||||
|
{ "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" },
|
||||||
|
{ "key": "module", "value": "bank" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "transfer",
|
||||||
|
"attributes": [
|
||||||
|
{ "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" },
|
||||||
|
{ "key": "amount", "value": "75000ucosm" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gas_wanted": "200000",
|
||||||
|
"gas_used": "65407",
|
||||||
|
"tx": {
|
||||||
|
"type": "cosmos-sdk/StdTx",
|
||||||
|
"value": {
|
||||||
|
"msg": [
|
||||||
|
{
|
||||||
|
"type": "cosmos-sdk/MsgSend",
|
||||||
|
"value": {
|
||||||
|
"from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||||
|
"to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2",
|
||||||
|
"amount": [{ "denom": "ucosm", "amount": "75000" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" },
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"pub_key": {
|
||||||
|
"type": "tendermint/PubKeySecp256k1",
|
||||||
|
"value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"
|
||||||
|
},
|
||||||
|
"signature": "US7oH8S/8TxVrtBQkOhHxAM+oDB2spNAEawgh6H8CCFLRMOJK+uvQZZ6ceUgUsvDbxwCz7re1RU272fymMYRZQ=="
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"memo": "My first payment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": "2020-02-14T11:25:55Z"
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"height": "16456",
|
||||||
|
"txhash": "7BFE4B93AF190F60132C62D08FDF50BE462FBCE374EB13D3FD0C32461E771EC0",
|
||||||
|
"raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]",
|
||||||
|
"logs": [
|
||||||
|
{
|
||||||
|
"msg_index": 0,
|
||||||
|
"log": "",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"attributes": [
|
||||||
|
{ "key": "action", "value": "send" },
|
||||||
|
{ "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" },
|
||||||
|
{ "key": "module", "value": "bank" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "transfer",
|
||||||
|
"attributes": [
|
||||||
|
{ "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" },
|
||||||
|
{ "key": "amount", "value": "75000ucosm" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gas_wanted": "200000",
|
||||||
|
"gas_used": "65407",
|
||||||
|
"tx": {
|
||||||
|
"type": "cosmos-sdk/StdTx",
|
||||||
|
"value": {
|
||||||
|
"msg": [
|
||||||
|
{
|
||||||
|
"type": "cosmos-sdk/MsgSend",
|
||||||
|
"value": {
|
||||||
|
"from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||||
|
"to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2",
|
||||||
|
"amount": [{ "denom": "ucosm", "amount": "75000" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" },
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"pub_key": {
|
||||||
|
"type": "tendermint/PubKeySecp256k1",
|
||||||
|
"value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"
|
||||||
|
},
|
||||||
|
"signature": "ltvd9Rb3RF4zjbUVrpDpkok34g+py7XR8ZcM0tZUYRxxVdcMEin010x+ZFd/mOuutPj9fDmSENnienc/yi4msw=="
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"memo": "My first payment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": "2020-02-14T11:35:41Z"
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"height": "20730",
|
||||||
|
"txhash": "625BC75E697F73DA037387C34002BB2F682E7ACDCC4E015D3E90420516C6D0C8",
|
||||||
|
"raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]",
|
||||||
|
"logs": [
|
||||||
|
{
|
||||||
|
"msg_index": 0,
|
||||||
|
"log": "",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"attributes": [
|
||||||
|
{ "key": "action", "value": "send" },
|
||||||
|
{ "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" },
|
||||||
|
{ "key": "module", "value": "bank" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "transfer",
|
||||||
|
"attributes": [
|
||||||
|
{ "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" },
|
||||||
|
{ "key": "amount", "value": "75000ucosm" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gas_wanted": "200000",
|
||||||
|
"gas_used": "65407",
|
||||||
|
"tx": {
|
||||||
|
"type": "cosmos-sdk/StdTx",
|
||||||
|
"value": {
|
||||||
|
"msg": [
|
||||||
|
{
|
||||||
|
"type": "cosmos-sdk/MsgSend",
|
||||||
|
"value": {
|
||||||
|
"from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||||
|
"to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2",
|
||||||
|
"amount": [{ "denom": "ucosm", "amount": "75000" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" },
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"pub_key": {
|
||||||
|
"type": "tendermint/PubKeySecp256k1",
|
||||||
|
"value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"
|
||||||
|
},
|
||||||
|
"signature": "eOFGl1tIHDMv3JdCK9fRSikVbYUD8+B0ksb3dJFya8MPYgpEpdSA7zZc+5n/cW6LR/BJdib4nqmJQv1yD9lm3g=="
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"memo": "My first payment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": "2020-02-14T12:48:56Z"
|
||||||
|
}
|
||||||
Vendored
+2
-1
@@ -5,7 +5,6 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr
|
|||||||
export { unmarshalTx } from "./decoding";
|
export { unmarshalTx } from "./decoding";
|
||||||
export { makeSignBytes, marshalTx } from "./encoding";
|
export { makeSignBytes, marshalTx } from "./encoding";
|
||||||
export { RestClient, TxsResponse } from "./restclient";
|
export { RestClient, TxsResponse } from "./restclient";
|
||||||
export { encodeSecp256k1Signature } from "./signature";
|
|
||||||
export {
|
export {
|
||||||
CosmWasmClient,
|
CosmWasmClient,
|
||||||
ExecuteResult,
|
ExecuteResult,
|
||||||
@@ -23,3 +22,5 @@ export {
|
|||||||
encodeBech32Pubkey,
|
encodeBech32Pubkey,
|
||||||
encodeSecp256k1Pubkey,
|
encodeSecp256k1Pubkey,
|
||||||
} from "./pubkey";
|
} from "./pubkey";
|
||||||
|
export { findSequenceForSignedTx } from "./sequence";
|
||||||
|
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
|
||||||
|
|||||||
Vendored
+6
@@ -42,7 +42,13 @@ export interface TxsResponse {
|
|||||||
readonly height: string;
|
readonly height: string;
|
||||||
readonly txhash: string;
|
readonly txhash: string;
|
||||||
readonly raw_log: string;
|
readonly raw_log: string;
|
||||||
|
readonly logs?: object;
|
||||||
readonly tx: CosmosSdkTx;
|
readonly tx: CosmosSdkTx;
|
||||||
|
/** The gas limit as set by the user */
|
||||||
|
readonly gas_wanted?: string;
|
||||||
|
/** The gas used by the execution */
|
||||||
|
readonly gas_used?: string;
|
||||||
|
readonly timestamp: string;
|
||||||
}
|
}
|
||||||
interface SearchTxsResponse {
|
interface SearchTxsResponse {
|
||||||
readonly total_count: string;
|
readonly total_count: string;
|
||||||
|
|||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import { CosmosSdkTx } from "./types";
|
||||||
|
/**
|
||||||
|
* Serach for sequence s with `min` <= `s` < `upperBound` to find the sequence that was used to sign the transaction
|
||||||
|
*
|
||||||
|
* @param tx The signed transaction
|
||||||
|
* @param chainId The chain ID for which this transaction was signed
|
||||||
|
* @param accountNumber The account number for which this transaction was signed
|
||||||
|
* @param upperBound The upper bound for the testing, i.e. sequence must be lower than this value
|
||||||
|
* @param min The lowest sequence that is tested
|
||||||
|
*
|
||||||
|
* @returns the sequence if a match was found and undefined otherwise
|
||||||
|
*/
|
||||||
|
export declare function findSequenceForSignedTx(
|
||||||
|
tx: CosmosSdkTx,
|
||||||
|
chainId: string,
|
||||||
|
accountNumber: number,
|
||||||
|
upperBound: number,
|
||||||
|
min?: number,
|
||||||
|
): Promise<number | undefined>;
|
||||||
Reference in New Issue
Block a user